Skip to content

Commit 462be8e

Browse files
authoredMay 15, 2022
fix(glob): properly handles tailing comma (#8181)
1 parent 7752b56 commit 462be8e

File tree

2 files changed

+237
-54
lines changed

2 files changed

+237
-54
lines changed
 

‎packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts

+203-50
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ async function run(input: string) {
88
process.cwd(),
99
(id) => id
1010
)
11-
return items.map((i) => ({ globs: i.globs, options: i.options }))
11+
return items.map((i) => ({
12+
globs: i.globs,
13+
options: i.options,
14+
start: i.start
15+
}))
1216
}
1317

1418
async function runError(input: string) {
@@ -23,31 +27,66 @@ describe('parse positives', async () => {
2327
it('basic', async () => {
2428
expect(
2529
await run(`
26-
import.meta.importGlob(\'./modules/*.ts\')
30+
import.meta.glob(\'./modules/*.ts\')
31+
`)
32+
).toMatchInlineSnapshot(`
33+
[
34+
{
35+
"globs": [
36+
"./modules/*.ts",
37+
],
38+
"options": {},
39+
"start": 5,
40+
},
41+
]
2742
`)
28-
).toMatchInlineSnapshot('[]')
2943
})
3044

3145
it('array', async () => {
3246
expect(
3347
await run(`
34-
import.meta.importGlob([\'./modules/*.ts\', './dir/*.{js,ts}\'])
48+
import.meta.glob([\'./modules/*.ts\', './dir/*.{js,ts}\'])
49+
`)
50+
).toMatchInlineSnapshot(`
51+
[
52+
{
53+
"globs": [
54+
"./modules/*.ts",
55+
"./dir/*.{js,ts}",
56+
],
57+
"options": {},
58+
"start": 5,
59+
},
60+
]
3561
`)
36-
).toMatchInlineSnapshot('[]')
3762
})
3863

3964
it('options with multilines', async () => {
4065
expect(
4166
await run(`
42-
import.meta.importGlob([
67+
import.meta.glob([
4368
\'./modules/*.ts\',
4469
"!./dir/*.{js,ts}"
4570
], {
4671
eager: true,
4772
import: 'named'
4873
})
4974
`)
50-
).toMatchInlineSnapshot('[]')
75+
).toMatchInlineSnapshot(`
76+
[
77+
{
78+
"globs": [
79+
"./modules/*.ts",
80+
"!./dir/*.{js,ts}",
81+
],
82+
"options": {
83+
"eager": true,
84+
"import": "named",
85+
},
86+
"start": 5,
87+
},
88+
]
89+
`)
5190
})
5291

5392
it('options with multilines', async () => {
@@ -68,6 +107,7 @@ describe('parse positives', async () => {
68107
"/dir/**",
69108
],
70109
"options": {},
110+
"start": 21,
71111
},
72112
]
73113
`)
@@ -98,6 +138,99 @@ describe('parse positives', async () => {
98138
"raw": true,
99139
},
100140
},
141+
"start": 21,
142+
},
143+
]
144+
`)
145+
})
146+
147+
it('object properties - 1', async () => {
148+
expect(
149+
await run(`
150+
export const pageFiles = {
151+
'.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])')
152+
};`)
153+
).toMatchInlineSnapshot(`
154+
[
155+
{
156+
"globs": [
157+
"/**/*.page.*([a-zA-Z0-9])",
158+
],
159+
"options": {},
160+
"start": 47,
161+
},
162+
]
163+
`)
164+
})
165+
166+
it('object properties - 2', async () => {
167+
expect(
168+
await run(`
169+
export const pageFiles = {
170+
'.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])'),
171+
};`)
172+
).toMatchInlineSnapshot(`
173+
[
174+
{
175+
"globs": [
176+
"/**/*.page.*([a-zA-Z0-9])",
177+
],
178+
"options": {},
179+
"start": 47,
180+
},
181+
]
182+
`)
183+
})
184+
185+
it('object properties - 3', async () => {
186+
expect(
187+
await run(`
188+
export const pageFiles = {
189+
'.page.client': import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'),
190+
'.page.server': import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'),
191+
};`)
192+
).toMatchInlineSnapshot(`
193+
[
194+
{
195+
"globs": [
196+
"/**/*.page.client.*([a-zA-Z0-9])",
197+
],
198+
"options": {},
199+
"start": 54,
200+
},
201+
{
202+
"globs": [
203+
"/**/*.page.server.*([a-zA-Z0-9])",
204+
],
205+
"options": {},
206+
"start": 130,
207+
},
208+
]
209+
`)
210+
})
211+
212+
it('array item', async () => {
213+
expect(
214+
await run(`
215+
export const pageFiles = [
216+
import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'),
217+
import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'),
218+
]`)
219+
).toMatchInlineSnapshot(`
220+
[
221+
{
222+
"globs": [
223+
"/**/*.page.client.*([a-zA-Z0-9])",
224+
],
225+
"options": {},
226+
"start": 38,
227+
},
228+
{
229+
"globs": [
230+
"/**/*.page.server.*([a-zA-Z0-9])",
231+
],
232+
"options": {},
233+
"start": 98,
101234
},
102235
]
103236
`)
@@ -106,97 +239,117 @@ describe('parse positives', async () => {
106239

107240
describe('parse negatives', async () => {
108241
it('syntax error', async () => {
109-
expect(await runError('import.meta.importGlob(')).toMatchInlineSnapshot(
110-
'undefined'
242+
expect(await runError('import.meta.glob(')).toMatchInlineSnapshot(
243+
'[SyntaxError: Unexpected token (1:17)]'
111244
)
112245
})
113246

114247
it('empty', async () => {
115-
expect(await runError('import.meta.importGlob()')).toMatchInlineSnapshot(
116-
'undefined'
248+
expect(await runError('import.meta.glob()')).toMatchInlineSnapshot(
249+
'[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 0]'
117250
)
118251
})
119252

120253
it('3 args', async () => {
121254
expect(
122-
await runError('import.meta.importGlob("", {}, {})')
123-
).toMatchInlineSnapshot('undefined')
255+
await runError('import.meta.glob("", {}, {})')
256+
).toMatchInlineSnapshot(
257+
'[Error: Invalid glob import syntax: Expected 1-2 arguments, but got 3]'
258+
)
124259
})
125260

126261
it('in string', async () => {
127-
expect(await runError('"import.meta.importGlob()"')).toBeUndefined()
262+
expect(await runError('"import.meta.glob()"')).toBeUndefined()
128263
})
129264

130265
it('variable', async () => {
131-
expect(await runError('import.meta.importGlob(hey)')).toMatchInlineSnapshot(
132-
'undefined'
266+
expect(await runError('import.meta.glob(hey)')).toMatchInlineSnapshot(
267+
'[Error: Invalid glob import syntax: Could only use literals]'
133268
)
134269
})
135270

136271
it('template', async () => {
137272
// eslint-disable-next-line no-template-curly-in-string
138273
expect(
139-
await runError('import.meta.importGlob(`hi ${hey}`)')
140-
).toMatchInlineSnapshot('undefined')
274+
await runError('import.meta.glob(`hi ${hey}`)')
275+
).toMatchInlineSnapshot(
276+
'[Error: Invalid glob import syntax: Could only use literals]'
277+
)
141278
})
142279

143280
it('be string', async () => {
144-
expect(await runError('import.meta.importGlob(1)')).toMatchInlineSnapshot(
145-
'undefined'
281+
expect(await runError('import.meta.glob(1)')).toMatchInlineSnapshot(
282+
'[Error: Invalid glob import syntax: Expected glob to be a string, but got "number"]'
146283
)
147284
})
148285

149286
it('be array variable', async () => {
287+
expect(await runError('import.meta.glob([hey])')).toMatchInlineSnapshot(
288+
'[Error: Invalid glob import syntax: Could only use literals]'
289+
)
150290
expect(
151-
await runError('import.meta.importGlob([hey])')
152-
).toMatchInlineSnapshot('undefined')
153-
expect(
154-
await runError('import.meta.importGlob(["1", hey])')
155-
).toMatchInlineSnapshot('undefined')
291+
await runError('import.meta.glob(["1", hey])')
292+
).toMatchInlineSnapshot(
293+
'[Error: Invalid glob import syntax: Could only use literals]'
294+
)
156295
})
157296

158297
it('options', async () => {
159298
expect(
160-
await runError('import.meta.importGlob("hey", hey)')
161-
).toMatchInlineSnapshot('undefined')
162-
expect(
163-
await runError('import.meta.importGlob("hey", [])')
164-
).toMatchInlineSnapshot('undefined')
299+
await runError('import.meta.glob("hey", hey)')
300+
).toMatchInlineSnapshot(
301+
'[Error: Invalid glob import syntax: Expected the second argument o to be a object literal, but got "Identifier"]'
302+
)
303+
expect(await runError('import.meta.glob("hey", [])')).toMatchInlineSnapshot(
304+
'[Error: Invalid glob import syntax: Expected the second argument o to be a object literal, but got "ArrayExpression"]'
305+
)
165306
})
166307

167308
it('options props', async () => {
168309
expect(
169-
await runError('import.meta.importGlob("hey", { hey: 1 })')
170-
).toMatchInlineSnapshot('undefined')
310+
await runError('import.meta.glob("hey", { hey: 1 })')
311+
).toMatchInlineSnapshot(
312+
'[Error: Invalid glob import syntax: Unknown options hey]'
313+
)
171314
expect(
172-
await runError('import.meta.importGlob("hey", { import: hey })')
173-
).toMatchInlineSnapshot('undefined')
315+
await runError('import.meta.glob("hey", { import: hey })')
316+
).toMatchInlineSnapshot(
317+
'[Error: Invalid glob import syntax: Could only use literals]'
318+
)
174319
expect(
175-
await runError('import.meta.importGlob("hey", { eager: 123 })')
176-
).toMatchInlineSnapshot('undefined')
320+
await runError('import.meta.glob("hey", { eager: 123 })')
321+
).toMatchInlineSnapshot(
322+
'[Error: Invalid glob import syntax: Expected the type of option "eager" to be "boolean", but got "number"]'
323+
)
177324
})
178325

179326
it('options query', async () => {
180327
expect(
181-
await runError(
182-
'import.meta.importGlob("./*.js", { as: "raw", query: "hi" })'
183-
)
184-
).toMatchInlineSnapshot('undefined')
328+
await runError('import.meta.glob("./*.js", { as: "raw", query: "hi" })')
329+
).toMatchInlineSnapshot(
330+
'[Error: Invalid glob import syntax: Options "as" and "query" cannot be used together]'
331+
)
185332
expect(
186-
await runError('import.meta.importGlob("./*.js", { query: 123 })')
187-
).toMatchInlineSnapshot('undefined')
333+
await runError('import.meta.glob("./*.js", { query: 123 })')
334+
).toMatchInlineSnapshot(
335+
'[Error: Invalid glob import syntax: Expected query to be a string, but got "number"]'
336+
)
188337
expect(
189-
await runError('import.meta.importGlob("./*.js", { query: { foo: {} } })')
190-
).toMatchInlineSnapshot('undefined')
338+
await runError('import.meta.glob("./*.js", { query: { foo: {} } })')
339+
).toMatchInlineSnapshot(
340+
'[Error: Invalid glob import syntax: Could only use literals]'
341+
)
191342
expect(
192-
await runError(
193-
'import.meta.importGlob("./*.js", { query: { foo: hey } })'
194-
)
195-
).toMatchInlineSnapshot('undefined')
343+
await runError('import.meta.glob("./*.js", { query: { foo: hey } })')
344+
).toMatchInlineSnapshot(
345+
'[Error: Invalid glob import syntax: Could only use literals]'
346+
)
196347
expect(
197348
await runError(
198-
'import.meta.importGlob("./*.js", { query: { foo: 123, ...a } })'
349+
'import.meta.glob("./*.js", { query: { foo: 123, ...a } })'
199350
)
200-
).toMatchInlineSnapshot('undefined')
351+
).toMatchInlineSnapshot(
352+
'[Error: Invalid glob import syntax: Could only use literals]'
353+
)
201354
})
202355
})

‎packages/vite/src/node/plugins/importMetaGlob.ts

+34-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { isAbsolute, posix } from 'path'
22
import { isMatch, scan } from 'micromatch'
33
import { stripLiteral } from 'strip-literal'
4-
import type { ArrayExpression, CallExpression, Literal, Node } from 'estree'
4+
import type {
5+
ArrayExpression,
6+
CallExpression,
7+
Literal,
8+
Node,
9+
SequenceExpression
10+
} from 'estree'
511
import { parseExpressionAt } from 'acorn'
612
import MagicString from 'magic-string'
713
import fg from 'fast-glob'
@@ -111,21 +117,45 @@ export async function parseImportGlob(
111117
return e
112118
}
113119

114-
let ast: CallExpression
120+
let ast: CallExpression | SequenceExpression
121+
let lastTokenPos: number | undefined
115122

116123
try {
117124
ast = parseExpressionAt(code, start, {
118125
ecmaVersion: 'latest',
119126
sourceType: 'module',
120-
ranges: true
127+
ranges: true,
128+
onToken: (token) => {
129+
lastTokenPos = token.end
130+
}
121131
}) as any
122132
} catch (e) {
123133
const _e = e as any
124134
if (_e.message && _e.message.startsWith('Unterminated string constant'))
125135
return undefined!
126-
throw _e
136+
if (lastTokenPos == null || lastTokenPos <= start) throw _e
137+
138+
// tailing comma in object or array will make the parser think it's a comma operation
139+
// we try to parse again removing the comma
140+
try {
141+
const statement = code.slice(start, lastTokenPos).replace(/[,\s]*$/, '')
142+
ast = parseExpressionAt(
143+
' '.repeat(start) + statement, // to keep the ast position
144+
start,
145+
{
146+
ecmaVersion: 'latest',
147+
sourceType: 'module',
148+
ranges: true
149+
}
150+
) as any
151+
} catch {
152+
throw _e
153+
}
127154
}
128155

156+
if (ast.type === 'SequenceExpression')
157+
ast = ast.expressions[0] as CallExpression
158+
129159
if (ast.type !== 'CallExpression')
130160
throw err(`Expect CallExpression, got ${ast.type}`)
131161

0 commit comments

Comments
 (0)
Please sign in to comment.