From 2083935ac4a0c9e86a8b4a5e94ae07c52bc87739 Mon Sep 17 00:00:00 2001 From: bcoll Date: Fri, 18 Nov 2022 10:33:49 +0000 Subject: [PATCH] Add pretty-error page infrastructure This adds Miniflare 2's pretty-error page powered by [Youch](https://github.com/poppinss/youch) to Miniflare 3. Unfortunately, due to a bug in `workerd`, errors thrown asynchronously by native APIs don't have `stack`s. This means we can't extract the `stack` trace from dispatching to the user worker by `try`/`catch`. As a stop-gap solution, if the `MF-Experimental-Error-Stack` header exists and is truthy on the response from the user worker, the body will be interpreted as a JSON-error of the form `{ message?: string, name?: string, stack?: string }`. `stack` will be source-mapped if possible. Another issue is that `workerd` gives all service-worker scripts the name "worker.js", so if multiple service-workers are defined, we can't identify which one threw. In this case, we don't display sources in the pretty-error page. Hopefully, we can fix both of these issues in `workerd`. We should be able to reuse most of this infrastructure with `try`/`catch`s too. --- package-lock.json | 139 +++++++- packages/tre/README.md | 5 + packages/tre/package.json | 2 + packages/tre/src/index.ts | 12 +- packages/tre/src/plugins/core/index.ts | 60 +++- packages/tre/src/plugins/core/modules.ts | 22 +- packages/tre/src/plugins/core/prettyerror.ts | 329 +++++++++++++++++++ 7 files changed, 547 insertions(+), 22 deletions(-) create mode 100644 packages/tre/src/plugins/core/prettyerror.ts diff --git a/package-lock.json b/package-lock.json index 947d9b3a2..1fbf25f99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -860,6 +860,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, "node_modules/ava": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ava/-/ava-5.0.1.tgz", @@ -1499,6 +1507,14 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1525,6 +1541,11 @@ "node": ">=0.10.0" } }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" + }, "node_modules/date-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", @@ -2557,6 +2578,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3509,6 +3539,14 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -4219,6 +4257,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -4597,7 +4640,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4629,6 +4671,15 @@ "node": ">=8" } }, + "node_modules/stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -5329,6 +5380,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youch": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.2.2.tgz", + "integrity": "sha512-+xhTK8sY9qV3nLbWVaOUFZPdlQ3wrMKiu3dKqKkAkLaYzzkYmpWY+v+eQIAfbPu7TZhS1G5FhEV++sl8fhuT4w==", + "dependencies": { + "cookie": "^0.5.0", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, "node_modules/z-schema": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", @@ -5785,10 +5846,12 @@ "glob-to-regexp": "^0.4.1", "http-cache-semantics": "^4.1.0", "kleur": "^4.1.5", + "source-map": "^0.7.4", "stoppable": "^1.1.0", "undici": "^5.12.0", "workerd": "^1.20221111.5", "ws": "^8.11.0", + "youch": "^3.2.2", "zod": "^3.18.0" }, "devDependencies": { @@ -5815,6 +5878,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/tre/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, "packages/watcher": { "name": "@miniflare/watcher", "version": "3.0.0-next.6", @@ -6030,10 +6101,12 @@ "glob-to-regexp": "^0.4.1", "http-cache-semantics": "^4.1.0", "kleur": "^4.1.5", + "source-map": "^0.7.4", "stoppable": "^1.1.0", "undici": "^5.12.0", "workerd": "^1.20221111.5", "ws": "^8.11.0", + "youch": "^3.2.2", "zod": "^3.18.0" }, "dependencies": { @@ -6041,6 +6114,11 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==" + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" } } }, @@ -6479,6 +6557,14 @@ "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==", "dev": true }, + "as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "requires": { + "printable-characters": "^1.0.42" + } + }, "ava": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ava/-/ava-5.0.1.tgz", @@ -6964,6 +7050,11 @@ "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "dev": true }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6984,6 +7075,11 @@ "array-find-index": "^1.0.1" } }, + "data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==" + }, "date-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", @@ -7763,6 +7859,15 @@ "has-symbols": "^1.0.1" } }, + "get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "requires": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -8418,6 +8523,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==" + }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -8915,6 +9025,11 @@ "parse-ms": "^3.0.0" } }, + "printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -9143,8 +9258,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "sprintf-js": { "version": "1.0.3", @@ -9169,6 +9283,15 @@ } } }, + "stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "requires": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, "stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -9677,6 +9800,16 @@ "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", "dev": true }, + "youch": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.2.2.tgz", + "integrity": "sha512-+xhTK8sY9qV3nLbWVaOUFZPdlQ3wrMKiu3dKqKkAkLaYzzkYmpWY+v+eQIAfbPu7TZhS1G5FhEV++sl8fhuT4w==", + "requires": { + "cookie": "^0.5.0", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, "z-schema": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", diff --git a/packages/tre/README.md b/packages/tre/README.md index 1f987e12d..7e5c99297 100644 --- a/packages/tre/README.md +++ b/packages/tre/README.md @@ -223,6 +223,11 @@ parameter in module format Workers. must also be defined. Note the first module must be the entrypoint and have type `"ESModule"`. +- `modulesRoot?: string` + + If `modules` is set to an array, modules' "name"s will be their `path`s + relative to this value. This ensures file paths in stack traces are correct. + diff --git a/packages/tre/package.json b/packages/tre/package.json index 3e9e5d95c..475429988 100644 --- a/packages/tre/package.json +++ b/packages/tre/package.json @@ -35,10 +35,12 @@ "glob-to-regexp": "^0.4.1", "http-cache-semantics": "^4.1.0", "kleur": "^4.1.5", + "source-map": "^0.7.4", "stoppable": "^1.1.0", "undici": "^5.12.0", "workerd": "^1.20221111.5", "ws": "^8.11.0", + "youch": "^3.2.2", "zod": "^3.18.0" }, "devDependencies": { diff --git a/packages/tre/src/index.ts b/packages/tre/src/index.ts index 8d39e14d3..f2165956b 100644 --- a/packages/tre/src/index.ts +++ b/packages/tre/src/index.ts @@ -27,7 +27,12 @@ import { maybeGetSitesManifestModule, normaliseDurableObject, } from "./plugins"; -import { HEADER_CUSTOM_SERVICE, getUserServiceName } from "./plugins/core"; +import { + HEADER_CUSTOM_SERVICE, + SourceOptions, + getUserServiceName, + handlePrettyErrorRequest, +} from "./plugins/core"; import { Config, Runtime, @@ -345,6 +350,11 @@ export class Miniflare { request, customService ); + } else if (url.pathname === "/core/error") { + const workerSrcOpts = this.#workerOpts.map( + ({ core }) => core + ); + response = await handlePrettyErrorRequest(workerSrcOpts, request); } else { // TODO: check for proxying/outbound fetch header first (with plans for fetch mocking) response = await this.#handleLoopbackPlugins(request, url); diff --git a/packages/tre/src/plugins/core/index.ts b/packages/tre/src/plugins/core/index.ts index ff28ca1f2..08081635c 100644 --- a/packages/tre/src/plugins/core/index.ts +++ b/packages/tre/src/plugins/core/index.ts @@ -21,9 +21,10 @@ import { ModuleDefinitionSchema, ModuleLocator, ModuleRuleSchema, - STRING_SCRIPT_PATH, + buildStringScriptPath, convertModuleDefinition, } from "./modules"; +import { HEADER_ERROR_STACK } from "./prettyerror"; import { ServiceDesignatorSchema } from "./services"; const encoder = new TextEncoder(); @@ -42,6 +43,7 @@ export const CoreOptionsSchema = z.object({ z.array(ModuleDefinitionSchema), ]) .optional(), + modulesRoot: z.string().optional(), modulesRules: z.array(ModuleRuleSchema).optional(), compatibilityDate: z.string().optional(), @@ -122,7 +124,6 @@ const LIVE_RELOAD_SCRIPT_TEMPLATE = ( })(); `; -// TODO: is there a way of capturing the full stack trace somehow? // Using `>=` for version check to handle multiple `setOptions` calls before // reload complete. export const SCRIPT_ENTRY = `async function handleEvent(event) { @@ -141,12 +142,32 @@ export const SCRIPT_ENTRY = `async function handleEvent(event) { return new Response("No entrypoint worker found", { status: 404 }); } try { - const response = await ${BINDING_SERVICE_USER}.fetch(request); + let response = await ${BINDING_SERVICE_USER}.fetch(request); + + if ( + response.status === 500 && + response.headers.get("${HEADER_ERROR_STACK}") !== null + ) { + const accept = request.headers.get("Accept")?.toLowerCase() ?? ""; + const userAgent = request.headers.get("User-Agent")?.toLowerCase() ?? ""; + const acceptsPrettyError = + !userAgent.includes("curl/") && + (accept.includes("text/html") || + accept.includes("*/*") || + accept.includes("text/*")); + if (acceptsPrettyError) { + response = await ${BINDING_SERVICE_LOOPBACK}.fetch("http://localhost/core/error", { + method: "POST", + headers: request.headers, + body: response.body, + }); + } + } const liveReloadScript = globalThis.${BINDING_DATA_LIVE_RELOAD_SCRIPT}; if ( liveReloadScript !== undefined && - response.headers.get("content-type")?.toLowerCase().includes("text/html") + response.headers.get("Content-Type")?.toLowerCase().includes("text/html") ) { const headers = new Headers(response.headers); const contentLength = parseInt(headers.get("content-length")); @@ -286,6 +307,7 @@ export const CORE_PLUGIN: Plugin< return Promise.all(bindings); }, async getServices({ + log, options, optionsVersion, workerBindings, @@ -294,12 +316,17 @@ export const CORE_PLUGIN: Plugin< durableObjectClassNames, additionalModules, loopbackPort, - log, }) { // Define core/shared services. + const loopbackBinding: Worker_Binding = { + name: BINDING_SERVICE_LOOPBACK, + service: { name: SERVICE_LOOPBACK }, + }; + // Services get de-duped by name, so only the first worker's // SERVICE_LOOPBACK and SERVICE_ENTRY will be used const serviceEntryBindings: Worker_Binding[] = [ + loopbackBinding, // For converting stack-traces to pretty-error pages { name: BINDING_JSON_VERSION, json: optionsVersion.toString() }, { name: BINDING_JSON_CF_BLOB, json: JSON.stringify(sharedOptions.cf) }, ]; @@ -335,7 +362,7 @@ export const CORE_PLUGIN: Plugin< ]; // Define regular user worker if script is set - const workerScript = getWorkerScript(options); + const workerScript = getWorkerScript(options, workerIndex); if (workerScript !== undefined) { // Add additional modules (e.g. "__STATIC_CONTENT_MANIFEST") if any if ("modules" in workerScript) { @@ -385,10 +412,7 @@ export const CORE_PLUGIN: Plugin< name: BINDING_TEXT_CUSTOM_SERVICE, text: `${workerIndex}/${name}`, }, - { - name: BINDING_SERVICE_LOOPBACK, - service: { name: SERVICE_LOOPBACK }, - }, + loopbackBinding, ], }, }); @@ -407,11 +431,17 @@ export const CORE_PLUGIN: Plugin< }; function getWorkerScript( - options: z.infer + options: z.infer, + workerIndex: number ): { serviceWorkerScript: string } | { modules: Worker_Module[] } | undefined { if (Array.isArray(options.modules)) { // If `modules` is a manually defined modules array, use that - return { modules: options.modules.map(convertModuleDefinition) }; + const modulesRoot = options.modulesRoot ?? ""; + return { + modules: options.modules.map((module) => + convertModuleDefinition(modulesRoot, module) + ), + }; } // Otherwise get code, preferring string `script` over `scriptPath` @@ -431,7 +461,10 @@ function getWorkerScript( const locator = new ModuleLocator(options.modulesRules); // If `script` and `scriptPath` are set, resolve modules in `script` // against `scriptPath`. - locator.visitEntrypoint(code, options.scriptPath ?? STRING_SCRIPT_PATH); + locator.visitEntrypoint( + code, + options.scriptPath ?? buildStringScriptPath(workerIndex) + ); return { modules: locator.modules }; } else { // ...otherwise, `modules` will either be `false` or `undefined`, so treat @@ -440,4 +473,5 @@ function getWorkerScript( } } +export * from "./prettyerror"; export * from "./services"; diff --git a/packages/tre/src/plugins/core/modules.ts b/packages/tre/src/plugins/core/modules.ts index 003d0f923..629f75958 100644 --- a/packages/tre/src/plugins/core/modules.ts +++ b/packages/tre/src/plugins/core/modules.ts @@ -26,7 +26,16 @@ const SUGGEST_NODE = "\n- Find an alternative package that doesn't require Node.js built-ins"; // Module identifier used if script came from `script` option -export const STRING_SCRIPT_PATH = "