From 12d02c4b1b85a02fc2681701758a448c276a1e1b Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 15 May 2022 10:05:21 +0800 Subject: [PATCH 1/2] fix(glob): properly handles tailing comma --- .../plugins/importGlob/parse.test.ts | 210 ++++++++++++++---- .../vite/src/node/plugins/importMetaGlob.ts | 37 ++- 2 files changed, 198 insertions(+), 49 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..1cf2c3870adea7 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,7 @@ 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 +23,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 +68,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 +103,7 @@ describe('parse positives', async () => { "/dir/**", ], "options": {}, + "start": 21, }, ] `) @@ -98,6 +134,92 @@ 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 +228,97 @@ 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.importGlob([hey])') - ).toMatchInlineSnapshot('undefined') + await runError('import.meta.glob([hey])') + ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') 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') + 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.importGlob("hey", [])') - ).toMatchInlineSnapshot('undefined') + 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" })' + 'import.meta.glob("./*.js", { as: "raw", query: "hi" })' ) - ).toMatchInlineSnapshot('undefined') + ).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 } })' + 'import.meta.glob("./*.js", { query: { foo: hey } })' ) - ).toMatchInlineSnapshot('undefined') + ).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..d70c452f98e238 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -1,7 +1,7 @@ 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 +111,48 @@ 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) { + } + 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}`) From facf831a99804f9ddad38bf886b1ed36586b3383 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sun, 15 May 2022 10:09:41 +0800 Subject: [PATCH 2/2] chore: lint --- .../plugins/importGlob/parse.test.ts | 99 ++++++++++++------- .../vite/src/node/plugins/importMetaGlob.ts | 27 ++--- 2 files changed, 80 insertions(+), 46 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 1cf2c3870adea7..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, start:i.start })) + return items.map((i) => ({ + globs: i.globs, + options: i.options, + start: i.start + })) } async function runError(input: string) { @@ -140,12 +144,13 @@ describe('parse positives', async () => { `) }) - it('object properties - 1', async () => { - expect(await run(` + expect( + await run(` export const pageFiles = { '.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])') -};`)).toMatchInlineSnapshot(` +};`) + ).toMatchInlineSnapshot(` [ { "globs": [ @@ -159,10 +164,12 @@ describe('parse positives', async () => { }) it('object properties - 2', async () => { - expect(await run(` + expect( + await run(` export const pageFiles = { '.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])'), -};`)).toMatchInlineSnapshot(` +};`) + ).toMatchInlineSnapshot(` [ { "globs": [ @@ -176,11 +183,13 @@ describe('parse positives', async () => { }) it('object properties - 3', async () => { - expect(await run(` + 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(` +};`) + ).toMatchInlineSnapshot(` [ { "globs": [ @@ -201,11 +210,13 @@ describe('parse positives', async () => { }) it('array item', async () => { - expect(await run(` + 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(` + ]`) + ).toMatchInlineSnapshot(` [ { "globs": [ @@ -242,7 +253,9 @@ describe('parse negatives', async () => { it('3 args', async () => { expect( await runError('import.meta.glob("", {}, {})') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 3]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 3]' + ) }) it('in string', async () => { @@ -259,7 +272,9 @@ describe('parse negatives', async () => { // eslint-disable-next-line no-template-curly-in-string expect( await runError('import.meta.glob(`hi ${hey}`)') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) }) it('be string', async () => { @@ -269,56 +284,72 @@ describe('parse negatives', async () => { }) 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.glob([hey])')).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( await runError('import.meta.glob(["1", hey])') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) }) it('options', async () => { expect( 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"]') + ).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.glob("hey", { hey: 1 })') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Unknown options hey]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Unknown options hey]' + ) expect( await runError('import.meta.glob("hey", { import: hey })') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( 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"]') + ).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.glob("./*.js", { as: "raw", query: "hi" })' - ) - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Options "as" and "query" cannot be used together]') + 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.glob("./*.js", { query: 123 })') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Expected query to be a string, but got "number"]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Expected query to be a string, but got "number"]' + ) expect( await runError('import.meta.glob("./*.js", { query: { foo: {} } })') - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( - await runError( - 'import.meta.glob("./*.js", { query: { foo: hey } })' - ) - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') + await runError('import.meta.glob("./*.js", { query: { foo: hey } })') + ).toMatchInlineSnapshot( + '[Error: Invalid glob import syntax: Could only use literals]' + ) expect( await runError( 'import.meta.glob("./*.js", { query: { foo: 123, ...a } })' ) - ).toMatchInlineSnapshot('[Error: Invalid glob import syntax: Could only use literals]') + ).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 d70c452f98e238..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, SequenceExpression } 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' @@ -121,31 +127,28 @@ export async function parseImportGlob( ranges: true, onToken: (token) => { lastTokenPos = token.end - }, + } }) as any - } - catch (e) { + } catch (e) { const _e = e as any if (_e.message && _e.message.startsWith('Unterminated string constant')) return undefined! - if (lastTokenPos == null || lastTokenPos <= start) - 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]*$/, '') + 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 { + ranges: true + } + ) as any + } catch { throw _e } }