diff --git a/documentation/docs/experimental/mapAndLogError.mdx b/documentation/docs/experimental/mapAndLogError.mdx new file mode 100644 index 0000000000..b9eff0b5ec --- /dev/null +++ b/documentation/docs/experimental/mapAndLogError.mdx @@ -0,0 +1,60 @@ +--- +hide_title: false +hide_table_of_contents: false +pagination_next: null +pagination_prev: null +--- +# mapAndLogError + +The **`mapAndLogError()`** function calls `mapError` on an `Error` object and sends the output to standard error. This includes the error name, message, and a call stack. + +If `--enable-stack-traces` is specified during the build, the call stack will be mapped using source maps. + +If `--enable-stack-traces` is specified and `--exclude-sources` is not specified during the build, then this will also include a code dump of neighboring lines of user code. + +## Syntax + +```js +mapAndLogError(error) +``` + +### Parameters + +- `error` _: Error _ or _string_ + - The error to retrieve information about. If a `string` is provided, then it is first converted to an `Error`. + +### Return value + +`undefined`. + +## Examples + +In this example, build the application using the `--enable-stack-traces` flag. + +```js +addEventListener('fetch', e => e.respondWith(handler(e))); +async function handler(event) { + try { + throw new TypeError('foo'); + } catch (err) { + mapAndLogError(mapError(err)); + } + return new Response('ok'); +} +``` + +The following is output to the error log. + +``` +TypeError: foo + at handler (src/index.ts:4:11) + 1 | addEventListener('fetch', e => e.respondWith(handler(e))); + 2 | async function handler(event) { + 3 | try { +> 4 | throw new TypeError('foo'); + ^ + 5 | } catch (err) { + 6 | mapAndLogError(mapError(err)); + 7 | } + at src/index.ts:1:45 +``` diff --git a/documentation/docs/experimental/mapError.mdx b/documentation/docs/experimental/mapError.mdx new file mode 100644 index 0000000000..498aa31c24 --- /dev/null +++ b/documentation/docs/experimental/mapError.mdx @@ -0,0 +1,60 @@ +--- +hide_title: false +hide_table_of_contents: false +pagination_next: null +pagination_prev: null +--- +# mapError + +The **`mapError()`** function extracts information from an `Error` object as a human-readable array of strings. This includes the error name, message, and a call stack. + +If `--enable-stack-traces` is specified during the build, the call stack will be mapped using source maps. + +If `--enable-stack-traces` is specified and `--exclude-sources` is not specified during the build, then this will also include a code dump of neighboring lines of user code. + +## Syntax + +```js +mapError(error) +``` + +### Parameters + +- `error` _: Error _ or _string_ + - The error to retrieve information about. If a `string` is provided, then it is first converted to an `Error`. + +### Return value + +Returns an array of `string`s. + +## Examples + +In this example, build the application using the `--enable-stack-traces` flag. + +```js +addEventListener('fetch', e => e.respondWith(handler(e))); +async function handler(event) { + try { + throw new TypeError('foo'); + } catch (err) { + console.error(mapError(err)); + } + return new Response('ok'); +} +``` + +The following is output to the error log. + +``` +TypeError: foo + at handler (src/index.ts:4:11) + 1 | addEventListener('fetch', e => e.respondWith(handler(e))); + 2 | async function handler(event) { + 3 | try { +> 4 | throw new TypeError('foo'); + ^ + 5 | } catch (err) { + 6 | console.error(mapError(err)); + 7 | } + at src/index.ts:1:45 +``` diff --git a/js-compute-runtime-cli.js b/js-compute-runtime-cli.js index 76037415cb..c05f48b040 100755 --- a/js-compute-runtime-cli.js +++ b/js-compute-runtime-cli.js @@ -12,6 +12,9 @@ const { enableExperimentalHighResolutionTimeMethods, moduleMode, bundle, + enableStackTraces, + excludeSources, + debugIntermediateFilesDir, wasmEngine, input, output, @@ -34,7 +37,7 @@ if (version) { const { compileApplicationToWasm } = await import( './src/compileApplicationToWasm.js' ); - await compileApplicationToWasm( + await compileApplicationToWasm({ input, output, wasmEngine, @@ -42,9 +45,12 @@ if (version) { enableExperimentalHighResolutionTimeMethods, enableAOT, aotCache, + enableStackTraces, + excludeSources, + debugIntermediateFilesDir, moduleMode, - bundle, + doBundle: bundle, env, - ); + }); await addSdkMetadataField(output, enableAOT); } diff --git a/package-lock.json b/package-lock.json index 0aed4cdd04..2ee256a087 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,13 @@ "@bytecodealliance/jco": "^1.7.0", "@bytecodealliance/weval": "^0.3.2", "@bytecodealliance/wizer": "^7.0.5", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/trace-mapping": "^0.3.31", "acorn": "^8.13.0", "acorn-walk": "^8.3.4", "esbuild": "^0.25.0", "magic-string": "^0.30.12", + "picomatch": "^4.0.3", "regexpu-core": "^6.1.1" }, "bin": { @@ -968,6 +971,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1003,9 +1016,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -4449,10 +4462,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 4b63e39ddf..78ef9070b8 100644 --- a/package.json +++ b/package.json @@ -57,12 +57,15 @@ }, "dependencies": { "@bytecodealliance/jco": "^1.7.0", - "@bytecodealliance/wizer": "^7.0.5", "@bytecodealliance/weval": "^0.3.2", + "@bytecodealliance/wizer": "^7.0.5", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/trace-mapping": "^0.3.31", "acorn": "^8.13.0", "acorn-walk": "^8.3.4", "esbuild": "^0.25.0", "magic-string": "^0.30.12", + "picomatch": "^4.0.3", "regexpu-core": "^6.1.1" }, "peerDependencies": { diff --git a/src/bundle.js b/src/bundle.js index 3b9ee4bd09..a28d9bdbec 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -1,6 +1,14 @@ +import { rename } from 'node:fs/promises'; +import { dirname, basename, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { build } from 'esbuild'; -let fastlyPlugin = { +import { swallowTopLevelExportsPlugin } from './swallowTopLevelExportsPlugin.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const fastlyPlugin = { name: 'fastly', setup(build) { build.onResolve({ filter: /^fastly:.*/ }, (args) => { @@ -74,6 +82,8 @@ export const setBaseURL = Object.getOwnPropertyDescriptor(globalThis.fastly, 'ba export const setDefaultBackend = Object.getOwnPropertyDescriptor(globalThis.fastly, 'defaultBackend').set; export const allowDynamicBackends = Object.getOwnPropertyDescriptor(globalThis.fastly, 'allowDynamicBackends').set; export const sdkVersion = globalThis.fastly.sdkVersion; +export const mapAndLogError = (e) => globalThis.__fastlyMapAndLogError(e); +export const mapError = (e) => globalThis.__fastlyMapError(e); `, }; } @@ -143,14 +153,40 @@ export const TransactionCacheEntry = globalThis.TransactionCacheEntry; }, }; -export async function bundle(input, moduleMode = false) { - return await build({ +export async function bundle( + input, + outfile, + { moduleMode = false, enableStackTraces = false }, +) { + // Build output file in cwd first to build sourcemap with correct paths + const bundle = resolve(basename(outfile)); + + const plugins = [fastlyPlugin]; + if (moduleMode) { + plugins.push(swallowTopLevelExportsPlugin({ entry: input })); + } + + const inject = []; + if (enableStackTraces) { + inject.push(resolve(__dirname, './rsrc/trace-mapping.inject.js')); + } + + await build({ conditions: ['fastly'], entryPoints: [input], bundle: true, - write: false, + write: true, + outfile: bundle, + sourcemap: 'external', + sourcesContent: true, format: moduleMode ? 'esm' : 'iife', tsconfig: undefined, - plugins: [fastlyPlugin], + plugins, + inject, }); + + await rename(bundle, outfile); + if (enableStackTraces) { + await rename(bundle + '.map', outfile + '.map'); + } } diff --git a/src/compileApplicationToWasm.js b/src/compileApplicationToWasm.js index 356e7d0cab..ea7e77a45d 100644 --- a/src/compileApplicationToWasm.js +++ b/src/compileApplicationToWasm.js @@ -1,14 +1,22 @@ import { dirname, resolve, sep, normalize } from 'node:path'; import { tmpdir, freemem } from 'node:os'; import { spawnSync } from 'node:child_process'; -import { mkdir, readFile, mkdtemp, writeFile } from 'node:fs/promises'; +import { + mkdir, + readFile, + mkdtemp, + writeFile, + copyFile, +} from 'node:fs/promises'; import { rmSync } from 'node:fs'; -import { isFile } from './isFile.js'; -import { isFileOrDoesNotExist } from './isFileOrDoesNotExist.js'; import wizer from '@bytecodealliance/wizer'; import weval from '@bytecodealliance/weval'; -import { precompile } from './precompile.js'; + +import { isFile } from './isFile.js'; +import { isFileOrDoesNotExist } from './isFileOrDoesNotExist.js'; +import { postbundle } from './postbundle.js'; import { bundle } from './bundle.js'; +import { composeSourcemaps } from './composeSourcemaps.js'; const maybeWindowsPath = process.platform === 'win32' @@ -21,7 +29,7 @@ async function getTmpDir() { return await mkdtemp(normalize(tmpdir() + sep)); } -export async function compileApplicationToWasm( +export async function compileApplicationToWasm({ input, output, wasmEngine, @@ -29,10 +37,13 @@ export async function compileApplicationToWasm( enableExperimentalHighResolutionTimeMethods = false, enableAOT = false, aotCache = '', + enableStackTraces, + excludeSources, + debugIntermediateFilesDir, moduleMode = false, doBundle = false, env, -) { +}) { try { if (!(await isFile(input))) { console.error( @@ -40,7 +51,7 @@ export async function compileApplicationToWasm( ); process.exit(1); } - } catch (error) { + } catch { console.error( `Error: The \`input\` path points to a non-existent file: ${input}`, ); @@ -64,7 +75,7 @@ export async function compileApplicationToWasm( ); process.exit(1); } - } catch (error) { + } catch { console.error( `Error: The \`wasmEngine\` path points to a non-existent file: ${wasmEngine}`, ); @@ -97,28 +108,137 @@ export async function compileApplicationToWasm( process.exit(1); } + if (debugIntermediateFilesDir != null) { + try { + console.log( + `Preparing \`debug-intermediate-files\` directory: ${debugIntermediateFilesDir}`, + ); + await mkdir(debugIntermediateFilesDir, { + recursive: true, + }); + } catch (error) { + console.error( + `Error: Failed to create the \`debug-intermediate-files\` (${debugIntermediateFilesDir}) directory`, + error.message, + ); + process.exit(1); + } + } + let tmpDir; if (doBundle) { - let contents; + tmpDir = await getTmpDir(); + + const sourceMaps = []; + + const bundleFilename = '__input_bundled.js'; + const bundleOutputFilePath = resolve(tmpDir, bundleFilename); + + // esbuild respects input source map, works if it's linked via sourceMappingURL + // either inline or as separate file try { - contents = await bundle(input, moduleMode); + await bundle(input, bundleOutputFilePath, { + moduleMode, + enableStackTraces, + }); } catch (error) { console.error(`Error:`, error.message); process.exit(1); } - const precompiled = precompile( - contents.outputFiles[0].text, - undefined, + if (debugIntermediateFilesDir != null) { + await copyFile( + bundleOutputFilePath, + resolve(debugIntermediateFilesDir, '__1_bundled.js'), + ); + if (enableStackTraces) { + await copyFile( + bundleOutputFilePath + '.map', + resolve(debugIntermediateFilesDir, '__1_bundled.js.map'), + ); + } + } + if (enableStackTraces) { + sourceMaps.push({ f: bundleFilename, s: bundleOutputFilePath + '.map' }); + } + + const postbundleFilename = '__fastly_post_bundle.js'; + const postbundleOutputFilepath = resolve(tmpDir, postbundleFilename); + + await postbundle(bundleOutputFilePath, postbundleOutputFilepath, { moduleMode, - ); + enableStackTraces, + }); - tmpDir = await getTmpDir(); - const outPath = resolve(tmpDir, 'input.js'); - await writeFile(outPath, precompiled); + if (debugIntermediateFilesDir != null) { + await copyFile( + postbundleOutputFilepath, + resolve(debugIntermediateFilesDir, '__2_postbundled.js'), + ); + if (enableStackTraces) { + await copyFile( + postbundleOutputFilepath + '.map', + resolve(debugIntermediateFilesDir, '__2_postbundled.js.map'), + ); + } + } + if (enableStackTraces) { + sourceMaps.push({ + f: postbundleFilename, + s: postbundleOutputFilepath + '.map', + }); + } - // the bundled output is now the Wizer input - input = outPath; + if (enableStackTraces) { + // Compose source maps + const replaceSourceMapToken = '__FINAL_SOURCE_MAP__'; + let excludePatterns = ['forbid-entry:/**', 'node_modules/**']; + if (excludeSources) { + excludePatterns = [() => true]; + } + const composed = await composeSourcemaps(sourceMaps, excludePatterns); + + const outputWithSourcemaps = '__fastly_bundle_with_sourcemaps.js'; + const outputWithSourcemapsFilePath = resolve( + tmpDir, + outputWithSourcemaps, + ); + + const postBundleContent = await readFile(postbundleOutputFilepath, { + encoding: 'utf-8', + }); + const outputWithSourcemapsContent = postBundleContent.replace( + replaceSourceMapToken, + () => JSON.stringify(composed), + ); + await writeFile( + outputWithSourcemapsFilePath, + outputWithSourcemapsContent, + ); + + if (debugIntermediateFilesDir != null) { + await copyFile( + outputWithSourcemapsFilePath, + resolve(debugIntermediateFilesDir, 'fastly_bundle.js'), + ); + await writeFile( + resolve(debugIntermediateFilesDir, 'fastly_sourcemaps.json'), + composed, + ); + } + + // the output with sourcemaps is now the Wizer input + input = outputWithSourcemapsFilePath; + } else { + // the bundled output is now the Wizer input + input = postbundleOutputFilepath; + if (debugIntermediateFilesDir != null) { + await copyFile( + postbundleOutputFilepath, + resolve(debugIntermediateFilesDir, 'fastly_bundle.js'), + ); + } + } } const spawnOpts = { diff --git a/src/composeSourcemaps.js b/src/composeSourcemaps.js new file mode 100644 index 0000000000..ee3f7895df --- /dev/null +++ b/src/composeSourcemaps.js @@ -0,0 +1,48 @@ +import { readFile } from 'node:fs/promises'; +import remapping from '@jridgewell/remapping'; +import { TraceMap } from '@jridgewell/trace-mapping'; +import picomatch from 'picomatch'; + +async function readSourcemap(e) { + const sourceMapJson = await readFile(e.s, { encoding: 'utf-8' }); + return JSON.parse(sourceMapJson); +} + +export async function composeSourcemaps(sourceMaps, excludePatterns = []) { + const top = new TraceMap(await readSourcemap(sourceMaps.pop())); + + const priors = {}; + for (const sourceMap of sourceMaps) { + priors[sourceMap.f] = await readSourcemap(sourceMap); + } + + // Loader: given a source name from mapXZ, return a TraceMap for that source (if any). + const loader = (source) => { + const m = priors[source]; + if (!m) return null; // no earlier map for this source + return new TraceMap(m); + }; + + const raw = JSON.parse(remapping(top, loader, false).toString()); + + return JSON.stringify(stripSourcesContent(raw, excludePatterns)); +} + +function stripSourcesContent(map, excludes) { + const matchers = excludes.map((p) => + typeof p === 'string' ? picomatch(p) : p, + ); + + for (let i = 0; i < map.sources.length; i++) { + const src = map.sources[i]; + + // [Windows] normalize slashes + const normalized = src.replace(/\\/g, '/'); + + if (matchers.some((fn) => fn(normalized))) { + map.sourcesContent[i] = null; + } + } + + return map; +} diff --git a/src/parseInputs.js b/src/parseInputs.js index f5027bf0aa..1f85670c90 100644 --- a/src/parseInputs.js +++ b/src/parseInputs.js @@ -19,6 +19,9 @@ export async function parseInputs(cliInputs) { let input = join(process.cwd(), 'bin/index.js'); let customOutputSet = false; let output = join(process.cwd(), 'bin/main.wasm'); + let enableStackTraces = false; + let excludeSources = false; + let debugIntermediateFilesDir = undefined; let cliInput; const envParser = new EnvParser(); @@ -120,6 +123,23 @@ export async function parseInputs(cliInputs) { } break; } + case '--enable-stack-traces': { + enableStackTraces = true; + break; + } + case '--exclude-sources': { + excludeSources = true; + break; + } + case '--debug-intermediate-files': { + const value = cliInputs.shift(); + if (isAbsolute(value)) { + debugIntermediateFilesDir = value; + } else { + debugIntermediateFilesDir = join(process.cwd(), value); + } + break; + } default: { if (cliInput.startsWith('--engine-wasm=')) { if (customEngineSet) { @@ -173,6 +193,9 @@ export async function parseInputs(cliInputs) { bundle, enableAOT, aotCache, + enableStackTraces, + excludeSources, + debugIntermediateFilesDir, input, output, wasmEngine, diff --git a/src/postbundle.js b/src/postbundle.js new file mode 100644 index 0000000000..86521129e7 --- /dev/null +++ b/src/postbundle.js @@ -0,0 +1,126 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { basename } from 'node:path'; +import regexpuc from 'regexpu-core'; +import { parse } from 'acorn'; +import MagicString from 'magic-string'; +import { simple as simpleWalk } from 'acorn-walk'; + +export async function postbundle( + input, + outfile, + { moduleMode = false, enableStackTraces = false }, +) { + const source = await readFile(input, { encoding: 'utf8' }); + const magicString = new MagicString(source); + + // PRECOMPILE REGEXES + // Emit a block of javascript that will pre-compile the regular expressions given. As spidermonkey + // will intern regular expressions, duplicating them at the top level and testing them with both + // an ascii and utf8 string should ensure that they won't be re-compiled when run in the fetch + // handler. + const PREAMBLE = `(function(){ + // Precompiled regular expressions + const precompile = (r) => { r.exec('a'); r.exec('\\u1000'); };`; + const POSTAMBLE = '})();'; + + const ast = parse(source, { + ecmaVersion: 'latest', + sourceType: moduleMode ? 'module' : 'script', + }); + + const precompileCalls = []; + simpleWalk(ast, { + Literal(node) { + if (!node.regex) return; + let transpiledPattern; + try { + transpiledPattern = regexpuc(node.regex.pattern, node.regex.flags, { + unicodePropertyEscapes: 'transform', + }); + } catch { + // swallow regex parse errors here to instead throw them at the engine level + // this then also avoids regex parser bugs being thrown unnecessarily + transpiledPattern = node.regex.pattern; + } + const transpiledRegex = `/${transpiledPattern}/${node.regex.flags}`; + precompileCalls.push(`precompile(${transpiledRegex});`); + magicString.overwrite(node.start, node.end, transpiledRegex); + }, + }); + + if (precompileCalls.length) { + magicString.prepend(`${PREAMBLE}${precompileCalls.join('')}${POSTAMBLE}\n`); + } + + if (enableStackTraces) { + // INSERT init guard + let initGuardPre, initGuardPost; + if (moduleMode) { + initGuardPre = `\ +await(async function __fastly_init_guard__() { + `; + initGuardPost = `\ +})().catch(e => { + console.error('Unhandled error while running top level module code'); + try { globalThis.__fastlyMapAndLogError(e); } catch { /* swallow */ } + console.error('Raw error below:'); + throw e; +}); +`; + } else { + initGuardPre = `\ +(function __fastly_init_guard__() { try { +`; + initGuardPost = `\ +} catch (e) { + console.error('Unhandled error while running top level script'); + try { globalThis.__fastlyMapAndLogError(e); } catch { /* swallow */ } + console.error('Raw error below:'); + throw e; +} +})(); +`; + } + + magicString.prepend(initGuardPre); + magicString.append(initGuardPost); + + // SOURCE MAPPING HEADER + const STACK_MAPPING_HEADER = `\ +globalThis.__FASTLY_SOURCE_MAP = JSON.parse(__FINAL_SOURCE_MAP__); +`; + magicString.prepend(STACK_MAPPING_HEADER); + } + + // MISC HEADER + const SOURCE_FILE_NAME = 'fastly:app.js'; + const STACK_MAPPING_HEADER = `\ +//# sourceURL=${SOURCE_FILE_NAME} +globalThis.__FASTLY_GEN_FILE = "${SOURCE_FILE_NAME}"; +globalThis.__orig_console_error = console.error.bind(console); +globalThis.__fastlyMapAndLogError = (e) => { + for (const line of globalThis.__fastlyMapError(e)) { + globalThis.__orig_console_error(line); + } +}; +globalThis.__fastlyMapError = (e) => { + return [ + '(Raw error) - build with --enable-stack-traces for mapped stack information.', + e, + ]; +}; +`; + magicString.prepend(STACK_MAPPING_HEADER); + + await writeFile(outfile, magicString.toString()); + + if (enableStackTraces) { + const map = magicString.generateMap({ + source: basename(input), + hires: true, + includeContent: true, + }); + + await writeFile(outfile + '.map', map.toString()); + } +} diff --git a/src/precompile.js b/src/precompile.js deleted file mode 100644 index dbf2aed128..0000000000 --- a/src/precompile.js +++ /dev/null @@ -1,57 +0,0 @@ -import regexpuc from 'regexpu-core'; -import { parse } from 'acorn'; -import MagicString from 'magic-string'; -import { simple as simpleWalk } from 'acorn-walk'; - -const PREAMBLE = `;{ - // Precompiled regular expressions - const precompile = (r) => { r.exec('a'); r.exec('\\u1000'); };`; -const POSTAMBLE = '}'; - -/// Emit a block of javascript that will pre-compile the regular expressions given. As spidermonkey -/// will intern regular expressions, duplicating them at the top level and testing them with both -/// an ascii and utf8 string should ensure that they won't be re-compiled when run in the fetch -/// handler. -export function precompile(source, filename = '', moduleMode = false) { - const magicString = new MagicString(source, { - filename, - }); - - const ast = parse(source, { - ecmaVersion: 'latest', - sourceType: moduleMode ? 'module' : 'script', - }); - - const precompileCalls = []; - simpleWalk(ast, { - Literal(node) { - if (!node.regex) return; - let transpiledPattern; - try { - transpiledPattern = regexpuc(node.regex.pattern, node.regex.flags, { - unicodePropertyEscapes: 'transform', - }); - } catch { - // swallow regex parse errors here to instead throw them at the engine level - // this then also avoids regex parser bugs being thrown unnecessarily - transpiledPattern = node.regex.pattern; - } - const transpiledRegex = `/${transpiledPattern}/${node.regex.flags}`; - precompileCalls.push(`precompile(${transpiledRegex});`); - magicString.overwrite(node.start, node.end, transpiledRegex); - }, - }); - - if (!precompileCalls.length) return source; - - magicString.prepend(`${PREAMBLE}${precompileCalls.join('\n')}${POSTAMBLE}`); - - // When we're ready to pipe in source maps: - // const map = magicString.generateMap({ - // source: 'source.js', - // file: 'converted.js.map', - // includeContent: true - // }); - - return magicString.toString(); -} diff --git a/src/printHelp.js b/src/printHelp.js index 565c3b26d0..52bfdf492d 100644 --- a/src/printHelp.js +++ b/src/printHelp.js @@ -25,6 +25,9 @@ OPTIONS: --enable-aot Enable AOT compilation for performance --enable-experimental-high-resolution-time-methods Enable experimental fastly.now() method --enable-experimental-top-level-await Enable experimental top level await + --enable-stack-traces Enable stack traces + --exclude-sources Don't include sources in stack traces + --debug-intermediate-files