diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f60f3280712ed6..6f10675c2f33ce 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = defineConfig({ root: true, extends: [ 'eslint:recommended', - 'plugin:node/recommended', + 'plugin:n/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:regexp/recommended', ], @@ -29,14 +29,9 @@ module.exports = defineConfig({ }, ], - 'node/no-missing-import': [ - 'error', - { - allowModules: ['types', 'estree', 'less', 'sass', 'stylus'], - tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], - }, - ], - 'node/no-missing-require': [ + 'n/no-process-exit': 'off', + 'n/no-missing-import': 'off', + 'n/no-missing-require': [ 'error', { // for try-catching yarn pnp @@ -44,22 +39,22 @@ module.exports = defineConfig({ tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], }, ], - 'node/no-extraneous-import': [ + 'n/no-extraneous-import': [ 'error', { - allowModules: ['vite', 'less', 'sass', 'vitest'], + allowModules: ['vite', 'less', 'sass', 'vitest', 'unbuild'], }, ], - 'node/no-extraneous-require': [ + 'n/no-extraneous-require': [ 'error', { allowModules: ['vite'], }, ], - 'node/no-deprecated-api': 'off', - 'node/no-unpublished-import': 'off', - 'node/no-unpublished-require': 'off', - 'node/no-unsupported-features/es-syntax': 'off', + 'n/no-deprecated-api': 'off', + 'n/no-unpublished-import': 'off', + 'n/no-unpublished-require': 'off', + 'n/no-unsupported-features/es-syntax': 'off', '@typescript-eslint/ban-ts-comment': 'error', '@typescript-eslint/ban-types': 'off', // TODO: we should turn this on in a new PR @@ -118,7 +113,7 @@ module.exports = defineConfig({ { files: 'packages/vite/**/*.*', rules: { - 'node/no-restricted-require': [ + 'n/no-restricted-require': [ 'error', Object.keys( require('./packages/vite/package.json').devDependencies, @@ -141,32 +136,32 @@ module.exports = defineConfig({ { files: ['packages/vite/src/types/**', '*.spec.ts'], rules: { - 'node/no-extraneous-import': 'off', + 'n/no-extraneous-import': 'off', }, }, { files: ['packages/create-vite/template-*/**', '**/build.config.ts'], rules: { 'no-undef': 'off', - 'node/no-missing-import': 'off', + 'n/no-missing-import': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, { files: ['playground/**'], rules: { - 'node/no-extraneous-import': 'off', - 'node/no-extraneous-require': 'off', - 'node/no-missing-import': 'off', - 'node/no-missing-require': 'off', + 'n/no-extraneous-import': 'off', + 'n/no-extraneous-require': 'off', + 'n/no-missing-import': 'off', + 'n/no-missing-require': 'off', // engine field doesn't exist in playgrounds - 'node/no-unsupported-features/es-builtins': [ + 'n/no-unsupported-features/es-builtins': [ 'error', { version: '^14.18.0 || >=16.0.0', }, ], - 'node/no-unsupported-features/node-builtins': [ + 'n/no-unsupported-features/node-builtins': [ 'error', { version: '^14.18.0 || >=16.0.0', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41a5a096fb3bd2..49a811bd741a6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node_version: [14, 16, 18] + node_version: [14, 16, 18, 20] include: # Active LTS + other OS - os: macos-latest @@ -58,7 +58,7 @@ jobs: - name: Get changed files id: changed-files - uses: tj-actions/changed-files@04124efe7560d15e11ea2ba96c0df2989f68f1f4 # v35.6.1 + uses: tj-actions/changed-files@9328bab880abf4acc377d77718d28c6ac167f154 # v35.7.2 with: files: | docs/** @@ -67,8 +67,14 @@ jobs: packages/create-vite/template** **.md + - name: Install pnpm (node 14, pnpm 7) + if: steps.changed-files.outputs.only_changed != 'true' && matrix.node_version == 14 + uses: pnpm/action-setup@v2.2.4 + with: + version: 7 + - name: Install pnpm - if: steps.changed-files.outputs.only_changed != 'true' + if: steps.changed-files.outputs.only_changed != 'true' && matrix.node_version != 14 uses: pnpm/action-setup@v2.2.4 - name: Set node version to ${{ matrix.node_version }} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index ad9b8942e53712..977420800979cf 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -94,10 +94,6 @@ export default defineConfig({ pt: { label: 'Português', link: 'https://pt.vitejs.dev' }, }, - vue: { - reactivityTransform: true, - }, - themeConfig: { logo: '/logo.svg', diff --git a/docs/.vitepress/theme/components/HomeSponsors.vue b/docs/.vitepress/theme/components/HomeSponsors.vue index 16ea073b423820..cb3a5503e58dfa 100644 --- a/docs/.vitepress/theme/components/HomeSponsors.vue +++ b/docs/.vitepress/theme/components/HomeSponsors.vue @@ -9,8 +9,59 @@ const { data } = useSponsor() +
+ + +
+ + diff --git a/docs/.vitepress/theme/composables/sponsor.ts b/docs/.vitepress/theme/composables/sponsor.ts index 976dbcfb75103e..3251509acc589d 100644 --- a/docs/.vitepress/theme/composables/sponsor.ts +++ b/docs/.vitepress/theme/composables/sponsor.ts @@ -21,6 +21,11 @@ const data = ref() const dataHost = 'https://sponsors.vuejs.org' const dataUrl = `${dataHost}/vite.json` +// no sponsors yet :( +const viteSponsors: Pick = { + gold: [], +} + export function useSponsor() { onMounted(async () => { if (data.value) { @@ -48,7 +53,7 @@ function mapSponsors(sponsors: Sponsors) { { tier: 'Gold Sponsors', size: 'medium', - items: mapImgPath(sponsors['gold']), + items: viteSponsors['gold'].concat(mapImgPath(sponsors['gold'])), }, ] } diff --git a/docs/_data/team.js b/docs/_data/team.js index 54f089f613c6c3..72641a597cee55 100644 --- a/docs/_data/team.js +++ b/docs/_data/team.js @@ -120,7 +120,23 @@ export const core = [ name: 'Dominik G.', title: 'Resident CI Expert', desc: 'Team Member of Vite and Svelte', - links: [{ icon: 'github', link: 'https://github.com/dominikg' }], + links: [ + { icon: 'github', link: 'https://github.com/dominikg' }, + { icon: 'mastodon', link: 'https://elk.zone/m.webtoo.ls/@dominikg' }, + ], + sponsor: 'https://github.com/sponsors/dominikg', + }, + { + avatar: 'https://github.com/sheremet-va.png', + name: 'Vladimir', + title: 'Core team member of Vitest & Vite', + desc: 'An open source fullstack developer', + links: [ + { icon: 'github', link: 'https://github.com/sheremet-va' }, + { icon: 'mastodon', link: 'https://elk.zone/m.webtoo.ls/@sheremet_va' }, + { icon: 'twitter', link: 'https://twitter.com/sheremet_va' }, + ], + sponsor: 'https://github.com/sponsors/sheremet-va', }, ] diff --git a/docs/config/build-options.md b/docs/config/build-options.md index b9971fdb383f64..ff4dbbd50e3bbf 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -10,7 +10,7 @@ Browser compatibility target for the final bundle. The default value is a Vite s Another special value is `'esnext'` - which assumes native dynamic imports support and will transpile as little as possible: -- If the [`build.minify`](#build-minify) option is `'terser'`, `'esnext'` will be forced down to `'es2021'`. +- If the [`build.minify`](#build-minify) option is `'terser'` and the installed Terser version is below 5.16.0, `'esnext'` will be forced down to `'es2021'`. - In other cases, it will perform no transpilation at all. The transform is performed with esbuild and the value should be a valid [esbuild target option](https://esbuild.github.io/api/#target). Custom targets can either be an ES version (e.g. `es2015`), a browser with version (e.g. `chrome58`), or an array of multiple target strings. diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 49c766cba59d5a..ab0f1b261ddb7c 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -324,7 +324,7 @@ export default defineConfig({ - **Type:** `false | (sourcePath: string, sourcemapPath: string) => boolean` - **Default:** `(sourcePath) => sourcePath.includes('node_modules')` -Whether or not to ignore source files in the server sourcemap, used to populate the [`x_google_ignoreList` source map extension](https://developer.chrome.com/blog/devtools-better-angular-debugging/#the-x_google_ignorelist-source-map-extension). +Whether or not to ignore source files in the server sourcemap, used to populate the [`x_google_ignoreList` source map extension](https://developer.chrome.com/articles/x-google-ignore-list/). `server.sourcemapIgnoreList` is the equivalent of [`build.rollupOptions.output.sourcemapIgnoreList`](https://rollupjs.org/configuration-options/#output-sourcemapignorelist) for the dev server. A difference between the two config options is that the rollup function is called with a relative path for `sourcePath` while `server.sourcemapIgnoreList` is called with an absolute path. During dev, most modules have the map and the source in the same folder, so the relative path for `sourcePath` is the file name itself. In these cases, absolute paths makes it convenient to be used instead. diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index 4def2201ee891f..e67ddb72a1f176 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -111,7 +111,7 @@ When aliasing to file system paths, always use absolute paths. Relative alias va More advanced custom resolution can be achieved through [plugins](/guide/api-plugin). ::: warning Using with SSR -If you have configured aliases for [SSR externalized dependencies](/guide/ssr.md#ssr-externals), you may want to alias the actual `node_modules` packages. Both [Yarn](https://classic.yarnpkg.com/en/docs/cli/add/#toc-yarn-add-alias) and [pnpm](https://pnpm.js.org/en/aliases) support aliasing via the `npm:` prefix. +If you have configured aliases for [SSR externalized dependencies](/guide/ssr.md#ssr-externals), you may want to alias the actual `node_modules` packages. Both [Yarn](https://classic.yarnpkg.com/en/docs/cli/add/#toc-yarn-add-alias) and [pnpm](https://pnpm.io/aliases/) support aliasing via the `npm:` prefix. ::: ## resolve.dedupe @@ -232,7 +232,7 @@ Specify options to pass to CSS pre-processors. The file extensions are used as k - `less` - [Options](https://lesscss.org/usage/#less-options). - `styl`/`stylus` - Only [`define`](https://stylus-lang.com/docs/js.html#define-name-node) is supported, which can be passed as an object. -All preprocessor options also support the `additionalData` option, which can be used to inject extra code for each style content. +All preprocessor options also support the `additionalData` option, which can be used to inject extra code for each style content. Note that if you include actual styles and not just variables, those styles will be duplicated in the final bundle. Example: diff --git a/docs/config/worker-options.md b/docs/config/worker-options.md index 840caa60915ba2..99500ae259ccfe 100644 --- a/docs/config/worker-options.md +++ b/docs/config/worker-options.md @@ -5,7 +5,7 @@ Options related to Web Workers. ## worker.format - **Type:** `'es' | 'iife'` -- **Default:** `iife` +- **Default:** `'iife'` Output format for worker bundle. diff --git a/docs/guide/api-javascript.md b/docs/guide/api-javascript.md index c66374ae1247e2..41086c8d3404df 100644 --- a/docs/guide/api-javascript.md +++ b/docs/guide/api-javascript.md @@ -198,6 +198,46 @@ import { preview } from 'vite' })() ``` +## `PreviewServer` + +```ts +interface PreviewServer extends PreviewServerForHook { + resolvedUrls: ResolvedServerUrls +} +``` + +## `PreviewServerForHook` + +```ts +interface PreviewServerForHook { + /** + * The resolved vite config object + */ + config: ResolvedConfig + /** + * A connect app instance. + * - Can be used to attach custom middlewares to the preview server. + * - Can also be used as the handler function of a custom http server + * or as a middleware in any connect-style Node.js frameworks + * + * https://github.com/senchalabs/connect#use-middleware + */ + middlewares: Connect.Server + /** + * native Node http server instance + */ + httpServer: http.Server + /** + * The resolved urls Vite prints on the CLI + */ + resolvedUrls: ResolvedServerUrls | null + /** + * Print server urls + */ + printUrls(): void +} +``` + ## `resolveConfig` **Type Signature:** diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index 9347c84e6f10e8..d5f92bad9ad879 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -309,10 +309,11 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo ### `configurePreviewServer` -- **Type:** `(server: { middlewares: Connect.Server, httpServer: http.Server }) => (() => void) | void | Promise<(() => void) | void>` +- **Type:** `(server: PreviewServerForHook) => (() => void) | void | Promise<(() => void) | void>` - **Kind:** `async`, `sequential` +- **See also:** [PreviewServerForHook](./api-javascript#previewserverforhook) - Same as [`configureServer`](/guide/api-plugin.html#configureserver) but for the preview server. It provides the [connect](https://github.com/senchalabs/connect) server and its underlying [http server](https://nodejs.org/api/http.html). Similarly to `configureServer`, the `configurePreviewServer` hook is called before other middlewares are installed. If you want to inject a middleware **after** other middlewares, you can return a function from `configurePreviewServer`, which will be called after internal middlewares are installed: + Same as [`configureServer`](/guide/api-plugin.html#configureserver) but for the preview server. Similarly to `configureServer`, the `configurePreviewServer` hook is called before other middlewares are installed. If you want to inject a middleware **after** other middlewares, you can return a function from `configurePreviewServer`, which will be called after internal middlewares are installed: ```js const myPlugin = () => ({ diff --git a/docs/guide/features.md b/docs/guide/features.md index b13049029d6724..a9e3fcd859b606 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -74,7 +74,7 @@ You can read more about the transition in the [TypeScript 3.7 release notes](htt If you are using a library that heavily relies on class fields, please be careful about the library's intended usage of it. -Most libraries expect `"useDefineForClassFields": true`, such as [MobX](https://mobx.js.org/installation.html#use-spec-compliant-transpilation-for-class-properties), [Vue Class Components 8.x](https://github.com/vuejs/vue-class-component/issues/465), etc. +Most libraries expect `"useDefineForClassFields": true`, such as [MobX](https://mobx.js.org/installation.html#use-spec-compliant-transpilation-for-class-properties). But a few libraries haven't transitioned to this new default yet, including [`lit-element`](https://github.com/lit/lit-element/issues/1030). Please explicitly set `useDefineForClassFields` to `false` in these cases. @@ -96,7 +96,7 @@ Vite's default types are for its Node.js API. To shim the environment of client /// ``` -Also, you can add `vite/client` to `compilerOptions.types` of your `tsconfig`: +Alternatively, you can add `vite/client` to `compilerOptions.types` inside `tsconfig.json`: ```json { diff --git a/docs/guide/ssr.md b/docs/guide/ssr.md index 27d431011a7b28..93830d1d8e5aaa 100644 --- a/docs/guide/ssr.md +++ b/docs/guide/ssr.md @@ -216,7 +216,7 @@ If a dependency needs to be transformed by Vite's pipeline, for example, because For linked dependencies, they are not externalized by default to take advantage of Vite's HMR. If this isn't desired, for example, to test dependencies as if they aren't linked, you can add it to [`ssr.external`](../config/ssr-options.md#ssr-external). :::warning Working with Aliases -If you have configured aliases that redirect one package to another, you may want to alias the actual `node_modules` packages instead to make it work for SSR externalized dependencies. Both [Yarn](https://classic.yarnpkg.com/en/docs/cli/add/#toc-yarn-add-alias) and [pnpm](https://pnpm.js.org/en/aliases) support aliasing via the `npm:` prefix. +If you have configured aliases that redirect one package to another, you may want to alias the actual `node_modules` packages instead to make it work for SSR externalized dependencies. Both [Yarn](https://classic.yarnpkg.com/en/docs/cli/add/#toc-yarn-add-alias) and [pnpm](https://pnpm.io/aliases/) support aliasing via the `npm:` prefix. ::: ## SSR-specific Plugin Logic diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index f97b0447e32ff4..c9eeab879c5c5a 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -121,6 +121,12 @@ See [Reason: CORS request not HTTP - HTTP | MDN](https://developer.mozilla.org/e You will need to access the file with `http` protocol. The easiest way to achieve this is to run `npx vite preview`. +## Optimized Dependencies + +### Outdated pre-bundled deps when linking to a local package + +The hash key used to invalidate optimized dependencies depend on the package lock contents, the patches applied to dependencies, and the options in the Vite config file that affects the bundling of node modules. This means that Vite will detect when a dependency is overridden using a feature as [npm overrides](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#overrides), and re-bundle your dependencies on the next server start. Vite won't invalidate the dependencies when you use a feature like [npm link](https://docs.npmjs.com/cli/v9/commands/npm-link). In case you link or unlink a dependency, you'll need to force re-optimization on the next server start by using `vite --force`. We recommend using overrides instead, which are supported now by every package manager (see also [pnpm overrides](https://pnpm.io/package_json#pnpmoverrides) and [yarn resolutions](https://yarnpkg.com/configuration/manifest/#resolutions)). + ## Others ### Module externalized for browser compatibility @@ -144,3 +150,7 @@ For example, you might see these errors. > TypeError: Cannot create property 'foo' on boolean 'false' If these code are used inside dependencies, you could use [`patch-package`](https://github.com/ds300/patch-package) (or [`yarn patch`](https://yarnpkg.com/cli/patch) or [`pnpm patch`](https://pnpm.io/cli/patch)) for an escape hatch. + +### Browser extensions + +Some browser extensions (like ad-blockers) may prevent the Vite client from sending requests to the Vite dev server. You may see a white screen without logged errors in this case. Try disabling extensions if you have this issue. diff --git a/package.json b/package.json index 7cdd7149ec2c01..9b915cc513878d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@vitejs/vite-monorepo", "private": true, + "type": "module", "engines": { "node": "^14.18.0 || >=16.0.0" }, @@ -36,7 +37,7 @@ "ci-docs": "run-s build docs-build" }, "devDependencies": { - "@babel/types": "^7.21.2", + "@babel/types": "^7.21.3", "@microsoft/api-extractor": "^7.34.4", "@rollup/plugin-typescript": "^11.0.0", "@types/babel__core": "^7.20.0", @@ -50,45 +51,40 @@ "@types/json-stable-stringify": "^1.0.34", "@types/less": "^3.0.3", "@types/micromatch": "^4.0.2", - "@types/minimist": "^1.2.2", - "@types/node": "^18.14.6", + "@types/node": "^18.15.5", "@types/picomatch": "^2.3.0", - "@types/prompts": "^2.4.2", "@types/resolve": "^1.20.2", "@types/sass": "~1.43.1", - "@types/semver": "^7.3.13", "@types/stylus": "^0.48.38", "@types/ws": "^8.5.4", - "@typescript-eslint/eslint-plugin": "^5.54.1", - "@typescript-eslint/parser": "^5.54.1", + "@typescript-eslint/eslint-plugin": "^5.56.0", + "@typescript-eslint/parser": "^5.56.0", + "@vitejs/release-scripts": "^1.1.0", "conventional-changelog-cli": "^2.2.2", - "eslint": "^8.35.0", - "eslint-define-config": "^1.15.0", + "eslint": "^8.36.0", + "eslint-define-config": "^1.17.0", "eslint-plugin-import": "^2.27.5", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-regexp": "^1.12.0", - "execa": "^7.0.0", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-regexp": "^1.13.0", + "execa": "^7.1.1", "fast-glob": "^3.2.12", - "fs-extra": "^11.1.0", - "lint-staged": "^13.1.2", - "minimist": "^1.2.8", + "fs-extra": "^11.1.1", + "lint-staged": "^13.2.0", "npm-run-all": "^4.1.5", "picocolors": "^1.0.0", "playwright-chromium": "^1.31.2", - "prettier": "2.8.4", - "prompts": "^2.4.2", + "prettier": "2.8.5", "resolve": "^1.22.1", - "rimraf": "^4.1.2", - "rollup": "^3.18.0", - "semver": "^7.3.8", + "rimraf": "^4.4.0", + "rollup": "^3.20.2", "simple-git-hooks": "^2.8.1", "tslib": "^2.5.0", - "tsx": "^3.12.3", - "typescript": "^4.9.3", + "tsx": "^3.12.6", + "typescript": "^5.0.2", "unbuild": "^1.1.2", "vite": "workspace:*", - "vitepress": "^1.0.0-alpha.49", - "vitest": "^0.29.2", + "vitepress": "^1.0.0-alpha.61", + "vitest": "^0.29.7", "vue": "^3.2.47" }, "simple-git-hooks": { @@ -108,7 +104,7 @@ "eslint --cache --fix" ] }, - "packageManager": "pnpm@7.29.0", + "packageManager": "pnpm@8.2.0", "pnpm": { "overrides": { "vite": "workspace:*" @@ -127,7 +123,8 @@ }, "patchedDependencies": { "dotenv-expand@9.0.0": "patches/dotenv-expand@9.0.0.patch", - "sirv@2.0.2": "patches/sirv@2.0.2.patch" + "sirv@2.0.2": "patches/sirv@2.0.2.patch", + "chokidar@3.5.3": "patches/chokidar@3.5.3.patch" } }, "stackblitz": { diff --git a/packages/create-vite/CHANGELOG.md b/packages/create-vite/CHANGELOG.md index d0aafa6f8e8770..99788f8cd68eae 100644 --- a/packages/create-vite/CHANGELOG.md +++ b/packages/create-vite/CHANGELOG.md @@ -1,3 +1,18 @@ +## 4.3.0-beta.0 (2023-04-06) + +* chore: use @vitejs/release-scripts (#12682) ([9c37cc1](https://github.com/vitejs/vite/commit/9c37cc1)), closes [#12682](https://github.com/vitejs/vite/issues/12682) +* chore(create-vite): revert to vite 4.2 (#12456) ([535c8c5](https://github.com/vitejs/vite/commit/535c8c5)), closes [#12456](https://github.com/vitejs/vite/issues/12456) +* chore(create-vite): rollback to vite 4.1 due to npm publish outage ([d8cb765](https://github.com/vitejs/vite/commit/d8cb765)) +* chore(create-vite): update to vite 4.3 beta ([9b0df5d](https://github.com/vitejs/vite/commit/9b0df5d)) +* feat(create-vite): lit templates will create application instead of library (#12459) ([8186b9b](https://github.com/vitejs/vite/commit/8186b9b)), closes [#12459](https://github.com/vitejs/vite/issues/12459) +* feat(create-vite): stricter TS configs in templates (#12604) ([4ffaeee](https://github.com/vitejs/vite/commit/4ffaeee)), closes [#12604](https://github.com/vitejs/vite/issues/12604) +* feat(create-vite): use typescript 5.0 in templates (#12481) ([8582e2d](https://github.com/vitejs/vite/commit/8582e2d)), closes [#12481](https://github.com/vitejs/vite/issues/12481) +* fix(create-vite): skip lib check in tsconfig templates (#12591) ([a59914c](https://github.com/vitejs/vite/commit/a59914c)), closes [#12591](https://github.com/vitejs/vite/issues/12591) +* fix(create-vite): updated js & ts templates with new react docs link (#12479) ([c327006](https://github.com/vitejs/vite/commit/c327006)), closes [#12479](https://github.com/vitejs/vite/issues/12479) +* fix(deps): update all non-major dependencies (#12389) ([3e60b77](https://github.com/vitejs/vite/commit/3e60b77)), closes [#12389](https://github.com/vitejs/vite/issues/12389) + + + ## 4.2.0 (2023-03-16) * chore(create-vite): update plugin-vue ([e06cda9](https://github.com/vitejs/vite/commit/e06cda9)) diff --git a/packages/create-vite/package.json b/packages/create-vite/package.json index 57f24d87f6ac7f..13202f53eadf96 100644 --- a/packages/create-vite/package.json +++ b/packages/create-vite/package.json @@ -1,6 +1,6 @@ { "name": "create-vite", - "version": "4.2.0", + "version": "4.3.0-beta.0", "type": "module", "license": "MIT", "author": "Evan You", @@ -13,7 +13,6 @@ "template-*", "dist" ], - "main": "index.js", "scripts": { "dev": "unbuild --stub", "build": "unbuild", @@ -33,6 +32,8 @@ }, "homepage": "https://github.com/vitejs/vite/tree/main/packages/create-vite#readme", "devDependencies": { + "@types/minimist": "^1.2.2", + "@types/prompts": "^2.4.4", "cross-spawn": "^7.0.3", "kolorist": "^1.7.0", "minimist": "^1.2.8", diff --git a/packages/create-vite/src/index.ts b/packages/create-vite/src/index.ts index 45d865530d9c3a..dfc9473e23ff55 100755 --- a/packages/create-vite/src/index.ts +++ b/packages/create-vite/src/index.ts @@ -44,16 +44,16 @@ const FRAMEWORKS: Framework[] = [ display: 'Vanilla', color: yellow, variants: [ - { - name: 'vanilla', - display: 'JavaScript', - color: yellow, - }, { name: 'vanilla-ts', display: 'TypeScript', color: blue, }, + { + name: 'vanilla', + display: 'JavaScript', + color: yellow, + }, ], }, { @@ -61,16 +61,16 @@ const FRAMEWORKS: Framework[] = [ display: 'Vue', color: green, variants: [ - { - name: 'vue', - display: 'JavaScript', - color: yellow, - }, { name: 'vue-ts', display: 'TypeScript', color: blue, }, + { + name: 'vue', + display: 'JavaScript', + color: yellow, + }, { name: 'custom-create-vue', display: 'Customize with create-vue ↗', @@ -90,26 +90,26 @@ const FRAMEWORKS: Framework[] = [ display: 'React', color: cyan, variants: [ - { - name: 'react', - display: 'JavaScript', - color: yellow, - }, { name: 'react-ts', display: 'TypeScript', color: blue, }, - { - name: 'react-swc', - display: 'JavaScript + SWC', - color: yellow, - }, { name: 'react-swc-ts', display: 'TypeScript + SWC', color: blue, }, + { + name: 'react', + display: 'JavaScript', + color: yellow, + }, + { + name: 'react-swc', + display: 'JavaScript + SWC', + color: yellow, + }, ], }, { @@ -117,16 +117,16 @@ const FRAMEWORKS: Framework[] = [ display: 'Preact', color: magenta, variants: [ - { - name: 'preact', - display: 'JavaScript', - color: yellow, - }, { name: 'preact-ts', display: 'TypeScript', color: blue, }, + { + name: 'preact', + display: 'JavaScript', + color: yellow, + }, ], }, { @@ -134,16 +134,16 @@ const FRAMEWORKS: Framework[] = [ display: 'Lit', color: lightRed, variants: [ - { - name: 'lit', - display: 'JavaScript', - color: yellow, - }, { name: 'lit-ts', display: 'TypeScript', color: blue, }, + { + name: 'lit', + display: 'JavaScript', + color: yellow, + }, ], }, { @@ -151,16 +151,16 @@ const FRAMEWORKS: Framework[] = [ display: 'Svelte', color: red, variants: [ - { - name: 'svelte', - display: 'JavaScript', - color: yellow, - }, { name: 'svelte-ts', display: 'TypeScript', color: blue, }, + { + name: 'svelte', + display: 'JavaScript', + color: yellow, + }, { name: 'custom-svelte-kit', display: 'SvelteKit ↗', diff --git a/packages/create-vite/template-lit-ts/package.json b/packages/create-vite/template-lit-ts/package.json index 90472689e40891..a65419c5a0adce 100644 --- a/packages/create-vite/template-lit-ts/package.json +++ b/packages/create-vite/template-lit-ts/package.json @@ -3,24 +3,16 @@ "private": true, "version": "0.0.0", "type": "module", - "main": "dist/my-element.es.js", - "exports": { - ".": "./dist/my-element.es.js" - }, - "types": "types/my-element.d.ts", - "files": [ - "dist", - "types" - ], "scripts": { "dev": "vite", - "build": "tsc && vite build" + "build": "tsc && vite build", + "preview": "vite preview" }, "dependencies": { "lit": "^2.6.1" }, "devDependencies": { - "typescript": "^4.9.3", - "vite": "^4.2.0" + "typescript": "^5.0.2", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-lit-ts/tsconfig.json b/packages/create-vite/template-lit-ts/tsconfig.json index b080b2b2cd41a7..75abdef2659446 100644 --- a/packages/create-vite/template-lit-ts/tsconfig.json +++ b/packages/create-vite/template-lit-ts/tsconfig.json @@ -1,23 +1,23 @@ { "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], - "declaration": true, - "emitDeclarationOnly": true, - "outDir": "./types", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "moduleResolution": "Node", - "isolatedModules": true, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "forceConsistentCasingInFileNames": true, - "useDefineForClassFields": false, - "skipLibCheck": true + "noFallthroughCasesInSwitch": true }, - "include": ["src/**/*.ts"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": ["src"] } diff --git a/packages/create-vite/template-lit-ts/tsconfig.node.json b/packages/create-vite/template-lit-ts/tsconfig.node.json deleted file mode 100644 index 9d31e2aed93c87..00000000000000 --- a/packages/create-vite/template-lit-ts/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/packages/create-vite/template-lit-ts/vite.config.ts b/packages/create-vite/template-lit-ts/vite.config.ts deleted file mode 100644 index fe69491e390523..00000000000000 --- a/packages/create-vite/template-lit-ts/vite.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite' - -// https://vitejs.dev/config/ -export default defineConfig({ - build: { - lib: { - entry: 'src/my-element.ts', - formats: ['es'], - }, - rollupOptions: { - external: /^lit/, - }, - }, -}) diff --git a/packages/create-vite/template-lit/package.json b/packages/create-vite/template-lit/package.json index d6dbd42d941a89..971f764124fc5b 100644 --- a/packages/create-vite/template-lit/package.json +++ b/packages/create-vite/template-lit/package.json @@ -3,21 +3,15 @@ "private": true, "version": "0.0.0", "type": "module", - "main": "dist/my-element.es.js", - "exports": { - ".": "./dist/my-element.es.js" - }, - "files": [ - "dist" - ], "scripts": { "dev": "vite", - "build": "vite build" + "build": "vite build", + "preview": "vite preview" }, "dependencies": { "lit": "^2.6.1" }, "devDependencies": { - "vite": "^4.2.0" + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-lit/vite.config.js b/packages/create-vite/template-lit/vite.config.js deleted file mode 100644 index 3847c1f38466f1..00000000000000 --- a/packages/create-vite/template-lit/vite.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite' - -// https://vitejs.dev/config/ -export default defineConfig({ - build: { - lib: { - entry: 'src/my-element.js', - formats: ['es'], - }, - rollupOptions: { - external: /^lit/, - }, - }, -}) diff --git a/packages/create-vite/template-preact-ts/package.json b/packages/create-vite/template-preact-ts/package.json index c077e8826aa894..5fa628bf8f7c67 100644 --- a/packages/create-vite/template-preact-ts/package.json +++ b/packages/create-vite/template-preact-ts/package.json @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "preact": "^10.13.0" + "preact": "^10.13.1" }, "devDependencies": { "@preact/preset-vite": "^2.5.0", - "typescript": "^4.9.3", - "vite": "^4.2.0" + "typescript": "^5.0.2", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-preact-ts/src/main.tsx b/packages/create-vite/template-preact-ts/src/main.tsx index e0ce3e9980eecd..125b2d586a34bc 100644 --- a/packages/create-vite/template-preact-ts/src/main.tsx +++ b/packages/create-vite/template-preact-ts/src/main.tsx @@ -1,5 +1,5 @@ import { render } from 'preact' -import { App } from './app' +import { App } from './app.tsx' import './index.css' render(, document.getElementById('app') as HTMLElement) diff --git a/packages/create-vite/template-preact-ts/tsconfig.json b/packages/create-vite/template-preact-ts/tsconfig.json index 9c1b1e0aa6f1df..21abced1d38ea6 100644 --- a/packages/create-vite/template-preact-ts/tsconfig.json +++ b/packages/create-vite/template-preact-ts/tsconfig.json @@ -1,21 +1,25 @@ { "compilerOptions": { - "target": "ESNext", + "target": "ES2020", "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, "module": "ESNext", - "moduleResolution": "Node", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "jsxImportSource": "preact" + "jsxImportSource": "preact", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/packages/create-vite/template-preact-ts/tsconfig.node.json b/packages/create-vite/template-preact-ts/tsconfig.node.json index 9d31e2aed93c87..42872c59f5b01c 100644 --- a/packages/create-vite/template-preact-ts/tsconfig.node.json +++ b/packages/create-vite/template-preact-ts/tsconfig.node.json @@ -1,8 +1,9 @@ { "compilerOptions": { "composite": true, + "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] diff --git a/packages/create-vite/template-preact/package.json b/packages/create-vite/template-preact/package.json index f723768617a466..5c13d5e6ffcba6 100644 --- a/packages/create-vite/template-preact/package.json +++ b/packages/create-vite/template-preact/package.json @@ -9,10 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "preact": "^10.13.0" + "preact": "^10.13.1" }, "devDependencies": { "@preact/preset-vite": "^2.5.0", - "vite": "^4.2.0" + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-preact/src/main.jsx b/packages/create-vite/template-preact/src/main.jsx index be3fbce92b202b..b088b63d9dad7c 100644 --- a/packages/create-vite/template-preact/src/main.jsx +++ b/packages/create-vite/template-preact/src/main.jsx @@ -1,5 +1,5 @@ import { render } from 'preact' -import { App } from './app' +import { App } from './app.jsx' import './index.css' render(, document.getElementById('app')) diff --git a/packages/create-vite/template-react-ts/package.json b/packages/create-vite/template-react-ts/package.json index 046d3a2cba8c69..700ce50d85d129 100644 --- a/packages/create-vite/template-react-ts/package.json +++ b/packages/create-vite/template-react-ts/package.json @@ -15,8 +15,8 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "@vitejs/plugin-react": "^3.1.0", - "typescript": "^4.9.3", - "vite": "^4.2.0" + "@vitejs/plugin-react": "^4.0.0-beta.0", + "typescript": "^5.0.2", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-react-ts/src/App.tsx b/packages/create-vite/template-react-ts/src/App.tsx index 776eaa027ab01e..afe48ac750194a 100644 --- a/packages/create-vite/template-react-ts/src/App.tsx +++ b/packages/create-vite/template-react-ts/src/App.tsx @@ -7,12 +7,12 @@ function App() { const [count, setCount] = useState(0) return ( -
+ <> @@ -28,7 +28,7 @@ function App() {

Click on the Vite and React logos to learn more

-
+ ) } diff --git a/packages/create-vite/template-react-ts/src/main.tsx b/packages/create-vite/template-react-ts/src/main.tsx index 791f139e242c70..91c03f3fb20c1e 100644 --- a/packages/create-vite/template-react-ts/src/main.tsx +++ b/packages/create-vite/template-react-ts/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App' +import App from './App.tsx' import './index.css' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/packages/create-vite/template-react-ts/tsconfig.json b/packages/create-vite/template-react-ts/tsconfig.json index 3d0a51a86e2024..c81ef9f382291a 100644 --- a/packages/create-vite/template-react-ts/tsconfig.json +++ b/packages/create-vite/template-react-ts/tsconfig.json @@ -1,20 +1,23 @@ { "compilerOptions": { "target": "ESNext", - "useDefineForClassFields": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, "module": "ESNext", - "moduleResolution": "Node", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/packages/create-vite/template-react-ts/tsconfig.node.json b/packages/create-vite/template-react-ts/tsconfig.node.json index 9d31e2aed93c87..42872c59f5b01c 100644 --- a/packages/create-vite/template-react-ts/tsconfig.node.json +++ b/packages/create-vite/template-react-ts/tsconfig.node.json @@ -1,8 +1,9 @@ { "compilerOptions": { "composite": true, + "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] diff --git a/packages/create-vite/template-react/package.json b/packages/create-vite/template-react/package.json index 19640fbd37aad6..08c96622385dd5 100644 --- a/packages/create-vite/template-react/package.json +++ b/packages/create-vite/template-react/package.json @@ -15,7 +15,7 @@ "devDependencies": { "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", - "@vitejs/plugin-react": "^3.1.0", - "vite": "^4.2.0" + "@vitejs/plugin-react": "^4.0.0-beta.0", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-react/src/App.jsx b/packages/create-vite/template-react/src/App.jsx index 2ba469dc82918f..b8b8473a3696b4 100644 --- a/packages/create-vite/template-react/src/App.jsx +++ b/packages/create-vite/template-react/src/App.jsx @@ -7,12 +7,12 @@ function App() { const [count, setCount] = useState(0) return ( -
+ <> @@ -28,7 +28,7 @@ function App() {

Click on the Vite and React logos to learn more

-
+ ) } diff --git a/packages/create-vite/template-react/src/main.jsx b/packages/create-vite/template-react/src/main.jsx index 5cc599199a2091..54b39dd1d900e8 100644 --- a/packages/create-vite/template-react/src/main.jsx +++ b/packages/create-vite/template-react/src/main.jsx @@ -1,6 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App' +import App from './App.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/packages/create-vite/template-svelte-ts/package.json b/packages/create-vite/template-svelte-ts/package.json index 70070a703a3266..fba07484329a9a 100644 --- a/packages/create-vite/template-svelte-ts/package.json +++ b/packages/create-vite/template-svelte-ts/package.json @@ -11,11 +11,11 @@ }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.3", - "@tsconfig/svelte": "^3.0.0", - "svelte": "^3.55.1", + "@tsconfig/svelte": "^4.0.1", + "svelte": "^3.57.0", "svelte-check": "^2.10.3", "tslib": "^2.5.0", - "typescript": "^4.9.3", - "vite": "^4.2.0" + "typescript": "^5.0.2", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-svelte-ts/tsconfig.node.json b/packages/create-vite/template-svelte-ts/tsconfig.node.json index 65dbdb96ae5dcf..494bfe0835347c 100644 --- a/packages/create-vite/template-svelte-ts/tsconfig.node.json +++ b/packages/create-vite/template-svelte-ts/tsconfig.node.json @@ -1,8 +1,9 @@ { "compilerOptions": { "composite": true, + "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "Node" + "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } diff --git a/packages/create-vite/template-svelte/README.md b/packages/create-vite/template-svelte/README.md index 69c2ac55e18166..382941e05ce856 100644 --- a/packages/create-vite/template-svelte/README.md +++ b/packages/create-vite/template-svelte/README.md @@ -35,7 +35,7 @@ It is likely that most cases of changing variable types in runtime are likely to **Why is HMR not preserving my local component state?** -HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state). If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. diff --git a/packages/create-vite/template-svelte/jsconfig.json b/packages/create-vite/template-svelte/jsconfig.json index e596c582326d98..5696a2de74a0cd 100644 --- a/packages/create-vite/template-svelte/jsconfig.json +++ b/packages/create-vite/template-svelte/jsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "moduleResolution": "Node", + "moduleResolution": "bundler", "target": "ESNext", "module": "ESNext", /** @@ -8,7 +8,7 @@ * a value or a type, so tell TypeScript to enforce using * `import type` instead of `import` for Types. */ - "importsNotUsedAsValues": "error", + "verbatimModuleSyntax": true, "isolatedModules": true, "resolveJsonModule": true, /** @@ -18,7 +18,6 @@ "sourceMap": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable this if you'd like to use dynamic types. diff --git a/packages/create-vite/template-svelte/package.json b/packages/create-vite/template-svelte/package.json index b33e29c76a5375..c10273bff3b697 100644 --- a/packages/create-vite/template-svelte/package.json +++ b/packages/create-vite/template-svelte/package.json @@ -10,7 +10,7 @@ }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.0.3", - "svelte": "^3.55.1", - "vite": "^4.2.0" + "svelte": "^3.57.0", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-vanilla-ts/package.json b/packages/create-vite/template-vanilla-ts/package.json index c23eef8e5a9902..78dd609fe24c15 100644 --- a/packages/create-vite/template-vanilla-ts/package.json +++ b/packages/create-vite/template-vanilla-ts/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "devDependencies": { - "typescript": "^4.9.3", - "vite": "^4.2.0" + "typescript": "^5.0.2", + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-vanilla-ts/src/main.ts b/packages/create-vite/template-vanilla-ts/src/main.ts index 2f852a7cf8532f..791547b0d382f5 100644 --- a/packages/create-vite/template-vanilla-ts/src/main.ts +++ b/packages/create-vite/template-vanilla-ts/src/main.ts @@ -1,7 +1,7 @@ import './style.css' import typescriptLogo from './typescript.svg' import viteLogo from '/vite.svg' -import { setupCounter } from './counter' +import { setupCounter } from './counter.ts' document.querySelector('#app')!.innerHTML = `
diff --git a/packages/create-vite/template-vanilla-ts/tsconfig.json b/packages/create-vite/template-vanilla-ts/tsconfig.json index eac16d14a6f2e2..75abdef2659446 100644 --- a/packages/create-vite/template-vanilla-ts/tsconfig.json +++ b/packages/create-vite/template-vanilla-ts/tsconfig.json @@ -1,19 +1,23 @@ { "compilerOptions": { - "target": "ESNext", + "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "lib": ["ESNext", "DOM"], - "moduleResolution": "Node", - "strict": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, - "esModuleInterop": true, "noEmit": true, + + /* Linting */ + "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noImplicitReturns": true, - "skipLibCheck": true + "noFallthroughCasesInSwitch": true }, "include": ["src"] } diff --git a/packages/create-vite/template-vanilla/package.json b/packages/create-vite/template-vanilla/package.json index 9de9447933d69f..f0b5e60c38ff33 100644 --- a/packages/create-vite/template-vanilla/package.json +++ b/packages/create-vite/template-vanilla/package.json @@ -9,6 +9,6 @@ "preview": "vite preview" }, "devDependencies": { - "vite": "^4.2.0" + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/template-vue-ts/package.json b/packages/create-vite/template-vue-ts/package.json index b3adc619dd6632..583a7267fe19b9 100644 --- a/packages/create-vite/template-vue-ts/package.json +++ b/packages/create-vite/template-vue-ts/package.json @@ -13,8 +13,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^4.1.0", - "typescript": "^4.9.3", - "vite": "^4.2.0", + "typescript": "^5.0.2", + "vite": "^4.3.0-beta.2", "vue-tsc": "^1.2.0" } } diff --git a/packages/create-vite/template-vue-ts/tsconfig.json b/packages/create-vite/template-vue-ts/tsconfig.json index b557c4047cac64..f82888f3d0965c 100644 --- a/packages/create-vite/template-vue-ts/tsconfig.json +++ b/packages/create-vite/template-vue-ts/tsconfig.json @@ -1,17 +1,24 @@ { "compilerOptions": { - "target": "ESNext", + "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", - "moduleResolution": "Node", - "strict": true, - "jsx": "preserve", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, - "esModuleInterop": true, - "lib": ["ESNext", "DOM"], - "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/packages/create-vite/template-vue-ts/tsconfig.node.json b/packages/create-vite/template-vue-ts/tsconfig.node.json index 9d31e2aed93c87..42872c59f5b01c 100644 --- a/packages/create-vite/template-vue-ts/tsconfig.node.json +++ b/packages/create-vite/template-vue-ts/tsconfig.node.json @@ -1,8 +1,9 @@ { "compilerOptions": { "composite": true, + "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] diff --git a/packages/create-vite/template-vue/package.json b/packages/create-vite/template-vue/package.json index 754830b42723d5..e54568959bec8d 100644 --- a/packages/create-vite/template-vue/package.json +++ b/packages/create-vite/template-vue/package.json @@ -13,6 +13,6 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^4.1.0", - "vite": "^4.2.0" + "vite": "^4.3.0-beta.2" } } diff --git a/packages/create-vite/tsconfig.json b/packages/create-vite/tsconfig.json index 3a66e6179b5f97..65fffe74592b9b 100644 --- a/packages/create-vite/tsconfig.json +++ b/packages/create-vite/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "dist", "target": "ES2020", "module": "ES2020", - "moduleResolution": "Node", + "moduleResolution": "bundler", "strict": true, "skipLibCheck": true, "declaration": false, diff --git a/packages/plugin-legacy/package.json b/packages/plugin-legacy/package.json index 4693a1338f99fb..80ed27af7cf374 100644 --- a/packages/plugin-legacy/package.json +++ b/packages/plugin-legacy/package.json @@ -41,10 +41,10 @@ }, "homepage": "https://github.com/vitejs/vite/tree/main/packages/plugin-legacy#readme", "dependencies": { - "@babel/core": "^7.21.0", + "@babel/core": "^7.21.3", "@babel/preset-env": "^7.20.2", "browserslist": "^4.21.5", - "core-js": "^3.29.0", + "core-js": "^3.29.1", "magic-string": "^0.30.0", "regenerator-runtime": "^0.13.11", "systemjs": "^6.14.0" diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index 84453271e65736..29dfb4f754a128 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -1,4 +1,4 @@ -/* eslint-disable node/no-extraneous-import */ +/* eslint-disable n/no-extraneous-import */ import path from 'node:path' import { createHash } from 'node:crypto' import { createRequire } from 'node:module' diff --git a/packages/vite/CHANGELOG.md b/packages/vite/CHANGELOG.md index 1d67de9254d524..5a938eb762f34c 100644 --- a/packages/vite/CHANGELOG.md +++ b/packages/vite/CHANGELOG.md @@ -1,3 +1,199 @@ +## 4.3.0-beta.8 (2023-04-19) + +* fix: escape msg in render restricted error html (#12889) ([3aa2127](https://github.com/vitejs/vite/commit/3aa2127)), closes [#12889](https://github.com/vitejs/vite/issues/12889) +* fix: yarn pnp considerBuiltins (#12903) ([a0e10d5](https://github.com/vitejs/vite/commit/a0e10d5)), closes [#12903](https://github.com/vitejs/vite/issues/12903) +* refactor(eslint): migrate to `eslint-plugin-n` (#12895) ([62ebe28](https://github.com/vitejs/vite/commit/62ebe28)), closes [#12895](https://github.com/vitejs/vite/issues/12895) +* feat: expose `isFileServingAllowed` as public utility (#12894) ([93e095c](https://github.com/vitejs/vite/commit/93e095c)), closes [#12894](https://github.com/vitejs/vite/issues/12894) + + + +## 4.3.0-beta.7 (2023-04-17) + +* fix: broken middleware name (#12871) ([32bef57](https://github.com/vitejs/vite/commit/32bef57)), closes [#12871](https://github.com/vitejs/vite/issues/12871) +* fix: cleanUpStaleCacheDirs once per process (#12847) ([2c58b6e](https://github.com/vitejs/vite/commit/2c58b6e)), closes [#12847](https://github.com/vitejs/vite/issues/12847) +* fix(build): do not warn when URL in CSS is externalized (#12873) ([1510996](https://github.com/vitejs/vite/commit/1510996)), closes [#12873](https://github.com/vitejs/vite/issues/12873) +* refactor: simplify crawlEndFinder (#12868) ([31f8b51](https://github.com/vitejs/vite/commit/31f8b51)), closes [#12868](https://github.com/vitejs/vite/issues/12868) +* perf: parallelize await exportsData from depsInfo (#12869) ([ab3a530](https://github.com/vitejs/vite/commit/ab3a530)), closes [#12869](https://github.com/vitejs/vite/issues/12869) + + + +## 4.3.0-beta.6 (2023-04-14) + +* fix: build time deps optimization, and ensure single crawl end call (#12851) ([fa30879](https://github.com/vitejs/vite/commit/fa30879)), closes [#12851](https://github.com/vitejs/vite/issues/12851) +* fix: correct vite config temporary name (#12833) ([cdd9c23](https://github.com/vitejs/vite/commit/cdd9c23)), closes [#12833](https://github.com/vitejs/vite/issues/12833) +* fix(importAnalysis): warning on ExportAllDeclaration (#12799) ([5136b9b](https://github.com/vitejs/vite/commit/5136b9b)), closes [#12799](https://github.com/vitejs/vite/issues/12799) +* fix(optimizer): start optimizer after buildStart (#12832) ([cfe75ee](https://github.com/vitejs/vite/commit/cfe75ee)), closes [#12832](https://github.com/vitejs/vite/issues/12832) + + + +## 4.3.0-beta.5 (2023-04-11) + +* fix: handle try-catch for fs promise when resolve https config (#12808) ([0bba402](https://github.com/vitejs/vite/commit/0bba402)), closes [#12808](https://github.com/vitejs/vite/issues/12808) +* fix(build): correctly handle warning ignore list (#12831) ([8830532](https://github.com/vitejs/vite/commit/8830532)), closes [#12831](https://github.com/vitejs/vite/issues/12831) +* fix(resolve): use different importer check for css imports (#12815) ([d037327](https://github.com/vitejs/vite/commit/d037327)), closes [#12815](https://github.com/vitejs/vite/issues/12815) +* docs: fix pnpm link (#12803) ([ad358da](https://github.com/vitejs/vite/commit/ad358da)), closes [#12803](https://github.com/vitejs/vite/issues/12803) + + + +## 4.3.0-beta.4 (2023-04-09) + +* fix: ignore sideEffects for scripts imported from html (#12786) ([f09551f](https://github.com/vitejs/vite/commit/f09551f)), closes [#12786](https://github.com/vitejs/vite/issues/12786) +* fix: warn on build when bundling code that uses nodejs built in module (#12616) ([72050f9](https://github.com/vitejs/vite/commit/72050f9)), closes [#12616](https://github.com/vitejs/vite/issues/12616) +* fix(cli): pass mode to optimize command (#12776) ([da38ad8](https://github.com/vitejs/vite/commit/da38ad8)), closes [#12776](https://github.com/vitejs/vite/issues/12776) +* fix(css): resolve at import from dependency basedir (#12796) ([46bdf7d](https://github.com/vitejs/vite/commit/46bdf7d)), closes [#12796](https://github.com/vitejs/vite/issues/12796) +* fix(worker): asset in iife worker and relative base (#12697) ([ddefc06](https://github.com/vitejs/vite/commit/ddefc06)), closes [#12697](https://github.com/vitejs/vite/issues/12697) +* fix(worker): return null for shouldTransformCachedModule (#12797) ([ea5f6fc](https://github.com/vitejs/vite/commit/ea5f6fc)), closes [#12797](https://github.com/vitejs/vite/issues/12797) +* perf: avoid side effects resolving in dev and in the optimizer/scanner (#12789) ([fb904f9](https://github.com/vitejs/vite/commit/fb904f9)), closes [#12789](https://github.com/vitejs/vite/issues/12789) + + + +## 4.3.0-beta.3 (2023-04-07) + +* fix: allow onwarn to override vite default warning handling (#12757) ([f736930](https://github.com/vitejs/vite/commit/f736930)), closes [#12757](https://github.com/vitejs/vite/issues/12757) +* fix: ensure module in graph before transforming (#12774) ([44ad321](https://github.com/vitejs/vite/commit/44ad321)), closes [#12774](https://github.com/vitejs/vite/issues/12774) +* fix: update package cache watcher (#12772) ([a78588f](https://github.com/vitejs/vite/commit/a78588f)), closes [#12772](https://github.com/vitejs/vite/issues/12772) +* perf: parallelize imports processing in import analysis plugin (#12754) ([037a6c7](https://github.com/vitejs/vite/commit/037a6c7)), closes [#12754](https://github.com/vitejs/vite/issues/12754) +* perf: unresolvedUrlToModule promise cache (#12725) ([80c526e](https://github.com/vitejs/vite/commit/80c526e)), closes [#12725](https://github.com/vitejs/vite/issues/12725) +* perf(resolve): avoid tryFsResolve for /@fs/ paths (#12450) ([3ef8aaa](https://github.com/vitejs/vite/commit/3ef8aaa)), closes [#12450](https://github.com/vitejs/vite/issues/12450) +* perf(resolve): reduce vite client path checks (#12471) ([c49af23](https://github.com/vitejs/vite/commit/c49af23)), closes [#12471](https://github.com/vitejs/vite/issues/12471) +* refactor: use simpler resolve for nested optimized deps (#12770) ([d202588](https://github.com/vitejs/vite/commit/d202588)), closes [#12770](https://github.com/vitejs/vite/issues/12770) +* chore: improve debug log filtering (#12763) ([da1cb02](https://github.com/vitejs/vite/commit/da1cb02)), closes [#12763](https://github.com/vitejs/vite/issues/12763) + + + +## 4.3.0-beta.2 (2023-04-05) + +* fix: avoid clean up while committing deps folder (#12722) ([3f4d109](https://github.com/vitejs/vite/commit/3f4d109)), closes [#12722](https://github.com/vitejs/vite/issues/12722) +* fix: ignore pnp resolve error (#12719) ([2d30ae5](https://github.com/vitejs/vite/commit/2d30ae5)), closes [#12719](https://github.com/vitejs/vite/issues/12719) +* fix: leave fully dynamic import.meta.url asset (fixes #10306) (#10549) ([56802b1](https://github.com/vitejs/vite/commit/56802b1)), closes [#10306](https://github.com/vitejs/vite/issues/10306) [#10549](https://github.com/vitejs/vite/issues/10549) +* fix: output combined sourcemap in importAnalysisBuild plugin (#12642) ([d051639](https://github.com/vitejs/vite/commit/d051639)), closes [#12642](https://github.com/vitejs/vite/issues/12642) +* fix: take in relative assets path fixes from rollup (#12695) ([81e44dd](https://github.com/vitejs/vite/commit/81e44dd)), closes [#12695](https://github.com/vitejs/vite/issues/12695) +* fix: throws error when plugin tries to resolve ID to external URL (#11731) ([49674b5](https://github.com/vitejs/vite/commit/49674b5)), closes [#11731](https://github.com/vitejs/vite/issues/11731) +* fix(css): css file emit synchronously (#12558) ([8e30025](https://github.com/vitejs/vite/commit/8e30025)), closes [#12558](https://github.com/vitejs/vite/issues/12558) +* fix(import-analysis): escape quotes correctly (#12688) ([1638ebd](https://github.com/vitejs/vite/commit/1638ebd)), closes [#12688](https://github.com/vitejs/vite/issues/12688) +* fix(optimizer): load the correct lock file (#12700) ([889eebe](https://github.com/vitejs/vite/commit/889eebe)), closes [#12700](https://github.com/vitejs/vite/issues/12700) +* fix(server): delay ws server listen when restart (#12734) ([abe9274](https://github.com/vitejs/vite/commit/abe9274)), closes [#12734](https://github.com/vitejs/vite/issues/12734) +* fix(ssr): load sourcemaps alongside modules (#11780) ([be95050](https://github.com/vitejs/vite/commit/be95050)), closes [#11780](https://github.com/vitejs/vite/issues/11780) +* fix(ssr): show ssr module loader error stack (#12651) ([050c0f9](https://github.com/vitejs/vite/commit/050c0f9)), closes [#12651](https://github.com/vitejs/vite/issues/12651) +* fix(worker): disable manifest plugins in worker build (#12661) ([20b8ef4](https://github.com/vitejs/vite/commit/20b8ef4)), closes [#12661](https://github.com/vitejs/vite/issues/12661) +* fix(worker): worker import.meta.url should not depends on document in iife mode (#12629) ([65f5ed2](https://github.com/vitejs/vite/commit/65f5ed2)), closes [#12629](https://github.com/vitejs/vite/issues/12629) +* refactor: `import.meta.url` condition from renderChunk hook of worker plugin (#12696) ([fdef8fd](https://github.com/vitejs/vite/commit/fdef8fd)), closes [#12696](https://github.com/vitejs/vite/issues/12696) +* refactor: clean up preTransformRequest (#12672) ([561227c](https://github.com/vitejs/vite/commit/561227c)), closes [#12672](https://github.com/vitejs/vite/issues/12672) +* refactor: make debugger nullable (#12687) ([89e4977](https://github.com/vitejs/vite/commit/89e4977)), closes [#12687](https://github.com/vitejs/vite/issues/12687) +* refactor: remove `ensureVolumeInPath` (#12690) ([a3150ee](https://github.com/vitejs/vite/commit/a3150ee)), closes [#12690](https://github.com/vitejs/vite/issues/12690) +* refactor: remove unused exports data props (#12740) ([4538bfe](https://github.com/vitejs/vite/commit/4538bfe)), closes [#12740](https://github.com/vitejs/vite/issues/12740) +* refactor: use `resolvePackageData` in `requireResolveFromRootWithFallback` (#12712) ([1ea38e2](https://github.com/vitejs/vite/commit/1ea38e2)), closes [#12712](https://github.com/vitejs/vite/issues/12712) +* refactor(css): simplify cached import code (#12730) ([0646754](https://github.com/vitejs/vite/commit/0646754)), closes [#12730](https://github.com/vitejs/vite/issues/12730) +* feat: reuse existing style elements in dev (#12678) ([3a41bd8](https://github.com/vitejs/vite/commit/3a41bd8)), closes [#12678](https://github.com/vitejs/vite/issues/12678) +* feat: skip pinging the server when the tab is not shown (#12698) ([bedcd8f](https://github.com/vitejs/vite/commit/bedcd8f)), closes [#12698](https://github.com/vitejs/vite/issues/12698) +* feat(create-vite): use typescript 5.0 in templates (#12481) ([8582e2d](https://github.com/vitejs/vite/commit/8582e2d)), closes [#12481](https://github.com/vitejs/vite/issues/12481) +* perf: avoid new URL() in hot path (#12654) ([f4e2fdf](https://github.com/vitejs/vite/commit/f4e2fdf)), closes [#12654](https://github.com/vitejs/vite/issues/12654) +* perf: improve isFileReadable performance (#12397) ([acf3a14](https://github.com/vitejs/vite/commit/acf3a14)), closes [#12397](https://github.com/vitejs/vite/issues/12397) +* perf: module graph url shortcuts (#12635) ([c268cfa](https://github.com/vitejs/vite/commit/c268cfa)), closes [#12635](https://github.com/vitejs/vite/issues/12635) +* perf: reduce runOptimizerIfIdleAfterMs time (#12614) ([d026a65](https://github.com/vitejs/vite/commit/d026a65)), closes [#12614](https://github.com/vitejs/vite/issues/12614) +* perf: shorcircuit resolve in ensure entry from url (#12655) ([82137d6](https://github.com/vitejs/vite/commit/82137d6)), closes [#12655](https://github.com/vitejs/vite/issues/12655) +* perf: skip es-module-lexer if have no dynamic imports (#12732) ([5d07d7c](https://github.com/vitejs/vite/commit/5d07d7c)), closes [#12732](https://github.com/vitejs/vite/issues/12732) +* perf: start preprocessing static imports before updating module graph (#12723) ([c90b46e](https://github.com/vitejs/vite/commit/c90b46e)), closes [#12723](https://github.com/vitejs/vite/issues/12723) +* perf: use package cache for one off resolve (#12744) ([77bf4ef](https://github.com/vitejs/vite/commit/77bf4ef)), closes [#12744](https://github.com/vitejs/vite/issues/12744) +* perf(css): cache lazy import (#12721) ([fedb080](https://github.com/vitejs/vite/commit/fedb080)), closes [#12721](https://github.com/vitejs/vite/issues/12721) +* perf(hmr): keep track of already traversed modules when propagating update (#12658) ([3b912fb](https://github.com/vitejs/vite/commit/3b912fb)), closes [#12658](https://github.com/vitejs/vite/issues/12658) +* perf(moduleGraph): resolve dep urls in parallel (#12619) ([4823fec](https://github.com/vitejs/vite/commit/4823fec)), closes [#12619](https://github.com/vitejs/vite/issues/12619) +* perf(resolve): skip for virtual files (#12638) ([9e13f5f](https://github.com/vitejs/vite/commit/9e13f5f)), closes [#12638](https://github.com/vitejs/vite/issues/12638) +* chore: fix resolve debug log timing (#12746) ([22f6ae6](https://github.com/vitejs/vite/commit/22f6ae6)), closes [#12746](https://github.com/vitejs/vite/issues/12746) +* chore: revert custom license resolve (#12709) ([621bb2f](https://github.com/vitejs/vite/commit/621bb2f)), closes [#12709](https://github.com/vitejs/vite/issues/12709) +* chore: set target in tsconfig.check.json (#12675) ([15177a1](https://github.com/vitejs/vite/commit/15177a1)), closes [#12675](https://github.com/vitejs/vite/issues/12675) +* chore(optimizer): remove redundant setTimeout call in scan process (#12718) ([0ce0e93](https://github.com/vitejs/vite/commit/0ce0e93)), closes [#12718](https://github.com/vitejs/vite/issues/12718) +* chore(optimizer): show full optimized deps list (#12686) ([8bef662](https://github.com/vitejs/vite/commit/8bef662)), closes [#12686](https://github.com/vitejs/vite/issues/12686) + + + +## 4.3.0-beta.1 (2023-03-29) + +* feat: use preview server parameter in preview server hook (#11647) ([4c142ea](https://github.com/vitejs/vite/commit/4c142ea)), closes [#11647](https://github.com/vitejs/vite/issues/11647) +* feat(reporter): show gzip info for all compressible files (fix #11288) (#12485) ([03502c8](https://github.com/vitejs/vite/commit/03502c8)), closes [#11288](https://github.com/vitejs/vite/issues/11288) [#12485](https://github.com/vitejs/vite/issues/12485) +* feat(server): allow to import `data:` uris (#12645) ([4886d9f](https://github.com/vitejs/vite/commit/4886d9f)), closes [#12645](https://github.com/vitejs/vite/issues/12645) +* fix: avoid temporal optimize deps dirs (#12582) ([ff92f2f](https://github.com/vitejs/vite/commit/ff92f2f)), closes [#12582](https://github.com/vitejs/vite/issues/12582) +* fix: await `buildStart` before server start (#12647) ([871d353](https://github.com/vitejs/vite/commit/871d353)), closes [#12647](https://github.com/vitejs/vite/issues/12647) +* fix: call `buildStart` only once when using next port (#12624) ([e10c6bd](https://github.com/vitejs/vite/commit/e10c6bd)), closes [#12624](https://github.com/vitejs/vite/issues/12624) +* fix: sourcemapIgnoreList for optimizedDeps (#12633) ([c1d3fc9](https://github.com/vitejs/vite/commit/c1d3fc9)), closes [#12633](https://github.com/vitejs/vite/issues/12633) +* fix: splitFileAndPostfix works as cleanUrl (#12572) ([276725f](https://github.com/vitejs/vite/commit/276725f)), closes [#12572](https://github.com/vitejs/vite/issues/12572) +* fix: throw error on build optimizeDeps issue (#12560) ([02a46d7](https://github.com/vitejs/vite/commit/02a46d7)), closes [#12560](https://github.com/vitejs/vite/issues/12560) +* fix: use nearest pkg to resolved for moduleSideEffects (#12628) ([1dfecc8](https://github.com/vitejs/vite/commit/1dfecc8)), closes [#12628](https://github.com/vitejs/vite/issues/12628) +* fix(css): use `charset: 'utf8'` by default for css (#12565) ([c20a064](https://github.com/vitejs/vite/commit/c20a064)), closes [#12565](https://github.com/vitejs/vite/issues/12565) +* fix(html): dont pretransform public scripts (#12650) ([4f0af3f](https://github.com/vitejs/vite/commit/4f0af3f)), closes [#12650](https://github.com/vitejs/vite/issues/12650) +* perf: avoid fsp.unlink if we don't use the promise (#12589) ([19d1980](https://github.com/vitejs/vite/commit/19d1980)), closes [#12589](https://github.com/vitejs/vite/issues/12589) +* perf: back to temporal optimizer dirs (#12622) ([8da0422](https://github.com/vitejs/vite/commit/8da0422)), closes [#12622](https://github.com/vitejs/vite/issues/12622) +* perf: cache `depsCacheDirPrefix` value for `isOptimizedDepFile` (#12601) ([edbd262](https://github.com/vitejs/vite/commit/edbd262)), closes [#12601](https://github.com/vitejs/vite/issues/12601) +* perf: improve cleanUrl util (#12573) ([68d500e](https://github.com/vitejs/vite/commit/68d500e)), closes [#12573](https://github.com/vitejs/vite/issues/12573) +* perf: non-blocking write of optimized dep files (#12603) ([2f5f968](https://github.com/vitejs/vite/commit/2f5f968)), closes [#12603](https://github.com/vitejs/vite/issues/12603) +* perf: try using realpathSync.native in Windows (#12580) ([1cc99f8](https://github.com/vitejs/vite/commit/1cc99f8)), closes [#12580](https://github.com/vitejs/vite/issues/12580) +* perf: use fsp in more cases (#12553) ([e9b92f5](https://github.com/vitejs/vite/commit/e9b92f5)), closes [#12553](https://github.com/vitejs/vite/issues/12553) +* perf(html): apply preTransformRequest for html scripts (#12599) ([420782c](https://github.com/vitejs/vite/commit/420782c)), closes [#12599](https://github.com/vitejs/vite/issues/12599) +* perf(optimizer): bulk optimizer delay (#12609) ([c881971](https://github.com/vitejs/vite/commit/c881971)), closes [#12609](https://github.com/vitejs/vite/issues/12609) +* perf(optimizer): start optimizer early (#12593) ([4f9b8b4](https://github.com/vitejs/vite/commit/4f9b8b4)), closes [#12593](https://github.com/vitejs/vite/issues/12593) +* perf(resolve): avoid isWorkerRequest and clean up .ts imported a .js (#12571) ([8ab1438](https://github.com/vitejs/vite/commit/8ab1438)), closes [#12571](https://github.com/vitejs/vite/issues/12571) +* perf(resolve): findNearestMainPackageData instead of lookupFile (#12576) ([54b376f](https://github.com/vitejs/vite/commit/54b376f)), closes [#12576](https://github.com/vitejs/vite/issues/12576) +* perf(server): only watch .env files in envDir (#12587) ([26d8e72](https://github.com/vitejs/vite/commit/26d8e72)), closes [#12587](https://github.com/vitejs/vite/issues/12587) +* refactor: improve scanner logs (#12578) ([9925a72](https://github.com/vitejs/vite/commit/9925a72)), closes [#12578](https://github.com/vitejs/vite/issues/12578) +* refactor: isInNodeModules util (#12588) ([fb3245a](https://github.com/vitejs/vite/commit/fb3245a)), closes [#12588](https://github.com/vitejs/vite/issues/12588) +* refactor: remove `idToPkgMap` (#12564) ([a326ec8](https://github.com/vitejs/vite/commit/a326ec8)), closes [#12564](https://github.com/vitejs/vite/issues/12564) +* refactor: simplify lookupFile (#12585) ([4215e22](https://github.com/vitejs/vite/commit/4215e22)), closes [#12585](https://github.com/vitejs/vite/issues/12585) +* refactor: tryStatSync as util (#12575) ([92601db](https://github.com/vitejs/vite/commit/92601db)), closes [#12575](https://github.com/vitejs/vite/issues/12575) +* refactor: use findNearestPackageData in more places (#12577) ([35faae9](https://github.com/vitejs/vite/commit/35faae9)), closes [#12577](https://github.com/vitejs/vite/issues/12577) + + + +## 4.3.0-beta.0 (2023-03-23) + +* perf: avoid execSync on openBrowser (#12510) ([a2af2f0](https://github.com/vitejs/vite/commit/a2af2f0)), closes [#12510](https://github.com/vitejs/vite/issues/12510) +* perf: extract regex and use Map in data-uri plugin (#12500) ([137e63d](https://github.com/vitejs/vite/commit/137e63d)), closes [#12500](https://github.com/vitejs/vite/issues/12500) +* perf: extract vite:resolve internal functions (#12522) ([6ea4be2](https://github.com/vitejs/vite/commit/6ea4be2)), closes [#12522](https://github.com/vitejs/vite/issues/12522) +* perf: improve package cache usage (#12512) ([abc2b9c](https://github.com/vitejs/vite/commit/abc2b9c)), closes [#12512](https://github.com/vitejs/vite/issues/12512) +* perf: more regex improvements (#12520) ([abf536f](https://github.com/vitejs/vite/commit/abf536f)), closes [#12520](https://github.com/vitejs/vite/issues/12520) +* perf: regex to startsWith/slice in utils (#12532) ([debc6e2](https://github.com/vitejs/vite/commit/debc6e2)), closes [#12532](https://github.com/vitejs/vite/issues/12532) +* perf: remove regex in ImportMetaURL plugins (#12502) ([1030049](https://github.com/vitejs/vite/commit/1030049)), closes [#12502](https://github.com/vitejs/vite/issues/12502) +* perf: replace endsWith with === (#12539) ([7eb52ec](https://github.com/vitejs/vite/commit/7eb52ec)), closes [#12539](https://github.com/vitejs/vite/issues/12539) +* perf: replace startsWith with === (#12531) ([9cce026](https://github.com/vitejs/vite/commit/9cce026)), closes [#12531](https://github.com/vitejs/vite/issues/12531) +* perf: reuse regex in plugins (#12518) ([da43936](https://github.com/vitejs/vite/commit/da43936)), closes [#12518](https://github.com/vitejs/vite/issues/12518) +* perf: use `safeRealpath` in `getRealpath` (#12551) ([cec2320](https://github.com/vitejs/vite/commit/cec2320)), closes [#12551](https://github.com/vitejs/vite/issues/12551) +* perf(css): improve postcss config resolve (#12484) ([58e99b6](https://github.com/vitejs/vite/commit/58e99b6)), closes [#12484](https://github.com/vitejs/vite/issues/12484) +* perf(esbuild): make tsconfck non-blocking (#12548) ([e5cdff7](https://github.com/vitejs/vite/commit/e5cdff7)), closes [#12548](https://github.com/vitejs/vite/issues/12548) +* perf(esbuild): update tsconfck to consume faster find-all implementation (#12541) ([b6ea25a](https://github.com/vitejs/vite/commit/b6ea25a)), closes [#12541](https://github.com/vitejs/vite/issues/12541) +* perf(resolve): fix browser mapping nearest package.json check (#12550) ([eac376e](https://github.com/vitejs/vite/commit/eac376e)), closes [#12550](https://github.com/vitejs/vite/issues/12550) +* perf(resolve): improve package.json resolve speed (#12441) ([1fc8c65](https://github.com/vitejs/vite/commit/1fc8c65)), closes [#12441](https://github.com/vitejs/vite/issues/12441) +* perf(resolve): refactor package.json handling for deep imports (#12461) ([596b661](https://github.com/vitejs/vite/commit/596b661)), closes [#12461](https://github.com/vitejs/vite/issues/12461) +* perf(resolve): refactor tryFsResolve and tryResolveFile (#12542) ([3f70f47](https://github.com/vitejs/vite/commit/3f70f47)) +* perf(resolve): skip absolute paths in root as url checks (#12476) ([8d2931b](https://github.com/vitejs/vite/commit/8d2931b)), closes [#12476](https://github.com/vitejs/vite/issues/12476) +* perf(resolve): support # in path only for dependencies (#12469) ([6559fc7](https://github.com/vitejs/vite/commit/6559fc7)), closes [#12469](https://github.com/vitejs/vite/issues/12469) +* fix: avoid crash because of no access permission (#12552) ([eea1682](https://github.com/vitejs/vite/commit/eea1682)), closes [#12552](https://github.com/vitejs/vite/issues/12552) +* fix: esbuild complains with extra fields (#12516) ([7be0ba5](https://github.com/vitejs/vite/commit/7be0ba5)), closes [#12516](https://github.com/vitejs/vite/issues/12516) +* fix: escape replacements in clientInjections (#12486) ([3765067](https://github.com/vitejs/vite/commit/3765067)), closes [#12486](https://github.com/vitejs/vite/issues/12486) +* fix: open browser reuse logic (#12535) ([04d14af](https://github.com/vitejs/vite/commit/04d14af)), closes [#12535](https://github.com/vitejs/vite/issues/12535) +* fix: prevent error on not set location href (#12494) ([2fb8527](https://github.com/vitejs/vite/commit/2fb8527)), closes [#12494](https://github.com/vitejs/vite/issues/12494) +* fix: simplify prettyUrl (#12488) ([ebe5aa5](https://github.com/vitejs/vite/commit/ebe5aa5)), closes [#12488](https://github.com/vitejs/vite/issues/12488) +* fix(config): add random number to temp transpiled file (#12150) ([2b2ba61](https://github.com/vitejs/vite/commit/2b2ba61)), closes [#12150](https://github.com/vitejs/vite/issues/12150) +* fix(deps): update all non-major dependencies (#12389) ([3e60b77](https://github.com/vitejs/vite/commit/3e60b77)), closes [#12389](https://github.com/vitejs/vite/issues/12389) +* fix(html): public asset urls always being treated as paths (fix #11857) (#11870) ([46d1352](https://github.com/vitejs/vite/commit/46d1352)), closes [#11857](https://github.com/vitejs/vite/issues/11857) [#11870](https://github.com/vitejs/vite/issues/11870) +* fix(ssr): hoist import statements to the top (#12274) ([33baff5](https://github.com/vitejs/vite/commit/33baff5)), closes [#12274](https://github.com/vitejs/vite/issues/12274) +* fix(ssr): hoist re-exports with imports (#12530) ([45549e4](https://github.com/vitejs/vite/commit/45549e4)), closes [#12530](https://github.com/vitejs/vite/issues/12530) +* feat: add opus filetype to assets & mime types (#12526) ([63524ba](https://github.com/vitejs/vite/commit/63524ba)), closes [#12526](https://github.com/vitejs/vite/issues/12526) +* refactor: esbuild plugin config logic (#12493) ([45b5b0f](https://github.com/vitejs/vite/commit/45b5b0f)), closes [#12493](https://github.com/vitejs/vite/issues/12493) +* chore: better error hint for data URI mime type (#12496) ([0cb9171](https://github.com/vitejs/vite/commit/0cb9171)), closes [#12496](https://github.com/vitejs/vite/issues/12496) +* chore: remove unneded async in resolveId hook (#12499) ([e6337aa](https://github.com/vitejs/vite/commit/e6337aa)), closes [#12499](https://github.com/vitejs/vite/issues/12499) +* chore: upgrade rollup 3.20.0 (#12497) ([7288a24](https://github.com/vitejs/vite/commit/7288a24)), closes [#12497](https://github.com/vitejs/vite/issues/12497) +* build: should generate Hi-res sourcemap for dev (#12501) ([1502617](https://github.com/vitejs/vite/commit/1502617)), closes [#12501](https://github.com/vitejs/vite/issues/12501) + + + +## 4.2.1 (2023-03-20) + +* fix: add `virtual:` to virtual module source map ignore (#12444) ([c4aa28f](https://github.com/vitejs/vite/commit/c4aa28f)), closes [#12444](https://github.com/vitejs/vite/issues/12444) +* fix(css): inject source content conditionally (#12449) ([3e665f6](https://github.com/vitejs/vite/commit/3e665f6)), closes [#12449](https://github.com/vitejs/vite/issues/12449) +* fix(worker): using data URLs for inline shared worker (#12014) ([79a5007](https://github.com/vitejs/vite/commit/79a5007)), closes [#12014](https://github.com/vitejs/vite/issues/12014) +* chore: changelog edits for 4.2 (#12438) ([ce047e3](https://github.com/vitejs/vite/commit/ce047e3)), closes [#12438](https://github.com/vitejs/vite/issues/12438) + + + ## 4.2.0 (2023-03-16) Vite 4.2 is out! diff --git a/packages/vite/client.d.ts b/packages/vite/client.d.ts index a7182c8fd454d9..b55e4eba33d41c 100644 --- a/packages/vite/client.d.ts +++ b/packages/vite/client.d.ts @@ -173,6 +173,11 @@ declare module '*.aac' { export default src } +declare module '*.opus' { + const src: string + export default src +} + // fonts declare module '*.woff' { const src: string diff --git a/packages/vite/package.json b/packages/vite/package.json index 31a2edc17f91cf..b4dc688b0de802 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,6 +1,6 @@ { "name": "vite", - "version": "4.2.0", + "version": "4.3.0-beta.8", "type": "module", "license": "MIT", "author": "Evan You", @@ -68,16 +68,15 @@ "dependencies": { "esbuild": "^0.17.5", "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "rollup": "^3.20.2" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "devDependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/parser": "^7.21.2", - "@babel/types": "^7.21.2", + "@babel/parser": "^7.21.3", + "@babel/types": "^7.21.3", "@jridgewell/trace-mapping": "^0.3.17", "@rollup/plugin-alias": "^4.0.3", "@rollup/plugin-commonjs": "^24.0.1", @@ -86,6 +85,8 @@ "@rollup/plugin-node-resolve": "15.0.1", "@rollup/plugin-typescript": "^11.0.0", "@rollup/pluginutils": "^5.0.2", + "@types/pnpapi": "^0.0.2", + "@types/escape-html": "^1.0.0", "acorn": "^8.8.2", "acorn-walk": "^8.2.0", "cac": "^6.7.14", @@ -100,6 +101,7 @@ "dotenv": "^16.0.3", "dotenv-expand": "^9.0.0", "es-module-lexer": "^1.2.0", + "escape-html": "^1.0.3", "estree-walker": "^3.0.3", "etag": "^1.8.1", "fast-glob": "^3.2.12", @@ -108,7 +110,7 @@ "launch-editor-middleware": "^2.6.0", "magic-string": "^0.30.0", "micromatch": "^4.0.5", - "mlly": "^1.1.1", + "mlly": "^1.2.0", "mrmime": "^1.0.1", "okie": "^1.0.1", "open": "^8.4.2", @@ -126,11 +128,11 @@ "source-map-support": "^0.5.21", "strip-ansi": "^7.0.1", "strip-literal": "^1.0.1", - "tsconfck": "^2.1.0", + "tsconfck": "^2.1.1", "tslib": "^2.5.0", "types": "link:./types", "ufo": "^1.1.1", - "ws": "^8.12.1" + "ws": "^8.13.0" }, "peerDependencies": { "@types/node": ">= 14", diff --git a/packages/vite/rollup.config.ts b/packages/vite/rollup.config.ts index 51f40a09b72995..7a1064778ae9f8 100644 --- a/packages/vite/rollup.config.ts +++ b/packages/vite/rollup.config.ts @@ -120,6 +120,13 @@ function createNodePlugins( pattern: /^var json = typeof JSON.+require\('jsonify'\);$/gm, replacement: 'var json = JSON', }, + // postcss-import uses the `resolve` dep if the `resolve` option is not passed. + // However, we always pass the `resolve` option. Remove this import to avoid + // bundling the `resolve` dep. + 'postcss-import/index.js': { + src: 'const resolveId = require("./lib/resolve-id")', + replacement: 'const resolveId = (id) => id', + }, }), commonjs({ @@ -300,7 +307,7 @@ const __require = require; return { code: s.toString(), - map: s.generateMap(), + map: s.generateMap({ hires: true }), } }, } diff --git a/packages/vite/rollupLicensePlugin.ts b/packages/vite/rollupLicensePlugin.ts index f1d24f4b7c8e82..d3dce56a7dfeb6 100644 --- a/packages/vite/rollupLicensePlugin.ts +++ b/packages/vite/rollupLicensePlugin.ts @@ -1,9 +1,6 @@ import fs from 'node:fs' -import path from 'node:path' import license from 'rollup-plugin-license' import colors from 'picocolors' -import fg from 'fast-glob' -import resolve from 'resolve' import type { Plugin } from 'rollup' export default function licensePlugin( @@ -66,21 +63,6 @@ export default function licensePlugin( typeof repository === 'string' ? repository : repository.url }\n` } - if (!licenseText && name) { - try { - const pkgDir = path.dirname( - resolve.sync(path.join(name, 'package.json'), { - preserveSymlinks: false, - }), - ) - const licenseFile = fg.sync(`${pkgDir}/LICENSE*`, { - caseSensitiveMatch: false, - })[0] - if (licenseFile) { - licenseText = fs.readFileSync(licenseFile, 'utf-8') - } - } catch {} - } if (licenseText) { text += '\n' + diff --git a/packages/vite/scripts/util.ts b/packages/vite/scripts/util.ts index 135c701098fc57..c742b3338e8717 100644 --- a/packages/vite/scripts/util.ts +++ b/packages/vite/scripts/util.ts @@ -17,8 +17,9 @@ export function rewriteImports( }) } +const windowsSlashRE = /\\/g export function slash(p: string): string { - return p.replace(/\\/g, '/') + return p.replace(windowsSlashRE, '/') } export function walkDir(dir: string, handleFile: (file: string) => void): void { diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index 40bb02a93c6114..0c3163adfb1c06 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -2,7 +2,6 @@ import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload' import type { ModuleNamespace, ViteHotContext } from 'types/hot' import type { InferCustomEventPayload } from 'types/customEvent' import { ErrorOverlay, overlayId } from './overlay' -// eslint-disable-next-line node/no-missing-import import '@vite/env' // injected by the hmr plugin when served @@ -315,24 +314,61 @@ async function waitForSuccessfulPing( ) { const pingHostProtocol = socketProtocol === 'wss' ? 'https' : 'http' - // eslint-disable-next-line no-constant-condition - while (true) { + const ping = async () => { + // A fetch on a websocket URL will return a successful promise with status 400, + // but will reject a networking error. + // When running on middleware mode, it returns status 426, and an cors error happens if mode is not no-cors try { - // A fetch on a websocket URL will return a successful promise with status 400, - // but will reject a networking error. - // When running on middleware mode, it returns status 426, and an cors error happens if mode is not no-cors await fetch(`${pingHostProtocol}://${hostAndPath}`, { mode: 'no-cors', }) - break - } catch (e) { - // wait ms before attempting to ping again - await new Promise((resolve) => setTimeout(resolve, ms)) + return true + } catch {} + return false + } + + if (await ping()) { + return + } + await wait(ms) + + // eslint-disable-next-line no-constant-condition + while (true) { + if (document.visibilityState === 'visible') { + if (await ping()) { + break + } + await wait(ms) + } else { + await waitForWindowShow() } } } +function wait(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function waitForWindowShow() { + return new Promise((resolve) => { + const onChange = async () => { + if (document.visibilityState === 'visible') { + resolve() + document.removeEventListener('visibilitychange', onChange) + } + } + document.addEventListener('visibilitychange', onChange) + }) +} + const sheetsMap = new Map() + +// collect existing style elements that may have been inserted during SSR +// to avoid FOUC or duplicate styles +document.querySelectorAll('style[data-vite-dev-id]').forEach((el) => { + sheetsMap.set(el.getAttribute('data-vite-dev-id')!, el as HTMLStyleElement) +}) + // all css imports should be inserted at the same position // because after build it will be a single css file let lastInsertedStyle: HTMLStyleElement | undefined @@ -560,7 +596,7 @@ export function createHotContext(ownerPath: string): ViteHotContext { */ export function injectQuery(url: string, queryToInject: string): string { // skip urls that won't be handled by vite - if (!url.startsWith('.') && !url.startsWith('/')) { + if (url[0] !== '.' && url[0] !== '/') { return url } diff --git a/packages/vite/src/node/__tests__/plugins/import.spec.ts b/packages/vite/src/node/__tests__/plugins/import.spec.ts index 792c58515f542e..679d5bdb3d0a38 100644 --- a/packages/vite/src/node/__tests__/plugins/import.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/import.spec.ts @@ -1,9 +1,19 @@ -import { describe, expect, test } from 'vitest' +import { beforeEach, describe, expect, test, vi } from 'vitest' import { transformCjsImport } from '../../plugins/importAnalysis' describe('transformCjsImport', () => { const url = './node_modules/.vite/deps/react.js' const rawUrl = 'react' + const config: any = { + command: 'serve', + logger: { + warn: vi.fn(), + }, + } + + beforeEach(() => { + config.logger.warn.mockClear() + }) test('import specifier', () => { expect( @@ -12,6 +22,8 @@ describe('transformCjsImport', () => { url, rawUrl, 0, + '', + config, ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + @@ -22,7 +34,14 @@ describe('transformCjsImport', () => { test('import default specifier', () => { expect( - transformCjsImport('import React from "react"', url, rawUrl, 0), + transformCjsImport( + 'import React from "react"', + url, + rawUrl, + 0, + '', + config, + ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + 'const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react', @@ -34,6 +53,8 @@ describe('transformCjsImport', () => { url, rawUrl, 0, + '', + config, ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + @@ -43,7 +64,14 @@ describe('transformCjsImport', () => { test('import all specifier', () => { expect( - transformCjsImport('import * as react from "react"', url, rawUrl, 0), + transformCjsImport( + 'import * as react from "react"', + url, + rawUrl, + 0, + '', + config, + ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + 'const react = __vite__cjsImport0_react', @@ -51,13 +79,33 @@ describe('transformCjsImport', () => { }) test('export all specifier', () => { - expect(transformCjsImport('export * from "react"', url, rawUrl, 0)).toBe( - undefined, + expect( + transformCjsImport( + 'export * from "react"', + url, + rawUrl, + 0, + 'modA', + config, + ), + ).toBe(undefined) + + expect(config.logger.warn).toBeCalledWith( + expect.stringContaining(`export * from "react"\` in modA`), ) expect( - transformCjsImport('export * as react from "react"', url, rawUrl, 0), + transformCjsImport( + 'export * as react from "react"', + url, + rawUrl, + 0, + '', + config, + ), ).toBe(undefined) + + expect(config.logger.warn).toBeCalledTimes(1) }) test('export name specifier', () => { @@ -67,6 +115,8 @@ describe('transformCjsImport', () => { url, rawUrl, 0, + '', + config, ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + @@ -81,6 +131,8 @@ describe('transformCjsImport', () => { url, rawUrl, 0, + '', + config, ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + @@ -92,7 +144,14 @@ describe('transformCjsImport', () => { test('export default specifier', () => { expect( - transformCjsImport('export { default } from "react"', url, rawUrl, 0), + transformCjsImport( + 'export { default } from "react"', + url, + rawUrl, + 0, + '', + config, + ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + 'const __vite__cjsExportDefault_0 = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react; ' + @@ -105,6 +164,8 @@ describe('transformCjsImport', () => { url, rawUrl, 0, + '', + config, ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + @@ -118,6 +179,8 @@ describe('transformCjsImport', () => { url, rawUrl, 0, + '', + config, ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + diff --git a/packages/vite/src/node/__tests__/utils.spec.ts b/packages/vite/src/node/__tests__/utils.spec.ts index 65a88ed708230b..2ee69665c49ae7 100644 --- a/packages/vite/src/node/__tests__/utils.spec.ts +++ b/packages/vite/src/node/__tests__/utils.spec.ts @@ -5,7 +5,6 @@ import { asyncFlatten, getHash, getLocalhostAddressIfDiffersFromDNS, - getPotentialTsSrcPaths, injectQuery, isFileReadable, isWindows, @@ -137,42 +136,6 @@ describe('resolveHostname', () => { }) }) -test('ts import of file with .js extension', () => { - expect(getPotentialTsSrcPaths('test-file.js')).toEqual([ - 'test-file.ts', - 'test-file.tsx', - ]) -}) - -test('ts import of file with .jsx extension', () => { - expect(getPotentialTsSrcPaths('test-file.jsx')).toEqual(['test-file.tsx']) -}) - -test('ts import of file .mjs,.cjs extension', () => { - expect(getPotentialTsSrcPaths('test-file.cjs')).toEqual([ - 'test-file.cts', - 'test-file.ctsx', - ]) - expect(getPotentialTsSrcPaths('test-file.mjs')).toEqual([ - 'test-file.mts', - 'test-file.mtsx', - ]) -}) - -test('ts import of file with .js before extension', () => { - expect(getPotentialTsSrcPaths('test-file.js.js')).toEqual([ - 'test-file.js.ts', - 'test-file.js.tsx', - ]) -}) - -test('ts import of file with .js and query param', () => { - expect(getPotentialTsSrcPaths('test-file.js.js?lee=123')).toEqual([ - 'test-file.js.ts?lee=123', - 'test-file.js.tsx?lee=123', - ]) -}) - describe('posToNumber', () => { test('simple', () => { const actual = posToNumber('a\nb', { line: 2, column: 0 }) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 527f0e9fbac204..26f80c9b37a72c 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -32,7 +32,6 @@ import { copyDir, emptyDir, joinUrlSegments, - lookupFile, normalizePath, requireResolveFromRootWithFallback, } from './utils' @@ -52,13 +51,14 @@ import { initDepsOptimizer, } from './optimizer' import { loadFallbackPlugin } from './plugins/loadFallback' -import type { PackageData } from './packages' -import { watchPackageDataPlugin } from './packages' +import { findNearestPackageData } from './packages' +import type { PackageCache } from './packages' import { ensureWatchPlugin } from './plugins/ensureWatch' import { ESBUILD_MODULES_TARGET, VERSION } from './constants' import { resolveChokidarOptions } from './watch' import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' import { mergeConfig } from './publicUtils' +import { webWorkerPostPlugin } from './plugins/worker' export interface BuildOptions { /** @@ -436,7 +436,6 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ pre: [ completeSystemWrapPlugin(), ...(options.watch ? [ensureWatchPlugin()] : []), - watchPackageDataPlugin(config), ...(usePluginCommonjs ? [commonjsPlugin(options.commonjsOptions)] : []), dataURIPlugin(), ...(( @@ -446,14 +445,19 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ : [rollupOptionsPlugins], ) ).filter(Boolean) as Plugin[]), + ...(config.isWorker ? [webWorkerPostPlugin()] : []), ], post: [ buildImportAnalysisPlugin(config), ...(config.esbuild !== false ? [buildEsbuildPlugin(config)] : []), ...(options.minify ? [terserPlugin(config)] : []), - ...(options.manifest ? [manifestPlugin(config)] : []), - ...(options.ssrManifest ? [ssrManifestPlugin(config)] : []), - ...(!config.isWorker ? [buildReporterPlugin(config)] : []), + ...(!config.isWorker + ? [ + ...(options.manifest ? [manifestPlugin(config)] : []), + ...(options.ssrManifest ? [ssrManifestPlugin(config)] : []), + buildReporterPlugin(config), + ] + : []), loadFallbackPlugin(), ], } @@ -578,7 +582,11 @@ export async function build( const format = output.format || (cjsSsrBuild ? 'cjs' : 'es') const jsExt = ssrNodeBuild || libOptions - ? resolveOutputJsExtension(format, getPkgJson(config.root)?.type) + ? resolveOutputJsExtension( + format, + findNearestPackageData(config.root, config.packageCache)?.data + .type, + ) : 'js' return { dir: outDir, @@ -595,7 +603,14 @@ export async function build( ? `[name].${jsExt}` : libOptions ? ({ name }) => - resolveLibFilename(libOptions, format, name, config.root, jsExt) + resolveLibFilename( + libOptions, + format, + name, + config.root, + jsExt, + config.packageCache, + ) : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`), chunkFileNames: libOptions ? `[name]-[hash].${jsExt}` @@ -742,12 +757,8 @@ function prepareOutDir( } } -function getPkgJson(root: string): PackageData['data'] { - return JSON.parse(lookupFile(root, ['package.json']) || `{}`) -} - function getPkgName(name: string) { - return name?.startsWith('@') ? name.split('/')[1] : name + return name?.[0] === '@' ? name.split('/')[1] : name } type JsExt = 'js' | 'cjs' | 'mjs' @@ -769,15 +780,16 @@ export function resolveLibFilename( entryName: string, root: string, extension?: JsExt, + packageCache?: PackageCache, ): string { if (typeof libOptions.fileName === 'function') { return libOptions.fileName(format, entryName) } - const packageJson = getPkgJson(root) + const packageJson = findNearestPackageData(root, packageCache)?.data const name = libOptions.fileName || - (typeof libOptions.entry === 'string' + (packageJson && typeof libOptions.entry === 'string' ? getPkgName(packageJson.name) : entryName) @@ -786,7 +798,7 @@ export function resolveLibFilename( 'Name in package.json is required if option "build.lib.fileName" is not provided.', ) - extension ??= resolveOutputJsExtension(format, packageJson.type) + extension ??= resolveOutputJsExtension(format, packageJson?.type) if (format === 'cjs' || format === 'es') { return `${name}.${extension}` @@ -858,40 +870,50 @@ export function onRollupWarning( warn: WarningHandler, config: ResolvedConfig, ): void { - if (warning.code === 'UNRESOLVED_IMPORT') { - const id = warning.id - const exporter = warning.exporter - // throw unless it's commonjs external... - if (!id || !/\?commonjs-external$/.test(id)) { - throw new Error( - `[vite]: Rollup failed to resolve import "${exporter}" from "${id}".\n` + - `This is most likely unintended because it can break your application at runtime.\n` + - `If you do want to externalize this module explicitly add it to\n` + - `\`build.rollupOptions.external\``, + function viteWarn(warning: RollupWarning) { + if (warning.code === 'UNRESOLVED_IMPORT') { + const id = warning.id + const exporter = warning.exporter + // throw unless it's commonjs external... + if (!id || !/\?commonjs-external$/.test(id)) { + throw new Error( + `[vite]: Rollup failed to resolve import "${exporter}" from "${id}".\n` + + `This is most likely unintended because it can break your application at runtime.\n` + + `If you do want to externalize this module explicitly add it to\n` + + `\`build.rollupOptions.external\``, + ) + } + } + + if ( + warning.plugin === 'rollup-plugin-dynamic-import-variables' && + dynamicImportWarningIgnoreList.some((msg) => + warning.message.includes(msg), ) + ) { + return } - } - if ( - warning.plugin === 'rollup-plugin-dynamic-import-variables' && - dynamicImportWarningIgnoreList.some((msg) => warning.message.includes(msg)) - ) { - return - } + if (warningIgnoreList.includes(warning.code!)) { + return + } - if (!warningIgnoreList.includes(warning.code!)) { - const userOnWarn = config.build.rollupOptions?.onwarn - if (userOnWarn) { - userOnWarn(warning, warn) - } else if (warning.code === 'PLUGIN_WARNING') { + if (warning.code === 'PLUGIN_WARNING') { config.logger.warn( `${colors.bold( colors.yellow(`[plugin:${warning.plugin}]`), )} ${colors.yellow(warning.message)}`, ) - } else { - warn(warning) } + + warn(warning) + } + + const userOnWarn = config.build.rollupOptions?.onwarn + if (userOnWarn) { + userOnWarn(warning, viteWarn) + } else { + viteWarn(warning) } } @@ -925,12 +947,12 @@ async function cjsSsrResolveExternal( } } -function resolveUserExternal( +export function resolveUserExternal( user: ExternalOption, id: string, parentId: string | undefined, isResolved: boolean, -) { +): boolean | null | void { if (typeof user === 'function') { return user(id, parentId, isResolved) } else if (Array.isArray(user)) { @@ -1022,7 +1044,7 @@ function injectSsrFlag>( /* The following functions are copied from rollup - https://github.com/rollup/rollup/blob/c5269747cd3dd14c4b306e8cea36f248d9c1aa01/src/ast/nodes/MetaProperty.ts#L189-L232 + https://github.com/rollup/rollup/blob/0bcf0a672ac087ff2eb88fbba45ec62389a4f45f/src/ast/nodes/MetaProperty.ts#L145-L193 https://github.com/rollup/rollup The MIT License (MIT) @@ -1031,15 +1053,30 @@ function injectSsrFlag>( 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. */ +const needsEscapeRegEx = /[\n\r'\\\u2028\u2029]/ +const quoteNewlineRegEx = /([\n\r'\u2028\u2029])/g +const backSlashRegEx = /\\/g + +function escapeId(id: string): string { + if (!needsEscapeRegEx.test(id)) return id + return id.replace(backSlashRegEx, '\\\\').replace(quoteNewlineRegEx, '\\$1') +} + const getResolveUrl = (path: string, URL = 'URL') => `new ${URL}(${path}).href` const getRelativeUrlFromDocument = (relativePath: string, umd = false) => getResolveUrl( - `'${relativePath}', ${ + `'${escapeId(relativePath)}', ${ umd ? `typeof document === 'undefined' ? location.href : ` : '' }document.currentScript && document.currentScript.src || document.baseURI`, ) +const getFileUrlFromFullPath = (path: string) => + `require('u' + 'rl').pathToFileURL(${path}).href` + +const getFileUrlFromRelativePath = (path: string) => + getFileUrlFromFullPath(`__dirname + '/${path}'`) + const relativeUrlMechanisms: Record< InternalModuleFormat, (relativePath: string) => string @@ -1049,22 +1086,26 @@ const relativeUrlMechanisms: Record< return getResolveUrl(`require.toUrl('${relativePath}'), document.baseURI`) }, cjs: (relativePath) => - `(typeof document === 'undefined' ? ${getResolveUrl( - `'file:' + __dirname + '/${relativePath}'`, - `(require('u' + 'rl').URL)`, + `(typeof document === 'undefined' ? ${getFileUrlFromRelativePath( + relativePath, )} : ${getRelativeUrlFromDocument(relativePath)})`, es: (relativePath) => getResolveUrl(`'${relativePath}', import.meta.url`), iife: (relativePath) => getRelativeUrlFromDocument(relativePath), // NOTE: make sure rollup generate `module` params system: (relativePath) => getResolveUrl(`'${relativePath}', module.meta.url`), umd: (relativePath) => - `(typeof document === 'undefined' && typeof location === 'undefined' ? ${getResolveUrl( - `'file:' + __dirname + '/${relativePath}'`, - `(require('u' + 'rl').URL)`, + `(typeof document === 'undefined' && typeof location === 'undefined' ? ${getFileUrlFromRelativePath( + relativePath, )} : ${getRelativeUrlFromDocument(relativePath, true)})`, } /* end of copy */ +const customRelativeUrlMechanisms = { + ...relativeUrlMechanisms, + 'worker-iife': (relativePath) => + getResolveUrl(`'${relativePath}', self.location.href`), +} as const satisfies Record string> + export type RenderBuiltAssetUrl = ( filename: string, type: { @@ -1114,8 +1155,10 @@ export function toOutputFilePathInJS( export function createToImportMetaURLBasedRelativeRuntime( format: InternalModuleFormat, + isWorker: boolean, ): (filename: string, importer: string) => { runtime: string } { - const toRelativePath = relativeUrlMechanisms[format] + const formatLong = isWorker && format === 'iife' ? 'worker-iife' : format + const toRelativePath = customRelativeUrlMechanisms[formatLong] return (filename, importer) => ({ runtime: toRelativePath( path.posix.relative(path.dirname(importer), filename), diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index ac9f5945c1a2f2..d18ab93fd0213f 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -276,6 +276,7 @@ cli base: options.base, configFile: options.config, logLevel: options.logLevel, + mode: options.mode, }, 'serve', ) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index fb56c92bca3ccf..7b4107c8aafe05 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import fsp from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' import { performance } from 'node:perf_hooks' @@ -47,6 +48,7 @@ import { DEFAULT_EXTENSIONS, DEFAULT_MAIN_FIELDS, ENV_ENTRY, + FS_PREFIX, } from './constants' import type { InternalResolveOptions, @@ -61,6 +63,7 @@ import type { JsonOptions } from './plugins/json' import type { PluginContainer } from './server/pluginContainer' import { createPluginContainer } from './server/pluginContainer' import type { PackageCache } from './packages' +import { findNearestPackageData } from './packages' import { loadEnv, resolveEnvPrefix } from './env' import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions } from './ssr' @@ -389,6 +392,7 @@ export async function resolveConfig( let configFileDependencies: string[] = [] let mode = inlineConfig.mode || defaultMode const isNodeEnvSet = !!process.env.NODE_ENV + const packageCache: PackageCache = new Map() // some dependencies e.g. @vue/compiler-* relies on NODE_ENV for getting // production-specific behavior, so set it early on @@ -472,8 +476,14 @@ export async function resolveConfig( ) const clientAlias = [ - { find: /^\/?@vite\/env/, replacement: ENV_ENTRY }, - { find: /^\/?@vite\/client/, replacement: CLIENT_ENTRY }, + { + find: /^\/?@vite\/env/, + replacement: path.posix.join(FS_PREFIX, normalizePath(ENV_ENTRY)), + }, + { + find: /^\/?@vite\/client/, + replacement: path.posix.join(FS_PREFIX, normalizePath(CLIENT_ENTRY)), + }, ] // resolve alias with internal client alias @@ -538,12 +548,12 @@ export async function resolveConfig( ) // resolve cache directory - const pkgPath = lookupFile(resolvedRoot, [`package.json`], { pathOnly: true }) + const pkgDir = findNearestPackageData(resolvedRoot, packageCache)?.dir const cacheDir = normalizePath( config.cacheDir ? path.resolve(resolvedRoot, config.cacheDir) - : pkgPath - ? path.join(path.dirname(pkgPath), `node_modules/.vite`) + : pkgDir + ? path.join(pkgDir, `node_modules/.vite`) : path.join(resolvedRoot, `.vite`), ) @@ -584,6 +594,7 @@ export async function resolveConfig( preferRelative: false, tryIndex: true, ...options, + idOnly: true, }), ], })) @@ -680,7 +691,7 @@ export async function resolveConfig( return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file) }, logger, - packageCache: new Map(), + packageCache, createResolver, optimizeDeps: { disabled: 'build', @@ -772,16 +783,14 @@ export async function resolveConfig( ) } - if (process.env.DEBUG) { - debug(`using resolved config: %O`, { - ...resolved, - plugins: resolved.plugins.map((p) => p.name), - worker: { - ...resolved.worker, - plugins: resolved.worker.plugins.map((p) => p.name), - }, - }) - } + debug?.(`using resolved config: %O`, { + ...resolved, + plugins: resolved.plugins.map((p) => p.name), + worker: { + ...resolved.worker, + plugins: resolved.worker.plugins.map((p) => p.name), + }, + }) if (config.build?.terserOptions && config.build.minify !== 'terser') { logger.warn( @@ -828,7 +837,7 @@ export function resolveBaseUrl( isBuild: boolean, logger: Logger, ): string { - if (base.startsWith('.')) { + if (base[0] === '.') { logger.warn( colors.yellow( colors.bold( @@ -843,7 +852,7 @@ export function resolveBaseUrl( // external URL flag const isExternal = isExternalUrl(base) // no leading slash warn - if (!isExternal && !base.startsWith('/')) { + if (!isExternal && base[0] !== '/') { logger.warn( colors.yellow( colors.bold(`(!) "base" option should start with a slash.`), @@ -855,7 +864,7 @@ export function resolveBaseUrl( if (!isBuild || !isExternal) { base = new URL(base, 'http://vitejs.dev').pathname // ensure leading slash - if (!base.startsWith('/')) { + if (base[0] !== '/') { base = '/' + base } } @@ -912,7 +921,7 @@ export async function loadConfigFromFile( } if (!resolvedPath) { - debug('no config file found.') + debug?.('no config file found.') return null } @@ -925,7 +934,8 @@ export async function loadConfigFromFile( // check package.json for type: "module" and set `isESM` to true try { const pkg = lookupFile(configRoot, ['package.json']) - isESM = !!pkg && JSON.parse(pkg).type === 'module' + isESM = + !!pkg && JSON.parse(fs.readFileSync(pkg, 'utf-8')).type === 'module' } catch (e) {} } @@ -936,7 +946,7 @@ export async function loadConfigFromFile( bundled.code, isESM, ) - debug(`bundled config file loaded in ${getTime()}`) + debug?.(`bundled config file loaded in ${getTime()}`) const config = await (typeof userConfig === 'function' ? userConfig(configEnv) @@ -999,6 +1009,7 @@ async function bundleConfigFile( dedupe: [], extensions: DEFAULT_EXTENSIONS, preserveSymlinks: false, + packageCache: new Map(), } // externalize bare imports @@ -1080,24 +1091,22 @@ async function loadConfigFromBundledFile( // with --experimental-loader themselves, we have to do a hack here: // write it to disk, load it with native Node ESM, then delete the file. if (isESM) { - const fileBase = `${fileName}.timestamp-${Date.now()}` + const fileBase = `${fileName}.timestamp-${Date.now()}-${Math.random() + .toString(16) + .slice(2)}` const fileNameTmp = `${fileBase}.mjs` const fileUrl = `${pathToFileURL(fileBase)}.mjs` - fs.writeFileSync(fileNameTmp, bundledCode) + await fsp.writeFile(fileNameTmp, bundledCode) try { return (await dynamicImport(fileUrl)).default } finally { - try { - fs.unlinkSync(fileNameTmp) - } catch { - // already removed if this function is called twice simultaneously - } + fs.unlink(fileNameTmp, () => {}) // Ignore errors } } // for cjs, we can register a custom loader via `_require.extensions` else { const extension = path.extname(fileName) - const realFileName = fs.realpathSync(fileName) + const realFileName = await fsp.realpath(fileName) const loaderExt = extension in _require.extensions ? extension : '.js' const defaultLoader = _require.extensions[loaderExt]! _require.extensions[loaderExt] = (module: NodeModule, filename: string) => { diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index f2d3c161d8d84c..0f378bbdfb9f9f 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -115,6 +115,7 @@ export const KNOWN_ASSET_TYPES = [ 'wav', 'flac', 'aac', + 'opus', // fonts 'woff2?', diff --git a/packages/vite/src/node/env.ts b/packages/vite/src/node/env.ts index 338de6784497d8..374e1f049c0618 100644 --- a/packages/vite/src/node/env.ts +++ b/packages/vite/src/node/env.ts @@ -1,7 +1,8 @@ import fs from 'node:fs' +import path from 'node:path' import { parse } from 'dotenv' import { expand } from 'dotenv-expand' -import { arraify, lookupFile } from './utils' +import { arraify, tryStatSync } from './utils' import type { UserConfig } from './config' export function loadEnv( @@ -26,12 +27,10 @@ export function loadEnv( const parsed = Object.fromEntries( envFiles.flatMap((file) => { - const path = lookupFile(envDir, [file], { - pathOnly: true, - rootDir: envDir, - }) - if (!path) return [] - return Object.entries(parse(fs.readFileSync(path))) + const filePath = path.join(envDir, file) + if (!tryStatSync(filePath)?.isFile()) return [] + + return Object.entries(parse(fs.readFileSync(filePath))) }), ) diff --git a/packages/vite/src/node/http.ts b/packages/vite/src/node/http.ts index 7af73c899cccb2..afefc14fc5e1d0 100644 --- a/packages/vite/src/node/http.ts +++ b/packages/vite/src/node/http.ts @@ -1,4 +1,4 @@ -import fs from 'node:fs' +import fsp from 'node:fs/promises' import path from 'node:path' import type { Server as HttpServer, @@ -124,26 +124,20 @@ export async function resolveHttpsConfig( https: boolean | HttpsServerOptions | undefined, ): Promise { if (!https) return undefined + if (!isObject(https)) return {} - const httpsOption = isObject(https) ? { ...https } : {} - - const { ca, cert, key, pfx } = httpsOption - Object.assign(httpsOption, { - ca: readFileIfExists(ca), - cert: readFileIfExists(cert), - key: readFileIfExists(key), - pfx: readFileIfExists(pfx), - }) - return httpsOption + const [ca, cert, key, pfx] = await Promise.all([ + readFileIfExists(https.ca), + readFileIfExists(https.cert), + readFileIfExists(https.key), + readFileIfExists(https.pfx), + ]) + return { ...https, ca, cert, key, pfx } } -function readFileIfExists(value?: string | Buffer | any[]) { +async function readFileIfExists(value?: string | Buffer | any[]) { if (typeof value === 'string') { - try { - return fs.readFileSync(path.resolve(value)) - } catch (e) { - return value - } + return fsp.readFile(path.resolve(value)).catch(() => value) } return value } diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index a0e3628484a202..cc44fe17b10524 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -34,6 +34,7 @@ export type { export type { PreviewOptions, PreviewServer, + PreviewServerForHook, PreviewServerHook, ResolvedPreviewOptions, } from './preview' diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index e6378bc4e94930..e3bb4730eef1d7 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -2,8 +2,9 @@ import path from 'node:path' import type { ImportKind, Plugin } from 'esbuild' import { CSS_LANGS_RE, KNOWN_ASSET_TYPES } from '../constants' import { getDepOptimizationConfig } from '..' -import type { ResolvedConfig } from '..' +import type { PackageCache, ResolvedConfig } from '..' import { + escapeRegex, flattenId, isBuiltin, isExternalUrl, @@ -57,14 +58,24 @@ export function esbuildDepPlugin( ? externalTypes.filter((type) => !extensions?.includes('.' + type)) : externalTypes + // use separate package cache for optimizer as it caches paths around node_modules + // and it's unlikely for the core Vite process to traverse into node_modules again + const esmPackageCache: PackageCache = new Map() + const cjsPackageCache: PackageCache = new Map() + // default resolver which prefers ESM - const _resolve = config.createResolver({ asSrc: false, scan: true }) + const _resolve = config.createResolver({ + asSrc: false, + scan: true, + packageCache: esmPackageCache, + }) // cjs resolver that prefers Node const _resolveRequire = config.createResolver({ asSrc: false, isRequire: true, scan: true, + packageCache: cjsPackageCache, }) const resolve = ( @@ -116,6 +127,12 @@ export function esbuildDepPlugin( return { name: 'vite:dep-pre-bundle', setup(build) { + // clear package cache when esbuild is finished + build.onEnd(() => { + esmPackageCache.clear() + cjsPackageCache.clear() + }) + // externalize assets and commonly known non-js file types // See #8459 for more details about this require-import conversion build.onResolve( @@ -265,6 +282,8 @@ module.exports = Object.create(new Proxy({}, { } } +const matchesEntireLine = (text: string) => `^${escapeRegex(text)}$` + // esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized // https://github.com/evanw/esbuild/issues/566#issuecomment-735551834 export function esbuildCjsExternalPlugin( @@ -274,9 +293,7 @@ export function esbuildCjsExternalPlugin( return { name: 'cjs-external', setup(build) { - const escape = (text: string) => - `^${text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}$` - const filter = new RegExp(externals.map(escape).join('|')) + const filter = new RegExp(externals.map(matchesEntireLine).join('|')) build.onResolve({ filter: new RegExp(`^${nonFacadePrefix}`) }, (args) => { return { diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 62b3a9f14d7494..2ccb4fef4f89d5 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -1,8 +1,8 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import path from 'node:path' +import { promisify } from 'node:util' import { performance } from 'node:perf_hooks' -import _debug from 'debug' import colors from 'picocolors' import type { BuildContext, BuildOptions as EsbuildBuildOptions } from 'esbuild' import esbuild, { build } from 'esbuild' @@ -13,20 +13,19 @@ import type { ResolvedConfig } from '../config' import { arraify, createDebugger, - emptyDir, flattenId, getHash, isOptimizable, + isWindows, lookupFile, - nestedResolveFrom, normalizeId, normalizePath, - removeDir, - renameDir, - writeFile, + removeLeadingSlash, + tryStatSync, } from '../utils' import { transformWithEsbuild } from '../plugins/esbuild' import { ESBUILD_MODULES_TARGET } from '../constants' +import { resolvePackageData } from '../packages' import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' import { scanImports } from './scan' export { @@ -35,9 +34,7 @@ export { getDepsOptimizer, } from './optimizer' -export const debuggerViteDeps = createDebugger('vite:deps') -const debug = debuggerViteDeps -const isDebugEnabled = _debug('vite:deps').enabled +const debug = createDebugger('vite:deps') const jsExtensionRE = /\.js$/i const jsMapExtensionRE = /\.js\.map$/i @@ -46,10 +43,6 @@ export type ExportsData = { hasImports: boolean // exported names (for `export { a as b }`, `b` is exported name) exports: readonly string[] - facade: boolean - // es-module-lexer has a facade detection but isn't always accurate for our - // use case when the module has default export - hasReExports?: boolean // hint if the dep requires loading as jsx jsxLoader?: boolean } @@ -231,7 +224,7 @@ export async function optimizeDeps( const ssr = config.command === 'build' && !!config.build.ssr - const cachedMetadata = loadCachedDepOptimizationMetadata( + const cachedMetadata = await loadCachedDepOptimizationMetadata( config, ssr, force, @@ -244,7 +237,7 @@ export async function optimizeDeps( const deps = await discoverProjectDependencies(config).result const depsString = depsLogString(Object.keys(deps)) - log(colors.green(`Optimizing dependencies:\n ${depsString}`)) + log?.(colors.green(`Optimizing dependencies:\n ${depsString}`)) await addManuallyIncludedOptimizeDeps(deps, config, ssr) @@ -261,7 +254,7 @@ export async function optimizeServerSsrDeps( config: ResolvedConfig, ): Promise { const ssr = true - const cachedMetadata = loadCachedDepOptimizationMetadata( + const cachedMetadata = await loadCachedDepOptimizationMetadata( config, ssr, config.optimizeDeps.force, @@ -334,22 +327,24 @@ export function addOptimizedDepInfo( return depInfo } +let firstLoadCachedDepOptimizationMetadata = true + /** * Creates the initial dep optimization metadata, loading it from the deps cache * if it exists and pre-bundling isn't forced */ -export function loadCachedDepOptimizationMetadata( +export async function loadCachedDepOptimizationMetadata( config: ResolvedConfig, ssr: boolean, force = config.optimizeDeps.force, asCommand = false, -): DepOptimizationMetadata | undefined { +): Promise { const log = asCommand ? config.logger.info : debug - // Before Vite 2.9, dependencies were cached in the root of the cacheDir - // For compat, we remove the cache if we find the old structure - if (fs.existsSync(path.join(config.cacheDir, '_metadata.json'))) { - emptyDir(config.cacheDir) + if (firstLoadCachedDepOptimizationMetadata) { + firstLoadCachedDepOptimizationMetadata = false + // Fire up a clean up of stale processing deps dirs if older process exited early + setTimeout(() => cleanupDepsCacheStaleDirs(config), 0) } const depsCacheDir = getDepsCacheDir(config, ssr) @@ -359,13 +354,13 @@ export function loadCachedDepOptimizationMetadata( try { const cachedMetadataPath = path.join(depsCacheDir, '_metadata.json') cachedMetadata = parseDepsOptimizerMetadata( - fs.readFileSync(cachedMetadataPath, 'utf-8'), + await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir, ) } catch (e) {} // hash is consistent, no need to re-bundle if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) { - log('Hash is consistent. Skipping. Use --force to override.') + log?.('Hash is consistent. Skipping. Use --force to override.') // Nothing to commit or cancel as we are using the cache, we only // need to resolve the processing promise so requests can move on return cachedMetadata @@ -375,7 +370,7 @@ export function loadCachedDepOptimizationMetadata( } // Start with a fresh cache - fs.rmSync(depsCacheDir, { recursive: true, force: true }) + await fsp.rm(depsCacheDir, { recursive: true, force: true }) } /** @@ -436,18 +431,7 @@ export function toDiscoveredDependencies( } export function depsLogString(qualifiedIds: string[]): string { - if (isDebugEnabled) { - return colors.yellow(qualifiedIds.join(`, `)) - } else { - const total = qualifiedIds.length - const maxListed = 5 - const listed = Math.min(total, maxListed) - const extra = Math.max(0, total - maxListed) - return colors.yellow( - qualifiedIds.slice(0, listed).join(`, `) + - (extra > 0 ? `, ...and ${extra} more` : ``), - ) - } + return colors.yellow(qualifiedIds.join(`, `)) } /** @@ -476,17 +460,13 @@ export function runOptimizeDeps( // Create a temporal directory so we don't need to delete optimized deps // until they have been processed. This also avoids leaving the deps cache // directory in a corrupted state if there is an error - if (fs.existsSync(processingCacheDir)) { - emptyDir(processingCacheDir) - } else { - fs.mkdirSync(processingCacheDir, { recursive: true }) - } + fs.mkdirSync(processingCacheDir, { recursive: true }) // a hint for Node.js // all files in the cache directory should be recognized as ES modules - writeFile( + fs.writeFileSync( path.resolve(processingCacheDir, 'package.json'), - JSON.stringify({ type: 'module' }), + `{\n "type": "module"\n}\n`, ) const metadata = initDepsOptimizerMetadata(config, ssr) @@ -501,38 +481,82 @@ export function runOptimizeDeps( // the optimizedDepInfo.processing promise for each dep const qualifiedIds = Object.keys(depsInfo) - let cleaned = false + let committed = false const cleanUp = () => { - if (!cleaned) { + // If commit was already called, ignore the clean up even if a cancel was requested + // This minimizes the chances of leaving the deps cache in a corrupted state + if (!cleaned && !committed) { cleaned = true - fs.rmSync(processingCacheDir, { recursive: true, force: true }) + // No need to wait, we can clean up in the background because temp folders + // are unique per run + fsp.rm(processingCacheDir, { recursive: true, force: true }).catch(() => { + // Ignore errors + }) } } - const createProcessingResult = () => ({ + + const succesfulResult: DepOptimizationResult = { metadata, - async commit() { + cancel: cleanUp, + commit: async () => { if (cleaned) { throw new Error( - `Vite Internal Error: Can't commit optimizeDeps processing result, it has already been cancelled.`, + 'Can not commit a Deps Optimization run as it was cancelled', ) } - // Write metadata file, delete `deps` folder and rename the `processing` folder to `deps` - // Processing is done, we can now replace the depsCacheDir with processingCacheDir + // Ignore clean up requests after this point so the temp folder isn't deleted before + // we finish commiting the new deps cache files to the deps folder + committed = true + + // Write metadata file, then commit the processing folder to the global deps cache // Rewire the file paths from the temporal processing dir to the final deps cache dir - await removeDir(depsCacheDir) - await renameDir(processingCacheDir, depsCacheDir) + const dataPath = path.join(processingCacheDir, '_metadata.json') + fs.writeFileSync( + dataPath, + stringifyDepsOptimizerMetadata(metadata, depsCacheDir), + ) + + // In order to minimize the time where the deps folder isn't in a consistent state, + // we first rename the old depsCacheDir to a temporal path, then we rename the + // new processing cache dir to the depsCacheDir. In systems where doing so in sync + // is safe, we do an atomic operation (at least for this thread). For Windows, we + // found there are cases where the rename operation may finish before it's done + // so we do a graceful rename checking that the folder has been properly renamed. + // We found that the rename-rename (then delete the old folder in the background) + // is safer than a delete-rename operation. + const temporalPath = depsCacheDir + getTempSuffix() + const depsCacheDirPresent = fs.existsSync(depsCacheDir) + if (isWindows) { + if (depsCacheDirPresent) await safeRename(depsCacheDir, temporalPath) + await safeRename(processingCacheDir, depsCacheDir) + } else { + if (depsCacheDirPresent) fs.renameSync(depsCacheDir, temporalPath) + fs.renameSync(processingCacheDir, depsCacheDir) + } + + // Delete temporal path in the background + if (depsCacheDirPresent) + fsp.rm(temporalPath, { recursive: true, force: true }) }, - cancel: cleanUp, - }) + } if (!qualifiedIds.length) { + // No deps to optimize, we still commit the processing cache dir to remove + // the previous optimized deps if they exist, and let the next server start + // skip the scanner step if the lockfile hasn't changed return { cancel: async () => cleanUp(), - result: Promise.resolve(createProcessingResult()), + result: Promise.resolve(succesfulResult), } } + const cancelledResult: DepOptimizationResult = { + metadata, + commit: async () => cleanUp(), + cancel: cleanUp, + } + const start = performance.now() const preparedRun = prepareEsbuildOptimizerRun( @@ -543,7 +567,7 @@ export function runOptimizeDeps( optimizerContext, ) - const result = preparedRun.then(({ context, idToExports }) => { + const runResult = preparedRun.then(({ context, idToExports }) => { function disposeContext() { return context?.dispose().catch((e) => { config.logger.error('Failed to dispose esbuild context', { error: e }) @@ -551,7 +575,7 @@ export function runOptimizeDeps( } if (!context || optimizerContext.cancelled) { disposeContext() - return createProcessingResult() + return cancelledResult } return context @@ -617,21 +641,18 @@ export function runOptimizeDeps( } } - const dataPath = path.join(processingCacheDir, '_metadata.json') - writeFile( - dataPath, - stringifyDepsOptimizerMetadata(metadata, depsCacheDir), + debug?.( + `Dependencies bundled in ${(performance.now() - start).toFixed(2)}ms`, ) - debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`) - - return createProcessingResult() + return succesfulResult }) + .catch((e) => { if (e.errors && e.message.includes('The build was canceled')) { // esbuild logs an error when cancelling, but this is expected so // return an empty result instead - return createProcessingResult() + return cancelledResult } throw e }) @@ -640,7 +661,7 @@ export function runOptimizeDeps( }) }) - result.catch(() => { + runResult.catch(() => { cleanUp() }) @@ -651,7 +672,7 @@ export function runOptimizeDeps( await context?.cancel() cleanUp() }, - result, + result: runResult, } } @@ -686,23 +707,25 @@ async function prepareEsbuildOptimizerRun( const { plugins: pluginsFromConfig = [], ...esbuildOptions } = optimizeDeps?.esbuildOptions ?? {} - for (const id in depsInfo) { - const src = depsInfo[id].src! - const exportsData = await (depsInfo[id].exportsData ?? - extractExportsData(src, config, ssr)) - if (exportsData.jsxLoader) { - // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. - // This is useful for packages such as Gatsby. - esbuildOptions.loader = { - '.js': 'jsx', - ...esbuildOptions.loader, + await Promise.all( + Object.keys(depsInfo).map(async (id) => { + const src = depsInfo[id].src! + const exportsData = await (depsInfo[id].exportsData ?? + extractExportsData(src, config, ssr)) + if (exportsData.jsxLoader && !esbuildOptions.loader?.['.js']) { + // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. + // This is useful for packages such as Gatsby. + esbuildOptions.loader = { + '.js': 'jsx', + ...esbuildOptions.loader, + } } - } - const flatId = flattenId(id) - flatIdDeps[flatId] = src - idToExports[id] = exportsData - flatIdToExports[flatId] = exportsData - } + const flatId = flattenId(id) + flatIdDeps[flatId] = src + idToExports[id] = exportsData + flatIdToExports[flatId] = exportsData + }), + ) if (optimizerContext.cancelled) return { context: undefined, idToExports } @@ -845,6 +868,7 @@ function createOptimizeDepsIncludeResolver( scan: true, ssrOptimizeCheck: ssr, ssrConfig: config.ssr, + packageCache: new Map(), }) return async (id: string) => { const lastArrowIndex = id.lastIndexOf('>') @@ -855,16 +879,30 @@ function createOptimizeDepsIncludeResolver( // 'foo > bar > baz' => 'foo > bar' & 'baz' const nestedRoot = id.substring(0, lastArrowIndex).trim() const nestedPath = id.substring(lastArrowIndex + 1).trim() - const basedir = nestedResolveFrom( + const basedir = nestedResolveBasedir( nestedRoot, config.root, config.resolve.preserveSymlinks, - ssr, ) return await resolve(nestedPath, basedir, undefined, ssr) } } +/** + * Continously resolve the basedir of packages separated by '>' + */ +function nestedResolveBasedir( + id: string, + basedir: string, + preserveSymlinks = false, +) { + const pkgs = id.split('>').map((pkg) => pkg.trim()) + for (const pkg of pkgs) { + basedir = resolvePackageData(pkg, basedir, preserveSymlinks)?.dir || basedir + } + return basedir +} + export function newDepOptimizationProcessing(): DepOptimizationProcessing { let resolve: () => void const promise = new Promise((_resolve) => { @@ -915,20 +953,30 @@ function getProcessingDepsCacheDir(config: ResolvedConfig, ssr: boolean) { return ( getDepsCacheDirPrefix(config) + getDepsCacheSuffix(config, ssr) + + getTempSuffix() + ) +} + +function getTempSuffix() { + return ( '_temp_' + - getHash(Date.now().toString()) + getHash( + `${process.pid}:${Date.now().toString()}:${Math.random() + .toString(16) + .slice(2)}`, + ) ) } -export function getDepsCacheDirPrefix(config: ResolvedConfig): string { +function getDepsCacheDirPrefix(config: ResolvedConfig): string { return normalizePath(path.resolve(config.cacheDir, 'deps')) } -export function isOptimizedDepFile( - id: string, +export function createIsOptimizedDepFile( config: ResolvedConfig, -): boolean { - return id.startsWith(getDepsCacheDirPrefix(config)) +): (id: string) => boolean { + const depsCacheDirPrefix = getDepsCacheDirPrefix(config) + return (id) => id.startsWith(depsCacheDirPrefix) } export function createIsOptimizedDepUrl( @@ -942,7 +990,7 @@ export function createIsOptimizedDepUrl( const depsCacheDirPrefix = depsCacheDirRelative.startsWith('../') ? // if the cache directory is outside root, the url prefix would be something // like '/@fs/absolute/path/to/node_modules/.vite' - `/@fs/${normalizePath(depsCacheDir).replace(/^\//, '')}` + `/@fs/${removeLeadingSlash(normalizePath(depsCacheDir))}` : // if the cache directory is inside root, the url prefix would be something // like '/node_modules/.vite' `/${depsCacheDirRelative}` @@ -1087,47 +1135,35 @@ export async function extractExportsData( write: false, format: 'esm', }) - const [imports, exports, facade] = parse(result.outputFiles[0].text) + const [imports, exports] = parse(result.outputFiles[0].text) return { hasImports: imports.length > 0, exports: exports.map((e) => e.n), - facade, } } let parseResult: ReturnType let usedJsxLoader = false - const entryContent = fs.readFileSync(filePath, 'utf-8') + const entryContent = await fsp.readFile(filePath, 'utf-8') try { parseResult = parse(entryContent) } catch { const loader = esbuildOptions.loader?.[path.extname(filePath)] || 'jsx' - debug( + debug?.( `Unable to parse: ${filePath}.\n Trying again with a ${loader} transform.`, ) const transformed = await transformWithEsbuild(entryContent, filePath, { loader, }) - // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. - // This is useful for packages such as Gatsby. - esbuildOptions.loader = { - '.js': 'jsx', - ...esbuildOptions.loader, - } parseResult = parse(transformed.code) usedJsxLoader = true } - const [imports, exports, facade] = parseResult + const [imports, exports] = parseResult const exportsData: ExportsData = { hasImports: imports.length > 0, exports: exports.map((e) => e.n), - facade, - hasReExports: imports.some(({ ss, se }) => { - const exp = entryContent.slice(ss, se) - return /export\s+\*\s+from/.test(exp) - }), jsxLoader: usedJsxLoader, } return exportsData @@ -1171,18 +1207,17 @@ function isSingleDefaultExport(exports: readonly string[]) { } const lockfileFormats = [ - { name: 'package-lock.json', checkPatches: true }, - { name: 'yarn.lock', checkPatches: true }, // Included in lockfile for v2+ - { name: 'pnpm-lock.yaml', checkPatches: false }, // Included in lockfile - { name: 'bun.lockb', checkPatches: true }, -] + { name: 'package-lock.json', checkPatches: true, manager: 'npm' }, + { name: 'yarn.lock', checkPatches: true, manager: 'yarn' }, // Included in lockfile for v2+ + { name: 'pnpm-lock.yaml', checkPatches: false, manager: 'pnpm' }, // Included in lockfile + { name: 'bun.lockb', checkPatches: true, manager: 'bun' }, +].sort((_, { manager }) => { + return process.env.npm_config_user_agent?.startsWith(manager) ? 1 : -1 +}) +const lockfileNames = lockfileFormats.map((l) => l.name) export function getDepHash(config: ResolvedConfig, ssr: boolean): string { - const lockfilePath = lookupFile( - config.root, - lockfileFormats.map((l) => l.name), - { pathOnly: true }, - ) + const lockfilePath = lookupFile(config.root, lockfileNames) let content = lockfilePath ? fs.readFileSync(lockfilePath, 'utf-8') : '' if (lockfilePath) { const lockfileName = path.basename(lockfilePath) @@ -1192,11 +1227,9 @@ export function getDepHash(config: ResolvedConfig, ssr: boolean): string { if (checkPatches) { // Default of https://github.com/ds300/patch-package const fullPath = path.join(path.dirname(lockfilePath), 'patches') - if (fs.existsSync(fullPath)) { - const stats = fs.statSync(fullPath) - if (stats.isDirectory()) { - content += stats.mtimeMs.toString() - } + const stat = tryStatSync(fullPath) + if (stat?.isDirectory()) { + content += stat.mtimeMs.toString() } } } @@ -1301,7 +1334,7 @@ export async function cleanupDepsCacheStaleDirs( stats?.mtime && Date.now() - stats.mtime.getTime() > MAX_TEMP_DIR_AGE_MS ) { - await removeDir(tempDirPath) + await fsp.rm(tempDirPath, { recursive: true, force: true }) } } } @@ -1310,3 +1343,44 @@ export async function cleanupDepsCacheStaleDirs( config.logger.error(err) } } + +// We found issues with renaming folders in some systems. This is a custom +// implementation for the optimizer. It isn't intended to be a general utility + +// Based on node-graceful-fs + +// The ISC License +// Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors +// https://github.com/isaacs/node-graceful-fs/blob/main/LICENSE + +// On Windows, A/V software can lock the directory, causing this +// to fail with an EACCES or EPERM if the directory contains newly +// created files. The original tried for up to 60 seconds, we only +// wait for 5 seconds, as a longer time would be seen as an error + +const GRACEFUL_RENAME_TIMEOUT = 5000 +const safeRename = promisify(function gracefulRename( + from: string, + to: string, + cb: (error: NodeJS.ErrnoException | null) => void, +) { + const start = Date.now() + let backoff = 0 + fs.rename(from, to, function CB(er) { + if ( + er && + (er.code === 'EACCES' || er.code === 'EPERM') && + Date.now() - start < GRACEFUL_RENAME_TIMEOUT + ) { + setTimeout(function () { + fs.stat(to, function (stater, st) { + if (stater && stater.code === 'ENOENT') fs.rename(from, to, CB) + else CB(er) + }) + }, backoff) + if (backoff < 100) backoff += 10 + return + } + if (cb) cb(er) + }) +}) diff --git a/packages/vite/src/node/optimizer/optimizer.ts b/packages/vite/src/node/optimizer/optimizer.ts index 629f8b9b9ccd3a..f82dbe0f3bcf85 100644 --- a/packages/vite/src/node/optimizer/optimizer.ts +++ b/packages/vite/src/node/optimizer/optimizer.ts @@ -1,20 +1,18 @@ import colors from 'picocolors' -import _debug from 'debug' -import { getHash } from '../utils' -import { getDepOptimizationConfig } from '..' +import { createDebugger, getHash } from '../utils' +import { getDepOptimizationConfig } from '../config' import type { ResolvedConfig, ViteDevServer } from '..' import { addManuallyIncludedOptimizeDeps, addOptimizedDepInfo, + createIsOptimizedDepFile, createIsOptimizedDepUrl, - debuggerViteDeps as debug, depsFromOptimizedDepInfo, depsLogString, discoverProjectDependencies, extractExportsData, getOptimizedDepPath, initDepsOptimizerMetadata, - isOptimizedDepFile, loadCachedDepOptimizationMetadata, newDepOptimizationProcessing, optimizeServerSsrDeps, @@ -28,7 +26,7 @@ import type { OptimizedDepInfo, } from '.' -const isDebugEnabled = _debug('vite:deps').enabled +const debug = createDebugger('vite:deps') /** * The amount to wait for requests to register newly found dependencies before triggering @@ -99,9 +97,9 @@ async function createDepsOptimizer( const sessionTimestamp = Date.now().toString() - const cachedMetadata = loadCachedDepOptimizationMetadata(config, ssr) + const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr) - let handle: NodeJS.Timeout | undefined + let debounceProcessingHandle: NodeJS.Timeout | undefined let closed = false @@ -112,7 +110,7 @@ async function createDepsOptimizer( metadata, registerMissingImport, run: () => debouncedProcessing(0), - isOptimizedDepFile: (id: string) => isOptimizedDepFile(id, config), + isOptimizedDepFile: createIsOptimizedDepFile(config), isOptimizedDepUrl: createIsOptimizedDepUrl(config), getOptimizedDepId: (depInfo: OptimizedDepInfo) => isBuild ? depInfo.file : `${depInfo.file}?v=${depInfo.browserHash}`, @@ -157,9 +155,20 @@ async function createDepsOptimizer( let enqueuedRerun: (() => void) | undefined let currentlyProcessing = false - // If there wasn't a cache or it is outdated, we need to prepare a first run let firstRunCalled = !!cachedMetadata + // During build, we wait for every module to be scanned before resolving + // optimized deps loading for rollup on each rebuild. It will be recreated + // after each buildStart. + // During dev, if this is a cold run, we wait for static imports discovered + // from the first request before resolving to minimize full page reloads. + // On warm start or after the first optimization is run, we use a simpler + // debounce strategy each time a new dep is discovered. + let crawlEndFinder: CrawlEndFinder | undefined + if (isBuild || !cachedMetadata) { + crawlEndFinder = setupOnCrawlEnd(onCrawlEnd) + } + let optimizationResult: | { cancel: () => Promise @@ -174,14 +183,13 @@ async function createDepsOptimizer( } | undefined - let optimizingNewDeps: Promise | undefined async function close() { closed = true + crawlEndFinder?.cancel() await Promise.allSettled([ discover?.cancel(), depsOptimizer.scanProcessing, optimizationResult?.cancel(), - optimizingNewDeps, ]) } @@ -194,7 +202,7 @@ async function createDepsOptimizer( const deps: Record = {} await addManuallyIncludedOptimizeDeps(deps, config, ssr) - const discovered = await toDiscoveredDependencies( + const discovered = toDiscoveredDependencies( config, deps, ssr, @@ -212,25 +220,15 @@ async function createDepsOptimizer( if (!isBuild) { // Important, the scanner is dev only depsOptimizer.scanProcessing = new Promise((resolve) => { - // Ensure server listen is called before the scanner - setTimeout(async () => { + // Runs in the background in case blocking high priority tasks + ;(async () => { try { - debug(colors.green(`scanning for dependencies...`)) + debug?.(colors.green(`scanning for dependencies...`)) discover = discoverProjectDependencies(config) const deps = await discover.result discover = undefined - debug( - colors.green( - Object.keys(deps).length > 0 - ? `dependencies found by scanner: ${depsLogString( - Object.keys(deps), - )}` - : `no dependencies found by scanner`, - ), - ) - // Add these dependencies to the discovered list, as these are currently // used by the preAliasPlugin to support aliased and optimized deps. // This is also used by the CJS externalization heuristics in legacy mode @@ -253,7 +251,7 @@ async function createDepsOptimizer( resolve() depsOptimizer.scanProcessing = undefined } - }, 0) + })() }) } } @@ -270,27 +268,6 @@ async function createDepsOptimizer( depOptimizationProcessing = newDepOptimizationProcessing() } - async function optimizeNewDeps() { - // a successful completion of the optimizeDeps rerun will end up - // creating new bundled version of all current and discovered deps - // in the cache dir and a new metadata info object assigned - // to _metadata. A fullReload is only issued if the previous bundled - // dependencies have changed. - - // if the rerun fails, _metadata remains untouched, current discovered - // deps are cleaned, and a fullReload is issued - - // All deps, previous known and newly discovered are rebundled, - // respect insertion order to keep the metadata file stable - - const knownDeps = prepareKnownDeps() - - startNextDiscoveredBatch() - - optimizationResult = runOptimizeDeps(config, knownDeps) - return await optimizationResult.result - } - function prepareKnownDeps() { const knownDeps: Record = {} // Clone optimized info objects, fileHash, browserHash may be changed for them @@ -306,6 +283,18 @@ async function createDepsOptimizer( } async function runOptimizer(preRunResult?: DepOptimizationResult) { + // a successful completion of the optimizeDeps rerun will end up + // creating new bundled version of all current and discovered deps + // in the cache dir and a new metadata info object assigned + // to _metadata. A fullReload is only issued if the previous bundled + // dependencies have changed. + + // if the rerun fails, _metadata remains untouched, current discovered + // deps are cleaned, and a fullReload is issued + + // All deps, previous known and newly discovered are rebundled, + // respect insertion order to keep the metadata file stable + const isRerun = firstRunCalled firstRunCalled = true @@ -313,7 +302,7 @@ async function createDepsOptimizer( enqueuedRerun = undefined // Ensure that a rerun will not be issued for current discovered deps - if (handle) clearTimeout(handle) + if (debounceProcessingHandle) clearTimeout(debounceProcessingHandle) if (closed || Object.keys(metadata.discovered).length === 0) { currentlyProcessing = false @@ -323,9 +312,17 @@ async function createDepsOptimizer( currentlyProcessing = true try { - const processingResult = - preRunResult ?? (await (optimizingNewDeps = optimizeNewDeps())) - optimizingNewDeps = undefined + let processingResult: DepOptimizationResult + if (preRunResult) { + processingResult = preRunResult + } else { + const knownDeps = prepareKnownDeps() + startNextDiscoveredBatch() + + optimizationResult = runOptimizeDeps(config, knownDeps) + processingResult = await optimizationResult.result + optimizationResult = undefined + } if (closed) { currentlyProcessing = false @@ -407,7 +404,7 @@ async function createDepsOptimizer( if (!needsReload) { await commitProcessing() - if (!isDebugEnabled) { + if (!debug) { if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle) newDepsToLogHandle = setTimeout(() => { newDepsToLogHandle = undefined @@ -432,7 +429,7 @@ async function createDepsOptimizer( // once a rerun is committed processingResult.cancel() - debug( + debug?.( colors.green( `✨ delaying reload as new dependencies have been found...`, ), @@ -440,7 +437,7 @@ async function createDepsOptimizer( } else { await commitProcessing() - if (!isDebugEnabled) { + if (!debug) { if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle) newDepsToLogHandle = undefined logNewlyDiscoveredDeps() @@ -504,7 +501,7 @@ async function createDepsOptimizer( // optimizeDeps processing is finished const deps = Object.keys(metadata.discovered) const depsString = depsLogString(deps) - debug(colors.green(`new dependencies found: ${depsString}`)) + debug?.(colors.green(`new dependencies found: ${depsString}`)) runOptimizer() } @@ -544,7 +541,12 @@ async function createDepsOptimizer( // we can get a list of every missing dependency before giving to the // browser a dependency that may be outdated, thus avoiding full page reloads - if (firstRunCalled) { + if (!crawlEndFinder) { + if (isBuild) { + logger.error( + 'Vite Internal Error: Missing dependency found after crawling ended', + ) + } // Debounced rerun, let other missing dependencies be discovered before // the running next optimizeDeps debouncedProcessing() @@ -585,11 +587,11 @@ async function createDepsOptimizer( // Debounced rerun, let other missing dependencies be discovered before // the running next optimizeDeps enqueuedRerun = undefined - if (handle) clearTimeout(handle) + if (debounceProcessingHandle) clearTimeout(debounceProcessingHandle) if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle) newDepsToLogHandle = undefined - handle = setTimeout(() => { - handle = undefined + debounceProcessingHandle = setTimeout(() => { + debounceProcessingHandle = undefined enqueuedRerun = rerun if (!currentlyProcessing) { enqueuedRerun() @@ -597,14 +599,22 @@ async function createDepsOptimizer( }, timeout) } + // During dev, onCrawlEnd is called once when the server starts and all static + // imports after the first request have been crawled (dynamic imports may also + // be crawled if the browser requests them right away). + // During build, onCrawlEnd will be called once after each buildStart (so in + // watch mode it will be called after each rebuild has processed every module). + // All modules are transformed first in this case (both static and dynamic). async function onCrawlEnd() { - debug(colors.green(`✨ static imports crawl ended`)) - if (firstRunCalled) { + // On build time, a missing dep appearing after onCrawlEnd is an internal error + // On dev, switch after this point to a simple debounce strategy + crawlEndFinder = undefined + + debug?.(colors.green(`✨ static imports crawl ended`)) + if (closed) { return } - currentlyProcessing = false - const crawlDeps = Object.keys(metadata.discovered) // Await for the scan+optimize step running in the background @@ -614,11 +624,12 @@ async function createDepsOptimizer( if (!isBuild && optimizationResult) { const result = await optimizationResult.result optimizationResult = undefined + currentlyProcessing = false const scanDeps = Object.keys(result.metadata.optimized) if (scanDeps.length === 0 && crawlDeps.length === 0) { - debug( + debug?.( colors.green( `✨ no dependencies found by the scanner or crawling static imports`, ), @@ -647,16 +658,16 @@ async function createDepsOptimizer( } } if (scannerMissedDeps) { - debug( + debug?.( colors.yellow( `✨ new dependencies were found while crawling that weren't detected by the scanner`, ), ) } - debug(colors.green(`✨ re-running optimizer`)) + debug?.(colors.green(`✨ re-running optimizer`)) debouncedProcessing(0) } else { - debug( + debug?.( colors.green( `✨ using post-scan optimizer result, the scanner found every used dependency`, ), @@ -665,8 +676,10 @@ async function createDepsOptimizer( runOptimizer(result) } } else { + currentlyProcessing = false + if (crawlDeps.length === 0) { - debug( + debug?.( colors.green( `✨ no dependencies found while crawling the static imports`, ), @@ -679,81 +692,113 @@ async function createDepsOptimizer( } } - const runOptimizerIfIdleAfterMs = 100 + // Called during buildStart at build time, when build --watch is used. + function resetRegisteredIds() { + crawlEndFinder?.cancel() + crawlEndFinder = setupOnCrawlEnd(onCrawlEnd) + } - let registeredIds: { id: string; done: () => Promise }[] = [] - let seenIds = new Set() - let workersSources = new Set() - let waitingOn: string | undefined - let firstRunEnsured = false + function registerWorkersSource(id: string) { + crawlEndFinder?.registerWorkersSource(id) + } + function delayDepsOptimizerUntil(id: string, done: () => Promise) { + if (crawlEndFinder && !depsOptimizer.isOptimizedDepFile(id)) { + crawlEndFinder.delayDepsOptimizerUntil(id, done) + } + } + function ensureFirstRun() { + crawlEndFinder?.ensureFirstRun() + } +} - function resetRegisteredIds() { - registeredIds = [] - seenIds = new Set() - workersSources = new Set() - waitingOn = undefined - firstRunEnsured = false +const callCrawlEndIfIdleAfterMs = 50 + +interface CrawlEndFinder { + ensureFirstRun: () => void + registerWorkersSource: (id: string) => void + delayDepsOptimizerUntil: (id: string, done: () => Promise) => void + cancel: () => void +} + +function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder { + const registeredIds = new Set() + const seenIds = new Set() + const workersSources = new Set() + let timeoutHandle: NodeJS.Timeout | undefined + + let cancelled = false + function cancel() { + cancelled = true + } + + let crawlEndCalled = false + function callOnCrawlEnd() { + if (!cancelled && !crawlEndCalled) { + crawlEndCalled = true + onCrawlEnd() + } } // If all the inputs are dependencies, we aren't going to get any // delayDepsOptimizerUntil(id) calls. We need to guard against this // by forcing a rerun if no deps have been registered + let firstRunEnsured = false function ensureFirstRun() { - if (!firstRunEnsured && !firstRunCalled && registeredIds.length === 0) { + if (!firstRunEnsured && seenIds.size === 0) { setTimeout(() => { - if (!closed && registeredIds.length === 0) { - onCrawlEnd() + if (seenIds.size === 0) { + callOnCrawlEnd() } - }, runOptimizerIfIdleAfterMs) + }, 200) } firstRunEnsured = true } function registerWorkersSource(id: string): void { workersSources.add(id) + // Avoid waiting for this id, as it may be blocked by the rollup // bundling process of the worker that also depends on the optimizer - registeredIds = registeredIds.filter((registered) => registered.id !== id) - if (waitingOn === id) { - waitingOn = undefined - runOptimizerWhenIdle() - } + registeredIds.delete(id) + + checkIfCrawlEndAfterTimeout() } function delayDepsOptimizerUntil(id: string, done: () => Promise): void { - if (!depsOptimizer.isOptimizedDepFile(id) && !seenIds.has(id)) { + if (!seenIds.has(id)) { seenIds.add(id) - registeredIds.push({ id, done }) - runOptimizerWhenIdle() + if (!workersSources.has(id)) { + registeredIds.add(id) + done() + .catch(() => {}) + .finally(() => markIdAsDone(id)) + } } } + function markIdAsDone(id: string): void { + registeredIds.delete(id) + checkIfCrawlEndAfterTimeout() + } - function runOptimizerWhenIdle() { - if (!waitingOn) { - const next = registeredIds.pop() - if (next) { - waitingOn = next.id - const afterLoad = () => { - waitingOn = undefined - if (!closed && !workersSources.has(next.id)) { - if (registeredIds.length > 0) { - runOptimizerWhenIdle() - } else { - onCrawlEnd() - } - } - } - next - .done() - .then(() => { - setTimeout( - afterLoad, - registeredIds.length > 0 ? 0 : runOptimizerIfIdleAfterMs, - ) - }) - .catch(afterLoad) - } - } + function checkIfCrawlEndAfterTimeout() { + if (cancelled || registeredIds.size > 0) return + + if (timeoutHandle) clearTimeout(timeoutHandle) + timeoutHandle = setTimeout( + callOnCrawlEndWhenIdle, + callCrawlEndIfIdleAfterMs, + ) + } + async function callOnCrawlEndWhenIdle() { + if (cancelled || registeredIds.size > 0) return + callOnCrawlEnd() + } + + return { + ensureFirstRun, + registerWorkersSource, + delayDepsOptimizerUntil, + cancel, } } @@ -764,7 +809,7 @@ async function createDevSsrDepsOptimizer( const depsOptimizer = { metadata, - isOptimizedDepFile: (id: string) => isOptimizedDepFile(id, config), + isOptimizedDepFile: createIsOptimizedDepFile(config), isOptimizedDepUrl: createIsOptimizedDepUrl(config), getOptimizedDepId: (depInfo: OptimizedDepInfo) => `${depInfo.file}?v=${depInfo.browserHash}`, @@ -804,7 +849,7 @@ function findInteropMismatches( // This only happens when a discovered dependency has mixed ESM and CJS syntax // and it hasn't been manually added to optimizeDeps.needsInterop needsInteropMismatch.push(dep) - debug(colors.cyan(`✨ needsInterop mismatch detected for ${dep}`)) + debug?.(colors.cyan(`✨ needsInterop mismatch detected for ${dep}`)) } } } diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 7929b337b8a6e4..5571d031245f6d 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import fsp from 'node:fs/promises' import path from 'node:path' import { performance } from 'node:perf_hooks' import glob from 'fast-glob' @@ -17,6 +18,7 @@ import { createDebugger, dataUrlRE, externalRE, + isInNodeModules, isObject, isOptimizable, moduleListContains, @@ -82,7 +84,11 @@ export function scanImports(config: ResolvedConfig): { } if (scanContext.cancelled) return - debug(`Crawling dependencies using entries:\n ${entries.join('\n ')}`) + debug?.( + `Crawling dependencies using entries: ${entries + .map((entry) => `\n ${colors.dim(entry)}`) + .join('')}`, + ) return prepareEsbuildScanner(config, entries, deps, missing, scanContext) }) @@ -134,10 +140,15 @@ export function scanImports(config: ResolvedConfig): { throw e }) .finally(() => { - debug( - `Scan completed in ${(performance.now() - start).toFixed(2)}ms:`, - deps, - ) + if (debug) { + const duration = (performance.now() - start).toFixed(2) + const depsStr = + Object.keys(orderedDependencies(deps)) + .sort() + .map((id) => `\n ${colors.cyan(id)} -> ${colors.dim(deps[id])}`) + .join('') || colors.dim('no dependencies found') + debug(`Scan completed in ${duration}ms: ${depsStr}`) + } }) return { @@ -349,7 +360,7 @@ function esbuildScanPlugin( // If we can optimize this html type, skip it so it's handled by the // bare import resolve, and recorded as optimization dep. if ( - resolved.includes('node_modules') && + isInNodeModules(resolved) && isOptimizable(resolved, config.optimizeDeps) ) return @@ -363,7 +374,7 @@ function esbuildScanPlugin( build.onLoad( { filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => { - let raw = fs.readFileSync(path, 'utf-8') + let raw = await fsp.readFile(path, 'utf-8') // Avoid matching the content of the comment raw = raw.replace(commentRE, '') const isHtml = path.endsWith('.html') @@ -490,7 +501,7 @@ function esbuildScanPlugin( if (shouldExternalizeDep(resolved, id)) { return externalUnlessEntry({ path: id }) } - if (resolved.includes('node_modules') || include?.includes(id)) { + if (isInNodeModules(resolved) || include?.includes(id)) { // dependency or forced included, externalize and stop crawling if (isOptimizable(resolved, config.optimizeDeps)) { depImports[id] = resolved @@ -576,7 +587,7 @@ function esbuildScanPlugin( let ext = path.extname(id).slice(1) if (ext === 'mjs') ext = 'js' - let contents = fs.readFileSync(id, 'utf-8') + let contents = await fsp.readFile(id, 'utf-8') if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) { contents = config.esbuild.jsxInject + `\n` + contents } diff --git a/packages/vite/src/node/packages.ts b/packages/vite/src/node/packages.ts index 1451d18853729a..c82367a48cca64 100644 --- a/packages/vite/src/node/packages.ts +++ b/packages/vite/src/node/packages.ts @@ -1,13 +1,16 @@ import fs from 'node:fs' import path from 'node:path' -import { createDebugger, createFilter, resolveFrom } from './utils' -import type { ResolvedConfig } from './config' +import { createRequire } from 'node:module' +import { createFilter, isInNodeModules, safeRealpathSync } from './utils' import type { Plugin } from './plugin' -const isDebug = process.env.DEBUG -const debug = createDebugger('vite:resolve-details', { - onlyWhenFocused: true, -}) +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +let pnp: typeof import('pnpapi') | undefined +if (process.versions.pnp) { + try { + pnp = createRequire(import.meta.url)('pnpapi') + } catch {} +} /** Cache for package.json resolution and package.json contents */ export type PackageCache = Map @@ -33,11 +36,10 @@ export interface PackageData { } } -export function invalidatePackageData( +function invalidatePackageData( packageCache: PackageCache, pkgPath: string, ): void { - packageCache.delete(pkgPath) const pkgDir = path.dirname(pkgPath) packageCache.forEach((pkg, cacheKey) => { if (pkg.dir === pkgDir) { @@ -47,53 +49,123 @@ export function invalidatePackageData( } export function resolvePackageData( - id: string, + pkgName: string, basedir: string, preserveSymlinks = false, packageCache?: PackageCache, ): PackageData | null { - let pkg: PackageData | undefined - let cacheKey: string | undefined - if (packageCache) { - cacheKey = `${id}&${basedir}&${preserveSymlinks}` - if ((pkg = packageCache.get(cacheKey))) { - return pkg + if (pnp) { + const cacheKey = getRpdCacheKey(pkgName, basedir, preserveSymlinks) + if (packageCache?.has(cacheKey)) return packageCache.get(cacheKey)! + + let pkg: string | null + try { + pkg = pnp.resolveToUnqualified(pkgName, basedir, { + considerBuiltins: false, + }) + } catch { + return null } + if (!pkg) return null + + const pkgData = loadPackageData(path.join(pkg, 'package.json')) + packageCache?.set(cacheKey, pkgData) + + return pkgData } - let pkgPath: string | undefined - try { - pkgPath = resolveFrom(`${id}/package.json`, basedir, preserveSymlinks) - pkg = loadPackageData(pkgPath, true, packageCache) + + const originalBasedir = basedir + while (basedir) { if (packageCache) { - packageCache.set(cacheKey!, pkg) - } - return pkg - } catch (e) { - if (e instanceof SyntaxError) { - isDebug && debug(`Parsing failed: ${pkgPath}`) - } - // Ignore error for missing package.json - else if (e.code !== 'MODULE_NOT_FOUND') { - throw e + const cached = getRpdCache( + packageCache, + pkgName, + basedir, + originalBasedir, + preserveSymlinks, + ) + if (cached) return cached } + + const pkg = path.join(basedir, 'node_modules', pkgName, 'package.json') + try { + if (fs.existsSync(pkg)) { + const pkgPath = preserveSymlinks ? pkg : safeRealpathSync(pkg) + const pkgData = loadPackageData(pkgPath) + + if (packageCache) { + setRpdCache( + packageCache, + pkgData, + pkgName, + basedir, + originalBasedir, + preserveSymlinks, + ) + } + + return pkgData + } + } catch {} + + const nextBasedir = path.dirname(basedir) + if (nextBasedir === basedir) break + basedir = nextBasedir } + return null } -export function loadPackageData( - pkgPath: string, - preserveSymlinks?: boolean, +export function findNearestPackageData( + basedir: string, packageCache?: PackageCache, -): PackageData { - if (!preserveSymlinks) { - pkgPath = fs.realpathSync.native(pkgPath) - } +): PackageData | null { + const originalBasedir = basedir + while (basedir) { + if (packageCache) { + const cached = getFnpdCache(packageCache, basedir, originalBasedir) + if (cached) return cached + } + + const pkgPath = path.join(basedir, 'package.json') + try { + if (fs.statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) { + const pkgData = loadPackageData(pkgPath) + + if (packageCache) { + setFnpdCache(packageCache, pkgData, basedir, originalBasedir) + } + + return pkgData + } + } catch {} - let cached: PackageData | undefined - if ((cached = packageCache?.get(pkgPath))) { - return cached + const nextBasedir = path.dirname(basedir) + if (nextBasedir === basedir) break + basedir = nextBasedir } + return null +} + +// Finds the nearest package.json with a `name` field +export function findNearestMainPackageData( + basedir: string, + packageCache?: PackageCache, +): PackageData | null { + const nearestPackage = findNearestPackageData(basedir, packageCache) + return ( + nearestPackage && + (nearestPackage.data.name + ? nearestPackage + : findNearestMainPackageData( + path.dirname(nearestPackage.dir), + packageCache, + )) + ) +} + +export function loadPackageData(pkgPath: string): PackageData { const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) const pkgDir = path.dirname(pkgPath) const { sideEffects } = data @@ -142,21 +214,24 @@ export function loadPackageData( }, } - packageCache?.set(pkgPath, pkg) return pkg } -export function watchPackageDataPlugin(config: ResolvedConfig): Plugin { +export function watchPackageDataPlugin(packageCache: PackageCache): Plugin { + // a list of files to watch before the plugin is ready const watchQueue = new Set() - let watchFile = (id: string) => { + const watchedDirs = new Set() + + const watchFileStub = (id: string) => { watchQueue.add(id) } + let watchFile = watchFileStub - const { packageCache } = config const setPackageData = packageCache.set.bind(packageCache) packageCache.set = (id, pkg) => { - if (id.endsWith('.json')) { - watchFile(id) + if (!isInNodeModules(pkg.dir) && !watchedDirs.has(pkg.dir)) { + watchedDirs.add(pkg.dir) + watchFile(path.join(pkg.dir, 'package.json')) } return setPackageData(id, pkg) } @@ -164,17 +239,124 @@ export function watchPackageDataPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:watch-package-data', buildStart() { - watchFile = this.addWatchFile + watchFile = this.addWatchFile.bind(this) watchQueue.forEach(watchFile) watchQueue.clear() }, buildEnd() { - watchFile = (id) => watchQueue.add(id) + watchFile = watchFileStub }, watchChange(id) { if (id.endsWith('/package.json')) { - invalidatePackageData(packageCache, id) + invalidatePackageData(packageCache, path.normalize(id)) + } + }, + handleHotUpdate({ file }) { + if (file.endsWith('/package.json')) { + invalidatePackageData(packageCache, path.normalize(file)) } }, } } + +/** + * Get cached `resolvePackageData` value based on `basedir`. When one is found, + * and we've already traversed some directories between `basedir` and `originalBasedir`, + * we cache the value for those in-between directories as well. + * + * This makes it so the fs is only read once for a shared `basedir`. + */ +function getRpdCache( + packageCache: PackageCache, + pkgName: string, + basedir: string, + originalBasedir: string, + preserveSymlinks: boolean, +) { + const cacheKey = getRpdCacheKey(pkgName, basedir, preserveSymlinks) + const pkgData = packageCache.get(cacheKey) + if (pkgData) { + traverseBetweenDirs(originalBasedir, basedir, (dir) => { + packageCache.set(getRpdCacheKey(pkgName, dir, preserveSymlinks), pkgData) + }) + return pkgData + } +} + +function setRpdCache( + packageCache: PackageCache, + pkgData: PackageData, + pkgName: string, + basedir: string, + originalBasedir: string, + preserveSymlinks: boolean, +) { + packageCache.set(getRpdCacheKey(pkgName, basedir, preserveSymlinks), pkgData) + traverseBetweenDirs(originalBasedir, basedir, (dir) => { + packageCache.set(getRpdCacheKey(pkgName, dir, preserveSymlinks), pkgData) + }) +} + +// package cache key for `resolvePackageData` +function getRpdCacheKey( + pkgName: string, + basedir: string, + preserveSymlinks: boolean, +) { + return `rpd_${pkgName}_${basedir}_${preserveSymlinks}` +} + +/** + * Get cached `findNearestPackageData` value based on `basedir`. When one is found, + * and we've already traversed some directories between `basedir` and `originalBasedir`, + * we cache the value for those in-between directories as well. + * + * This makes it so the fs is only read once for a shared `basedir`. + */ +function getFnpdCache( + packageCache: PackageCache, + basedir: string, + originalBasedir: string, +) { + const cacheKey = getFnpdCacheKey(basedir) + const pkgData = packageCache.get(cacheKey) + if (pkgData) { + traverseBetweenDirs(originalBasedir, basedir, (dir) => { + packageCache.set(getFnpdCacheKey(dir), pkgData) + }) + return pkgData + } +} + +function setFnpdCache( + packageCache: PackageCache, + pkgData: PackageData, + basedir: string, + originalBasedir: string, +) { + packageCache.set(getFnpdCacheKey(basedir), pkgData) + traverseBetweenDirs(originalBasedir, basedir, (dir) => { + packageCache.set(getFnpdCacheKey(dir), pkgData) + }) +} + +// package cache key for `findNearestPackageData` +function getFnpdCacheKey(basedir: string) { + return `fnpd_${basedir}` +} + +/** + * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir. + * @param longerDir Longer dir path, e.g. `/User/foo/bar/baz` + * @param shorterDir Shorter dir path, e.g. `/User/foo` + */ +function traverseBetweenDirs( + longerDir: string, + shorterDir: string, + cb: (dir: string) => void, +) { + while (longerDir !== shorterDir) { + cb(longerDir) + longerDir = path.dirname(longerDir) + } +} diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index 64b702c2b1b03c..e522295f30c896 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -9,13 +9,12 @@ import type { TransformResult, } from 'rollup' export type { PluginContext } from 'rollup' -import type { UserConfig } from './config' +import type { ConfigEnv, ResolvedConfig, UserConfig } from './config' import type { ServerHook } from './server' import type { IndexHtmlTransform } from './plugins/html' import type { ModuleNode } from './server/moduleGraph' import type { HmrContext } from './server/hmr' import type { PreviewServerHook } from './preview' -import type { ConfigEnv, ResolvedConfig } from './' /** * Vite plugins extends the Rollup plugin interface with a few extra @@ -91,8 +90,9 @@ export interface Plugin extends RollupPlugin { */ configureServer?: ObjectHook /** - * Configure the preview server. The hook receives the connect server and - * its underlying http server. + * Configure the preview server. The hook receives the {@link PreviewServerForHook} + * instance. This can also be used to store a reference to the server + * for use in other hooks. * * The hooks are called before other middlewares are applied. A hook can * return a post hook that will be called after other middlewares are diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 134625c87dd078..174b3e5826646c 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -16,7 +16,13 @@ import { } from '../build' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' -import { cleanUrl, getHash, joinUrlSegments, normalizePath } from '../utils' +import { + cleanUrl, + getHash, + joinUrlSegments, + normalizePath, + removeLeadingSlash, +} from '../utils' import { FS_PREFIX } from '../constants' export const assetUrlRE = /__VITE_ASSET__([a-z\d]+)__(?:\$_(.*?)__)?/g @@ -24,6 +30,7 @@ export const assetUrlRE = /__VITE_ASSET__([a-z\d]+)__(?:\$_(.*?)__)?/g const rawRE = /(?:\?|&)raw(?:&|$)/ const urlRE = /(\?|&)url(?:&|$)/ const jsSourceMapRE = /\.[cm]?js\.map$/ +const unnededFinalQueryCharRE = /[?&]$/ const assetCache = new WeakMap>() @@ -47,6 +54,8 @@ export function registerCustomMime(): void { mrmime.mimes['flac'] = 'audio/flac' // mrmime and mime-db is not released yet: https://github.com/jshttp/mime-db/commit/c9242a9b7d4bb25d7a0c9244adec74aeef08d8a1 mrmime.mimes['aac'] = 'audio/aac' + // https://wiki.xiph.org/MIME_Types_and_File_Extensions#.opus_-_audio/ogg + mrmime.mimes['opus'] = 'audio/ogg' // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types mrmime.mimes['eot'] = 'application/vnd.ms-fontobject' } @@ -60,6 +69,7 @@ export function renderAssetUrlInJS( ): MagicString | undefined { const toRelativeRuntime = createToImportMetaURLBasedRelativeRuntime( opts.format, + config.isWorker, ) let match: RegExpExecArray | null @@ -148,7 +158,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { }, async load(id) { - if (id.startsWith('\0')) { + if (id[0] === '\0') { // Rollup convention, this id should be handled by the // plugin that marked it with \0 return @@ -167,7 +177,7 @@ export function assetPlugin(config: ResolvedConfig): Plugin { return } - id = id.replace(urlRE, '$1').replace(/[?&]$/, '') + id = id.replace(urlRE, '$1').replace(unnededFinalQueryCharRE, '') const url = await fileToUrl(id, config, this) return `export default ${JSON.stringify(url)}` }, @@ -212,7 +222,7 @@ export function checkPublicFile( ): string | undefined { // note if the file is in /public, the resolver would have returned it // as-is so it's not going to be a fully resolved path. - if (!publicDir || !url.startsWith('/')) { + if (!publicDir || url[0] !== '/') { return } const publicFile = path.join(publicDir, cleanUrl(url)) @@ -253,7 +263,7 @@ function fileToDevUrl(id: string, config: ResolvedConfig) { rtn = path.posix.join(FS_PREFIX, id) } const base = joinUrlSegments(config.server?.origin ?? '', config.base) - return joinUrlSegments(base, rtn.replace(/^\//, '')) + return joinUrlSegments(base, removeLeadingSlash(rtn)) } export function getPublicAssetFilename( @@ -369,9 +379,10 @@ export async function urlToBuiltUrl( if (checkPublicFile(url, config)) { return publicFileToBuiltUrl(url, config) } - const file = url.startsWith('/') - ? path.join(config.root, url) - : path.join(path.dirname(importer), url) + const file = + url[0] === '/' + ? path.join(config.root, url) + : path.join(path.dirname(importer), url) return fileToBuiltUrl( file, config, diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index 00aa1c466c406b..6872de5249e014 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -10,6 +10,7 @@ import { slash, transformStableResult, } from '../utils' +import { CLIENT_ENTRY } from '../constants' import { fileToUrl } from './asset' import { preloadHelperId } from './importAnalysisBuild' @@ -33,6 +34,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { if ( !options?.ssr && id !== preloadHelperId && + id !== CLIENT_ENTRY && code.includes('new URL') && code.includes(`import.meta.url`) ) { @@ -52,11 +54,17 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { if (!s) s = new MagicString(code) // potential dynamic template string - if (rawUrl[0] === '`' && /\$\{/.test(rawUrl)) { + if (rawUrl[0] === '`' && rawUrl.includes('${')) { const ast = this.parse(rawUrl) const templateLiteral = (ast as any).body[0].expression if (templateLiteral.expressions.length) { - const pattern = JSON.stringify(buildGlobPattern(templateLiteral)) + const pattern = buildGlobPattern(templateLiteral) + if (pattern.startsWith('**')) { + // don't transform for patterns like this + // because users won't intend to do that in most cases + continue + } + // Note: native import.meta.url is not supported in the baseline // target so we use the global location here. It can be // window.location or self.location in case it is used in a Web Worker. @@ -64,7 +72,9 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { s.update( index, index + exp.length, - `new URL((import.meta.glob(${pattern}, { eager: true, import: 'default', as: 'url' }))[${rawUrl}], self.location)`, + `new URL((import.meta.glob(${JSON.stringify( + pattern, + )}, { eager: true, import: 'default', as: 'url' }))[${rawUrl}], self.location)`, ) continue } @@ -72,7 +82,7 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { const url = rawUrl.slice(1, -1) let file: string | undefined - if (url.startsWith('.')) { + if (url[0] === '.') { file = slash(path.resolve(path.dirname(id), url)) } else { assetResolver ??= config.createResolver({ diff --git a/packages/vite/src/node/plugins/clientInjections.ts b/packages/vite/src/node/plugins/clientInjections.ts index bf46c0e42ce597..5ac79c8d14ef8e 100644 --- a/packages/vite/src/node/plugins/clientInjections.ts +++ b/packages/vite/src/node/plugins/clientInjections.ts @@ -4,6 +4,9 @@ import type { ResolvedConfig } from '../config' import { CLIENT_ENTRY, ENV_ENTRY } from '../constants' import { isObject, normalizePath, resolveHostname } from '../utils' +const process_env_NODE_ENV_RE = + /(\bglobal(This)?\.)?\bprocess\.env\.NODE_ENV\b/g + // ids in transform are normalized to unix style const normalizedClientEntry = normalizePath(CLIENT_ENTRY) const normalizedEnvEntry = normalizePath(ENV_ENTRY) @@ -13,60 +16,80 @@ const normalizedEnvEntry = normalizePath(ENV_ENTRY) * @server-only */ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { + let injectConfigValues: (code: string) => string + return { name: 'vite:client-inject', - async transform(code, id, options) { - if (id === normalizedClientEntry || id === normalizedEnvEntry) { - const resolvedServerHostname = ( - await resolveHostname(config.server.host) - ).name - const resolvedServerPort = config.server.port! - const devBase = config.base + async buildStart() { + const resolvedServerHostname = (await resolveHostname(config.server.host)) + .name + const resolvedServerPort = config.server.port! + const devBase = config.base - const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}` + const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}` + + let hmrConfig = config.server.hmr + hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined + const host = hmrConfig?.host || null + const protocol = hmrConfig?.protocol || null + const timeout = hmrConfig?.timeout || 30000 + const overlay = hmrConfig?.overlay !== false + const isHmrServerSpecified = !!hmrConfig?.server + + // hmr.clientPort -> hmr.port + // -> (24678 if middleware mode and HMR server is not specified) -> new URL(import.meta.url).port + let port = hmrConfig?.clientPort || hmrConfig?.port || null + if (config.server.middlewareMode && !isHmrServerSpecified) { + port ||= 24678 + } - let hmrConfig = config.server.hmr - hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined - const host = hmrConfig?.host || null - const protocol = hmrConfig?.protocol || null - const timeout = hmrConfig?.timeout || 30000 - const overlay = hmrConfig?.overlay !== false - const isHmrServerSpecified = !!hmrConfig?.server + let directTarget = hmrConfig?.host || resolvedServerHostname + directTarget += `:${hmrConfig?.port || resolvedServerPort}` + directTarget += devBase - // hmr.clientPort -> hmr.port - // -> (24678 if middleware mode and HMR server is not specified) -> new URL(import.meta.url).port - let port = hmrConfig?.clientPort || hmrConfig?.port || null - if (config.server.middlewareMode && !isHmrServerSpecified) { - port ||= 24678 - } + let hmrBase = devBase + if (hmrConfig?.path) { + hmrBase = path.posix.join(hmrBase, hmrConfig.path) + } - let directTarget = hmrConfig?.host || resolvedServerHostname - directTarget += `:${hmrConfig?.port || resolvedServerPort}` - directTarget += devBase + const serializedDefines = serializeDefine(config.define || {}) - let hmrBase = devBase - if (hmrConfig?.path) { - hmrBase = path.posix.join(hmrBase, hmrConfig.path) - } + const modeReplacement = escapeReplacement(config.mode) + const baseReplacement = escapeReplacement(devBase) + const definesReplacement = () => serializedDefines + const serverHostReplacement = escapeReplacement(serverHost) + const hmrProtocolReplacement = escapeReplacement(protocol) + const hmrHostnameReplacement = escapeReplacement(host) + const hmrPortReplacement = escapeReplacement(port) + const hmrDirectTargetReplacement = escapeReplacement(directTarget) + const hmrBaseReplacement = escapeReplacement(hmrBase) + const hmrTimeoutReplacement = escapeReplacement(timeout) + const hmrEnableOverlayReplacement = escapeReplacement(overlay) + injectConfigValues = (code: string) => { return code - .replace(`__MODE__`, JSON.stringify(config.mode)) - .replace(/__BASE__/g, JSON.stringify(devBase)) - .replace(`__DEFINES__`, serializeDefine(config.define || {})) - .replace(`__SERVER_HOST__`, JSON.stringify(serverHost)) - .replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)) - .replace(`__HMR_HOSTNAME__`, JSON.stringify(host)) - .replace(`__HMR_PORT__`, JSON.stringify(port)) - .replace(`__HMR_DIRECT_TARGET__`, JSON.stringify(directTarget)) - .replace(`__HMR_BASE__`, JSON.stringify(hmrBase)) - .replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout)) - .replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay)) + .replace(`__MODE__`, modeReplacement) + .replace(/__BASE__/g, baseReplacement) + .replace(`__DEFINES__`, definesReplacement) + .replace(`__SERVER_HOST__`, serverHostReplacement) + .replace(`__HMR_PROTOCOL__`, hmrProtocolReplacement) + .replace(`__HMR_HOSTNAME__`, hmrHostnameReplacement) + .replace(`__HMR_PORT__`, hmrPortReplacement) + .replace(`__HMR_DIRECT_TARGET__`, hmrDirectTargetReplacement) + .replace(`__HMR_BASE__`, hmrBaseReplacement) + .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement) + .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement) + } + }, + transform(code, id, options) { + if (id === normalizedClientEntry || id === normalizedEnvEntry) { + return injectConfigValues(code) } else if (!options?.ssr && code.includes('process.env.NODE_ENV')) { // replace process.env.NODE_ENV instead of defining a global // for it to avoid shimming a `process` object during dev, // avoiding inconsistencies between dev and build return code.replace( - /(\bglobal(This)?\.)?\bprocess\.env\.NODE_ENV\b/g, + process_env_NODE_ENV_RE, config.define?.['process.env.NODE_ENV'] || JSON.stringify(process.env.NODE_ENV || config.mode), ) @@ -75,6 +98,11 @@ export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { } } +function escapeReplacement(value: string | number | boolean | null) { + const jsonValue = JSON.stringify(value) + return () => jsonValue +} + function serializeDefine(define: Record): string { let res = `{` for (const key in define) { diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 9511646956ea29..7366c1b093799d 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import fsp from 'node:fs/promises' import path from 'node:path' import { createRequire } from 'node:module' import glob from 'fast-glob' @@ -25,7 +26,7 @@ import type { RawSourceMap } from '@ampproject/remapping' import { getCodeWithSourcemap, injectSourcesContent } from '../server/sourcemap' import type { ModuleNode } from '../server/moduleGraph' import type { ResolveFn, ViteDevServer } from '../' -import { toOutputFilePathInCss } from '../build' +import { resolveUserExternal, toOutputFilePathInCss } from '../build' import { CLIENT_PUBLIC_PATH, CSS_LANGS_RE, @@ -166,10 +167,10 @@ export const removedPureCssFilesCache = new WeakMap< Map >() -const postcssConfigCache: Record< - string, - WeakMap -> = {} +const postcssConfigCache = new WeakMap< + ResolvedConfig, + PostCSSConfigResult | null | Promise +>() function encodePublicUrlsInCSS(config: ResolvedConfig) { return config.command === 'build' @@ -188,6 +189,9 @@ export function cssPlugin(config: ResolvedConfig): Plugin { extensions: [], }) + // warm up cache for resolved postcss config + resolvePostcssConfig(config) + return { name: 'vite:css', @@ -226,10 +230,21 @@ export function cssPlugin(config: ResolvedConfig): Plugin { return fileToUrl(resolved, config, this) } if (config.command === 'build') { - // #9800 If we cannot resolve the css url, leave a warning. - config.logger.warnOnce( - `\n${url} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`, - ) + const isExternal = config.build.rollupOptions.external + ? resolveUserExternal( + config.build.rollupOptions.external, + url, // use URL as id since id could not be resolved + id, + false, + ) + : false + + if (!isExternal) { + // #9800 If we cannot resolve the css url, leave a warning. + config.logger.warnOnce( + `\n${url} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`, + ) + } } return url } @@ -312,6 +327,8 @@ export function cssPlugin(config: ResolvedConfig): Plugin { export function cssPostPlugin(config: ResolvedConfig): Plugin { // styles initialization in buildStart causes a styling loss in watch const styles: Map = new Map() + // list of css emit tasks to guarantee the files are emitted in a deterministic order + let emitTasks: Promise[] = [] let pureCssChunks: Set // when there are multiple rollup outputs and extracting CSS, only emit once, @@ -349,6 +366,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { pureCssChunks = new Set() outputToExtractedCSSMap = new Map() hasEmitted = false + emitTasks = [] }, async transform(css, id, options) { @@ -502,9 +520,7 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const toRelative = (filename: string, importer: string) => { // relative base + extracted CSS const relativePath = path.posix.relative(cssAssetDirname!, filename) - return relativePath.startsWith('.') - ? relativePath - : './' + relativePath + return relativePath[0] === '.' ? relativePath : './' + relativePath } // replace asset url references with resolved url. @@ -561,7 +577,22 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { const cssFileName = ensureFileExt(cssAssetName, '.css') chunkCSS = resolveAssetUrlsInCss(chunkCSS, cssAssetName) - chunkCSS = await finalizeCss(chunkCSS, true, config) + + const previousTask = emitTasks[emitTasks.length - 1] + // finalizeCss is async which makes `emitFile` non-deterministic, so + // we use a `.then` to wait for previous tasks before finishing this + const thisTask = finalizeCss(chunkCSS, true, config).then((css) => { + chunkCSS = css + // make sure the previous task is also finished, this works recursively + return previousTask + }) + + // push this task so the next task can wait for this one + emitTasks.push(thisTask) + const emitTasksLength = emitTasks.length + + // wait for this and previous tasks to finish + await thisTask // emit corresponding css file const referenceId = this.emitFile({ @@ -575,6 +606,11 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { .get(config)! .set(referenceId, { originalName, isEntry }) chunk.viteMetadata!.importedCss.add(this.getFileName(referenceId)) + + if (emitTasksLength === emitTasks.length) { + // this is the last task, clear `emitTasks` to free up memory + emitTasks = [] + } } else if (!config.build.ssr) { // legacy build and inline css @@ -803,7 +839,7 @@ async function compileCSS( const needInlineImport = code.includes('@import') const hasUrl = cssUrlRE.test(code) || cssImageSetRE.test(code) const lang = id.match(CSS_LANGS_RE)?.[1] as CssLang | undefined - const postcssConfig = await resolvePostcssConfig(config, getCssDialect(lang)) + const postcssConfig = await resolvePostcssConfig(config) // 1. plain css that needs no processing if ( @@ -884,20 +920,12 @@ async function compileCSS( // 3. postcss const postcssOptions = (postcssConfig && postcssConfig.options) || {} - // for sugarss change parser - if (lang === 'sss') { - postcssOptions.parser = loadPreprocessor( - PostCssDialectLang.sss, - config.root, - ) - } - const postcssPlugins = postcssConfig && postcssConfig.plugins ? postcssConfig.plugins.slice() : [] if (needInlineImport) { postcssPlugins.unshift( - (await import('postcss-import')).default({ + (await importPostcssImport()).default({ async resolve(id, basedir) { const publicFile = checkPublicFile(id, config) if (publicFile) { @@ -912,6 +940,18 @@ async function compileCSS( if (resolved) { return path.resolve(resolved) } + + // postcss-import falls back to `resolve` dep if this is unresolved, + // but we've shimmed to remove the `resolve` dep to cut on bundle size. + // warn here to provide a better error message. + if (!path.isAbsolute(id)) { + config.logger.error( + colors.red( + `Unable to resolve \`@import "${id}"\` from ${basedir}`, + ), + ) + } + return id }, nameLayer(index) { @@ -932,7 +972,7 @@ async function compileCSS( if (isModule) { postcssPlugins.unshift( - (await import('postcss-modules')).default({ + (await importPostcssModules()).default({ ...modulesOptions, localsConvention: modulesOptions?.localsConvention, getJSON( @@ -969,27 +1009,30 @@ async function compileCSS( let postcssResult: PostCSS.Result try { const source = removeDirectQuery(id) + const postcss = await importPostcss() // postcss is an unbundled dep and should be lazy imported - postcssResult = await (await import('postcss')) - .default(postcssPlugins) - .process(code, { - ...postcssOptions, - to: source, - from: source, - ...(devSourcemap - ? { - map: { - inline: false, - annotation: false, - // postcss may return virtual files - // we cannot obtain content of them, so this needs to be enabled - sourcesContent: true, - // when "prev: preprocessorMap", the result map may include duplicate filename in `postcssResult.map.sources` - // prev: preprocessorMap, - }, - } - : {}), - }) + postcssResult = await postcss.default(postcssPlugins).process(code, { + ...postcssOptions, + parser: + lang === 'sss' + ? loadPreprocessor(PostCssDialectLang.sss, config.root) + : postcssOptions.parser, + to: source, + from: source, + ...(devSourcemap + ? { + map: { + inline: false, + annotation: false, + // postcss may return virtual files + // we cannot obtain content of them, so this needs to be enabled + sourcesContent: true, + // when "prev: preprocessorMap", the result map may include duplicate filename in `postcssResult.map.sources` + // prev: preprocessorMap, + }, + } + : {}), + }) // record CSS dependencies from @imports for (const message of postcssResult.messages) { @@ -1057,6 +1100,22 @@ async function compileCSS( } } +function createCachedImport(imp: () => Promise): () => T | Promise { + let cached: T | Promise + return () => { + if (!cached) { + cached = imp().then((module) => { + cached = module + return module + }) + } + return cached + } +} +const importPostcssImport = createCachedImport(() => import('postcss-import')) +const importPostcssModules = createCachedImport(() => import('postcss-modules')) +const importPostcss = createCachedImport(() => import('postcss')) + export interface PreprocessCSSResult { code: string map?: SourceMapInput @@ -1075,6 +1134,8 @@ export async function preprocessCSS( return await compileCSS(filename, code, config) } +const postcssReturnsVirtualFilesRE = /^<.+>$/ + export async function formatPostcssSourceMap( rawMap: ExistingRawSourceMap, file: string, @@ -1084,8 +1145,7 @@ export async function formatPostcssSourceMap( const sources = rawMap.sources.map((source) => { const cleanSource = cleanUrl(decodeURIComponent(source)) - // postcss returns virtual files - if (/^<.+>$/.test(cleanSource)) { + if (postcssReturnsVirtualFilesRE.test(cleanSource)) { return `\0${cleanSource}` } @@ -1139,16 +1199,10 @@ interface PostCSSConfigResult { async function resolvePostcssConfig( config: ResolvedConfig, - dialect = 'css', ): Promise { - postcssConfigCache[dialect] ??= new WeakMap< - ResolvedConfig, - PostCSSConfigResult | null - >() - - let result = postcssConfigCache[dialect].get(config) + let result = postcssConfigCache.get(config) if (result !== undefined) { - return result + return await result } // inline postcss config via vite config @@ -1164,9 +1218,7 @@ async function resolvePostcssConfig( } else { const searchPath = typeof inlineOptions === 'string' ? inlineOptions : config.root - try { - result = await postcssrc({}, searchPath) - } catch (e) { + result = postcssrc({}, searchPath).catch((e) => { if (!/No PostCSS Config found/.test(e.message)) { if (e instanceof Error) { const { name, message, stack } = e @@ -1178,11 +1230,15 @@ async function resolvePostcssConfig( throw new Error(`Failed to load PostCSS config: ${e}`) } } - result = null - } + return null + }) + // replace cached promise to result object when finished + result.then((resolved) => { + postcssConfigCache.set(config, resolved) + }) } - postcssConfigCache[dialect].set(config, result) + postcssConfigCache.set(config, result) return result } @@ -1318,7 +1374,7 @@ async function doUrlReplace( if ( isExternalUrl(rawUrl) || isDataUrl(rawUrl) || - rawUrl.startsWith('#') || + rawUrl[0] === '#' || varRE.test(rawUrl) ) { return matched @@ -1343,7 +1399,7 @@ async function doImportCSSReplace( wrap = first rawUrl = rawUrl.slice(1, -1) } - if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl.startsWith('#')) { + if (isExternalUrl(rawUrl) || isDataUrl(rawUrl) || rawUrl[0] === '#') { return matched } @@ -1355,6 +1411,7 @@ async function minifyCSS(css: string, config: ResolvedConfig) { const { code, warnings } = await transform(css, { loader: 'css', target: config.build.cssTarget || undefined, + charset: 'utf8', ...resolveEsbuildMinifyOptions(config.esbuild || {}), }) if (warnings.length) { @@ -1543,7 +1600,7 @@ function cleanScssBugUrl(url: string) { if ( // check bug via `window` and `location` global typeof window !== 'undefined' && - typeof location !== 'undefined' + typeof location?.href === 'string' ) { const prefix = location.href.replace(/\/$/, '') return url.replace(prefix, '') @@ -1680,7 +1737,7 @@ async function rebaseUrls( return { file } } - const content = fs.readFileSync(file, 'utf-8') + const content = await fsp.readFile(file, 'utf-8') // no url() const hasUrls = cssUrlRE.test(content) // data-uri() calls @@ -1694,7 +1751,7 @@ async function rebaseUrls( let rebased const rebaseFn = (url: string) => { - if (url.startsWith('/')) return url + if (url[0] === '/') return url // ignore url's starting with variable if (url.startsWith(variablePrefix)) return url // match alias, no need to rewrite @@ -1839,7 +1896,7 @@ function createViteLessPlugin( if (result && 'contents' in result) { contents = result.contents } else { - contents = fs.readFileSync(resolved, 'utf-8') + contents = await fsp.readFile(resolved, 'utf-8') } return { filename: path.resolve(resolved), @@ -1977,7 +2034,3 @@ const preProcessors = Object.freeze({ function isPreProcessor(lang: any): lang is PreprocessLang { return lang && lang in preProcessors } - -function getCssDialect(lang: CssLang | undefined): string { - return lang === 'sss' ? 'sss' : 'css' -} diff --git a/packages/vite/src/node/plugins/dataUri.ts b/packages/vite/src/node/plugins/dataUri.ts index 8d34be298714d4..36c5c0b28b731c 100644 --- a/packages/vite/src/node/plugins/dataUri.ts +++ b/packages/vite/src/node/plugins/dataUri.ts @@ -5,59 +5,56 @@ import { URL } from 'node:url' import type { Plugin } from '../plugin' const dataUriRE = /^([^/]+\/[^;,]+)(;base64)?,([\s\S]*)$/ - -const dataUriPrefix = `/@data-uri/` +const base64RE = /base64/i +const dataUriPrefix = `\0/@data-uri/` /** * Build only, since importing from a data URI works natively. */ export function dataURIPlugin(): Plugin { - let resolved: { - [key: string]: string - } + let resolved: Map return { name: 'vite:data-uri', buildStart() { - resolved = {} + resolved = new Map() }, resolveId(id) { if (!dataUriRE.test(id)) { - return null + return } const uri = new URL(id) if (uri.protocol !== 'data:') { - return null + return } const match = uri.pathname.match(dataUriRE) if (!match) { - return null + return } const [, mime, format, data] = match if (mime !== 'text/javascript') { throw new Error( - `data URI with non-JavaScript mime type is not supported.`, + `data URI with non-JavaScript mime type is not supported. If you're using legacy JavaScript MIME types (such as 'application/javascript'), please use 'text/javascript' instead.`, ) } // decode data - const base64 = format && /base64/i.test(format.substring(1)) + const base64 = format && base64RE.test(format.substring(1)) const content = base64 ? Buffer.from(data, 'base64').toString('utf-8') : data - resolved[id] = content + resolved.set(id, content) return dataUriPrefix + id }, load(id) { if (id.startsWith(dataUriPrefix)) { - id = id.slice(dataUriPrefix.length) - return resolved[id] || null + return resolved.get(id.slice(dataUriPrefix.length)) } }, } diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index f47a3ff97fed64..c0c37183b81aaf 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { transformStableResult } from '../utils' +import { escapeRegex, transformStableResult } from '../utils' import { isCSSRequest } from './css' import { isHTMLRequest } from './html' @@ -113,11 +113,7 @@ export function definePlugin(config: ResolvedConfig): Plugin { // Mustn't be preceded by a char that can be part of an identifier // or a '.' that isn't part of a spread operator '(? { - return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') - }) - .join('|') + + replacementsKeys.map(escapeRegex).join('|') + // Mustn't be followed by a char that can be part of an identifier // or an assignment (but allow equality operators) ')(?:(?<=\\.)|(?![\\p{L}\\p{N}_$]|\\s*?=[^=]))', diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index f730fc9e093bbf..45a27c6f466431 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -7,6 +7,7 @@ import { dynamicImportToGlob } from '@rollup/plugin-dynamic-import-vars' import type { KnownAsTypeMap } from 'types/importGlob' import type { Plugin } from '../plugin' import type { ResolvedConfig } from '../config' +import { CLIENT_ENTRY } from '../constants' import { createFilter, normalizePath, @@ -19,6 +20,12 @@ import { toAbsoluteGlob } from './importMetaGlob' export const dynamicImportHelperId = '\0vite/dynamic-import-helper' +const relativePathRE = /^\.{1,2}\// +// fast path to check if source contains a dynamic import. we check for a +// trailing slash too as a dynamic import statement can have comments between +// the `import` and the `(`. +const hasDynamicImportRE = /\bimport\s*[(/]/ + interface DynamicImportRequest { as?: keyof KnownAsTypeMap } @@ -122,7 +129,7 @@ export async function transformDynamicImport( await toAbsoluteGlob(rawPattern, root, importer, resolve), ) - if (!/^\.{1,2}\//.test(newRawPattern)) { + if (!relativePathRE.test(newRawPattern)) { newRawPattern = `./${newRawPattern}` } @@ -161,7 +168,11 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { }, async transform(source, importer) { - if (!filter(importer)) { + if ( + !filter(importer) || + importer === CLIENT_ENTRY || + !hasDynamicImportRE.test(source) + ) { return } diff --git a/packages/vite/src/node/plugins/esbuild.ts b/packages/vite/src/node/plugins/esbuild.ts index 5db99c648bf4e0..c92e7fe80c1e84 100644 --- a/packages/vite/src/node/plugins/esbuild.ts +++ b/packages/vite/src/node/plugins/esbuild.ts @@ -9,7 +9,7 @@ import type { import { transform } from 'esbuild' import type { RawSourceMap } from '@ampproject/remapping' import type { InternalModuleFormat, SourceMap } from 'rollup' -import type { TSConfckParseOptions, TSConfckParseResult } from 'tsconfck' +import type { TSConfckParseOptions } from 'tsconfck' import { TSConfckParseError, findAll, parse } from 'tsconfck' import { cleanUrl, @@ -18,10 +18,12 @@ import { createFilter, ensureWatchedFile, generateCodeFrame, + timeFrom, } from '../utils' -import type { ResolvedConfig, ViteDevServer } from '..' +import type { ViteDevServer } from '../server' +import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' -import { searchForWorkspaceRoot } from '..' +import { searchForWorkspaceRoot } from '../server/searchRoot' const debug = createDebugger('vite:esbuild') @@ -30,6 +32,9 @@ const INJECT_HELPERS_IIFE_RE = const INJECT_HELPERS_UMD_RE = /^(.*?)(\(function\([^)]*\)\s*\{.+?amd.+?function\([^)]*\)\s*\{.*?"use strict";)/s +const validExtensionRE = /\.\w+$/ +const jsxExtensionsRE = /\.(?:j|t)sx\b/ + let server: ViteDevServer export interface ESBuildOptions extends TransformOptions { @@ -75,7 +80,7 @@ export async function transformWithEsbuild( // if the id ends with a valid ext, use it (e.g. vue blocks) // otherwise, cleanup the query before checking the ext const ext = path - .extname(/\.\w+$/.test(filename) ? filename : cleanUrl(filename)) + .extname(validExtensionRE.test(filename) ? filename : cleanUrl(filename)) .slice(1) if (ext === 'cjs' || ext === 'mjs') { @@ -117,30 +122,39 @@ export async function transformWithEsbuild( } } - tsconfigRaw = { - ...tsconfigRaw, - compilerOptions: { - ...compilerOptionsForFile, - ...tsconfigRaw?.compilerOptions, - }, + const compilerOptions = { + ...compilerOptionsForFile, + ...tsconfigRaw?.compilerOptions, } - const { compilerOptions } = tsconfigRaw - if (compilerOptions) { - // esbuild derives `useDefineForClassFields` from `target` instead of `tsconfig.compilerOptions.target` - // https://github.com/evanw/esbuild/issues/2584 - // but we want `useDefineForClassFields` to be derived from `tsconfig.compilerOptions.target` - if (compilerOptions.useDefineForClassFields === undefined) { - const lowercaseTarget = compilerOptions.target?.toLowerCase() ?? 'es3' - if (lowercaseTarget.startsWith('es')) { - const esVersion = lowercaseTarget.slice(2) - compilerOptions.useDefineForClassFields = - esVersion === 'next' || +esVersion >= 2022 - } else { - compilerOptions.useDefineForClassFields = false - } + // esbuild derives `useDefineForClassFields` from `target` instead of `tsconfig.compilerOptions.target` + // https://github.com/evanw/esbuild/issues/2584 + // but we want `useDefineForClassFields` to be derived from `tsconfig.compilerOptions.target` + if (compilerOptions.useDefineForClassFields === undefined) { + const lowercaseTarget = compilerOptions.target?.toLowerCase() ?? 'es3' + if (lowercaseTarget.startsWith('es')) { + const esVersion = lowercaseTarget.slice(2) + compilerOptions.useDefineForClassFields = + esVersion === 'next' || +esVersion >= 2022 + } else { + compilerOptions.useDefineForClassFields = false } } + + // esbuild uses tsconfig fields when both the normal options and tsconfig was set + // but we want to prioritize the normal options + if (options) { + options.jsx && (compilerOptions.jsx = undefined) + options.jsxFactory && (compilerOptions.jsxFactory = undefined) + options.jsxFragment && (compilerOptions.jsxFragmentFactory = undefined) + options.jsxImportSource && (compilerOptions.jsxImportSource = undefined) + options.target && (compilerOptions.target = undefined) + } + + tsconfigRaw = { + ...tsconfigRaw, + compilerOptions, + } } const resolvedOptions = { @@ -152,25 +166,8 @@ export async function transformWithEsbuild( tsconfigRaw, } as ESBuildOptions - // esbuild uses tsconfig fields when both the normal options and tsconfig was set - // but we want to prioritize the normal options - if ( - options && - typeof resolvedOptions.tsconfigRaw === 'object' && - resolvedOptions.tsconfigRaw.compilerOptions - ) { - options.jsx && (resolvedOptions.tsconfigRaw.compilerOptions.jsx = undefined) - options.jsxFactory && - (resolvedOptions.tsconfigRaw.compilerOptions.jsxFactory = undefined) - options.jsxFragment && - (resolvedOptions.tsconfigRaw.compilerOptions.jsxFragmentFactory = - undefined) - options.jsxImportSource && - (resolvedOptions.tsconfigRaw.compilerOptions.jsxImportSource = undefined) - options.target && - (resolvedOptions.tsconfigRaw.compilerOptions.target = undefined) - } - + // Some projects in the ecosystem are calling this function with an ESBuildOptions + // object and esbuild throws an error for extra fields delete resolvedOptions.include delete resolvedOptions.exclude delete resolvedOptions.jsxInject @@ -196,7 +193,7 @@ export async function transformWithEsbuild( map, } } catch (e: any) { - debug(`esbuild error with options used: `, resolvedOptions) + debug?.(`esbuild error with options used: `, resolvedOptions) // patch error information if (e.errors) { e.frame = '' @@ -209,18 +206,18 @@ export async function transformWithEsbuild( } } -export function esbuildPlugin(options: ESBuildOptions): Plugin { - const filter = createFilter( - options.include || /\.(m?ts|[jt]sx)$/, - options.exclude || /\.js$/, - ) +export function esbuildPlugin(config: ResolvedConfig): Plugin { + const options = config.esbuild as ESBuildOptions + const { jsxInject, include, exclude, ...esbuildTransformOptions } = options + + const filter = createFilter(include || /\.(m?ts|[jt]sx)$/, exclude || /\.js$/) // Remove optimization options for dev as we only need to transpile them, // and for build as the final optimization is in `buildEsbuildPlugin` const transformOptions: TransformOptions = { target: 'esnext', charset: 'utf8', - ...options, + ...esbuildTransformOptions, minify: false, minifyIdentifiers: false, minifySyntax: false, @@ -232,6 +229,8 @@ export function esbuildPlugin(options: ESBuildOptions): Plugin { keepNames: false, } + initTSConfck(config.root) + return { name: 'vite:esbuild', configureServer(_server) { @@ -241,9 +240,6 @@ export function esbuildPlugin(options: ESBuildOptions): Plugin { .on('change', reloadOnTsconfigChange) .on('unlink', reloadOnTsconfigChange) }, - async configResolved(config) { - await initTSConfck(config) - }, buildEnd() { // recycle serve to avoid preventing Node self-exit (#6815) server = null as any @@ -256,8 +252,8 @@ export function esbuildPlugin(options: ESBuildOptions): Plugin { this.warn(prettifyMessage(m, code)) }) } - if (options.jsxInject && /\.(?:j|t)sx\b/.test(id)) { - result.code = options.jsxInject + ';' + result.code + if (jsxInject && jsxExtensionsRE.test(id)) { + result.code = jsxInject + ';' + result.code } return { code: result.code, @@ -287,11 +283,10 @@ const rollupToEsbuildFormatMap: Record< } export const buildEsbuildPlugin = (config: ResolvedConfig): Plugin => { + initTSConfck(config.root) + return { name: 'vite:esbuild-transpile', - async configResolved(config) { - await initTSConfck(config) - }, async renderChunk(code, chunk, opts) { // @ts-expect-error injected by @vitejs/plugin-legacy if (opts.__vite_skip_esbuild__) { @@ -438,32 +433,51 @@ function prettifyMessage(m: Message, code: string): string { return res + `\n` } -const tsconfckParseOptions: TSConfckParseOptions = { - cache: new Map(), - tsConfigPaths: undefined, - root: undefined, - resolveWithEmptyIfConfigNotFound: true, +let tsconfckRoot: string | undefined +let tsconfckParseOptions: TSConfckParseOptions | Promise = + { resolveWithEmptyIfConfigNotFound: true } + +function initTSConfck(root: string, force = false) { + // bail if already cached + if (!force && root === tsconfckRoot) return + + const workspaceRoot = searchForWorkspaceRoot(root) + + tsconfckRoot = root + tsconfckParseOptions = initTSConfckParseOptions(workspaceRoot) + + // cached as the options value itself when promise is resolved + tsconfckParseOptions.then((options) => { + if (root === tsconfckRoot) { + tsconfckParseOptions = options + } + }) } -async function initTSConfck(config: ResolvedConfig) { - const workspaceRoot = searchForWorkspaceRoot(config.root) - debug(`init tsconfck (root: ${colors.cyan(workspaceRoot)})`) - - tsconfckParseOptions.cache!.clear() - tsconfckParseOptions.root = workspaceRoot - tsconfckParseOptions.tsConfigPaths = new Set([ - ...(await findAll(workspaceRoot, { - skip: (dir) => dir === 'node_modules' || dir === '.git', - })), - ]) - debug(`init tsconfck end`) +async function initTSConfckParseOptions(workspaceRoot: string) { + const start = debug ? performance.now() : 0 + + const options: TSConfckParseOptions = { + cache: new Map(), + root: workspaceRoot, + tsConfigPaths: new Set( + await findAll(workspaceRoot, { + skip: (dir) => dir === 'node_modules' || dir === '.git', + }), + ), + resolveWithEmptyIfConfigNotFound: true, + } + + debug?.(timeFrom(start), 'tsconfck init', colors.dim(workspaceRoot)) + + return options } async function loadTsconfigJsonForFile( filename: string, ): Promise { try { - const result = await parse(filename, tsconfckParseOptions) + const result = await parse(filename, await tsconfckParseOptions) // tsconfig could be out of root, make sure it is watched on dev if (server && result.tsconfigFile !== 'no_tsconfig_file_found') { ensureWatchedFile(server.watcher, result.tsconfigFile, server.config.root) @@ -480,7 +494,7 @@ async function loadTsconfigJsonForFile( } } -function reloadOnTsconfigChange(changedFile: string) { +async function reloadOnTsconfigChange(changedFile: string) { // server could be closed externally after a file change is detected if (!server) return // any tsconfig.json that's added in the workspace could be closer to a code file than a previously cached one @@ -488,7 +502,7 @@ function reloadOnTsconfigChange(changedFile: string) { if ( path.basename(changedFile) === 'tsconfig.json' || (changedFile.endsWith('.json') && - tsconfckParseOptions?.cache?.has(changedFile)) + (await tsconfckParseOptions)?.cache?.has(changedFile)) ) { server.config.logger.info( `changed tsconfig file detected: ${changedFile} - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.`, @@ -499,15 +513,15 @@ function reloadOnTsconfigChange(changedFile: string) { server.moduleGraph.invalidateAll() // reset tsconfck so that recompile works with up2date configs - initTSConfck(server.config).finally(() => { - // server may not be available if vite config is updated at the same time - if (server) { - // force full reload - server.ws.send({ - type: 'full-reload', - path: '*', - }) - } - }) + initTSConfck(server.config.root, true) + + // server may not be available if vite config is updated at the same time + if (server) { + // force full reload + server.ws.send({ + type: 'full-reload', + path: '*', + }) + } } } diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 9737dcb3a9e9b7..769de344b15290 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -18,8 +18,10 @@ import { getHash, isDataUrl, isExternalUrl, + isUrl, normalizePath, processSrcSet, + removeLeadingSlash, } from '../utils' import type { ResolvedConfig } from '../config' import { toOutputFilePathInHtml } from '../build' @@ -290,7 +292,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { postHooks.push(postImportMapHook()) const processedHtml = new Map() const isExcludedUrl = (url: string) => - url.startsWith('#') || + url[0] === '#' || isExternalUrl(url) || isDataUrl(url) || checkPublicFile(url, config) @@ -537,7 +539,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { if ( content !== '' && // Empty attribute !namedOutput.includes(content) && // Direct reference to named output - !namedOutput.includes(content.replace(/^\//, '')) // Allow for absolute references as named output can't be an absolute path + !namedOutput.includes(removeLeadingSlash(content)) // Allow for absolute references as named output can't be an absolute path ) { try { const url = @@ -811,11 +813,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }) result = result.replace(publicAssetUrlRE, (_, fileHash) => { - return normalizePath( - toOutputPublicAssetFilePath( - getPublicAssetFilename(fileHash, config)!, - ), + const publicAssetPath = toOutputPublicAssetFilePath( + getPublicAssetFilename(fileHash, config)!, ) + + return isUrl(publicAssetPath) + ? publicAssetPath + : normalizePath(publicAssetPath) }) if (chunk && canInlineEntry) { diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index b18c9c6bc2f511..119f0192cabb43 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -32,6 +32,7 @@ import { isBuiltin, isDataUrl, isExternalUrl, + isInNodeModules, isJSRequest, joinUrlSegments, moduleListContains, @@ -52,12 +53,7 @@ import { cjsShouldExternalizeForSSR, shouldExternalizeForSSR, } from '../ssr/ssrExternal' -import { transformRequest } from '../server/transformRequest' -import { - getDepsCacheDirPrefix, - getDepsOptimizer, - optimizedDepNeedsInterop, -} from '../optimizer' +import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer' import { checkPublicFile } from './asset' import { ERR_OUTDATED_OPTIMIZED_DEP, @@ -66,7 +62,6 @@ import { import { isCSSRequest, isDirectCSSRequest, isModuleCSSRequest } from './css' import { browserExternalId } from './resolve' -const isDebug = !!process.env.DEBUG const debug = createDebugger('vite:import-analysis') const clientDir = normalizePath(CLIENT_DIR) @@ -78,6 +73,19 @@ export const canSkipImportAnalysis = (id: string): boolean => const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/ const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/ +const hasImportInQueryParamsRE = /[?&]import=?\b/ + +const hasViteIgnoreRE = /\/\*\s*@vite-ignore\s*\*\// + +const cleanUpRawUrlRE = /\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm +const urlIsStringRE = /^(?:'.*'|".*"|`.*`)$/ + +interface UrlPosition { + url: string + start: number + end: number +} + export function isExplicitImportRequired(url: string): boolean { return !isJSRequest(cleanUrl(url)) && !isCSSRequest(url) } @@ -204,7 +212,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const prettyImporter = prettifyUrl(importer, root) if (canSkipImportAnalysis(importer)) { - isDebug && debug(colors.dim(`[skipped] ${prettyImporter}`)) + debug?.(colors.dim(`[skipped] ${prettyImporter}`)) return null } @@ -256,12 +264,9 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { if (!imports.length && !(this as any)._addedImports) { importerModule.isSelfAccepting = false - isDebug && - debug( - `${timeFrom(start)} ${colors.dim( - `[no imports] ${prettyImporter}`, - )}`, - ) + debug?.( + `${timeFrom(start)} ${colors.dim(`[no imports] ${prettyImporter}`)}`, + ) return source } @@ -272,14 +277,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { let s: MagicString | undefined const str = () => s || (s = new MagicString(source)) const importedUrls = new Set() - const staticImportedUrls = new Set<{ url: string; id: string }>() - const acceptedUrls = new Set<{ - url: string - start: number - end: number - }>() let isPartiallySelfAccepting = false - const acceptedExports = new Set() const importedBindings = enablePartialAccept ? new Map>() : null @@ -331,7 +329,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { ) } - const isRelative = url.startsWith('.') + const isRelative = url[0] === '.' const isSelfImport = !isRelative && cleanUrl(url) === cleanUrl(importer) // normalize all imports into resolved URLs @@ -340,7 +338,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else if ( - resolved.id.startsWith(getDepsCacheDirPrefix(config)) || + depsOptimizer?.isOptimizedDepFile(resolved.id) || fs.existsSync(cleanUrl(resolved.id)) ) { // an optimized deps may not yet exists in the filesystem, or @@ -357,7 +355,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // if the resolved id is not a valid browser import specifier, // prefix it to make it valid. We will strip this before feeding it // back into the transform pipeline - if (!url.startsWith('.') && !url.startsWith('/')) { + if (url[0] !== '.' && url[0] !== '/') { url = wrapId(resolved.id) } @@ -373,7 +371,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // query can break 3rd party plugin's extension checks. if ( (isRelative || isSelfImport) && - !/[?&]import=?\b/.test(url) && + !hasImportInQueryParamsRE.test(url) && !url.match(DEP_VERSION_RE) ) { const versionMatch = importer.match(DEP_VERSION_RE) @@ -387,10 +385,12 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // up-to-date version of this module. try { // delay setting `isSelfAccepting` until the file is actually used (#7870) - const depModule = await moduleGraph.ensureEntryFromUrl( + // We use an internal function to avoid resolving the url again + const depModule = await moduleGraph._ensureEntryFromUrl( unwrapId(url), ssr, canSkipImportAnalysis(url) || forceSkipImportAnalysis, + resolved, ) if (depModule.lastHMRTimestamp > 0) { url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) @@ -409,252 +409,302 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return [url, resolved.id] } - for (let index = 0; index < imports.length; index++) { - const { - s: start, - e: end, - ss: expStart, - se: expEnd, - d: dynamicIndex, - // #2083 User may use escape path, - // so use imports[index].n to get the unescaped string - n: specifier, - a: assertIndex, - } = imports[index] - - const rawUrl = source.slice(start, end) - - // check import.meta usage - if (rawUrl === 'import.meta') { - const prop = source.slice(end, end + 4) - if (prop === '.hot') { - hasHMR = true - const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0) - if (source.slice(endHot, endHot + 7) === '.accept') { - // further analyze accepted modules - if (source.slice(endHot, endHot + 14) === '.acceptExports') { - lexAcceptedHmrExports( - source, - source.indexOf('(', endHot + 14) + 1, - acceptedExports, - ) - isPartiallySelfAccepting = true - } else if ( - lexAcceptedHmrDeps( - source, - source.indexOf('(', endHot + 7) + 1, - acceptedUrls, - ) - ) { - isSelfAccepting = true + const orderedAcceptedUrls = new Array | undefined>( + imports.length, + ) + const orderedAcceptedExports = new Array | undefined>( + imports.length, + ) + + await Promise.all( + imports.map(async (importSpecifier, index) => { + const { + s: start, + e: end, + ss: expStart, + se: expEnd, + d: dynamicIndex, + // #2083 User may use escape path, + // so use imports[index].n to get the unescaped string + n: specifier, + a: assertIndex, + } = importSpecifier + + const rawUrl = source.slice(start, end) + + // check import.meta usage + if (rawUrl === 'import.meta') { + const prop = source.slice(end, end + 4) + if (prop === '.hot') { + hasHMR = true + const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0) + if (source.slice(endHot, endHot + 7) === '.accept') { + // further analyze accepted modules + if (source.slice(endHot, endHot + 14) === '.acceptExports') { + const importAcceptedExports = (orderedAcceptedExports[index] = + new Set()) + lexAcceptedHmrExports( + source, + source.indexOf('(', endHot + 14) + 1, + importAcceptedExports, + ) + isPartiallySelfAccepting = true + } else { + const importAcceptedUrls = (orderedAcceptedUrls[index] = + new Set()) + if ( + lexAcceptedHmrDeps( + source, + source.indexOf('(', endHot + 7) + 1, + importAcceptedUrls, + ) + ) { + isSelfAccepting = true + } + } } + } else if (prop === '.env') { + hasEnv = true } - } else if (prop === '.env') { - hasEnv = true + return } - continue - } - - const isDynamicImport = dynamicIndex > -1 - // strip import assertions as we can process them ourselves - if (!isDynamicImport && assertIndex > -1) { - str().remove(end + 1, expEnd) - } + const isDynamicImport = dynamicIndex > -1 - // static import or valid string in dynamic import - // If resolvable, let's resolve it - if (specifier) { - // skip external / data uri - if (isExternalUrl(specifier) || isDataUrl(specifier)) { - continue + // strip import assertions as we can process them ourselves + if (!isDynamicImport && assertIndex > -1) { + str().remove(end + 1, expEnd) } - // skip ssr external - if (ssr) { - if (config.legacy?.buildSsrCjsExternalHeuristics) { - if (cjsShouldExternalizeForSSR(specifier, server._ssrExternals)) { - continue + + // static import or valid string in dynamic import + // If resolvable, let's resolve it + if (specifier) { + // skip external / data uri + if (isExternalUrl(specifier) || isDataUrl(specifier)) { + return + } + // skip ssr external + if (ssr) { + if (config.legacy?.buildSsrCjsExternalHeuristics) { + if ( + cjsShouldExternalizeForSSR(specifier, server._ssrExternals) + ) { + return + } + } else if (shouldExternalizeForSSR(specifier, config)) { + return + } + if (isBuiltin(specifier)) { + return } - } else if (shouldExternalizeForSSR(specifier, config)) { - continue } - if (isBuiltin(specifier)) { - continue + // skip client + if (specifier === clientPublicPath) { + return } - } - // skip client - if (specifier === clientPublicPath) { - continue - } - - // warn imports to non-asset /public files - if ( - specifier.startsWith('/') && - !config.assetsInclude(cleanUrl(specifier)) && - !specifier.endsWith('.json') && - checkPublicFile(specifier, config) - ) { - throw new Error( - `Cannot import non-asset file ${specifier} which is inside /public.` + - `JS/CSS files inside /public are copied as-is on build and ` + - `can only be referenced via `, ) + preTransformRequest(server!, modulePath, base) } await traverseHtml(html, filename, (node) => { @@ -212,7 +224,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( config, htmlPath, originalUrl, - moduleGraph, + server, ) } else if (isModule && node.childNodes.length) { addInlineModule(node, 'js') @@ -292,7 +304,7 @@ export function indexHtmlMiddleware( const filename = getHtmlFilename(url, server) if (fs.existsSync(filename)) { try { - let html = fs.readFileSync(filename, 'utf-8') + let html = await fsp.readFile(filename, 'utf-8') html = await server.transformIndexHtml(url, html, req.originalUrl) return send(req, res, html, 'html', { headers: server.config.server.headers, @@ -305,3 +317,15 @@ export function indexHtmlMiddleware( next() } } + +function preTransformRequest(server: ViteDevServer, url: string, base: string) { + if (!server.config.server.preTransformRequests) return + + url = unwrapId(stripBase(url, base)) + + // transform all url as non-ssr as html includes client-side assets only + server.transformRequest(url).catch((e) => { + // Unexpected error, log the issue but avoid an unhandled exception + server.config.logger.error(e.message) + }) +} diff --git a/packages/vite/src/node/server/middlewares/proxy.ts b/packages/vite/src/node/server/middlewares/proxy.ts index c668d66b83edfa..f07b93e0746223 100644 --- a/packages/vite/src/node/server/middlewares/proxy.ts +++ b/packages/vite/src/node/server/middlewares/proxy.ts @@ -98,7 +98,7 @@ export function proxyMiddleware( if (opts.rewrite) { req.url = opts.rewrite(url) } - debug(`${req.url} -> ws ${opts.target}`) + debug?.(`${req.url} -> ws ${opts.target}`) proxy.ws(req, socket, head) return } @@ -119,15 +119,15 @@ export function proxyMiddleware( const bypassResult = opts.bypass(req, res, opts) if (typeof bypassResult === 'string') { req.url = bypassResult - debug(`bypass: ${req.url} -> ${bypassResult}`) + debug?.(`bypass: ${req.url} -> ${bypassResult}`) return next() } else if (bypassResult === false) { - debug(`bypass: ${req.url} -> 404`) + debug?.(`bypass: ${req.url} -> 404`) return res.end(404) } } - debug(`${req.url} -> ${opts.target || opts.forward}`) + debug?.(`${req.url} -> ${opts.target || opts.forward}`) if (opts.rewrite) { req.url = opts.rewrite(req.url!) } @@ -141,7 +141,7 @@ export function proxyMiddleware( function doesProxyContextMatchUrl(context: string, url: string): boolean { return ( - (context.startsWith('^') && new RegExp(context).test(url)) || + (context[0] === '^' && new RegExp(context).test(url)) || url.startsWith(context) ) } diff --git a/packages/vite/src/node/server/middlewares/static.ts b/packages/vite/src/node/server/middlewares/static.ts index 229b860dbcec56..13904ce04922a4 100644 --- a/packages/vite/src/node/server/middlewares/static.ts +++ b/packages/vite/src/node/server/middlewares/static.ts @@ -3,6 +3,7 @@ import type { OutgoingHttpHeaders, ServerResponse } from 'node:http' import type { Options } from 'sirv' import sirv from 'sirv' import type { Connect } from 'dep-types/connect' +import escapeHtml from 'escape-html' import type { ViteDevServer } from '../..' import { FS_PREFIX } from '../../constants' import { @@ -14,10 +15,13 @@ import { isInternalRequest, isParentDirectory, isWindows, + removeLeadingSlash, shouldServeFile, slash, } from '../../utils' +const knownJavascriptExtensionRE = /\.[tj]sx?$/ + const sirvOptions = ({ headers, shouldServe, @@ -35,7 +39,7 @@ const sirvOptions = ({ // for the MIME type video/mp2t. In almost all cases, we can expect // these files to be TypeScript files, and for Vite to serve them with // this Content-Type. - if (/\.[tj]sx?$/.test(pathname)) { + if (knownJavascriptExtensionRE.test(pathname)) { res.setHeader('Content-Type', 'application/javascript') } if (headers) { @@ -89,7 +93,7 @@ export function serveStaticMiddleware( // also skip internal requests `/@fs/ /@vite-client` etc... const cleanedUrl = cleanUrl(req.url!) if ( - cleanedUrl.endsWith('/') || + cleanedUrl[cleanedUrl.length - 1] === '/' || path.extname(cleanedUrl) === '.html' || isInternalRequest(req.url!) ) { @@ -119,8 +123,11 @@ export function serveStaticMiddleware( } const resolvedPathname = redirectedPathname || pathname - let fileUrl = path.resolve(dir, resolvedPathname.replace(/^\//, '')) - if (resolvedPathname.endsWith('/') && !fileUrl.endsWith('/')) { + let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname)) + if ( + resolvedPathname[resolvedPathname.length - 1] === '/' && + fileUrl[fileUrl.length - 1] !== '/' + ) { fileUrl = fileUrl + '/' } if (!ensureServingAccess(fileUrl, server, res, next)) { @@ -177,6 +184,9 @@ export function serveRawFsMiddleware( } } +/** + * Check if the url is allowed to be served, via the `server.fs` config. + */ export function isFileServingAllowed( url: string, server: ViteDevServer, @@ -230,7 +240,7 @@ function renderRestrictedErrorHTML(msg: string): string { return html`

403 Restricted

-

${msg.replace(/\n/g, '
')}

+

${escapeHtml(msg).replace(/\n/g, '
')}