From 462be8e962d15f1e26a3a734670fa3bbf9535204 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 15 May 2022 12:39:50 +0800 Subject: [PATCH] fix(glob): properly handles tailing comma (#8181) --- .../plugins/importGlob/parse.test.ts | 253 ++++++++++++++---- .../vite/src/node/plugins/importMetaGlob.ts | 38 ++- 2 files changed, 237 insertions(+), 54 deletions(-) diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts b/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts index df1e0d758e8849..aaa03ebef8f8ef 100644 --- a/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts +++ b/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts @@ -8,7 +8,11 @@ async function run(input: string) { process.cwd(), (id) => id ) - return items.map((i) => ({ globs: i.globs, options: i.options })) + return items.map((i) => ({ + globs: i.globs, + options: i.options, + start: i.start + })) } async function runError(input: string) { @@ -23,23 +27,44 @@ describe('parse positives', async () => { it('basic', async () => { expect( await run(` - import.meta.importGlob(\'./modules/*.ts\') + import.meta.glob(\'./modules/*.ts\') + `) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "./modules/*.ts", + ], + "options": {}, + "start": 5, + }, + ] `) - ).toMatchInlineSnapshot('[]') }) it('array', async () => { expect( await run(` - import.meta.importGlob([\'./modules/*.ts\', './dir/*.{js,ts}\']) + import.meta.glob([\'./modules/*.ts\', './dir/*.{js,ts}\']) + `) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "./modules/*.ts", + "./dir/*.{js,ts}", + ], + "options": {}, + "start": 5, + }, + ] `) - ).toMatchInlineSnapshot('[]') }) it('options with multilines', async () => { expect( await run(` - import.meta.importGlob([ + import.meta.glob([ \'./modules/*.ts\', "!./dir/*.{js,ts}" ], { @@ -47,7 +72,21 @@ describe('parse positives', async () => { import: 'named' }) `) - ).toMatchInlineSnapshot('[]') + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "./modules/*.ts", + "!./dir/*.{js,ts}", + ], + "options": { + "eager": true, + "import": "named", + }, + "start": 5, + }, + ] + `) }) it('options with multilines', async () => { @@ -68,6 +107,7 @@ describe('parse positives', async () => { "/dir/**", ], "options": {}, + "start": 21, }, ] `) @@ -98,6 +138,99 @@ describe('parse positives', async () => { "raw": true, }, }, + "start": 21, + }, + ] + `) + }) + + it('object properties - 1', async () => { + expect( + await run(` + export const pageFiles = { + '.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])') +};`) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "/**/*.page.*([a-zA-Z0-9])", + ], + "options": {}, + "start": 47, + }, + ] +`) + }) + + it('object properties - 2', async () => { + expect( + await run(` + export const pageFiles = { + '.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])'), +};`) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "/**/*.page.*([a-zA-Z0-9])", + ], + "options": {}, + "start": 47, + }, + ] +`) + }) + + it('object properties - 3', async () => { + expect( + await run(` + export const pageFiles = { + '.page.client': import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'), + '.page.server': import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'), +};`) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "/**/*.page.client.*([a-zA-Z0-9])", + ], + "options": {}, + "start": 54, + }, + { + "globs": [ + "/**/*.page.server.*([a-zA-Z0-9])", + ], + "options": {}, + "start": 130, + }, + ] +`) + }) + + it('array item', async () => { + expect( + await run(` + export const pageFiles = [ + import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'), + import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'), + ]`) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "/**/*.page.client.*([a-zA-Z0-9])", + ], + "options": {}, + "start": 38, + }, + { + "globs": [ + "/**/*.page.server.*([a-zA-Z0-9])", + ], + "options": {}, + "start": 98, }, ] `) @@ -106,97 +239,117 @@ describe('parse positives', async () => { describe('parse negatives', async () => { it('syntax error', async () => { - expect(await runError('import.meta.importGlob(')).toMatchInlineSnapshot( - 'undefined' + expect(await runError('import.meta.glob(')).toMatchInlineSnapshot( + '[SyntaxError: Unexpected token (1:17)]' ) }) it('empty', async () => { - expect(await runError('import.meta.importGlob()')).toMatchInlineSnapshot( - 'undefined' + expect(await runError('import.meta.glob()')).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 0]' ) }) it('3 args', async () => { expect( - await runError('import.meta.importGlob("", {}, {})') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("", {}, {})') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 3]' + ) }) it('in string', async () => { - expect(await runError('"import.meta.importGlob()"')).toBeUndefined() + expect(await runError('"import.meta.glob()"')).toBeUndefined() }) it('variable', async () => { - expect(await runError('import.meta.importGlob(hey)')).toMatchInlineSnapshot( - 'undefined' + expect(await runError('import.meta.glob(hey)')).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' ) }) it('template', async () => { // eslint-disable-next-line no-template-curly-in-string expect( - await runError('import.meta.importGlob(`hi ${hey}`)') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob(`hi ${hey}`)') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) }) it('be string', async () => { - expect(await runError('import.meta.importGlob(1)')).toMatchInlineSnapshot( - 'undefined' + expect(await runError('import.meta.glob(1)')).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected glob to be a string, but got "number"]' ) }) it('be array variable', async () => { + expect(await runError('import.meta.glob([hey])')).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( - await runError('import.meta.importGlob([hey])') - ).toMatchInlineSnapshot('undefined') - expect( - await runError('import.meta.importGlob(["1", hey])') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob(["1", hey])') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) }) it('options', async () => { expect( - await runError('import.meta.importGlob("hey", hey)') - ).toMatchInlineSnapshot('undefined') - expect( - await runError('import.meta.importGlob("hey", [])') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("hey", hey)') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected the second argument o to be a object literal, but got "Identifier"]' + ) + expect(await runError('import.meta.glob("hey", [])')).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected the second argument o to be a object literal, but got "ArrayExpression"]' + ) }) it('options props', async () => { expect( - await runError('import.meta.importGlob("hey", { hey: 1 })') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("hey", { hey: 1 })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Unknown options hey]' + ) expect( - await runError('import.meta.importGlob("hey", { import: hey })') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("hey", { import: hey })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( - await runError('import.meta.importGlob("hey", { eager: 123 })') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("hey", { eager: 123 })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected the type of option "eager" to be "boolean", but got "number"]' + ) }) it('options query', async () => { expect( - await runError( - 'import.meta.importGlob("./*.js", { as: "raw", query: "hi" })' - ) - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("./*.js", { as: "raw", query: "hi" })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Options "as" and "query" cannot be used together]' + ) expect( - await runError('import.meta.importGlob("./*.js", { query: 123 })') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("./*.js", { query: 123 })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected query to be a string, but got "number"]' + ) expect( - await runError('import.meta.importGlob("./*.js", { query: { foo: {} } })') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("./*.js", { query: { foo: {} } })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( - await runError( - 'import.meta.importGlob("./*.js", { query: { foo: hey } })' - ) - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob("./*.js", { query: { foo: hey } })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( await runError( - 'import.meta.importGlob("./*.js", { query: { foo: 123, ...a } })' + 'import.meta.glob("./*.js", { query: { foo: 123, ...a } })' ) - ).toMatchInlineSnapshot('undefined') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) }) }) diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 3282014d2ffca3..5564bedefb8eac 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -1,7 +1,13 @@ import { isAbsolute, posix } from 'path' import { isMatch, scan } from 'micromatch' import { stripLiteral } from 'strip-literal' -import type { ArrayExpression, CallExpression, Literal, Node } from 'estree' +import type { + ArrayExpression, + CallExpression, + Literal, + Node, + SequenceExpression +} from 'estree' import { parseExpressionAt } from 'acorn' import MagicString from 'magic-string' import fg from 'fast-glob' @@ -111,21 +117,45 @@ export async function parseImportGlob( return e } - let ast: CallExpression + let ast: CallExpression | SequenceExpression + let lastTokenPos: number | undefined try { ast = parseExpressionAt(code, start, { ecmaVersion: 'latest', sourceType: 'module', - ranges: true + ranges: true, + onToken: (token) => { + lastTokenPos = token.end + } }) as any } catch (e) { const _e = e as any if (_e.message && _e.message.startsWith('Unterminated string constant')) return undefined! - throw _e + if (lastTokenPos == null || lastTokenPos <= start) throw _e + + // tailing comma in object or array will make the parser think it's a comma operation + // we try to parse again removing the comma + try { + const statement = code.slice(start, lastTokenPos).replace(/[,\s]*$/, '') + ast = parseExpressionAt( + ' '.repeat(start) + statement, // to keep the ast position + start, + { + ecmaVersion: 'latest', + sourceType: 'module', + ranges: true + } + ) as any + } catch { + throw _e + } } + if (ast.type === 'SequenceExpression') + ast = ast.expressions[0] as CallExpression + if (ast.type !== 'CallExpression') throw err(`Expect CallExpression, got ${ast.type}`)