diff --git a/src/content/content-types.yml b/src/content/content-types.yml index fa95d1a..0664e21 100644 --- a/src/content/content-types.yml +++ b/src/content/content-types.yml @@ -1762,7 +1762,7 @@ body: - p: 'Loader: `empty`' - p: > - This loader tells tells esbuild to pretend that a file is empty. It can be + This loader tells esbuild to pretend that a file is empty. It can be a helpful way to remove content from your bundle in certain situations. For example, you can configure `.css` files to load with `empty` to prevent esbuild from bundling CSS files that are imported into JavaScript files: diff --git a/src/try/ipc.ts b/src/try/ipc.ts index b6d7ae3..622184a 100644 --- a/src/try/ipc.ts +++ b/src/try/ipc.ts @@ -160,7 +160,7 @@ async function reloadWorker(version: Version): Promise { } export function sendIPC(message: IPCRequest): Promise { - function activateTask(worker: Worker, task: Task): void { + const activateTask = (worker: Worker, task: Task): void => { if (activeTask) { if (pendingTask) pendingTask.abort_() pendingTask = task @@ -183,9 +183,22 @@ export function sendIPC(message: IPCRequest): Promise { return new Promise((resolve, reject) => { workerPromise.then(worker => activateTask(worker, { - message_: message, + message_: serializeFunctions(message), resolve_: resolve, abort_: () => reject(new Error('Task aborted')), }), reject) }) } + +// Hack: Serialize "Function" objects as "EvalError" objects instead +const serializeFunctions = (value: any): any => { + if (typeof value === 'function') { + const text = value + '' + return new EvalError('function ' + value.name + text.slice(text.indexOf('('))) + } + if (typeof value === 'object' && value) { + if (Array.isArray(value)) return value.map(serializeFunctions) + else return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, serializeFunctions(v)])) + } + return value +} diff --git a/src/try/options.ts b/src/try/options.ts index c4397f0..8d1d87a 100644 --- a/src/try/options.ts +++ b/src/try/options.ts @@ -7,7 +7,7 @@ import { Mode } from './mode' import { RichError } from './output' const enum Kind { - // These must be in the order "-,:[]{}+" + // These must be in the order "-,:[]{}()+" Minus, Comma, Colon, @@ -15,14 +15,23 @@ const enum Kind { CloseBracket, OpenBrace, CloseBrace, + OpenParen, + CloseParen, Plus, // The order doesn't matter for these Identifier, + Function, Literal, String, } +const enum Flags { + None = 0, + KeywordsAreIdentifiers = 1 << 0, + ExpectEndOfFile = 1 << 1, +} + interface Token { line_: number column_: number @@ -390,7 +399,7 @@ function parseOptionsAsLooseJSON(input: string): Record { token.line_, token.column_ + token.text_.length, 0, '', 0, 0, 0, expected) } - const nextToken = (expectEndOfFile = false): void => { + const nextToken = (flags = Flags.None): void => { while (i < n) { const tokenLine = line const tokenColumn = i - lineStart @@ -472,12 +481,12 @@ function parseOptionsAsLooseJSON(input: string): Record { } // End of file - if (expectEndOfFile) { + if (flags & Flags.ExpectEndOfFile) { throwRichError(input, `Expected end of file after ${where}`, line, i - lineStart, 0) } // Punctuation - const index = '-,:[]{}+'.indexOf(c) + const index = '-,:[]{}()+'.indexOf(c) if (index >= 0) { i++ token = { line_: tokenLine, column_: tokenColumn, kind_: index as Kind, text_: c, value_: c } @@ -510,12 +519,14 @@ function parseOptionsAsLooseJSON(input: string): Record { const text = input.slice(start, i) let kind = Kind.Literal let value: any = text - if (text === 'null') value = null + if (flags & Flags.KeywordsAreIdentifiers) kind = Kind.Identifier + else if (text === 'null') value = null else if (text === 'true') value = true else if (text === 'false') value = false else if (text === 'undefined') value = undefined else if (text === 'Infinity') value = Infinity else if (text === 'NaN') value = NaN + else if (text === 'function') kind = Kind.Function else kind = Kind.Identifier token = { line_: tokenLine, column_: tokenColumn, kind_: kind, text_: text, value_: value } return @@ -549,17 +560,36 @@ function parseOptionsAsLooseJSON(input: string): Record { throwRichError(input, `Unexpected ${JSON.stringify(c)} in ${where}`, line, i - lineStart, 1) } - if (!expectEndOfFile) { + if (!(flags & Flags.ExpectEndOfFile)) { throwRichError(input, `Unexpected end of file in ${where}`, line, i - lineStart, 0) } } + // Hack: Parse function literals by repeatedly using the browser's parser after each '}' character + const scanForFunctionLiteral = (name: string, from: number): Function => { + let closeBrace = /\}/g + let error = '' + closeBrace.lastIndex = from + + for (let match: RegExpExecArray | null; match = closeBrace.exec(input);) { + try { + const code = new Function('return {' + name + input.slice(from, match.index + 1) + '}.' + name) + i = match.index + 1 + return code() + } catch (err) { + error = ': ' + err.message + } + } + + throwRichError(input, 'Invalid function literal' + error, token.line_, token.column_, token.text_.length) + } + const parseExpression = (): any => { if (token.kind_ as number === Kind.OpenBrace) { const object: Record = Object.create(null) const originals: Record = Object.create(null) while (true) { - nextToken() + nextToken(Flags.KeywordsAreIdentifiers) if (token.kind_ === Kind.CloseBrace) break if (token.kind_ !== Kind.String && token.kind_ !== Kind.Identifier) throwUnexpectedToken() const original = originals[token.value_] @@ -568,10 +598,17 @@ function parseOptionsAsLooseJSON(input: string): Record { `The original key ${JSON.stringify(token.value_)} is here:`, original.line_, original.column_, original.text_.length) } const key = token + const endOfKey = i + let value: any nextToken() - if (token.kind_ !== Kind.Colon) throwExpectedAfter(key, ':', 'property ' + JSON.stringify(key.value_)) - nextToken() - object[key.value_] = parseExpression() + if (token.kind_ === Kind.OpenParen) value = scanForFunctionLiteral(key.value_, endOfKey) + else { + if (token.kind_ !== Kind.Colon) throwExpectedAfter(key, ':', 'property ' + JSON.stringify(key.value_)) + nextToken() + if (token.kind_ === Kind.Function) value = scanForFunctionLiteral(key.value_, endOfKey) + else value = parseExpression() + } + object[key.value_] = value originals[key.value_] = key const beforeComma = token nextToken() @@ -617,7 +654,7 @@ function parseOptionsAsLooseJSON(input: string): Record { nextToken() const root = parseExpression() - nextToken(true) + nextToken(Flags.ExpectEndOfFile) return root } diff --git a/src/try/worker.ts b/src/try/worker.ts index 089b93f..9baaf97 100644 --- a/src/try/worker.ts +++ b/src/try/worker.ts @@ -115,6 +115,16 @@ const formatMessages = (api: API, messages: Message[], options: FormatMessagesOp })) } +// Hack: Deserialize "EvalError" objects as "Function" objects instead +const deserializeFunctions = (value: any): any => { + if (typeof value === 'object' && value) { + if (value instanceof EvalError) return new Function('return ' + value.message)() + else if (Array.isArray(value)) return value.map(deserializeFunctions) + else return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deserializeFunctions(v)])) + } + return value +} + onmessage = e => { setup(e.data).then(api => { onmessage = e => { @@ -167,7 +177,7 @@ onmessage = e => { } } - const request: IPCRequest = e.data + const request: IPCRequest = deserializeFunctions(e.data) const respond: (response: IPCResponse) => void = postMessage let color = true @@ -197,7 +207,7 @@ onmessage = e => { api.build(request.options_).then( ({ warnings, outputFiles, metafile, mangleCache }) => finish(warnings, (stderr: string) => respond({ - outputFiles_: outputFiles.map(({ path, text }) => ({ path, text })), + outputFiles_: outputFiles, metafile_: metafile, mangleCache_: mangleCache, stderr_: stderr,