diff --git a/docs/guide/features.md b/docs/guide/features.md index 1a8c03bbd0be22..01798ad0d4ea56 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -282,10 +282,10 @@ for (const path in modules) { } ``` -Matched files are by default lazy loaded via dynamic import and will be split into separate chunks during build. If you'd rather import all the modules directly (e.g. relying on side-effects in these modules to be applied first), you can use `import.meta.globEager` instead: +Matched files are by default lazy-loaded via dynamic import and will be split into separate chunks during build. If you'd rather import all the modules directly (e.g. relying on side-effects in these modules to be applied first), you can pass `{ eager: true }` as the second argument: ```js -const modules = import.meta.globEager('./dir/*.js') +const modules = import.meta.glob('./dir/*.js', { eager: true }) ``` The above will be transformed into the following: @@ -300,7 +300,9 @@ const modules = { } ``` -`import.meta.glob` and `import.meta.globEager` also support importing files as strings (similar to [Importing Asset as String](https://vitejs.dev/guide/assets.html#importing-asset-as-string)) with the [Import Reflection](https://github.com/tc39/proposal-import-reflection) syntax: +### Glob Import As + +`import.meta.glob` also supports importing files as strings (similar to [Importing Asset as String](https://vitejs.dev/guide/assets.html#importing-asset-as-string)) with the [Import Reflection](https://github.com/tc39/proposal-import-reflection) syntax: ```js const modules = import.meta.glob('./dir/*.js', { as: 'raw' }) @@ -311,18 +313,115 @@ The above will be transformed into the following: ```js // code produced by vite const modules = { - './dir/foo.js': '{\n "msg": "foo"\n}\n', - './dir/bar.js': '{\n "msg": "bar"\n}\n' + './dir/foo.js': 'export default "foo"\n', + './dir/bar.js': 'export default "bar"\n' +} +``` + +`{ as: 'url' }` is also supported for loading assets as URLs. + +### Multiple Patterns + +The first argument can be an array of globs, for example + +```js +const modules = import.meta.glob(['./dir/*.js', './another/*.js']) +``` + +### Negative Patterns + +Negative glob patterns are also supported (prefixed with `!`). To ignore some files from the result, you can add exclude glob patterns to the first argument: + +```js +const modules = import.meta.glob(['./dir/*.js', '!**/bar.js']) +``` + +```js +// code produced by vite +const modules = { + './dir/foo.js': () => import('./dir/foo.js') +} +``` + +#### Named Imports + +It's possible to only import parts of the modules with the `import` options. + +```ts +const modules = import.meta.glob('./dir/*.js', { import: 'setup' }) +``` + +```ts +// code produced by vite +const modules = { + './dir/foo.js': () => import('./dir/foo.js').then((m) => m.setup), + './dir/bar.js': () => import('./dir/bar.js').then((m) => m.setup) +} +``` + +When combined with `eager` it's even possible to have tree-shaking enabled for those modules. + +```ts +const modules = import.meta.glob('./dir/*.js', { import: 'setup', eager: true }) +``` + +```ts +// code produced by vite: +import { setup as __glob__0_0 } from './dir/foo.js' +import { setup as __glob__0_1 } from './dir/bar.js' +const modules = { + './dir/foo.js': __glob__0_0, + './dir/bar.js': __glob__0_1 +} +``` + +Set `import` to `default` to import the default export. + +```ts +const modules = import.meta.glob('./dir/*.js', { + import: 'default', + eager: true +}) +``` + +```ts +// code produced by vite: +import __glob__0_0 from './dir/foo.js' +import __glob__0_1 from './dir/bar.js' +const modules = { + './dir/foo.js': __glob__0_0, + './dir/bar.js': __glob__0_1 +} +``` + +#### Custom Queries + +You can also use the `query` option to provide custom queries to imports for other plugins to consume. + +```ts +const modules = import.meta.glob('./dir/*.js', { + query: { foo: 'bar', bar: true } +}) +``` + +```ts +// code produced by vite: +const modules = { + './dir/foo.js': () => + import('./dir/foo.js?foo=bar&bar=true').then((m) => m.setup), + './dir/bar.js': () => + import('./dir/bar.js?foo=bar&bar=true').then((m) => m.setup) } ``` +### Glob Import Caveats + Note that: - This is a Vite-only feature and is not a web or ES standard. - The glob patterns are treated like import specifiers: they must be either relative (start with `./`) or absolute (start with `/`, resolved relative to project root) or an alias path (see [`resolve.alias` option](/config/#resolve-alias)). -- The glob matching is done via `fast-glob` - check out its documentation for [supported glob patterns](https://github.com/mrmlnc/fast-glob#pattern-syntax). -- You should also be aware that glob imports do not accept variables, you need to directly pass the string pattern. -- The glob patterns cannot contain the same quote string (i.e. `'`, `"`, `` ` ``) as outer quotes, e.g. `'/Tom\'s files/**'`, use `"/Tom's files/**"` instead. +- The glob matching is done via [`fast-glob`](https://github.com/mrmlnc/fast-glob) - check out its documentation for [supported glob patterns](https://github.com/mrmlnc/fast-glob#pattern-syntax). +- You should also be aware that all the arguments in the `import.meta.glob` must be **passed as literals**. You can NOT use variables or expressions in them. ## WebAssembly diff --git a/packages/playground/assets/__tests__/assets.spec.ts b/packages/playground/assets/__tests__/assets.spec.ts index f1075f6fe1cb39..c63ffc7cc1c0c2 100644 --- a/packages/playground/assets/__tests__/assets.spec.ts +++ b/packages/playground/assets/__tests__/assets.spec.ts @@ -145,8 +145,8 @@ describe('css url() references', () => { expect(await getBg('.css-url-quotes-base64-inline')).toMatch(match) const icoMatch = isBuild ? `data:image/x-icon;base64` : `favicon.ico` const el = await page.$(`link.ico`) - const herf = await el.getAttribute('href') - expect(herf).toMatch(icoMatch) + const href = await el.getAttribute('href') + expect(href).toMatch(icoMatch) }) test('multiple urls on the same line', async () => { diff --git a/packages/playground/css/main.js b/packages/playground/css/main.js index f728b0251066d1..f6072ae751df76 100644 --- a/packages/playground/css/main.js +++ b/packages/playground/css/main.js @@ -80,7 +80,9 @@ text('.inlined-code', inlined) // glob const glob = import.meta.glob('./glob-import/*.css') -Promise.all(Object.keys(glob).map((key) => glob[key]())).then((res) => { +Promise.all( + Object.keys(glob).map((key) => glob[key]().then((i) => i.default)) +).then((res) => { text('.imported-css-glob', JSON.stringify(res, null, 2)) }) diff --git a/packages/playground/glob-import/__tests__/glob-import.spec.ts b/packages/playground/glob-import/__tests__/glob-import.spec.ts index ebdf6c0ab29193..d738ccec1d4c97 100644 --- a/packages/playground/glob-import/__tests__/glob-import.spec.ts +++ b/packages/playground/glob-import/__tests__/glob-import.spec.ts @@ -42,7 +42,7 @@ const allResult = { }, '/dir/index.js': { globWithAlias: { - './alias.js': { + '/dir/alias.js': { default: 'hi' } }, @@ -67,7 +67,7 @@ const rawResult = { } const relativeRawResult = { - '../glob-import/dir/baz.json': { + './dir/baz.json': { msg: 'baz' } } diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index 80feb7f95c6941..2b43883f1b2b67 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -3606,6 +3606,34 @@ Repository: git+https://github.com/dominikg/tsconfck.git --------------------------------------- +## ufo +License: MIT +Repository: unjs/ufo + +> MIT License +> +> Copyright (c) 2020 Nuxt Contrib +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. + +--------------------------------------- + ## unpipe License: MIT By: Douglas Christopher Wilson diff --git a/packages/vite/package.json b/packages/vite/package.json index b508842619033e..2e930f0f1f6741 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -112,11 +112,12 @@ "source-map-js": "^1.0.2", "source-map-support": "^0.5.21", "strip-ansi": "^6.0.1", - "strip-literal": "^0.2.0", + "strip-literal": "^0.3.0", "terser": "^5.13.1", "tsconfck": "^1.2.2", "tslib": "^2.4.0", "types": "link:./types", + "ufo": "^0.8.4", "ws": "^8.6.0" }, "peerDependencies": { diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap b/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap new file mode 100644 index 00000000000000..3b611c60c9cedc --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/__snapshots__/fixture.test.ts.snap @@ -0,0 +1,154 @@ +// Vitest Snapshot v1 + +exports[`fixture > transform 1`] = ` +"import * as __vite_glob_1_0 from \\"./modules/a.ts\\" +import * as __vite_glob_1_1 from \\"./modules/b.ts\\" +import * as __vite_glob_1_2 from \\"./modules/index.ts\\" +import { name as __vite_glob_3_0 } from \\"./modules/a.ts\\" +import { name as __vite_glob_3_1 } from \\"./modules/b.ts\\" +import { name as __vite_glob_3_2 } from \\"./modules/index.ts\\" +import { default as __vite_glob_5_0 } from \\"./modules/a.ts?raw\\" +import { default as __vite_glob_5_1 } from \\"./modules/b.ts?raw\\" +import \\"../../../../../../types/importMeta\\"; +export const basic = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\"), +\\"./modules/index.ts\\": () => import(\\"./modules/index.ts\\") +}; +export const basicEager = { +\\"./modules/a.ts\\": __vite_glob_1_0, +\\"./modules/b.ts\\": __vite_glob_1_1, +\\"./modules/index.ts\\": __vite_glob_1_2 +}; +export const ignore = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\") +}; +export const namedEager = { +\\"./modules/a.ts\\": __vite_glob_3_0, +\\"./modules/b.ts\\": __vite_glob_3_1, +\\"./modules/index.ts\\": __vite_glob_3_2 +}; +export const namedDefault = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\").then(m => m[\\"default\\"]), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\").then(m => m[\\"default\\"]), +\\"./modules/index.ts\\": () => import(\\"./modules/index.ts\\").then(m => m[\\"default\\"]) +}; +export const eagerAs = { +\\"./modules/a.ts\\": __vite_glob_5_0, +\\"./modules/b.ts\\": __vite_glob_5_1 +}; +export const excludeSelf = { +\\"./sibling.ts\\": () => import(\\"./sibling.ts\\") +}; +export const customQueryString = { +\\"./sibling.ts\\": () => import(\\"./sibling.ts?custom\\") +}; +export const customQueryObject = { +\\"./sibling.ts\\": () => import(\\"./sibling.ts?foo=bar&raw=true\\") +}; +export const parent = { + +}; +export const rootMixedRelative = { +\\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url\\").then(m => m[\\"default\\"]), +\\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url\\").then(m => m[\\"default\\"]), +\\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url\\").then(m => m[\\"default\\"]), +\\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url\\").then(m => m[\\"default\\"]), +\\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url\\").then(m => m[\\"default\\"]), +\\"/importGlob/fixture-b/index.ts\\": () => import(\\"../fixture-b/index.ts?url\\").then(m => m[\\"default\\"]) +}; +export const cleverCwd1 = { +\\"./node_modules/framework/pages/hello.page.js\\": () => import(\\"./node_modules/framework/pages/hello.page.js\\") +}; +export const cleverCwd2 = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\"), +\\"../fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts\\"), +\\"../fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts\\") +}; +" +`; + +exports[`fixture > transform with restoreQueryExtension 1`] = ` +"import * as __vite_glob_1_0 from \\"./modules/a.ts\\" +import * as __vite_glob_1_1 from \\"./modules/b.ts\\" +import * as __vite_glob_1_2 from \\"./modules/index.ts\\" +import { name as __vite_glob_3_0 } from \\"./modules/a.ts\\" +import { name as __vite_glob_3_1 } from \\"./modules/b.ts\\" +import { name as __vite_glob_3_2 } from \\"./modules/index.ts\\" +import { default as __vite_glob_5_0 } from \\"./modules/a.ts?raw\\" +import { default as __vite_glob_5_1 } from \\"./modules/b.ts?raw\\" +import \\"../../../../../../types/importMeta\\"; +export const basic = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\"), +\\"./modules/index.ts\\": () => import(\\"./modules/index.ts\\") +}; +export const basicEager = { +\\"./modules/a.ts\\": __vite_glob_1_0, +\\"./modules/b.ts\\": __vite_glob_1_1, +\\"./modules/index.ts\\": __vite_glob_1_2 +}; +export const ignore = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\") +}; +export const namedEager = { +\\"./modules/a.ts\\": __vite_glob_3_0, +\\"./modules/b.ts\\": __vite_glob_3_1, +\\"./modules/index.ts\\": __vite_glob_3_2 +}; +export const namedDefault = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\").then(m => m[\\"default\\"]), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\").then(m => m[\\"default\\"]), +\\"./modules/index.ts\\": () => import(\\"./modules/index.ts\\").then(m => m[\\"default\\"]) +}; +export const eagerAs = { +\\"./modules/a.ts\\": __vite_glob_5_0, +\\"./modules/b.ts\\": __vite_glob_5_1 +}; +export const excludeSelf = { +\\"./sibling.ts\\": () => import(\\"./sibling.ts\\") +}; +export const customQueryString = { +\\"./sibling.ts\\": () => import(\\"./sibling.ts?custom&lang.ts\\") +}; +export const customQueryObject = { +\\"./sibling.ts\\": () => import(\\"./sibling.ts?foo=bar&raw=true&lang.ts\\") +}; +export const parent = { + +}; +export const rootMixedRelative = { +\\"/css.spec.ts\\": () => import(\\"../../css.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), +\\"/define.spec.ts\\": () => import(\\"../../define.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), +\\"/import.spec.ts\\": () => import(\\"../../import.spec.ts?url&lang.ts\\").then(m => m[\\"default\\"]), +\\"/importGlob/fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts?url&lang.ts\\").then(m => m[\\"default\\"]), +\\"/importGlob/fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts?url&lang.ts\\").then(m => m[\\"default\\"]), +\\"/importGlob/fixture-b/index.ts\\": () => import(\\"../fixture-b/index.ts?url&lang.ts\\").then(m => m[\\"default\\"]) +}; +export const cleverCwd1 = { +\\"./node_modules/framework/pages/hello.page.js\\": () => import(\\"./node_modules/framework/pages/hello.page.js\\") +}; +export const cleverCwd2 = { +\\"./modules/a.ts\\": () => import(\\"./modules/a.ts\\"), +\\"./modules/b.ts\\": () => import(\\"./modules/b.ts\\"), +\\"../fixture-b/a.ts\\": () => import(\\"../fixture-b/a.ts\\"), +\\"../fixture-b/b.ts\\": () => import(\\"../fixture-b/b.ts\\") +}; +" +`; + +exports[`fixture > virtual modules 1`] = ` +"{ +\\"/modules/a.ts\\": () => import(\\"/modules/a.ts\\"), +\\"/modules/b.ts\\": () => import(\\"/modules/b.ts\\"), +\\"/modules/index.ts\\": () => import(\\"/modules/index.ts\\") +} +{ +\\"/../fixture-b/a.ts\\": () => import(\\"/../fixture-b/a.ts\\"), +\\"/../fixture-b/b.ts\\": () => import(\\"/../fixture-b/b.ts\\"), +\\"/../fixture-b/index.ts\\": () => import(\\"/../fixture-b/index.ts\\") +}" +`; diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/.gitignore b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/.gitignore new file mode 100644 index 00000000000000..2b9b8877da603f --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/.gitignore @@ -0,0 +1 @@ +!/node_modules/ diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts new file mode 100644 index 00000000000000..c5b806da06a4b4 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/index.ts @@ -0,0 +1,64 @@ +import '../../../../../../types/importMeta' + +/* eslint-disable @typescript-eslint/comma-dangle */ +export interface ModuleType { + name: string +} + +export const basic = import.meta.glob('./modules/*.ts') + +export const basicEager = import.meta.glob('./modules/*.ts', { + eager: true +}) + +export const ignore = import.meta.glob(['./modules/*.ts', '!**/index.ts']) + +export const namedEager = import.meta.glob('./modules/*.ts', { + eager: true, + import: 'name' +}) + +export const namedDefault = import.meta.glob('./modules/*.ts', { + import: 'default' +}) + +export const eagerAs = import.meta.glob( + ['./modules/*.ts', '!**/index.ts'], + { eager: true, as: 'raw' } +) + +export const excludeSelf = import.meta.glob( + './*.ts' + // for test: annotation contain ")" + /* + * for test: annotation contain ")" + * */ +) + +export const customQueryString = import.meta.glob('./*.ts', { query: 'custom' }) + +export const customQueryObject = import.meta.glob('./*.ts', { + query: { + foo: 'bar', + raw: true + } +}) + +export const parent = import.meta.glob('../../playground/src/*.ts', { + as: 'url' +}) + +export const rootMixedRelative = import.meta.glob( + ['/*.ts', '../fixture-b/*.ts'], + { as: 'url' } +) + +export const cleverCwd1 = import.meta.glob( + './node_modules/framework/**/*.page.js' +) + +export const cleverCwd2 = import.meta.glob([ + './modules/*.ts', + '../fixture-b/*.ts', + '!**/index.ts' +]) diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/a.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/a.ts new file mode 100644 index 00000000000000..facd48a0875e65 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/a.ts @@ -0,0 +1 @@ +export const name = 'a' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/b.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/b.ts new file mode 100644 index 00000000000000..0b1eb38d9087a2 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/b.ts @@ -0,0 +1 @@ +export const name = 'b' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/index.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/index.ts new file mode 100644 index 00000000000000..25b59ae7d30714 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/modules/index.ts @@ -0,0 +1,6 @@ +export { name as a } from './a' +export { name as b } from './b' + +export const name = 'index' + +export default 'indexDefault' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/node_modules/framework/pages/hello.page.js b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/node_modules/framework/pages/hello.page.js new file mode 100644 index 00000000000000..cbe518a8e79477 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/node_modules/framework/pages/hello.page.js @@ -0,0 +1,4 @@ +// A fake Page file. (This technique of globbing into `node_modules/` +// is used by vite-plugin-ssr frameworks and Hydrogen.) + +export const a = 1 diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/sibling.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/sibling.ts new file mode 100644 index 00000000000000..b286816bf5d63a --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-a/sibling.ts @@ -0,0 +1 @@ +export const name = 'I am your sibling!' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/a.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/a.ts new file mode 100644 index 00000000000000..facd48a0875e65 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/a.ts @@ -0,0 +1 @@ +export const name = 'a' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/b.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/b.ts new file mode 100644 index 00000000000000..0b1eb38d9087a2 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/b.ts @@ -0,0 +1 @@ +export const name = 'b' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/index.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/index.ts new file mode 100644 index 00000000000000..39bdbfd1a8befb --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture-b/index.ts @@ -0,0 +1,2 @@ +export { name as a } from './a' +export { name as b } from './b' diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/fixture.test.ts b/packages/vite/src/node/__tests__/plugins/importGlob/fixture.test.ts new file mode 100644 index 00000000000000..985263d91db85a --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/fixture.test.ts @@ -0,0 +1,59 @@ +import { resolve } from 'path' +import { promises as fs } from 'fs' +import { describe, expect, it } from 'vitest' +import { transformGlobImport } from '../../../plugins/importMetaGlob' +import { transformWithEsbuild } from '../../../plugins/esbuild' + +describe('fixture', async () => { + const resolveId = (id: string) => id + const root = resolve(__dirname, '..') + + it('transform', async () => { + const id = resolve(__dirname, './fixture-a/index.ts') + const code = ( + await transformWithEsbuild(await fs.readFile(id, 'utf-8'), id) + ).code + + expect( + (await transformGlobImport(code, id, root, resolveId))?.s.toString() + ).toMatchSnapshot() + }) + + it('virtual modules', async () => { + const root = resolve(__dirname, './fixture-a') + const code = [ + "import.meta.glob('/modules/*.ts')", + "import.meta.glob(['/../fixture-b/*.ts'])" + ].join('\n') + expect( + ( + await transformGlobImport(code, 'virtual:module', root, resolveId) + )?.s.toString() + ).toMatchSnapshot() + + try { + await transformGlobImport( + "import.meta.glob('./modules/*.ts')", + 'virtual:module', + root, + resolveId + ) + expect('no error').toBe('should throw an error') + } catch (err) { + expect(err).toMatchInlineSnapshot( + "[Error: In virtual modules, all globs must start with '/']" + ) + } + }) + + it('transform with restoreQueryExtension', async () => { + const id = resolve(__dirname, './fixture-a/index.ts') + const code = ( + await transformWithEsbuild(await fs.readFile(id, 'utf-8'), id) + ).code + + expect( + (await transformGlobImport(code, id, root, resolveId, true))?.s.toString() + ).toMatchSnapshot() + }) +}) diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts b/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts new file mode 100644 index 00000000000000..df1e0d758e8849 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/parse.test.ts @@ -0,0 +1,202 @@ +import { describe, expect, it } from 'vitest' +import { parseImportGlob } from '../../../plugins/importMetaGlob' + +async function run(input: string) { + const items = await parseImportGlob( + input, + process.cwd(), + process.cwd(), + (id) => id + ) + return items.map((i) => ({ globs: i.globs, options: i.options })) +} + +async function runError(input: string) { + try { + await run(input) + } catch (e) { + return e + } +} + +describe('parse positives', async () => { + it('basic', async () => { + expect( + await run(` + import.meta.importGlob(\'./modules/*.ts\') + `) + ).toMatchInlineSnapshot('[]') + }) + + it('array', async () => { + expect( + await run(` + import.meta.importGlob([\'./modules/*.ts\', './dir/*.{js,ts}\']) + `) + ).toMatchInlineSnapshot('[]') + }) + + it('options with multilines', async () => { + expect( + await run(` + import.meta.importGlob([ + \'./modules/*.ts\', + "!./dir/*.{js,ts}" + ], { + eager: true, + import: 'named' + }) + `) + ).toMatchInlineSnapshot('[]') + }) + + it('options with multilines', async () => { + expect( + await run(` + const modules = import.meta.glob( + '/dir/**' + // for test: annotation contain ")" + /* + * for test: annotation contain ")" + * */ + ) + `) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "/dir/**", + ], + "options": {}, + }, + ] + `) + }) + + it('options query', async () => { + expect( + await run(` + const modules = import.meta.glob( + '/dir/**', + { + query: { + foo: 'bar', + raw: true, + } + } + ) + `) + ).toMatchInlineSnapshot(` + [ + { + "globs": [ + "/dir/**", + ], + "options": { + "query": { + "foo": "bar", + "raw": true, + }, + }, + }, + ] + `) + }) +}) + +describe('parse negatives', async () => { + it('syntax error', async () => { + expect(await runError('import.meta.importGlob(')).toMatchInlineSnapshot( + 'undefined' + ) + }) + + it('empty', async () => { + expect(await runError('import.meta.importGlob()')).toMatchInlineSnapshot( + 'undefined' + ) + }) + + it('3 args', async () => { + expect( + await runError('import.meta.importGlob("", {}, {})') + ).toMatchInlineSnapshot('undefined') + }) + + it('in string', async () => { + expect(await runError('"import.meta.importGlob()"')).toBeUndefined() + }) + + it('variable', async () => { + expect(await runError('import.meta.importGlob(hey)')).toMatchInlineSnapshot( + 'undefined' + ) + }) + + it('template', async () => { + // eslint-disable-next-line no-template-curly-in-string + expect( + await runError('import.meta.importGlob(`hi ${hey}`)') + ).toMatchInlineSnapshot('undefined') + }) + + it('be string', async () => { + expect(await runError('import.meta.importGlob(1)')).toMatchInlineSnapshot( + 'undefined' + ) + }) + + it('be array variable', async () => { + expect( + await runError('import.meta.importGlob([hey])') + ).toMatchInlineSnapshot('undefined') + expect( + await runError('import.meta.importGlob(["1", hey])') + ).toMatchInlineSnapshot('undefined') + }) + + it('options', async () => { + expect( + await runError('import.meta.importGlob("hey", hey)') + ).toMatchInlineSnapshot('undefined') + expect( + await runError('import.meta.importGlob("hey", [])') + ).toMatchInlineSnapshot('undefined') + }) + + it('options props', async () => { + expect( + await runError('import.meta.importGlob("hey", { hey: 1 })') + ).toMatchInlineSnapshot('undefined') + expect( + await runError('import.meta.importGlob("hey", { import: hey })') + ).toMatchInlineSnapshot('undefined') + expect( + await runError('import.meta.importGlob("hey", { eager: 123 })') + ).toMatchInlineSnapshot('undefined') + }) + + it('options query', async () => { + expect( + await runError( + 'import.meta.importGlob("./*.js", { as: "raw", query: "hi" })' + ) + ).toMatchInlineSnapshot('undefined') + expect( + await runError('import.meta.importGlob("./*.js", { query: 123 })') + ).toMatchInlineSnapshot('undefined') + expect( + await runError('import.meta.importGlob("./*.js", { query: { foo: {} } })') + ).toMatchInlineSnapshot('undefined') + expect( + await runError( + 'import.meta.importGlob("./*.js", { query: { foo: hey } })' + ) + ).toMatchInlineSnapshot('undefined') + expect( + await runError( + 'import.meta.importGlob("./*.js", { query: { foo: 123, ...a } })' + ) + ).toMatchInlineSnapshot('undefined') + }) +}) diff --git a/packages/vite/src/node/__tests__/plugins/importGlob/utils.test.ts b/packages/vite/src/node/__tests__/plugins/importGlob/utils.test.ts new file mode 100644 index 00000000000000..302df97ec92ede --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/importGlob/utils.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest' +import { getCommonBase } from '../../../plugins/importMetaGlob' + +describe('getCommonBase()', async () => { + it('basic', () => { + expect(getCommonBase(['/a/b/*.js', '/a/c/*.js'])).toBe('/a') + }) + it('common base', () => { + expect(getCommonBase(['/a/b/**/*.vue', '/a/b/**/*.jsx'])).toBe('/a/b') + }) + it('static file', () => { + expect( + getCommonBase(['/a/b/**/*.vue', '/a/b/**/*.jsx', '/a/b/foo.js']) + ).toBe('/a/b') + expect(getCommonBase(['/a/b/**/*.vue', '/a/b/**/*.jsx', '/a/foo.js'])).toBe( + '/a' + ) + }) + it('correct `scan()`', () => { + expect(getCommonBase(['/a/*.vue'])).toBe('/a') + expect(getCommonBase(['/a/some.vue'])).toBe('/a') + expect(getCommonBase(['/a/b/**/c/foo.vue', '/a/b/c/**/*.jsx'])).toBe('/a/b') + }) + it('single', () => { + expect(getCommonBase(['/a/b/c/*.vue'])).toBe('/a/b/c') + expect(getCommonBase(['/a/b/c/foo.vue'])).toBe('/a/b/c') + }) + it('no common base', () => { + expect(getCommonBase(['/a/b/*.js', '/c/a/b/*.js'])).toBe('/') + }) +}) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 29dc3fe2045a5a..e7fcaefeb2a799 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -152,6 +152,14 @@ export interface UserConfig { * @alpha */ ssr?: SSROptions + /** + * Experimental features + * + * Features under this field are addressed to be changed that might NOT follow semver. + * Please be careful and always pin Vite's version when using them. + * @experimental + */ + experimental?: ExperimentalOptions /** * Log level. * Default: 'info' @@ -199,6 +207,16 @@ export interface UserConfig { } } +export interface ExperimentalOptions { + /** + * Append fake `&lang.(ext)` when queries are specified, to preseve the file extension for following plugins to process. + * + * @experimental + * @default false + */ + importGlobRestoreExtension?: boolean +} + export type SSRTarget = 'node' | 'webworker' export interface SSROptions { diff --git a/packages/vite/src/node/importGlob.ts b/packages/vite/src/node/importGlob.ts deleted file mode 100644 index ab425bb596c68a..00000000000000 --- a/packages/vite/src/node/importGlob.ts +++ /dev/null @@ -1,246 +0,0 @@ -import path from 'path' -import { promises as fsp } from 'fs' -import glob from 'fast-glob' -import JSON5 from 'json5' -import { - isModernFlag, - preloadMethod, - preloadMarker -} from './plugins/importAnalysisBuild' -import { isCSSRequest } from './plugins/css' -import { - cleanUrl, - singlelineCommentsRE, - multilineCommentsRE, - blankReplacer, - normalizePath -} from './utils' -import type { RollupError } from 'rollup' -import type { Logger } from '.' - -interface GlobParams { - base: string - pattern: string - parentDepth: number - isAbsolute: boolean -} - -interface GlobOptions { - as?: string -} - -function formatGlobRelativePattern(base: string, pattern: string): GlobParams { - let parentDepth = 0 - while (pattern.startsWith('../')) { - pattern = pattern.slice(3) - base = path.resolve(base, '../') - parentDepth++ - } - if (pattern.startsWith('./')) { - pattern = pattern.slice(2) - } - - return { base, pattern, parentDepth, isAbsolute: false } -} - -export async function transformImportGlob( - source: string, - pos: number, - importer: string, - importIndex: number, - root: string, - logger: Logger, - normalizeUrl?: (url: string, pos: number) => Promise<[string, string]>, - resolve?: (url: string, importer?: string) => Promise, - preload = true -): Promise<{ - importsString: string - imports: string[] - exp: string - endIndex: number - isEager: boolean - pattern: string - base: string -}> { - const isEager = source.slice(pos, pos + 21) === 'import.meta.globEager' - const isEagerDefault = - isEager && source.slice(pos + 21, pos + 28) === 'Default' - - const err = (msg: string) => { - const e = new Error(`Invalid glob import syntax: ${msg}`) - ;(e as any).pos = pos - return e - } - - importer = cleanUrl(importer) - const importerBasename = path.basename(importer) - - const [userPattern, options, endIndex] = lexGlobPattern(source, pos) - - let globParams: GlobParams | null = null - if (userPattern.startsWith('/')) { - globParams = { - isAbsolute: true, - base: path.resolve(root), - pattern: userPattern.slice(1), - parentDepth: 0 - } - } else if (userPattern.startsWith('.')) { - globParams = formatGlobRelativePattern(path.dirname(importer), userPattern) - } else if (resolve) { - const resolvedId = await resolve(userPattern, importer) - if (resolvedId) { - const importerDirname = path.dirname(importer) - globParams = formatGlobRelativePattern( - importerDirname, - normalizePath(path.relative(importerDirname, resolvedId)) - ) - } - } - - if (!globParams) { - throw err( - `pattern must start with "." or "/" (relative to project root) or alias path` - ) - } - const { base, parentDepth, isAbsolute, pattern } = globParams - - const files = glob.sync(pattern, { - cwd: base, - // Ignore node_modules by default unless explicitly indicated in the pattern - ignore: /(^|\/)node_modules\//.test(pattern) ? [] : ['**/node_modules/**'] - }) - const imports: string[] = [] - let importsString = `` - let entries = `` - for (let i = 0; i < files.length; i++) { - // skip importer itself - if (files[i] === importerBasename) continue - const file = isAbsolute - ? `/${files[i]}` - : parentDepth - ? `${'../'.repeat(parentDepth)}${files[i]}` - : `./${files[i]}` - let importee = file - if (normalizeUrl) { - ;[importee] = await normalizeUrl(file, pos) - } - imports.push(importee) - - const isRawType = options?.as === 'raw' - if (isRawType) { - entries += ` ${JSON.stringify(file)}: ${JSON.stringify( - await fsp.readFile(path.join(base, files[i]), 'utf-8') - )},` - } else { - const importeeUrl = isCSSRequest(importee) ? `${importee}?used` : importee - if (isEager) { - const identifier = `__glob_${importIndex}_${i}` - // css imports injecting a ?used query to export the css string - importsString += `import ${ - isEagerDefault ? `` : `* as ` - }${identifier} from ${JSON.stringify(importeeUrl)};` - entries += ` ${JSON.stringify(file)}: ${identifier},` - } else { - let imp = `import(${JSON.stringify(importeeUrl)})` - if (!normalizeUrl && preload) { - imp = - `(${isModernFlag}` + - `? ${preloadMethod}(()=>${imp},"${preloadMarker}")` + - `: ${imp})` - } - entries += ` ${JSON.stringify(file)}: () => ${imp},` - } - } - } - - return { - imports, - importsString, - exp: `{${entries}}`, - endIndex, - isEager, - pattern, - base - } -} - -const enum LexerState { - inCall, - inSingleQuoteString, - inDoubleQuoteString, - inTemplateString -} - -function lexGlobPattern( - code: string, - pos: number -): [string, GlobOptions, number] { - let state = LexerState.inCall - let pattern = '' - - let i = code.indexOf(`(`, pos) + 1 - outer: for (; i < code.length; i++) { - const char = code.charAt(i) - switch (state) { - case LexerState.inCall: - if (char === `'`) { - state = LexerState.inSingleQuoteString - } else if (char === `"`) { - state = LexerState.inDoubleQuoteString - } else if (char === '`') { - state = LexerState.inTemplateString - } else if (/\s/.test(char)) { - continue - } else { - error(i) - } - break - case LexerState.inSingleQuoteString: - if (char === `'`) { - break outer - } else { - pattern += char - } - break - case LexerState.inDoubleQuoteString: - if (char === `"`) { - break outer - } else { - pattern += char - } - break - case LexerState.inTemplateString: - if (char === '`') { - break outer - } else { - pattern += char - } - break - default: - throw new Error('unknown import.meta.glob lexer state') - } - } - const noCommentCode = code - .slice(i + 1) - .replace(singlelineCommentsRE, blankReplacer) - .replace(multilineCommentsRE, blankReplacer) - - const endIndex = noCommentCode.indexOf(')') - const optionString = noCommentCode.substring(0, endIndex) - const commaIndex = optionString.indexOf(',') - - let options = {} - if (commaIndex > -1) { - options = JSON5.parse(optionString.substring(commaIndex + 1)) - } - return [pattern, options, endIndex + i + 2] -} - -function error(pos: number) { - const err = new Error( - `import.meta.glob() can only accept string literals.` - ) as RollupError - err.pos = pos - throw err -} diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index e56f4c6e765756..0d401363b8a3b6 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -74,6 +74,9 @@ export type { TransformOptions as EsbuildTransformOptions } from 'esbuild' export type { ESBuildOptions, ESBuildTransformResult } from './plugins/esbuild' export type { Manifest, ManifestChunk } from './plugins/manifest' export type { ResolveOptions, InternalResolveOptions } from './plugins/resolve' +export type { SplitVendorChunkCache } from './plugins/splitVendorChunk' +import type { ChunkMetadata } from './plugins/metadata' + export type { WebSocketServer, WebSocketClient, @@ -88,6 +91,7 @@ export type { TransformResult } from './server/transformRequest' export type { HmrOptions, HmrContext } from './server/hmr' + export type { HMRPayload, ConnectedPayload, @@ -111,9 +115,12 @@ export type { RollupCommonJSOptions } from 'types/commonjs' export type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars' export type { CustomEventMap, InferCustomEventPayload } from 'types/customEvent' export type { Matcher, AnymatchPattern, AnymatchFn } from 'types/anymatch' -export type { SplitVendorChunkCache } from './plugins/splitVendorChunk' - -import type { ChunkMetadata } from './plugins/metadata' +export type { + ImportGlobFunction, + ImportGlobEagerFunction, + ImportGlobOptions, + KnownAsTypeMap +} from 'types/importGlob' declare module 'rollup' { export interface RenderedChunk { diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index ef59a35b1d22d3..111aefc40dea15 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -1,9 +1,9 @@ import fs from 'fs' import path from 'path' import glob from 'fast-glob' -import type { ResolvedConfig, Logger } from '..' +import type { ResolvedConfig } from '..' import type { Loader, Plugin, OnLoadResult } from 'esbuild' -import { build, transform } from 'esbuild' +import { build } from 'esbuild' import { KNOWN_ASSET_TYPES, JS_TYPES_RE, @@ -25,11 +25,9 @@ import { } from '../utils' import type { PluginContainer } from '../server/pluginContainer' import { createPluginContainer } from '../server/pluginContainer' -import { init, parse } from 'es-module-lexer' -import MagicString from 'magic-string' -import { transformImportGlob } from '../importGlob' import { performance } from 'perf_hooks' import colors from 'picocolors' +import { transformGlobImport } from '../plugins/importMetaGlob' const debug = createDebugger('vite:deps') @@ -300,19 +298,18 @@ function esbuildScanPlugin( (loader.startsWith('ts') ? extractImportPaths(content) : '') const key = `${path}?id=${scriptId++}` - if (contents.includes('import.meta.glob')) { scripts[key] = { - // transformGlob already transforms to js loader: 'js', - contents: await transformGlob( - contents, - path, - config.root, - loader, - resolve, - config.logger - ) + contents: + ( + await transformGlobImport( + contents, + path, + config.root, + resolve + ) + )?.s.toString() || contents } } else { scripts[key] = { @@ -467,20 +464,6 @@ function esbuildScanPlugin( config.optimizeDeps?.esbuildOptions?.loader?.[`.${ext}`] || (ext as Loader) - if (contents.includes('import.meta.glob')) { - return transformGlob( - contents, - id, - config.root, - loader, - resolve, - config.logger - ).then((contents) => ({ - loader, - contents - })) - } - return { loader, contents @@ -490,43 +473,6 @@ function esbuildScanPlugin( } } -async function transformGlob( - source: string, - importer: string, - root: string, - loader: Loader, - resolve: (url: string, importer?: string) => Promise, - logger: Logger -) { - // transform the content first since es-module-lexer can't handle non-js - if (loader !== 'js') { - source = (await transform(source, { loader })).code - } - - await init - const imports = parse(source)[0] - const s = new MagicString(source) - for (let index = 0; index < imports.length; index++) { - const { s: start, e: end, ss: expStart } = imports[index] - const url = source.slice(start, end) - if (url !== 'import.meta') continue - if (source.slice(end, end + 5) !== '.glob') continue - const { importsString, exp, endIndex } = await transformImportGlob( - source, - start, - normalizePath(importer), - index, - root, - logger, - undefined, - resolve - ) - s.prepend(importsString) - s.overwrite(expStart, endIndex, exp, { contentOnly: true }) - } - return s.toString() -} - /** * when using TS + (Vue + `