diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 2974196847e467..00000000000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -playground-temp -temp diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index b3c31d519d30d5..00000000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,243 +0,0 @@ -// @ts-check -const { builtinModules } = require('node:module') -const { defineConfig } = require('eslint-define-config') -const pkg = require('./package.json') - -/// - -module.exports = defineConfig({ - root: true, - extends: [ - 'eslint:recommended', - 'plugin:n/recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/stylistic', - 'plugin:regexp/recommended', - ], - ignorePatterns: ['packages/create-vite/template-**'], - plugins: ['i', 'regexp'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaVersion: 2022, - }, - rules: { - eqeqeq: ['warn', 'always', { null: 'never' }], - 'no-debugger': ['error'], - 'no-empty': ['warn', { allowEmptyCatch: true }], - 'no-process-exit': 'off', - 'no-useless-escape': 'off', - 'prefer-const': [ - 'warn', - { - destructuring: 'all', - }, - ], - - 'n/no-process-exit': 'off', - 'n/no-missing-import': 'off', - 'n/no-missing-require': [ - 'error', - { - // for try-catching yarn pnp - allowModules: ['pnpapi', 'vite'], - tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], - }, - ], - 'n/no-extraneous-import': [ - 'error', - { - allowModules: ['vite', 'less', 'sass', 'vitest', 'unbuild'], - }, - ], - 'n/no-extraneous-require': [ - 'error', - { - allowModules: ['vite'], - }, - ], - '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 - '@typescript-eslint/explicit-module-boundary-types': [ - 'error', - { allowArgumentsExplicitlyTypedAsAny: true }, - ], - '@typescript-eslint/no-empty-function': [ - 'error', - { allow: ['arrowFunctions'] }, - ], - '@typescript-eslint/no-empty-interface': 'off', - '@typescript-eslint/no-explicit-any': 'off', // maybe we should turn this on in a new PR - 'no-extra-semi': 'off', - '@typescript-eslint/no-extra-semi': 'off', // conflicts with prettier - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-unused-vars': 'off', // maybe we should turn this on in a new PR - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/consistent-type-imports': [ - 'error', - { prefer: 'type-imports', disallowTypeAnnotations: false }, - ], - // disable rules set in @typescript-eslint/stylistic v6 that wasn't set in @typescript-eslint/recommended v5 and which conflict with current code - // maybe we should turn them on in a new PR - '@typescript-eslint/array-type': 'off', - '@typescript-eslint/ban-tslint-comment': 'off', - '@typescript-eslint/consistent-generic-constructors': 'off', - '@typescript-eslint/consistent-indexed-object-style': 'off', - '@typescript-eslint/consistent-type-definitions': 'off', - '@typescript-eslint/prefer-for-of': 'off', - '@typescript-eslint/prefer-function-type': 'off', - - 'i/no-nodejs-modules': [ - 'error', - { allow: builtinModules.map((mod) => `node:${mod}`) }, - ], - 'i/no-duplicates': 'error', - 'i/order': 'error', - 'sort-imports': [ - 'error', - { - ignoreCase: false, - ignoreDeclarationSort: true, - ignoreMemberSort: false, - memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], - allowSeparatedGroups: false, - }, - ], - - 'regexp/no-contradiction-with-assertion': 'error', - // in some cases using explicit letter-casing is more performant than the `i` flag - 'regexp/use-ignore-case': 'off', - }, - overrides: [ - { - files: ['packages/**'], - excludedFiles: '**/__tests__/**', - rules: { - 'no-restricted-globals': [ - 'error', - 'require', - '__dirname', - '__filename', - ], - }, - }, - { - files: 'packages/vite/**/*.*', - rules: { - 'n/no-restricted-require': [ - 'error', - Object.keys( - require('./packages/vite/package.json').devDependencies, - ).map((d) => ({ - name: d, - message: - `devDependencies can only be imported using ESM syntax so ` + - `that they are included in the rollup bundle. If you are trying to ` + - `lazy load a dependency, use (await import('dependency')).default instead.`, - })), - ], - }, - }, - { - files: ['packages/vite/src/node/**'], - excludedFiles: '**/__tests__/**', - rules: { - 'no-console': ['error'], - }, - }, - { - files: [ - 'packages/vite/src/types/**', - 'packages/vite/scripts/**', - '*.spec.ts', - ], - rules: { - 'n/no-extraneous-import': 'off', - }, - }, - { - files: ['packages/create-vite/template-*/**', '**/build.config.ts'], - rules: { - 'no-undef': 'off', - 'n/no-missing-import': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - }, - }, - { - files: ['playground/**'], - rules: { - '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 - 'n/no-unsupported-features/es-builtins': [ - 'error', - { - version: pkg.engines.node, - }, - ], - 'n/no-unsupported-features/node-builtins': [ - 'error', - { - version: pkg.engines.node, - }, - ], - '@typescript-eslint/explicit-module-boundary-types': 'off', - }, - }, - { - files: ['playground/**'], - excludedFiles: '**/__tests__/**', - rules: { - 'no-undef': 'off', - 'no-empty': 'off', - 'no-constant-condition': 'off', - '@typescript-eslint/no-empty-function': 'off', - }, - }, - { - files: ['playground/**'], - excludedFiles: [ - 'playground/ssr-resolve/**', - 'playground/**/*{commonjs,cjs}*/**', - 'playground/**/*{commonjs,cjs}*', - 'playground/**/*dep*/**', - 'playground/resolve/browser-module-field2/index.web.js', - 'playground/resolve/browser-field/**', - 'playground/tailwind/**', // blocked by https://github.com/postcss/postcss-load-config/issues/239 - ], - rules: { - 'i/no-commonjs': 'error', - }, - }, - { - files: [ - 'playground/tsconfig-json/**', - 'playground/tsconfig-json-load-error/**', - ], - excludedFiles: '**/__tests__/**', - rules: { - '@typescript-eslint/ban-ts-comment': 'off', - }, - }, - { - files: ['*.js', '*.mjs', '*.cjs'], - rules: { - '@typescript-eslint/explicit-module-boundary-types': 'off', - }, - }, - { - files: ['*.d.ts'], - rules: { - '@typescript-eslint/triple-slash-reference': 'off', - }, - }, - ], - reportUnusedDisableDirectives: true, -}) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3e60e7c9736596..91b3cfd45d7866 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,6 +46,7 @@ body: - npm - yarn - pnpm + - bun validations: required: true - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e73b70ddfcd7c0..2734afc421a080 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,26 +1,14 @@ - - ### Description - - -### Additional context - - - ---- - -### What is the purpose of this pull request? + -- [ ] Bug fix -- [ ] New Feature -- [ ] Documentation update -- [ ] Other + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5833d85e7d929..efb7c4905b4405 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node_version: [18, 20] + node_version: [18, 20, 22] 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@800a2825992141ddde1a8bca8ad394cec34d3188 # v42.0.5 + uses: tj-actions/changed-files@03334d095e2739fa9ac4034ec16f66d5d01e9eba # v44.5.1 with: files: | docs/** @@ -69,7 +69,7 @@ jobs: - name: Install pnpm if: steps.changed-files.outputs.only_changed != 'true' - uses: pnpm/action-setup@v3.0.0 + uses: pnpm/action-setup@v4.0.0 - name: Set node version to ${{ matrix.node_version }} if: steps.changed-files.outputs.only_changed != 'true' @@ -129,17 +129,17 @@ jobs: lint: timeout-minutes: 10 runs-on: ubuntu-latest - name: "Lint: node-18, ubuntu-latest" + name: "Lint: node-20, ubuntu-latest" steps: - uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v3.0.0 + uses: pnpm/action-setup@v4.0.0 - - name: Set node version to 18 + - name: Set node version to 20 uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 cache: "pnpm" - name: Install deps diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7b6efbdbed27da..37d8c135b458db 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,12 +21,12 @@ jobs: uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v3.0.0 + uses: pnpm/action-setup@v4.0.0 - - name: Set node version to 18 + - name: Set node version to 20 uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 registry-url: https://registry.npmjs.org/ cache: "pnpm" diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 26948527230128..ef12c0b8836dfb 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -19,6 +19,11 @@ jobs: - name: Get pkgName for tag id: tag run: | + # skip if alpha + if [[ $GITHUB_REF_NAME =~ alpha ]]; then + exit 0 + fi + # matching v2.0.0 / v2.0.0-beta.8 etc if [[ $GITHUB_REF_NAME =~ ^v.+ ]]; then pkgName="vite" @@ -31,6 +36,8 @@ jobs: echo "pkgName=$pkgName" >> $GITHUB_OUTPUT - name: Create Release for Tag + # only run if tag is not alpha + if: steps.tag.outputs.pkgName id: release_tag uses: yyx990803/release-tag@master env: diff --git a/.npmrc b/.npmrc index 110d846c83d4e0..0b5ac442e7823f 100644 --- a/.npmrc +++ b/.npmrc @@ -3,3 +3,4 @@ hoist-pattern[]=postcss # package/vite hoist-pattern[]=pug # playground/tailwind: @vue/compiler-sfc shell-emulator=true auto-install-peers=false +package-manager-strict=false \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a7e364a42be68..f624e76897e578 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,14 @@ To make this file used by `git blame`, you need to run the following command. git config --local blame.ignoreRevsFile .git-blame-ignore-revs ``` +## Documentation + +To develop the `docs/` site: + +1. Run `pnpm run build` in Vite's root folder. This will generate the types for `twoslash` to work in the code examples. If the types are not available, errors will be logged in step 2 but does not prevent the site from working. + +2. Run `pnpm run docs` in Vite's root folder. + ## Debugging To use breakpoints and explore code execution, you can use the ["Run and Debug"](https://code.visualstudio.com/docs/editor/debugging) feature from VS Code. @@ -166,7 +174,7 @@ For a mock dependency, make sure you add a `@vitejs/test-` prefix to the package ## Debug Logging -You can set the `DEBUG` environment variable to turn on debugging logs (e.g. `DEBUG="vite:resolve"`). To see all debug logs, you can set `DEBUG="vite:*"`, but be warned that it will be quite noisy. You can run `grep -r "createDebugger('vite:" packages/vite/src/` to see a list of available debug scopes. +You can set the `--debug` option to turn on debugging logs (e.g. `vite --debug resolve`). To see all debug logs, you can set `vite --debug *`, but be warned that it will be quite noisy. You can run `grep -r "createDebugger('vite:" packages/vite/src/` to see a list of available debug scopes. ## Pull Request Guidelines diff --git a/docs/.vitepress/buildEnd.config.ts b/docs/.vitepress/buildEnd.config.ts index b819e3acf487c0..52a3625ceb3957 100644 --- a/docs/.vitepress/buildEnd.config.ts +++ b/docs/.vitepress/buildEnd.config.ts @@ -1,12 +1,13 @@ -import path from 'path' -import { writeFileSync } from 'fs' +import path from 'node:path' +import { writeFileSync } from 'node:fs' import { Feed } from 'feed' -import { createContentLoader, type SiteConfig } from 'vitepress' +import type { SiteConfig } from 'vitepress' +import { createContentLoader } from 'vitepress' const siteUrl = 'https://vitejs.dev' const blogUrl = `${siteUrl}/blog` -export const buildEnd = async (config: SiteConfig) => { +export const buildEnd = async (config: SiteConfig): Promise => { const feed = new Feed({ title: 'Vite', description: 'Next Generation Frontend Tooling', diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index ad88cf1e2618dc..440a14d3aea2a2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,4 +1,6 @@ -import { defineConfig, DefaultTheme } from 'vitepress' +import type { DefaultTheme } from 'vitepress' +import { defineConfig } from 'vitepress' +import { transformerTwoslash } from '@shikijs/vitepress-twoslash' import { buildEnd } from './buildEnd.config' const ogDescription = 'Next Generation Frontend Tooling' @@ -77,6 +79,7 @@ export default defineConfig({ ['meta', { property: 'og:image', content: ogImage }], ['meta', { property: 'og:url', content: ogUrl }], ['meta', { property: 'og:description', content: ogDescription }], + ['meta', { property: 'og:site_name', content: 'vitejs' }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:site', content: '@vite_js' }], ['meta', { name: 'theme-color', content: '#646cff' }], @@ -147,6 +150,10 @@ export default defineConfig({ { text: 'Releases', link: '/releases' }, { items: [ + { + text: 'Mastodon', + link: 'https://elk.zone/m.webtoo.ls/@vite', + }, { text: 'Twitter', link: 'https://twitter.com/vite_js', @@ -336,11 +343,14 @@ export default defineConfig({ .replace(/\/index\.md$/, '/') .replace(/\.md$/, '/') pageData.frontmatter.head ??= [] - pageData.frontmatter.head.unshift([ - 'link', - { rel: 'canonical', href: canonicalUrl }, - ]) + pageData.frontmatter.head.unshift( + ['link', { rel: 'canonical', href: canonicalUrl }], + ['meta', { property: 'og:title', content: pageData.title }], + ) return pageData }, + markdown: { + codeTransformers: [transformerTwoslash()], + }, buildEnd, }) diff --git a/docs/.vitepress/theme/composables/sponsor.ts b/docs/.vitepress/theme/composables/sponsor.ts index ea8a180aa05c35..63c91e0822719c 100644 --- a/docs/.vitepress/theme/composables/sponsor.ts +++ b/docs/.vitepress/theme/composables/sponsor.ts @@ -1,4 +1,4 @@ -import { ref, onMounted } from 'vue' +import { onMounted, onUnmounted, ref } from 'vue' interface Sponsors { special: Sponsor[] @@ -13,6 +13,10 @@ interface Sponsor { name: string img: string url: string + /** + * Expects to also have an **inversed** image with `-dark` postfix. + */ + hasDark?: true } // shared data across instances so we load only once. @@ -58,12 +62,40 @@ const viteSponsors: Pick = { name: 'Transloadit', url: 'https://transloadit.com/?utm_source=vite&utm_medium=referral&utm_campaign=sponsorship&utm_content=website', img: '/transloadit.svg', + hasDark: true, }, ], } +function toggleDarkLogos() { + if (data.value) { + const isDark = document.documentElement.classList.contains('dark') + data.value.forEach(({ items }) => { + items.forEach((s: Sponsor) => { + if (s.hasDark) { + s.img = isDark + ? s.img.replace(/(\.\w+)$/, '-dark$1') + : s.img.replace(/-dark(\.\w+)$/, '$1') + } + }) + }) + } +} + export function useSponsor() { onMounted(async () => { + const ob = new MutationObserver((list) => { + for (const m of list) { + if (m.attributeName === 'class') { + toggleDarkLogos() + } + } + }) + ob.observe(document.documentElement, { attributes: true }) + onUnmounted(() => { + ob.disconnect() + }) + if (data.value) { return } @@ -72,6 +104,7 @@ export function useSponsor() { const json = await result.json() data.value = mapSponsors(json) + toggleDarkLogos() }) return { diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index a85c67e1df22f5..5fa0d638a3f9a1 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,6 +1,8 @@ import { h } from 'vue' import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme' +import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' +import '@shikijs/vitepress-twoslash/style.css' import './styles/vars.css' import HomeSponsors from './components/HomeSponsors.vue' import AsideSponsors from './components/AsideSponsors.vue' @@ -16,5 +18,6 @@ export default { }, enhanceApp({ app }) { app.component('SvgImage', SvgImage) + app.use(TwoslashFloatingVue) }, } satisfies Theme diff --git a/docs/.vitepress/tsconfig.json b/docs/.vitepress/tsconfig.json new file mode 100644 index 00000000000000..20b9618d576e3e --- /dev/null +++ b/docs/.vitepress/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "noEmit": true + }, + "exclude": ["cache", "dist"] +} diff --git a/docs/blog/announcing-vite5-1.md b/docs/blog/announcing-vite5-1.md index b5f7c72a8dd6c7..35ee61c172870e 100644 --- a/docs/blog/announcing-vite5-1.md +++ b/docs/blog/announcing-vite5-1.md @@ -66,7 +66,7 @@ Import CSS files as URLs now works reliably and correctly. This was the last rem ### `build.assetsInlineLimit` now supports a callback -Users can now [provide a callback](/config/build-options.html#build-assetsinlinelimit) that returns a boolean to opt-in or opt-out of inlining for specific assets. If `undefined` is returned, the defalt logic applies. See ([#15366](https://github.com/vitejs/vite/issues/15366)). +Users can now [provide a callback](/config/build-options.html#build-assetsinlinelimit) that returns a boolean to opt-in or opt-out of inlining for specific assets. If `undefined` is returned, the default logic applies. See ([#15366](https://github.com/vitejs/vite/issues/15366)). ### Improved HMR for circular import @@ -82,7 +82,7 @@ The preview server now exposes a `close` method, which will properly teardown th ## Performance improvements -Vite keeps getting faster with each release, and Vite 5.1 is packed with performance improvements. We measured the loading time for 10K modules (25 level deep tree) using [vite-dev-server-perf](https://github.com/yyx990803/vite-dev-server-perf) for all minor versions from Vite 4.0. This is a good benchmark to meassure the effect of Vite's bundle-less approach. Each module is a small TypeScript file with a counter and imports to other files in the tree, so this mostly meassuring the time it takes to do the requests a separate modules. In Vite 4.0, loading 10K modules took 8 seconds on a M1 MAX. We had a breakthrough in [Vite 4.3 were we focused on performance](./announcing-vite4-3.md), and we were able to load them in 6.35 seconds. In Vite 5.1, we managed to do another performance leap. Vite is now serving the 10K modules in 5.35 seconds. +Vite keeps getting faster with each release, and Vite 5.1 is packed with performance improvements. We measured the loading time for 10K modules (25 level deep tree) using [vite-dev-server-perf](https://github.com/yyx990803/vite-dev-server-perf) for all minor versions from Vite 4.0. This is a good benchmark to measure the effect of Vite's bundle-less approach. Each module is a small TypeScript file with a counter and imports to other files in the tree, so this mostly measuring the time it takes to do the requests a separate modules. In Vite 4.0, loading 10K modules took 8 seconds on a M1 MAX. We had a breakthrough in [Vite 4.3 were we focused on performance](./announcing-vite4-3.md), and we were able to load them in 6.35 seconds. In Vite 5.1, we managed to do another performance leap. Vite is now serving the 10K modules in 5.35 seconds. ![Vite 10K Modules Loading time progression](/vite5-1-10K-modules-loading-time.png) @@ -113,7 +113,7 @@ The dev server had several incremental performance gains. A new middleware to sh ## Deprecations -We continue to reduce Vite's API surface where possible to make the project manintainable long term. +We continue to reduce Vite's API surface where possible to make the project maintainable long term. ### Deprecated `as` option in `import.meta.glob` @@ -129,4 +129,4 @@ We are grateful to the [900 contributors to Vite Core](https://github.com/vitejs ## Acknowledgments -Vite 5.1 is possible thanks to our community of contributors, maintainers in the ecosystem, and the [Vite Team](/team). A shoutout the individuals and companies sponsoring Vite development. [StackBlitz](https://stackblitz.com/), [Nuxt Labs](https://nuxtlabs.com/), and [Astro](https://astro.build) for hiring Vite team members. And also to the sponsors on [Vite's GitHub Sponsors](https://github.com/sponsors/vitejs), [Vite's Open Collective](https://opencollective.com/vite), and [Evan You's GitHub Sponsors](https://github.com/sponsors/yyx990803). +Vite 5.1 is possible thanks to our community of contributors, maintainers in the ecosystem, and the [Vite Team](/team). A shout out to the individuals and companies sponsoring Vite development. [StackBlitz](https://stackblitz.com/), [Nuxt Labs](https://nuxtlabs.com/), and [Astro](https://astro.build) for hiring Vite team members. And also to the sponsors on [Vite's GitHub Sponsors](https://github.com/sponsors/vitejs), [Vite's Open Collective](https://opencollective.com/vite), and [Evan You's GitHub Sponsors](https://github.com/sponsors/yyx990803). diff --git a/docs/blog/announcing-vite5.md b/docs/blog/announcing-vite5.md index 1ce435792d2c1b..dd4c1d46a275b0 100644 --- a/docs/blog/announcing-vite5.md +++ b/docs/blog/announcing-vite5.md @@ -33,7 +33,7 @@ _November 16, 2023_ Vite 4 [was released](./announcing-vite4.md) almost a year ago, and it served as a solid base for the ecosystem. npm downloads per week jumped from 2.5 million to 7.5 million, as projects keep building on a shared infrastructure. Frameworks continued to innovate, and on top of [Astro](https://astro.build/), [Nuxt](https://nuxt.com/), [SvelteKit](https://kit.svelte.dev/), [Solid Start](https://www.solidjs.com/blog/introducing-solidstart), [Qwik City](https://qwik.builder.io/qwikcity/overview/), between others, we saw new frameworks joining and making the ecosystem stronger. [RedwoodJS](https://redwoodjs.com/) and [Remix](https://remix.run/) switching to Vite paves the way for further adoption in the React ecosystem. [Vitest](https://vitest.dev) kept growing at an even faster pace than Vite. Its team has been hard at work and will soon [release Vitest 1.0](https://github.com/vitest-dev/vitest/issues/3596). The story of Vite when used with other tools such as [Storybook](https://storybook.js.org), [Nx](https://nx.dev), and [Playwright](https://playwright.dev) kept improving, and the same goes for environments, with Vite dev working both in [Deno](https://deno.com) and [Bun](https://bun.sh). -We had the second edition of [ViteConf](https://viteconf.org/23/replay) a month ago, hosted by [StackBlitz](https://stackblitz.com). Like last year, most of the projects in the ecosystem got together to share ideas and connect to keep expanding the commons. We're also seeing new pieces complement the meta-framework toolbelt like [Volar](https://volarjs.dev/) and [Nitro](https://nitro.unjs.io/). The Rollup team released [Rollup 4](https://rollupjs.org) that same day, a tradition Lukas started last year. +We had the second edition of [ViteConf](https://viteconf.org/23/replay) a month ago, hosted by [StackBlitz](https://stackblitz.com). Like last year, most of the projects in the ecosystem got together to share ideas and connect to keep expanding the commons. We're also seeing new pieces complement the meta-framework tool belt like [Volar](https://volarjs.dev/) and [Nitro](https://nitro.unjs.io/). The Rollup team released [Rollup 4](https://rollupjs.org) that same day, a tradition Lukas started last year. Six months ago, Vite 4.3 [was released](./announcing-vite4.md). This release significantly improved the dev server performance. However, there is still ample room for improvement. At ViteConf, [Evan You unveiled Vite's long-term plan to work on Rolldown](https://www.youtube.com/watch?v=hrdwQHoAp0M), a Rust-port of Rollup with compatible APIs. Once it is ready, we intend to use it in Vite Core to take on the tasks of both Rollup and esbuild. This will mean a boost in build performance (and later on in dev performance too as we move perf-sensitive parts of Vite itself to Rust), and a big reduction of inconsistencies between dev and build. Rolldown is currently in early stages and the team is preparing to open source the codebase before the end of the year. Stay tuned! @@ -105,6 +105,6 @@ A low level breakdown with the full list of changes to Vite core can be found at ## Acknowledgments -Vite 5 is the result of long hours of work by our community of contributors, downstream maintainers, plugins authors, and the [Vite Team](/team). A big shoutout to [Bjorn Lu](https://twitter.com/bluwyoo) for leading the release process for this major. +Vite 5 is the result of long hours of work by our community of contributors, downstream maintainers, plugins authors, and the [Vite Team](/team). A big shout out to [Bjorn Lu](https://twitter.com/bluwyoo) for leading the release process for this major. -We're also thankful to individuals and companies sponsoring Vite development. [StackBlitz](https://stackblitz.com/), [Nuxt Labs](https://nuxtlabs.com/), and [Astro](https://astro.build) continue to invest in Vite by hiring Vite team members. A shoutout to sponsors on [Vite's GitHub Sponsors](https://github.com/sponsors/vitejs), [Vite's Open Collective](https://opencollective.com/vite), and [Evan You's GitHub Sponsors](https://github.com/sponsors/yyx990803). A special mention to [Remix](https://remix.run/) for becoming a Gold sponsor and contributing back after switching to Vite. +We're also thankful to individuals and companies sponsoring Vite development. [StackBlitz](https://stackblitz.com/), [Nuxt Labs](https://nuxtlabs.com/), and [Astro](https://astro.build) continue to invest in Vite by hiring Vite team members. A shout out to sponsors on [Vite's GitHub Sponsors](https://github.com/sponsors/vitejs), [Vite's Open Collective](https://opencollective.com/vite), and [Evan You's GitHub Sponsors](https://github.com/sponsors/yyx990803). A special mention to [Remix](https://remix.run/) for becoming a Gold sponsor and contributing back after switching to Vite. diff --git a/docs/config/build-options.md b/docs/config/build-options.md index 2f8a54eb8c98a7..ee89fb8273704d 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -49,11 +49,19 @@ type ResolveModulePreloadDependenciesFn = ( The `resolveDependencies` function will be called for each dynamic import with a list of the chunks it depends on, and it will also be called for each chunk imported in entry HTML files. A new dependencies array can be returned with these filtered or more dependencies injected, and their paths modified. The `deps` paths are relative to the `build.outDir`. The return value should be a relative path to the `build.outDir`. -```js +```js twoslash +/** @type {import('vite').UserConfig} */ +const config = { + // prettier-ignore + build: { +// ---cut-before--- modulePreload: { resolveDependencies: (filename, deps, { hostId, hostType }) => { return deps.filter(condition) - } + }, +}, +// ---cut-after--- + }, } ``` diff --git a/docs/config/dep-optimization-options.md b/docs/config/dep-optimization-options.md index e8ddddb09ceaf0..be248e65d7a8bf 100644 --- a/docs/config/dep-optimization-options.md +++ b/docs/config/dep-optimization-options.md @@ -8,7 +8,7 @@ By default, Vite will crawl all your `.html` files to detect dependencies that need to be pre-bundled (ignoring `node_modules`, `build.outDir`, `__tests__` and `coverage`). If `build.rollupOptions.input` is specified, Vite will crawl those entry points instead. -If neither of these fit your needs, you can specify custom entries using this option - the value should be a [fast-glob pattern](https://github.com/mrmlnc/fast-glob#basic-syntax) or array of patterns that are relative from Vite project root. This will overwrite default entries inference. Only `node_modules` and `build.outDir` folders will be ignored by default when `optimizeDeps.entries` is explicitly defined. If other folders need to be ignored, you can use an ignore pattern as part of the entries list, marked with an initial `!`. +If neither of these fit your needs, you can specify custom entries using this option - the value should be a [fast-glob pattern](https://github.com/mrmlnc/fast-glob#basic-syntax) or array of patterns that are relative from Vite project root. This will overwrite default entries inference. Only `node_modules` and `build.outDir` folders will be ignored by default when `optimizeDeps.entries` is explicitly defined. If other folders need to be ignored, you can use an ignore pattern as part of the entries list, marked with an initial `!`. If you don't want to ignore `node_modules` and `build.outDir`, you can specify using literal string paths (without fast-glob patterns) instead. ## optimizeDeps.exclude @@ -19,7 +19,9 @@ Dependencies to exclude from pre-bundling. :::warning CommonJS CommonJS dependencies should not be excluded from optimization. If an ESM dependency is excluded from optimization, but has a nested CommonJS dependency, the CommonJS dependency should be added to `optimizeDeps.include`. Example: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig({ optimizeDeps: { include: ['esm-dep > cjs-dep'], @@ -37,7 +39,9 @@ By default, linked packages not inside `node_modules` are not pre-bundled. Use t **Experimental:** If you're using a library with many deep imports, you can also specify a trailing glob pattern to pre-bundle all deep imports at once. This will avoid constantly pre-bundling whenever a new deep import is used. [Give Feedback](https://github.com/vitejs/vite/discussions/15833). For example: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig({ optimizeDeps: { include: ['my-lib/components/**/*.vue'], diff --git a/docs/config/index.md b/docs/config/index.md index 6932c7f29dc595..e599295b3bd80b 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -50,7 +50,9 @@ Vite also directly supports TS config files. You can use `vite.config.ts` with t If the config needs to conditionally determine options based on the command (`serve` or `build`), the [mode](/guide/env-and-mode) being used, if it's an SSR build (`isSsrBuild`), or is previewing the build (`isPreview`), it can export a function instead: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { if (command === 'serve') { return { @@ -65,7 +67,7 @@ export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { }) ``` -It is important to note that in Vite's API the `command` value is `serve` during dev (in the cli `vite`, `vite dev`, and `vite serve` are aliases), and `build` when building for production (`vite build`). +It is important to note that in Vite's API the `command` value is `serve` during dev (in the cli [`vite`](/guide/cli#vite), `vite dev`, and `vite serve` are aliases), and `build` when building for production ([`vite build`](/guide/cli#vite-build)). `isSsrBuild` and `isPreview` are additional optional flags to differentiate the kind of `build` and `serve` commands respectively. Some tools that load the Vite config may not support these flags and will pass `undefined` instead. Hence, it's recommended to use explicit comparison against `true` and `false`. @@ -73,7 +75,9 @@ It is important to note that in Vite's API the `command` value is `serve` during If the config needs to call async functions, it can export an async function instead. And this async function can also be passed through `defineConfig` for improved intellisense support: -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig(async ({ command, mode }) => { const data = await asyncFunction() return { @@ -88,7 +92,7 @@ Environmental Variables can be obtained from `process.env` as usual. Note that Vite doesn't load `.env` files by default as the files to load can only be determined after evaluating the Vite config, for example, the `root` and `envDir` options affect the loading behaviour. However, you can use the exported `loadEnv` helper to load the specific `.env` file if needed. -```js +```js twoslash import { defineConfig, loadEnv } from 'vite' export default defineConfig(({ command, mode }) => { diff --git a/docs/config/server-options.md b/docs/config/server-options.md index 8ae5f5cb5f3624..dbeec4428120f5 100644 --- a/docs/config/server-options.md +++ b/docs/config/server-options.md @@ -18,10 +18,10 @@ The first case is when `localhost` is used. Node.js under v17 reorders the resul You can set [`dns.setDefaultResultOrder('verbatim')`](https://nodejs.org/api/dns.html#dns_dns_setdefaultresultorder_order) to disable the reordering behavior. Vite will then print the address as `localhost`. -```js +```js twoslash // vite.config.js import { defineConfig } from 'vite' -import dns from 'dns' +import dns from 'node:dns' dns.setDefaultResultOrder('verbatim') @@ -90,7 +90,7 @@ Configure custom proxy rules for the dev server. Expects an object of `{ key: op Note that if you are using non-relative [`base`](/config/shared-options.md#base), you must prefix each key with that `base`. -Extends [`http-proxy`](https://github.com/http-party/node-http-proxy#options). Additional options are [here](https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/proxy.ts#L13). +Extends [`http-proxy`](https://github.com/http-party/node-http-proxy#options). Additional options are [here](https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/proxy.ts#L13). Note that [unlike http-proxy](https://github.com/http-party/node-http-proxy/issues/1669), the `changeOrigin` option will change both host and origin headers to match the target. In some cases, you might also want to configure the underlying dev server (e.g. to add custom middlewares to the internal [connect](https://github.com/senchalabs/connect) app). In order to do that, you need to write your own [plugin](/guide/using-plugins.html) and use [configureServer](/guide/api-plugin.html#configureserver) function. @@ -238,7 +238,7 @@ Create Vite server in middleware mode. - **Example:** -```js +```js twoslash import express from 'express' import { createServer as createViteServer } from 'vite' @@ -358,9 +358,9 @@ export default defineConfig({ // in their paths to the ignore list. sourcemapIgnoreList(sourcePath, sourcemapPath) { return sourcePath.includes('node_modules') - } - } -}; + }, + }, +}) ``` ::: tip Note diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index d8b81b4531f571..39eef1a5e5f2e9 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -163,6 +163,13 @@ Enabling this setting causes vite to determine file identity by the original fil - **Related:** [esbuild#preserve-symlinks](https://esbuild.github.io/api/#preserve-symlinks), [webpack#resolve.symlinks ](https://webpack.js.org/configuration/resolve/#resolvesymlinks) +## html.cspNonce + +- **Type:** `string` +- **Related:** [Content Security Policy (CSP)](/guide/features#content-security-policy-csp) + +A nonce value placeholder that will be used when generating script / style tags. Setting this value will also generate a meta tag with nonce value. + ## css.modules - **Type:** @@ -285,6 +292,10 @@ Whether to enable sourcemaps during dev. Selects the engine used for CSS processing. Check out [Lightning CSS](../guide/features.md#lightning-css) for more information. +::: info Duplicate `@import`s +Note that postcss (postcss-import) has a different behavior with duplicated `@import` from browsers. See [postcss/postcss-import#462](https://github.com/postcss/postcss-import/issues/462). +::: + ## css.lightningcss - **Experimental:** [Give Feedback](https://github.com/vitejs/vite/discussions/13835) @@ -408,7 +419,7 @@ Adjust console output verbosity. Default is `'info'`. Use a custom logger to log messages. You can use Vite's `createLogger` API to get the default logger and customize it to, for example, change the message or filter out certain warnings. -```js +```ts twoslash import { createLogger, defineConfig } from 'vite' const logger = createLogger() diff --git a/docs/config/ssr-options.md b/docs/config/ssr-options.md index 0111aa56031e15..1f5cd8e47f325a 100644 --- a/docs/config/ssr-options.md +++ b/docs/config/ssr-options.md @@ -18,7 +18,7 @@ Note that the explicitly listed dependencies (using `string[]` type) will always Prevent listed dependencies from being externalized for SSR, which they will get bundled in build. By default, only linked dependencies are not externalized (for HMR). If you prefer to externalize the linked dependency, you can pass its name to the `ssr.external` option. -If `true`, no dependencies are externalized. However, dependencies explicitly listed in `ssr.external` (using `string[]` type) can take priority and still be externalized. +If `true`, no dependencies are externalized. However, dependencies explicitly listed in `ssr.external` (using `string[]` type) can take priority and still be externalized. If `ssr.target: 'node'` is set, Node.js built-ins will also be externalized by default. Note that if both `ssr.noExternal: true` and `ssr.external: true` are configured, `ssr.noExternal` takes priority and no dependencies are externalized. diff --git a/docs/guide/api-hmr.md b/docs/guide/api-hmr.md index a24fe277fbfe24..e9a44eb0aaca88 100644 --- a/docs/guide/api-hmr.md +++ b/docs/guide/api-hmr.md @@ -8,15 +8,15 @@ The manual HMR API is primarily intended for framework and tooling authors. As a Vite exposes its manual HMR API via the special `import.meta.hot` object: -```ts +```ts twoslash +import type { ModuleNamespace } from 'vite/types/hot.d.ts' +import type { InferCustomEventPayload } from 'vite/types/customEvent.d.ts' + +// ---cut--- interface ImportMeta { readonly hot?: ViteHotContext } -type ModuleNamespace = Record & { - [Symbol.toStringTag]: 'Module' -} - interface ViteHotContext { readonly data: any @@ -32,7 +32,6 @@ interface ViteHotContext { prune(cb: (data: any) => void): void invalidate(message?: string): void - // `InferCustomEventPayload` provides types for built-in Vite events on( event: T, cb: (payload: InferCustomEventPayload) => void, @@ -67,7 +66,9 @@ Vite provides type definitions for `import.meta.hot` in [`vite/client.d.ts`](htt For a module to self-accept, use `import.meta.hot.accept` with a callback which receives the updated module: -```js +```js twoslash +import 'vite/client' +// ---cut--- export const count = 1 if (import.meta.hot) { @@ -90,7 +91,13 @@ Vite requires that the call to this function appears as `import.meta.hot.accept( A module can also accept updates from direct dependencies without reloading itself: -```js +```js twoslash +// @filename: /foo.d.ts +export declare const foo: () => void + +// @filename: /example.js +import 'vite/client' +// ---cut--- import { foo } from './foo.js' foo() @@ -117,7 +124,9 @@ if (import.meta.hot) { A self-accepting module or a module that expects to be accepted by others can use `hot.dispose` to clean-up any persistent side effects created by its updated copy: -```js +```js twoslash +import 'vite/client' +// ---cut--- function setupSideEffect() {} setupSideEffect() @@ -133,7 +142,9 @@ if (import.meta.hot) { Register a callback that will call when the module is no longer imported on the page. Compared to `hot.dispose`, this can be used if the source code cleans up side-effects by itself on updates and you only need to clean-up when it's removed from the page. Vite currently uses this for `.css` imports. -```js +```js twoslash +import 'vite/client' +// ---cut--- function setupOrReuseSideEffect() {} setupOrReuseSideEffect() @@ -151,7 +162,9 @@ The `import.meta.hot.data` object is persisted across different instances of the Note that re-assignment of `data` itself is not supported. Instead, you should mutate properties of the `data` object so information added from other handlers are preserved. -```js +```js twoslash +import 'vite/client' +// ---cut--- // ok import.meta.hot.data.someValue = 'hello' @@ -169,7 +182,9 @@ A self-accepting module may realize during runtime that it can't handle a HMR up Note that you should always call `import.meta.hot.accept` even if you plan to call `invalidate` immediately afterwards, or else the HMR client won't listen for future changes to the self-accepting module. To communicate your intent clearly, we recommend calling `invalidate` within the `accept` callback like so: -```js +```js twoslash +import 'vite/client' +// ---cut--- import.meta.hot.accept((module) => { // You may use the new module instance to decide whether to invalidate. if (cannotHandleUpdate(module)) { @@ -197,7 +212,7 @@ Custom HMR events can also be sent from plugins. See [handleHotUpdate](./api-plu ## `hot.off(event, cb)` -Remove callback from the event listeners +Remove callback from the event listeners. ## `hot.send(event, data)` @@ -205,7 +220,7 @@ Send custom events back to Vite's dev server. If called before connected, the data will be buffered and sent once the connection is established. -See [Client-server Communication](/guide/api-plugin.html#client-server-communication) for more details. +See [Client-server Communication](/guide/api-plugin.html#client-server-communication) for more details, including a section on [Typing Custom Events](/guide/api-plugin.html#typescript-for-custom-events). ## Further Reading diff --git a/docs/guide/api-javascript.md b/docs/guide/api-javascript.md index fbfba3b7d9e2c8..eda204c2cf951d 100644 --- a/docs/guide/api-javascript.md +++ b/docs/guide/api-javascript.md @@ -12,26 +12,24 @@ async function createServer(inlineConfig?: InlineConfig): Promise **Example Usage:** -```js -import { fileURLToPath } from 'url' +```ts twoslash +import { fileURLToPath } from 'node:url' import { createServer } from 'vite' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -;(async () => { - const server = await createServer({ - // any valid user config options, plus `mode` and `configFile` - configFile: false, - root: __dirname, - server: { - port: 1337, - }, - }) - await server.listen() +const server = await createServer({ + // any valid user config options, plus `mode` and `configFile` + configFile: false, + root: __dirname, + server: { + port: 1337, + }, +}) +await server.listen() - server.printUrls() - server.bindCLIShortcuts({ print: true }) -})() +server.printUrls() +server.bindCLIShortcuts({ print: true }) ``` ::: tip NOTE @@ -44,7 +42,7 @@ When using [middleware mode](/config/server-options.html#server-middlewaremode)
Example -```ts +```ts twoslash import http from 'http' import { createServer } from 'vite' @@ -57,16 +55,17 @@ const vite = await createServer({ // Provide the parent http server for proxy WebSocket server: parentServer, }, - }, - proxy: { - '/ws': { - target: 'ws://localhost:3000', - // Proxying WebSocket - ws: true, + proxy: { + '/ws': { + target: 'ws://localhost:3000', + // Proxying WebSocket + ws: true, + }, }, }, }) +// @noErrors: 2339 parentServer.use(vite.middlewares) ``` @@ -183,9 +182,21 @@ interface ViteDevServer { * Bind CLI shortcuts */ bindCLIShortcuts(options?: BindCLIShortcutsOptions): void + /** + * Calling `await server.waitForRequestsIdle(id)` will wait until all static imports + * are processed. If called from a load or transform plugin hook, the id needs to be + * passed as a parameter to avoid deadlocks. Calling this function after the first + * static imports section of the module graph has been processed will resolve immediately. + * @experimental + */ + waitForRequestsIdle: (ignoredId?: string) => Promise } ``` +:::info +`waitForRequestsIdle` is meant to be used as a escape hatch to improve DX for features that can't be implemented following the on-demand nature of the Vite dev server. It can be used during startup by tools like Tailwind to delay generating the app CSS classes until the app code has been seen, avoiding flashes of style changes. When this function is used in a load or transform hook, and the default HTTP1 server is used, one of the six http channels will be blocked until the server processes all static imports. Vite's dependency optimizer currently uses this function to avoid full-page reloads on missing dependencies by delaying loading of pre-bundled dependencies until all imported dependencies have been collected from static imported sources. Vite may switch to a different strategy in a future major release, setting `optimizeDeps.crawlUntilStaticImports: false` by default to avoid the performance hit in large applications during cold start. +::: + ## `build` **Type Signature:** @@ -198,24 +209,22 @@ async function build( **Example Usage:** -```js -import path from 'path' -import { fileURLToPath } from 'url' +```ts twoslash +import path from 'node:path' +import { fileURLToPath } from 'node:url' import { build } from 'vite' const __dirname = fileURLToPath(new URL('.', import.meta.url)) -;(async () => { - await build({ - root: path.resolve(__dirname, './project'), - base: '/foo/', - build: { - rollupOptions: { - // ... - }, +await build({ + root: path.resolve(__dirname, './project'), + base: '/foo/', + build: { + rollupOptions: { + // ... }, - }) -})() + }, +}) ``` ## `preview` @@ -228,20 +237,19 @@ async function preview(inlineConfig?: InlineConfig): Promise **Example Usage:** -```js +```ts twoslash import { preview } from 'vite' -;(async () => { - const previewServer = await preview({ - // any valid user config options, plus `mode` and `configFile` - preview: { - port: 8080, - open: true, - }, - }) - previewServer.printUrls() - previewServer.bindCLIShortcuts({ print: true }) -})() +const previewServer = await preview({ + // any valid user config options, plus `mode` and `configFile` + preview: { + port: 8080, + open: true, + }, +}) + +previewServer.printUrls() +previewServer.bindCLIShortcuts({ print: true }) ``` ## `PreviewServer` @@ -316,7 +324,17 @@ Deeply merge two Vite configs. `isRoot` represents the level within the Vite con You can use the `defineConfig` helper to merge a config in callback form with another config: -```ts +```ts twoslash +import { + defineConfig, + mergeConfig, + type UserConfigFnObject, + type UserConfig, +} from 'vite' +declare const configAsCallback: UserConfigFnObject +declare const configAsObject: UserConfig + +// ---cut--- export default defineConfig((configEnv) => mergeConfig(configAsCallback(configEnv), configAsObject), ) diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index eb62b70cc809d8..852a6e408cfb37 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -428,8 +428,7 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo ```js handleHotUpdate({ server, modules, timestamp }) { - // Also use `server.ws.send` to support Vite <5.1 if needed - server.hot.send({ type: 'full-reload' }) + server.ws.send({ type: 'full-reload' }) // Invalidate modules manually const invalidatedModules = new Set() for (const mod of modules) { @@ -448,8 +447,7 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo ```js handleHotUpdate({ server }) { - // Also use `server.ws.send` to support Vite <5.1 if needed - server.hot.send({ + server.ws.send({ type: 'custom', event: 'special-update', data: {} @@ -480,6 +478,8 @@ A Vite plugin can additionally specify an `enforce` property (similar to webpack - User plugins with `enforce: 'post'` - Vite post build plugins (minify, manifest, reporting) +Note that this is separate from hooks ordering, those are still separately subject to their `order` attribute [as usual for Rollup hooks](https://rollupjs.org/plugin-development/#build-hooks). + ## Conditional Application By default plugins are invoked for both serve and build. In cases where a plugin needs to be conditionally applied only during serve or build, use the `apply` property to only invoke them during `'build'` or `'serve'`: @@ -554,7 +554,7 @@ Since Vite 2.9, we provide some utilities for plugins to help handle the communi ### Server to Client -On the plugin side, we could use `server.hot.send` (since Vite 5.1) or `server.ws.send` to broadcast events to all the clients: +On the plugin side, we could use `server.ws.send` to broadcast events to the client: ```js // vite.config.js @@ -563,9 +563,8 @@ export default defineConfig({ { // ... configureServer(server) { - // Example: wait for a client to connect before sending a message - server.hot.on('connection', () => { - server.hot.send('my:greetings', { msg: 'hello' }) + server.ws.on('connection', () => { + server.ws.send('my:greetings', { msg: 'hello' }) }) }, }, @@ -579,7 +578,9 @@ We recommend **always prefixing** your event names to avoid collisions with othe On the client side, use [`hot.on`](/guide/api-hmr.html#hot-on-event-cb) to listen to the events: -```ts +```ts twoslash +import 'vite/client' +// ---cut--- // client side if (import.meta.hot) { import.meta.hot.on('my:greetings', (data) => { @@ -599,7 +600,7 @@ if (import.meta.hot) { } ``` -Then use `server.hot.on` (since Vite 5.1) or `server.ws.on` and listen to the events on the server side: +Then use `server.ws.on` and listen to the events on the server side: ```js // vite.config.js @@ -608,7 +609,7 @@ export default defineConfig({ { // ... configureServer(server) { - server.hot.on('my:from-client', (data, client) => { + server.ws.on('my:from-client', (data, client) => { console.log('Message from client:', data.msg) // Hey! // reply only to the client (if needed) client.send('my:ack', { msg: 'Hi! I got your message!' }) @@ -621,16 +622,40 @@ export default defineConfig({ ### TypeScript for Custom Events -It is possible to type custom events by extending the `CustomEventMap` interface: +Internally, vite infers the type of a payload from the `CustomEventMap` interface, it is possible to type custom events by extending the interface: + +:::tip Note +Make sure to include the `.d.ts` extension when specifying TypeScript declaration files. Otherwise, Typescript may not know which file the module is trying to extend. +::: ```ts // events.d.ts -import 'vite/types/customEvent' +import 'vite/types/customEvent.d.ts' -declare module 'vite/types/customEvent' { +declare module 'vite/types/customEvent.d.ts' { interface CustomEventMap { 'custom:foo': { msg: string } // 'event-key': payload } } ``` + +This interface extension is utilized by `InferCustomEventPayload` to infer the payload type for event `T`. For more information on how this interface is utilized, refer to the [HMR API Documentation](./api-hmr#hmr-api). + +```ts twoslash +import 'vite/client' +import type { InferCustomEventPayload } from 'vite/types/customEvent.d.ts' +declare module 'vite/types/customEvent.d.ts' { + interface CustomEventMap { + 'custom:foo': { msg: string } + } +} +// ---cut--- +type CustomFooPayload = InferCustomEventPayload<'custom:foo'> +import.meta.hot?.on('custom:foo', (payload) => { + // The type of payload will be { msg: string } +}) +import.meta.hot?.on('unknown:event', (payload) => { + // The type of payload will be any +}) +``` diff --git a/docs/guide/api-vite-runtime.md b/docs/guide/api-vite-runtime.md index 5c4ee3f07c3ec5..9aa579d268ddcf 100644 --- a/docs/guide/api-vite-runtime.md +++ b/docs/guide/api-vite-runtime.md @@ -1,7 +1,9 @@ # Vite Runtime API :::warning Low-level API -This API was introduced in Vite 5.1 as an experimental feature. It was added to [gather feedback](https://github.com/vitejs/vite/discussions/15774). There will probably be breaking changes to it in Vite 5.2, so make sure to pin the Vite version to `~5.1.0` when using it. This is a low-level API meant for library and framework authors. If your goal is to create an application, make sure to check out the higher-level SSR plugins and tools at [Awesome Vite SSR section](https://github.com/vitejs/awesome-vite#ssr) first. +This API was introduced in Vite 5.1 as an experimental feature. It was added to [gather feedback](https://github.com/vitejs/vite/discussions/15774). There will likely be breaking changes, so make sure to pin the Vite version to `~5.1.0` when using it. This is a low-level API meant for library and framework authors. If your goal is to create an application, make sure to check out the higher-level SSR plugins and tools at [Awesome Vite SSR section](https://github.com/vitejs/awesome-vite#ssr) first. + +Currently, the API is being revised as the [Environment API](https://github.com/vitejs/vite/discussions/16358) which is released at `^6.0.0-alpha.0`. ::: The "Vite Runtime" is a tool that allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runtime implementation is decoupled from the server. This allows library and framework authors to implement their own layer of communication between the server and the runtime. diff --git a/docs/guide/assets.md b/docs/guide/assets.md index dff926f2c2da31..4f81d04e496a80 100644 --- a/docs/guide/assets.md +++ b/docs/guide/assets.md @@ -7,7 +7,9 @@ Importing a static asset will return the resolved public URL when it is served: -```js +```js twoslash +import 'vite/client' +// ---cut--- import imgUrl from './img.png' document.getElementById('hero-img').src = imgUrl ``` @@ -30,11 +32,25 @@ The behavior is similar to webpack's `file-loader`. The difference is that the i - TypeScript, by default, does not recognize static asset imports as valid modules. To fix this, include [`vite/client`](./features#client-types). +::: tip Inlining SVGs through `url()` +When passing a URL of SVG to a manually constructed `url()` by JS, the variable should be wrapped within double quotes. + +```js twoslash +import 'vite/client' +// ---cut--- +import imgUrl from './img.svg' +document.getElementById('hero-img').style.background = `url("${imgUrl}")` +``` + +::: + ### Explicit URL Imports Assets that are not included in the internal list or in `assetsInclude`, can be explicitly imported as a URL using the `?url` suffix. This is useful, for example, to import [Houdini Paint Worklets](https://houdini.how/usage). -```js +```js twoslash +import 'vite/client' +// ---cut--- import workletURL from 'extra-scalloped-border/worklet.js?url' CSS.paintWorklet.addModule(workletURL) ``` @@ -43,7 +59,9 @@ CSS.paintWorklet.addModule(workletURL) Assets can be imported as strings using the `?raw` suffix. -```js +```js twoslash +import 'vite/client' +// ---cut--- import shaderString from './shader.glsl?raw' ``` @@ -51,19 +69,25 @@ import shaderString from './shader.glsl?raw' Scripts can be imported as web workers with the `?worker` or `?sharedworker` suffix. -```js +```js twoslash +import 'vite/client' +// ---cut--- // Separate chunk in the production build import Worker from './shader.js?worker' const worker = new Worker() ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // sharedworker import SharedWorker from './shader.js?sharedworker' const sharedWorker = new SharedWorker() ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // Inlined as base64 strings import InlineWorker from './shader.js?worker&inline' ``` diff --git a/docs/guide/backend-integration.md b/docs/guide/backend-integration.md index 6e391e48b23261..8509082bbdf2ea 100644 --- a/docs/guide/backend-integration.md +++ b/docs/guide/backend-integration.md @@ -8,7 +8,9 @@ If you need a custom integration, you can follow the steps in this guide to conf 1. In your Vite config, configure the entry and enable build manifest: - ```js + ```js twoslash + import { defineConfig } from 'vite' + // ---cut--- // vite.config.js export default defineConfig({ build: { @@ -60,22 +62,36 @@ If you need a custom integration, you can follow the steps in this guide to conf ```json { - "main.js": { - "file": "assets/main.4889e940.js", - "src": "main.js", + "_shared-!~{003}~.js": { + "file": "assets/shared-ChJ_j-JJ.css", + "src": "_shared-!~{003}~.js" + }, + "_shared-B7PI925R.js": { + "file": "assets/shared-B7PI925R.js", + "name": "shared", + "css": ["assets/shared-ChJ_j-JJ.css"] + }, + "baz.js": { + "file": "assets/baz-B2H3sXNv.js", + "name": "baz", + "src": "baz.js", + "isDynamicEntry": true + }, + "views/bar.js": { + "file": "assets/bar-gkvgaI9m.js", + "name": "bar", + "src": "views/bar.js", "isEntry": true, - "dynamicImports": ["views/foo.js"], - "css": ["assets/main.b82dbe22.css"], - "assets": ["assets/asset.0ab0f9cd.png"] + "imports": ["_shared-B7PI925R.js"], + "dynamicImports": ["baz.js"] }, "views/foo.js": { - "file": "assets/foo.869aea0d.js", + "file": "assets/foo-BRBmoGS9.js", + "name": "foo", "src": "views/foo.js", - "isDynamicEntry": true, - "imports": ["_shared.83069a53.js"] - }, - "_shared.83069a53.js": { - "file": "assets/shared.83069a53.js" + "isEntry": true, + "imports": ["_shared-B7PI925R.js"], + "css": ["assets/foo-5UjPuW-k.css"] } } ``` @@ -85,10 +101,54 @@ If you need a custom integration, you can follow the steps in this guide to conf - For non entry chunks, the key is the base name of the generated file prefixed with `_`. - Chunks will contain information on its static and dynamic imports (both are keys that map to the corresponding chunk in the manifest), and also its corresponding CSS and asset files (if any). - You can use this file to render links or preload directives with hashed filenames (note: the syntax here is for explanation only, substitute with your server templating language): +4. You can use this file to render links or preload directives with hashed filenames. + + Here is an example HTML template to render the proper links. The syntax here is for + explanation only, substitute with your server templating language. The `importedChunks` + function is for illustration and isn't provided by Vite. ```html - - + + + + + + + + + + + + + ``` + + Specifically, a backend generating HTML should include the following tags given a manifest + file and an entry point: + + - A `` tag for each file in the entry point chunk's `css` list + - Recursively follow all chunks in the entry point's `imports` list and include a + `` tag for each CSS file of each imported chunk. + - A tag for the `file` key of the entry point chunk (` + + + ``` + + While the following should be included for the entry point `views/bar.js`: + + ```html + + + + ``` diff --git a/docs/guide/build.md b/docs/guide/build.md index a9c79c3d44c1f5..50abd7c91cea56 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -34,7 +34,6 @@ For advanced base path control, check out [Advanced Base Options](#advanced-base The build can be customized via various [build config options](/config/build-options.md). Specifically, you can directly adjust the underlying [Rollup options](https://rollupjs.org/configuration-options/) via `build.rollupOptions`: ```js -// vite.config.js export default defineConfig({ build: { rollupOptions: { @@ -48,29 +47,15 @@ For example, you can specify multiple Rollup outputs with plugins that are only ## Chunking Strategy -You can configure how chunks are split using `build.rollupOptions.output.manualChunks` (see [Rollup docs](https://rollupjs.org/configuration-options/#output-manualchunks)). Until Vite 2.8, the default chunking strategy divided the chunks into `index` and `vendor`. It is a good strategy for some SPAs, but it is hard to provide a general solution for every Vite target use case. From Vite 2.9, `manualChunks` is no longer modified by default. You can continue to use the Split Vendor Chunk strategy by adding the `splitVendorChunkPlugin` in your config file: - -```js -// vite.config.js -import { splitVendorChunkPlugin } from 'vite' -export default defineConfig({ - plugins: [splitVendorChunkPlugin()], -}) -``` - -This strategy is also provided as a `splitVendorChunk({ cache: SplitVendorChunkCache })` factory, in case composition with custom logic is needed. `cache.reset()` needs to be called at `buildStart` for build watch mode to work correctly in this case. - -::: warning -You should use `build.rollupOptions.output.manualChunks` function form when using this plugin. If the object form is used, the plugin won't have any effect. -::: +You can configure how chunks are split using `build.rollupOptions.output.manualChunks` (see [Rollup docs](https://rollupjs.org/configuration-options/#output-manualchunks)). If you use a framework, refer to their documentation for configuring how chunks are splitted. ## Load Error Handling Vite emits `vite:preloadError` event when it fails to load dynamic imports. `event.payload` contains the original import error. If you call `event.preventDefault()`, the error will not be thrown. -```js +```js twoslash window.addEventListener('vite:preloadError', (event) => { - window.reload() // for example, refresh the page + window.location.reload() // for example, refresh the page }) ``` @@ -111,7 +96,7 @@ During dev, simply navigate or link to `/nested/` - it works as expected, just l During build, all you need to do is to specify multiple `.html` files as entry points: -```js +```js twoslash // vite.config.js import { resolve } from 'path' import { defineConfig } from 'vite' @@ -138,7 +123,7 @@ When you are developing a browser-oriented library, you are likely spending most When it is time to bundle your library for distribution, use the [`build.lib` config option](/config/build-options.md#build-lib). Make sure to also externalize any dependencies that you do not want to bundle into your library, e.g. `vue` or `react`: -```js +```js twoslash // vite.config.js import { resolve } from 'path' import { defineConfig } from 'vite' @@ -253,32 +238,45 @@ A user may choose to deploy in three different paths: A single static [base](#public-base-path) isn't enough in these scenarios. Vite provides experimental support for advanced base options during build, using `experimental.renderBuiltUrl`. -```ts +```ts twoslash +import type { UserConfig } from 'vite' +// prettier-ignore +const config: UserConfig = { +// ---cut-before--- experimental: { - renderBuiltUrl(filename: string, { hostType }: { hostType: 'js' | 'css' | 'html' }) { + renderBuiltUrl(filename, { hostType }) { if (hostType === 'js') { return { runtime: `window.__toCdnUrl(${JSON.stringify(filename)})` } } else { return { relative: true } } - } + }, +}, +// ---cut-after--- } ``` If the hashed assets and public files aren't deployed together, options for each group can be defined independently using asset `type` included in the second `context` param given to the function. -```ts +```ts twoslash +import type { UserConfig } from 'vite' +import path from 'node:path' +// prettier-ignore +const config: UserConfig = { +// ---cut-before--- experimental: { - renderBuiltUrl(filename: string, { hostId, hostType, type }: { hostId: string, hostType: 'js' | 'css' | 'html', type: 'public' | 'asset' }) { + renderBuiltUrl(filename, { hostId, hostType, type }) { if (type === 'public') { return 'https://www.domain.com/' + filename - } - else if (path.extname(hostId) === '.js') { + } else if (path.extname(hostId) === '.js') { return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` } - } - else { + } else { return 'https://cdn.domain.com/assets/' + filename } - } + }, +}, +// ---cut-after--- } ``` + +Note that the `filename` passed is a decoded URL, and if the function returns a URL string, it should also be decoded. Vite will handle the encoding automatically when rendering the URLs. If an object with `runtime` is returned, encoding should be handled yourself where needed as the runtime code will be rendered as is. diff --git a/docs/guide/cli.md b/docs/guide/cli.md index bfbd6ca7de6e0c..8256f0811ddf29 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -4,7 +4,7 @@ ### `vite` -Start Vite dev server in the current directory. +Start Vite dev server in the current directory. `vite dev` and `vite serve` are aliases for `vite`. #### Usage diff --git a/docs/guide/dep-pre-bundling.md b/docs/guide/dep-pre-bundling.md index 331563fd4c36af..e387a6ae1b8a48 100644 --- a/docs/guide/dep-pre-bundling.md +++ b/docs/guide/dep-pre-bundling.md @@ -37,7 +37,9 @@ In a monorepo setup, a dependency may be a linked package from the same repo. Vi However, this requires the linked dep to be exported as ESM. If not, you can add the dependency to [`optimizeDeps.include`](/config/dep-optimization-options.md#optimizedeps-include) and [`build.commonjsOptions.include`](/config/build-options.md#build-commonjsoptions) in your config. -```js +```js twoslash +import { defineConfig } from 'vite' +// ---cut--- export default defineConfig({ optimizeDeps: { include: ['linked-dep'], diff --git a/docs/guide/env-and-mode.md b/docs/guide/env-and-mode.md index 7a817e2e752e7c..ef8ceb882d4c72 100644 --- a/docs/guide/env-and-mode.md +++ b/docs/guide/env-and-mode.md @@ -2,7 +2,7 @@ ## Env Variables -Vite exposes env variables on the special **`import.meta.env`** object. Some built-in variables are available in all cases: +Vite exposes env variables on the special **`import.meta.env`** object, which are statically replaced at build time. Some built-in variables are available in all cases: - **`import.meta.env.MODE`**: {string} the [mode](#modes) the app is running in. diff --git a/docs/guide/features.md b/docs/guide/features.md index 19e4e3adc22617..75940b8d17310c 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -65,7 +65,7 @@ It is because `esbuild` only performs transpilation without type information, it You must set `"isolatedModules": true` in your `tsconfig.json` under `compilerOptions`, so that TS will warn you against the features that do not work with isolated transpilation. -However, some libraries (e.g. [`vue`](https://github.com/vuejs/core/issues/1228)) don't work well with `"isolatedModules": true`. You can use `"skipLibCheck": true` to temporarily suppress the errors until it is fixed upstream. +If a dependency doesn't work well with `"isolatedModules": true`. You can use `"skipLibCheck": true` to temporarily suppress the errors until it is fixed upstream. #### `useDefineForClassFields` @@ -176,7 +176,7 @@ Vue users should use the official [@vitejs/plugin-vue-jsx](https://github.com/vi If using JSX without React or Vue, custom `jsxFactory` and `jsxFragment` can be configured using the [`esbuild` option](/config/shared-options.md#esbuild). For example for Preact: -```js +```js twoslash // vite.config.js import { defineConfig } from 'vite' @@ -192,7 +192,7 @@ More details in [esbuild docs](https://esbuild.github.io/content-types/#jsx). You can inject the JSX helpers using `jsxInject` (which is a Vite-only option) to avoid manual imports: -```js +```js twoslash // vite.config.js import { defineConfig } from 'vite' @@ -230,7 +230,9 @@ Any CSS file ending with `.module.css` is considered a [CSS modules file](https: } ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- import classes from './example.module.css' document.getElementById('foo').className = classes.red ``` @@ -239,7 +241,9 @@ CSS modules behavior can be configured via the [`css.modules` option](/config/sh If `css.modules.localsConvention` is set to enable camelCase locals (e.g. `localsConvention: 'camelCaseOnly'`), you can also use named imports: -```js +```js twoslash +import 'vite/client' +// ---cut--- // .apply-color -> applyColor import { applyColor } from './example.module.css' document.getElementById('foo').className = applyColor @@ -274,7 +278,9 @@ You can also use CSS modules combined with pre-processors by prepending `.module The automatic injection of CSS contents can be turned off via the `?inline` query parameter. In this case, the processed CSS string is returned as the module's default export as usual, but the styles aren't injected to the page. -```js +```js twoslash +import 'vite/client' +// ---cut--- import './foo.css' // will be injected into the page import otherStyles from './bar.css?inline' // will not be injected ``` @@ -305,29 +311,39 @@ By default, Vite uses esbuild to minify CSS. Lightning CSS can also be used as t Importing a static asset will return the resolved public URL when it is served: -```js +```js twoslash +import 'vite/client' +// ---cut--- import imgUrl from './img.png' document.getElementById('hero-img').src = imgUrl ``` Special queries can modify how assets are loaded: -```js +```js twoslash +import 'vite/client' +// ---cut--- // Explicitly load assets as URL import assetAsURL from './asset.js?url' ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // Load assets as strings import assetAsString from './shader.glsl?raw' ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // Load Web Workers import Worker from './worker.js?worker' ``` -```js +```js twoslash +import 'vite/client' +// ---cut--- // Web Workers inlined as base64 strings at build time import InlineWorker from './worker.js?worker&inline' ``` @@ -338,7 +354,9 @@ More details in [Static Asset Handling](./assets). JSON files can be directly imported - named imports are also supported: -```js +```js twoslash +import 'vite/client' +// ---cut--- // import the entire object import json from './example.json' // import a root field as named exports - helps with tree-shaking! @@ -349,7 +367,9 @@ import { field } from './example.json' Vite supports importing multiple modules from the file system via the special `import.meta.glob` function: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js') ``` @@ -375,7 +395,9 @@ for (const path in modules) { Matched files are by default lazy-loaded via dynamic import and will be split into separate chunks during build. If you'd rather import all the modules directly (e.g. relying on side-effects in these modules to be applied first), you can pass `{ eager: true }` as the second argument: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { eager: true }) ``` @@ -395,7 +417,9 @@ const modules = { The first argument can be an array of globs, for example -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob(['./dir/*.js', './another/*.js']) ``` @@ -403,7 +427,9 @@ const modules = import.meta.glob(['./dir/*.js', './another/*.js']) Negative glob patterns are also supported (prefixed with `!`). To ignore some files from the result, you can add exclude glob patterns to the first argument: -```js +```js twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob(['./dir/*.js', '!**/bar.js']) ``` @@ -418,7 +444,9 @@ const modules = { It's possible to only import parts of the modules with the `import` options. -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { import: 'setup' }) ``` @@ -432,7 +460,9 @@ const modules = { When combined with `eager` it's even possible to have tree-shaking enabled for those modules. -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { import: 'setup', eager: true, @@ -451,7 +481,9 @@ const modules = { Set `import` to `default` to import the default export. -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { import: 'default', eager: true, @@ -472,7 +504,9 @@ const modules = { You can also use the `query` option to provide queries to imports, for example, to import assets [as a string](https://vitejs.dev/guide/assets.html#importing-asset-as-string) or [as a url](https://vitejs.dev/guide/assets.html#importing-asset-as-url): -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const moduleStrings = import.meta.glob('./dir/*.svg', { query: '?raw', import: 'default', @@ -497,7 +531,9 @@ const moduleUrls = { You can also provide custom queries for other plugins to consume: -```ts +```ts twoslash +import 'vite/client' +// ---cut--- const modules = import.meta.glob('./dir/*.js', { query: { foo: 'bar', bar: true }, }) @@ -527,7 +563,9 @@ Note that variables only represent file names one level deep. If `file` is `'foo Pre-compiled `.wasm` files can be imported with `?init`. The default export will be an initialization function that returns a Promise of the [`WebAssembly.Instance`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance): -```js +```js twoslash +import 'vite/client' +// ---cut--- import init from './example.wasm?init' init().then((instance) => { @@ -537,7 +575,10 @@ init().then((instance) => { The init function can also take an importObject which is passed along to [`WebAssembly.instantiate`](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiate) as its second argument: -```js +```js twoslash +import 'vite/client' +import init from './example.wasm?init' +// ---cut--- init({ imports: { someFunc: () => { @@ -560,7 +601,9 @@ Use [`vite-plugin-wasm`](https://github.com/Menci/vite-plugin-wasm) or other com If you need access to the `Module` object, e.g. to instantiate it multiple times, use an [explicit URL import](./assets#explicit-url-imports) to resolve the asset, and then perform the instantiation: -```js +```js twoslash +import 'vite/client' +// ---cut--- import wasmUrl from 'foo.wasm?url' const main = async () => { @@ -580,7 +623,9 @@ See the issue [Support wasm in SSR](https://github.com/vitejs/vite/issues/8882). Here is an alternative, assuming the project base is the current directory: -```js +```js twoslash +import 'vite/client' +// ---cut--- import wasmUrl from 'foo.wasm?url' import { readFile } from 'node:fs/promises' @@ -620,7 +665,9 @@ The worker detection will only work if the `new URL()` constructor is used direc A web worker script can be directly imported by appending `?worker` or `?sharedworker` to the import request. The default export will be a custom worker constructor: -```js +```js twoslash +import 'vite/client' +// ---cut--- import MyWorker from './worker?worker' const worker = new MyWorker() @@ -630,18 +677,44 @@ The worker script can also use ESM `import` statements instead of `importScripts By default, the worker script will be emitted as a separate chunk in the production build. If you wish to inline the worker as base64 strings, add the `inline` query: -```js +```js twoslash +import 'vite/client' +// ---cut--- import MyWorker from './worker?worker&inline' ``` If you wish to retrieve the worker as a URL, add the `url` query: -```js +```js twoslash +import 'vite/client' +// ---cut--- import MyWorker from './worker?worker&url' ``` See [Worker Options](/config/worker-options.md) for details on configuring the bundling of all workers. +## Content Security Policy (CSP) + +To deploy CSP, certain directives or configs must be set due to Vite's internals. + +### [`'nonce-{RANDOM}'`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources#nonce-base64-value) + +When [`html.cspNonce`](/config/shared-options#html-cspnonce) is set, Vite adds a nonce attribute with the specified value to any ` diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000000000..d3a9f8747d3ba5 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,17 @@ +{ + "name": "docs", + "private": true, + "type": "module", + "scripts": { + "docs": "vitepress dev", + "docs-build": "vitepress build", + "docs-serve": "vitepress serve" + }, + "devDependencies": { + "@shikijs/vitepress-twoslash": "^1.6.0", + "@types/express": "^4.17.21", + "feed": "^4.2.2", + "vitepress": "1.2.2", + "vue": "^3.4.27" + } +} diff --git a/docs/public/logo-uwu.png b/docs/public/logo-uwu.png new file mode 100644 index 00000000000000..e45e40af12a3e2 Binary files /dev/null and b/docs/public/logo-uwu.png differ diff --git a/docs/public/transloadit-dark.svg b/docs/public/transloadit-dark.svg new file mode 100644 index 00000000000000..f402f7a97d4d28 --- /dev/null +++ b/docs/public/transloadit-dark.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000000000..5784d7484af2df --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,289 @@ +// @ts-check +import { builtinModules, createRequire } from 'node:module' +import eslint from '@eslint/js' +import pluginN from 'eslint-plugin-n' +import * as pluginI from 'eslint-plugin-i' +import pluginRegExp from 'eslint-plugin-regexp' +import tsParser from '@typescript-eslint/parser' +import tseslint from 'typescript-eslint' +import globals from 'globals' + +const require = createRequire(import.meta.url) +const pkg = require('./package.json') +const pkgVite = require('./packages/vite/package.json') + +export default tseslint.config( + { + ignores: [ + 'packages/create-vite/template-*', + '**/dist/**', + '**/fixtures/**', + '**/playground-temp/**', + '**/temp/**', + '**/.vitepress/cache/**', + '**/*.snap', + ], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.stylistic, + /** @type {any} */ (pluginRegExp.configs['flat/recommended']), + { + name: 'main', + languageOptions: { + parser: tsParser, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2022, + }, + globals: { + ...globals.es2021, + ...globals.node, + }, + }, + plugins: { + n: pluginN, + i: pluginI, + }, + rules: { + 'n/no-exports-assign': 'error', + 'n/no-unpublished-bin': 'error', + 'n/no-unsupported-features/es-builtins': 'error', + 'n/no-unsupported-features/node-builtins': 'error', + 'n/process-exit-as-throw': 'error', + 'n/hashbang': 'error', + + eqeqeq: ['warn', 'always', { null: 'never' }], + 'no-debugger': ['error'], + 'no-empty': ['warn', { allowEmptyCatch: true }], + 'no-process-exit': 'off', + 'no-useless-escape': 'off', + 'prefer-const': [ + 'warn', + { + destructuring: 'all', + }, + ], + + 'n/no-missing-require': [ + 'error', + { + // for try-catching yarn pnp + allowModules: ['pnpapi', 'vite'], + tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], + }, + ], + 'n/no-extraneous-import': [ + 'error', + { + allowModules: ['vite', 'less', 'sass', 'vitest', 'unbuild'], + }, + ], + 'n/no-extraneous-require': [ + 'error', + { + allowModules: ['vite'], + }, + ], + + '@typescript-eslint/ban-ts-comment': 'error', + '@typescript-eslint/ban-types': 'off', // TODO: we should turn this on in a new PR + '@typescript-eslint/explicit-module-boundary-types': [ + 'error', + { allowArgumentsExplicitlyTypedAsAny: true }, + ], + '@typescript-eslint/no-empty-function': [ + 'error', + { allow: ['arrowFunctions'] }, + ], + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', // maybe we should turn this on in a new PR + 'no-extra-semi': 'off', + '@typescript-eslint/no-extra-semi': 'off', // conflicts with prettier + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-unused-vars': 'off', // maybe we should turn this on in a new PR + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', disallowTypeAnnotations: false }, + ], + // disable rules set in @typescript-eslint/stylistic v6 that wasn't set in @typescript-eslint/recommended v5 and which conflict with current code + // maybe we should turn them on in a new PR + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/ban-tslint-comment': 'off', + '@typescript-eslint/consistent-generic-constructors': 'off', + '@typescript-eslint/consistent-indexed-object-style': 'off', + '@typescript-eslint/consistent-type-definitions': 'off', + '@typescript-eslint/prefer-for-of': 'off', + '@typescript-eslint/prefer-function-type': 'off', + + 'i/no-nodejs-modules': [ + 'error', + { allow: builtinModules.map((mod) => `node:${mod}`) }, + ], + 'i/no-duplicates': 'error', + 'i/order': 'error', + 'sort-imports': [ + 'error', + { + ignoreCase: false, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + allowSeparatedGroups: false, + }, + ], + + 'regexp/no-contradiction-with-assertion': 'error', + // in some cases using explicit letter-casing is more performant than the `i` flag + 'regexp/use-ignore-case': 'off', + }, + }, + { + name: 'vite/globals', + files: ['packages/**/*.?([cm])[jt]s?(x)'], + ignores: ['**/__tests__/**'], + rules: { + 'no-restricted-globals': ['error', 'require', '__dirname', '__filename'], + }, + }, + { + name: 'vite/node', + files: ['packages/vite/src/node/**/*.?([cm])[jt]s?(x)'], + rules: { + 'no-console': ['error'], + 'n/no-restricted-require': [ + 'error', + Object.keys(pkgVite.devDependencies).map((d) => ({ + name: d, + message: + `devDependencies can only be imported using ESM syntax so ` + + `that they are included in the rollup bundle. If you are trying to ` + + `lazy load a dependency, use (await import('dependency')).default instead.`, + })), + ], + }, + }, + { + name: 'playground/enforce-esm', + files: ['playground/**/*.?([cm])[jt]s?(x)'], + ignores: [ + 'playground/ssr-resolve/**', + 'playground/**/*{commonjs,cjs}*/**', + 'playground/**/*{commonjs,cjs}*', + 'playground/**/*dep*/**', + 'playground/resolve/browser-module-field2/index.web.js', + 'playground/resolve/browser-field/**', + 'playground/tailwind/**', // blocked by https://github.com/postcss/postcss-load-config/issues/239 + ], + rules: { + 'i/no-commonjs': 'error', + }, + }, + { + name: 'playground/test', + files: ['playground/**/__tests__/**/*.?([cm])[jt]s?(x)'], + rules: { + // engine field doesn't exist in playgrounds + 'n/no-unsupported-features/es-builtins': [ + 'error', + { + version: pkg.engines.node, + }, + ], + 'n/no-unsupported-features/node-builtins': [ + 'error', + { + version: pkg.engines.node, + // ideally we would like to allow all experimental features + // https://github.com/eslint-community/eslint-plugin-n/issues/199 + ignores: ['fetch'], + }, + ], + }, + }, + + { + name: 'disables/vite/client', + files: ['packages/vite/src/client/**/*.?([cm])[jt]s?(x)'], + ignores: ['**/__tests__/**'], + rules: { + 'n/no-unsupported-features/node-builtins': 'off', + }, + }, + { + name: 'disables/vite/types', + files: [ + 'packages/vite/src/types/**/*.?([cm])[jt]s?(x)', + 'packages/vite/scripts/**/*.?([cm])[jt]s?(x)', + '**/*.spec.ts', + ], + rules: { + 'n/no-extraneous-import': 'off', + }, + }, + { + name: 'disables/create-vite/templates', + files: [ + 'packages/create-vite/template-*/**/*.?([cm])[jt]s?(x)', + '**/build.config.ts', + ], + rules: { + 'no-undef': 'off', + 'n/no-missing-import': 'off', + 'n/no-extraneous-import': 'off', + 'n/no-extraneous-require': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, + }, + { + name: 'disables/playground', + files: ['playground/**/*.?([cm])[jt]s?(x)', 'docs/**/*.?([cm])[jt]s?(x)'], + rules: { + 'n/no-extraneous-import': 'off', + 'n/no-extraneous-require': 'off', + 'n/no-missing-import': 'off', + 'n/no-missing-require': 'off', + 'n/no-unsupported-features/es-builtins': 'off', + 'n/no-unsupported-features/node-builtins': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'no-undef': 'off', + 'no-empty': 'off', + 'no-constant-condition': 'off', + '@typescript-eslint/no-empty-function': 'off', + }, + }, + { + name: 'disables/playground/tsconfig-json', + files: [ + 'playground/tsconfig-json/**/*.?([cm])[jt]s?(x)', + 'playground/tsconfig-json-load-error/**/*.?([cm])[jt]s?(x)', + ], + ignores: ['**/__tests__/**'], + rules: { + '@typescript-eslint/ban-ts-comment': 'off', + }, + }, + { + name: 'disables/js', + files: ['**/*.js', '**/*.mjs', '**/*.cjs'], + rules: { + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, + }, + { + name: 'disables/dts', + files: ['**/*.d.ts'], + rules: { + '@typescript-eslint/triple-slash-reference': 'off', + }, + }, + { + name: 'disables/test', + files: ['**/__tests__/**/*.?([cm])[jt]s?(x)'], + rules: { + 'no-console': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + }, + }, +) diff --git a/package.json b/package.json index e18cfd57fb1946..859284831cdf8a 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,9 @@ "test-docs": "pnpm run docs-build", "debug-serve": "VITE_DEBUG_SERVE=1 vitest run -c vitest.config.e2e.ts", "debug-build": "VITE_TEST_BUILD=1 VITE_PRESERVE_BUILD_ARTIFACTS=1 vitest run -c vitest.config.e2e.ts", - "docs": "vitepress dev docs", - "docs-build": "vitepress build docs", - "docs-serve": "vitepress serve docs", + "docs": "pnpm --filter=docs run docs", + "docs-build": "pnpm --filter=docs run docs-build", + "docs-serve": "pnpm --filter=docs run docs-serve", "build": "pnpm -r --filter='./packages/*' run build", "dev": "pnpm -r --parallel --filter='./packages/*' run dev", "release": "tsx scripts/release.ts", @@ -40,8 +40,7 @@ "ci-docs": "run-s build docs-build" }, "devDependencies": { - "@babel/types": "^7.23.9", - "@eslint-types/typescript-eslint": "^6.21.0", + "@eslint/js": "^9.3.0", "@rollup/plugin-typescript": "^11.1.6", "@types/babel__core": "^7.20.5", "@types/babel__preset-env": "^7.9.6", @@ -52,42 +51,36 @@ "@types/etag": "^1.8.3", "@types/fs-extra": "^11.0.4", "@types/less": "^3.0.6", - "@types/micromatch": "^4.0.6", - "@types/node": "^20.11.20", + "@types/micromatch": "^4.0.7", + "@types/node": "^20.12.12", "@types/picomatch": "^2.3.3", - "@types/sass": "~1.43.1", - "@types/semver": "^7.5.8", "@types/stylus": "^0.48.42", "@types/ws": "^8.5.10", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0", "@vitejs/release-scripts": "^1.3.1", - "conventional-changelog-cli": "^4.1.0", + "conventional-changelog-cli": "^5.0.0", "eslint": "^8.57.0", - "eslint-define-config": "^2.1.0", "eslint-plugin-i": "^2.29.1", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-regexp": "^2.2.0", - "execa": "^8.0.1", - "feed": "^4.2.2", + "eslint-plugin-n": "^17.7.0", + "eslint-plugin-regexp": "^2.6.0", + "execa": "^9.1.0", "fs-extra": "^11.2.0", - "lint-staged": "^15.2.2", - "npm-run-all2": "^6.1.2", - "picocolors": "^1.0.0", - "playwright-chromium": "^1.41.2", + "globals": "^15.3.0", + "lint-staged": "^15.2.5", + "npm-run-all2": "^6.2.0", + "picocolors": "^1.0.1", + "playwright-chromium": "^1.44.1", "prettier": "3.2.5", - "rimraf": "^5.0.5", - "rollup": "^4.2.0", - "semver": "^7.6.0", - "simple-git-hooks": "^2.9.0", + "rimraf": "^5.0.7", + "rollup": "^4.13.0", + "simple-git-hooks": "^2.11.1", "tslib": "^2.6.2", - "tsx": "^4.7.1", + "tsx": "^4.11.0", "typescript": "^5.2.2", - "unbuild": "^2.0.0", + "typescript-eslint": "^7.10.0", "vite": "workspace:*", - "vitepress": "1.0.0-rc.44", - "vitest": "^1.3.1", - "vue": "^3.4.20" + "vitest": "^1.6.0" }, "simple-git-hooks": { "pre-commit": "pnpm exec lint-staged --concurrent false" @@ -106,27 +99,16 @@ "eslint --cache --fix" ] }, - "packageManager": "pnpm@8.15.4", + "packageManager": "pnpm@8.15.8", "pnpm": { "overrides": { "vite": "workspace:*" }, - "packageExtensions": { - "acorn-walk": { - "peerDependencies": { - "acorn": "*" - }, - "peerDependenciesMeta": { - "acorn": { - "optional": true - } - } - } - }, "patchedDependencies": { + "acorn@8.11.3": "patches/acorn@8.11.3.patch", "chokidar@3.6.0": "patches/chokidar@3.6.0.patch", - "sirv@2.0.4": "patches/sirv@2.0.4.patch", - "postcss-import@16.0.1": "patches/postcss-import@16.0.1.patch" + "http-proxy@1.18.1": "patches/http-proxy@1.18.1.patch", + "sirv@2.0.4": "patches/sirv@2.0.4.patch" }, "peerDependencyRules": { "allowedVersions": { diff --git a/packages/create-vite/CHANGELOG.md b/packages/create-vite/CHANGELOG.md index ecc6f7f7e2a9fb..5a0ecf70c22ce2 100644 --- a/packages/create-vite/CHANGELOG.md +++ b/packages/create-vite/CHANGELOG.md @@ -1,3 +1,21 @@ +## 5.2.3 (2024-03-20) + +* docs: update volar name and remove takeover mode related docs (#16171) ([0a56177](https://github.com/vitejs/vite/commit/0a56177)), closes [#16171](https://github.com/vitejs/vite/issues/16171) +* fix(create-vite): remove vue3 deprecated plugin (TypeScript Vue Plugin) (#16158) ([1645fc0](https://github.com/vitejs/vite/commit/1645fc0)), closes [#16158](https://github.com/vitejs/vite/issues/16158) +* fix(create-vite): switch to default Remix template (#16203) ([ea480df](https://github.com/vitejs/vite/commit/ea480df)), closes [#16203](https://github.com/vitejs/vite/issues/16203) +* chore(deps): update all non-major dependencies (#16186) ([842643d](https://github.com/vitejs/vite/commit/842643d)), closes [#16186](https://github.com/vitejs/vite/issues/16186) +* chore(deps): update dependency vue-tsc to v2 (#16187) ([72104f6](https://github.com/vitejs/vite/commit/72104f6)), closes [#16187](https://github.com/vitejs/vite/issues/16187) + + + +## 5.2.2 (2024-03-11) + +* chore(deps): update all non-major dependencies (#16028) ([7cfe80d](https://github.com/vitejs/vite/commit/7cfe80d)), closes [#16028](https://github.com/vitejs/vite/issues/16028) +* chore(deps): update all non-major dependencies (#16131) ([a862ecb](https://github.com/vitejs/vite/commit/a862ecb)), closes [#16131](https://github.com/vitejs/vite/issues/16131) +* fix(create-vite): ts error in the svelte-ts template (#16031) ([ff4c834](https://github.com/vitejs/vite/commit/ff4c834)), closes [#16031](https://github.com/vitejs/vite/issues/16031) + + + ## 5.2.1 (2024-02-21) * fix(create-vite): remove tsc command from qwik template (#15982) ([5e05f10](https://github.com/vitejs/vite/commit/5e05f10)), closes [#15982](https://github.com/vitejs/vite/issues/15982) diff --git a/packages/create-vite/__tests__/cli.spec.ts b/packages/create-vite/__tests__/cli.spec.ts index e34d4e14f6e4cf..4f297032834fcf 100644 --- a/packages/create-vite/__tests__/cli.spec.ts +++ b/packages/create-vite/__tests__/cli.spec.ts @@ -1,5 +1,5 @@ import { join } from 'node:path' -import type { ExecaSyncReturnValue, SyncOptions } from 'execa' +import type { SyncOptions, SyncResult } from 'execa' import { execaCommandSync } from 'execa' import fs from 'fs-extra' import { afterEach, beforeAll, expect, test } from 'vitest' @@ -9,10 +9,10 @@ const CLI_PATH = join(__dirname, '..') const projectName = 'test-app' const genPath = join(__dirname, projectName) -const run = ( +const run = ( args: string[], - options: SyncOptions = {}, -): ExecaSyncReturnValue => { + options?: SO, +): SyncResult => { return execaCommandSync(`node ${CLI_PATH} ${args.join(' ')}`, options) } diff --git a/packages/create-vite/package.json b/packages/create-vite/package.json index e1283e72221e59..1dbf426b84c933 100644 --- a/packages/create-vite/package.json +++ b/packages/create-vite/package.json @@ -1,6 +1,6 @@ { "name": "create-vite", - "version": "5.2.1", + "version": "5.2.3", "type": "module", "license": "MIT", "author": "Evan You", diff --git a/packages/create-vite/src/index.ts b/packages/create-vite/src/index.ts index 71d04b00f44846..ea22e52c2384a7 100755 --- a/packages/create-vite/src/index.ts +++ b/packages/create-vite/src/index.ts @@ -115,8 +115,7 @@ const FRAMEWORKS: Framework[] = [ name: 'custom-remix', display: 'Remix ↗', color: cyan, - customCommand: - 'npm create remix@latest TARGET_DIR -- --template remix-run/remix/templates/vite', + customCommand: 'npm create remix@latest TARGET_DIR', }, ], }, diff --git a/packages/create-vite/template-lit-ts/package.json b/packages/create-vite/template-lit-ts/package.json index 73d7cf4d3004b4..742f963ad98273 100644 --- a/packages/create-vite/template-lit-ts/package.json +++ b/packages/create-vite/template-lit-ts/package.json @@ -9,10 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "lit": "^3.1.2" + "lit": "^3.1.3" }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-lit/package.json b/packages/create-vite/template-lit/package.json index 4c4f081dab9528..b34c893b53a7e1 100644 --- a/packages/create-vite/template-lit/package.json +++ b/packages/create-vite/template-lit/package.json @@ -9,9 +9,9 @@ "preview": "vite preview" }, "dependencies": { - "lit": "^3.1.2" + "lit": "^3.1.3" }, "devDependencies": { - "vite": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-preact-ts/package.json b/packages/create-vite/template-preact-ts/package.json index 4f77e8324e2e27..1504fe9f117963 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.19.6" + "preact": "^10.22.0" }, "devDependencies": { - "@preact/preset-vite": "^2.8.1", + "@preact/preset-vite": "^2.8.2", "typescript": "^5.2.2", - "vite": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-preact/package.json b/packages/create-vite/template-preact/package.json index ecc5132ca83917..bffd5f9ced7c9b 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.19.6" + "preact": "^10.22.0" }, "devDependencies": { - "@preact/preset-vite": "^2.8.1", - "vite": "^5.1.4" + "@preact/preset-vite": "^2.8.2", + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-qwik-ts/package.json b/packages/create-vite/template-qwik-ts/package.json index d1bd9cab390278..e5dc88e364a60c 100644 --- a/packages/create-vite/template-qwik-ts/package.json +++ b/packages/create-vite/template-qwik-ts/package.json @@ -9,11 +9,11 @@ "preview": "serve dist" }, "devDependencies": { - "serve": "^14.2.1", + "serve": "^14.2.3", "typescript": "^5.2.2", - "vite": "^5.1.4" + "vite": "^5.2.11" }, "dependencies": { - "@builder.io/qwik": "^1.4.5" + "@builder.io/qwik": "^1.5.5" } } diff --git a/packages/create-vite/template-qwik/package.json b/packages/create-vite/template-qwik/package.json index 8e6af06161047b..0c56cdc67fc64b 100644 --- a/packages/create-vite/template-qwik/package.json +++ b/packages/create-vite/template-qwik/package.json @@ -9,10 +9,10 @@ "preview": "serve dist" }, "devDependencies": { - "serve": "^14.2.1", - "vite": "^5.1.4" + "serve": "^14.2.3", + "vite": "^5.2.11" }, "dependencies": { - "@builder.io/qwik": "^1.4.5" + "@builder.io/qwik": "^1.5.5" } } diff --git a/packages/create-vite/template-react-ts/package.json b/packages/create-vite/template-react-ts/package.json index 84c1a03751579d..d75dcb00487435 100644 --- a/packages/create-vite/template-react-ts/package.json +++ b/packages/create-vite/template-react-ts/package.json @@ -10,19 +10,19 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@types/react": "^18.2.59", - "@types/react-dom": "^18.2.19", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", - "@vitejs/plugin-react": "^4.2.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.10.0", + "@typescript-eslint/parser": "^7.10.0", + "@vitejs/plugin-react": "^4.3.0", "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", "typescript": "^5.2.2", - "vite": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-react/package.json b/packages/create-vite/template-react/package.json index c735ebc40a4c09..9bdf8bd900e120 100644 --- a/packages/create-vite/template-react/package.json +++ b/packages/create-vite/template-react/package.json @@ -10,17 +10,17 @@ "preview": "vite preview" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "devDependencies": { - "@types/react": "^18.2.59", - "@types/react-dom": "^18.2.19", - "@vitejs/plugin-react": "^4.2.1", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", "eslint": "^8.57.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "vite": "^5.1.4" + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-solid-ts/package.json b/packages/create-vite/template-solid-ts/package.json index 5e926eb2b57db6..e77b944d91791c 100644 --- a/packages/create-vite/template-solid-ts/package.json +++ b/packages/create-vite/template-solid-ts/package.json @@ -9,11 +9,11 @@ "preview": "vite preview" }, "dependencies": { - "solid-js": "^1.8.15" + "solid-js": "^1.8.17" }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^5.1.4", - "vite-plugin-solid": "^2.10.1" + "vite": "^5.2.11", + "vite-plugin-solid": "^2.10.2" } } diff --git a/packages/create-vite/template-solid/package.json b/packages/create-vite/template-solid/package.json index d0808064d09df8..7f907499422cc5 100644 --- a/packages/create-vite/template-solid/package.json +++ b/packages/create-vite/template-solid/package.json @@ -9,10 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "solid-js": "^1.8.15" + "solid-js": "^1.8.17" }, "devDependencies": { - "vite": "^5.1.4", - "vite-plugin-solid": "^2.10.1" + "vite": "^5.2.11", + "vite-plugin-solid": "^2.10.2" } } diff --git a/packages/create-vite/template-svelte-ts/package.json b/packages/create-vite/template-svelte-ts/package.json index a16441f61c494b..4735ebc10b0b12 100644 --- a/packages/create-vite/template-svelte-ts/package.json +++ b/packages/create-vite/template-svelte-ts/package.json @@ -10,12 +10,12 @@ "check": "svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@tsconfig/svelte": "^5.0.2", - "svelte": "^4.2.12", - "svelte-check": "^3.6.4", + "@sveltejs/vite-plugin-svelte": "^3.1.0", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^4.2.17", + "svelte-check": "^3.7.1", "tslib": "^2.6.2", "typescript": "^5.2.2", - "vite": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-svelte/package.json b/packages/create-vite/template-svelte/package.json index f7069e833a5bed..ea88ea72a28e09 100644 --- a/packages/create-vite/template-svelte/package.json +++ b/packages/create-vite/template-svelte/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.2", - "svelte": "^4.2.12", - "vite": "^5.1.4" + "@sveltejs/vite-plugin-svelte": "^3.1.0", + "svelte": "^4.2.17", + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-vanilla-ts/package.json b/packages/create-vite/template-vanilla-ts/package.json index 12438737577138..8b534d057c0b25 100644 --- a/packages/create-vite/template-vanilla-ts/package.json +++ b/packages/create-vite/template-vanilla-ts/package.json @@ -10,6 +10,6 @@ }, "devDependencies": { "typescript": "^5.2.2", - "vite": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-vanilla/package.json b/packages/create-vite/template-vanilla/package.json index 8abe7d4ed01de6..40b1479981b3e7 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": "^5.1.4" + "vite": "^5.2.11" } } diff --git a/packages/create-vite/template-vue-ts/.vscode/extensions.json b/packages/create-vite/template-vue-ts/.vscode/extensions.json index c0a6e5a48110e4..a7cea0b0678120 100644 --- a/packages/create-vite/template-vue-ts/.vscode/extensions.json +++ b/packages/create-vite/template-vue-ts/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] + "recommendations": ["Vue.volar"] } diff --git a/packages/create-vite/template-vue-ts/README.md b/packages/create-vite/template-vue-ts/README.md index ef72fd52424558..33895ab2002862 100644 --- a/packages/create-vite/template-vue-ts/README.md +++ b/packages/create-vite/template-vue-ts/README.md @@ -2,17 +2,4 @@ This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` asset for (const { start, end, url } of scriptUrls) { if (checkPublicFile(url, config)) { - s.update(start, end, toOutputPublicFilePath(url)) + s.update( + start, + end, + partialEncodeURIPath(toOutputPublicFilePath(url)), + ) } else if (!isExcludedUrl(url)) { - s.update(start, end, await urlToBuiltUrl(url, id, config, this)) + s.update( + start, + end, + partialEncodeURIPath(await urlToBuiltUrl(url, id, config, this)), + ) } } @@ -762,11 +775,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { return tags } - for (const [id, html] of processedHtml) { - const relativeUrlPath = path.posix.relative( - config.root, - normalizePath(id), - ) + for (const [normalizedId, html] of processedHtml) { + const relativeUrlPath = path.posix.relative(config.root, normalizedId) const assetsBase = getBaseInHTML(relativeUrlPath, config) const toOutputFilePath = ( filename: string, @@ -792,7 +802,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { const toOutputPublicAssetFilePath = (filename: string) => toOutputFilePath(filename, 'public') - const isAsync = isAsyncScriptMap.get(config)!.get(id)! + const isAsync = isAsyncScriptMap.get(config)!.get(normalizedId)! let result = html @@ -801,7 +811,8 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { (chunk) => chunk.type === 'chunk' && chunk.isEntry && - chunk.facadeModuleId === id, + chunk.facadeModuleId && + normalizePath(chunk.facadeModuleId) === normalizedId, ) as OutputChunk | undefined let canInlineEntry = false @@ -886,7 +897,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { [...normalHooks, ...postHooks], { path: '/' + relativeUrlPath, - filename: id, + filename: normalizedId, bundle, chunk, }, @@ -897,7 +908,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { if (chunk) { chunk.viteMetadata!.importedAssets.add(cleanUrl(file)) } - return toOutputAssetFilePath(file) + postfix + return encodeURIPath(toOutputAssetFilePath(file)) + postfix }) result = result.replace(publicAssetUrlRE, (_, fileHash) => { @@ -905,16 +916,20 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { getPublicAssetFilename(fileHash, config)!, ) - return urlCanParse(publicAssetPath) - ? publicAssetPath - : normalizePath(publicAssetPath) + return encodeURIPath( + urlCanParse(publicAssetPath) + ? publicAssetPath + : normalizePath(publicAssetPath), + ) }) if (chunk && canInlineEntry) { inlineEntryChunk.add(chunk.fileName) } - const shortEmitName = normalizePath(path.relative(config.root, id)) + const shortEmitName = normalizePath( + path.relative(config.root, normalizedId), + ) this.emitFile({ type: 'asset', fileName: shortEmitName, @@ -930,6 +945,10 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { } } +export function parseRelAttr(attr: string): string[] { + return attr.split(spaceRe).map((v) => v.toLowerCase()) +} + // // extract inline styles as virtual css export function findNeedTransformStyleAttribute( @@ -1079,6 +1098,24 @@ export function postImportMapHook(): IndexHtmlTransformHook { } } +export function injectCspNonceMetaTagHook( + config: ResolvedConfig, +): IndexHtmlTransformHook { + return () => { + if (!config.html?.cspNonce) return + + return [ + { + tag: 'meta', + injectTo: 'head', + // use nonce attribute so that it's hidden + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce#accessing_nonces_and_nonce_hiding + attrs: { property: 'csp-nonce', nonce: config.html.cspNonce }, + }, + ] + } +} + /** * Support `%ENV_NAME%` syntax in html files */ @@ -1128,6 +1165,53 @@ export function htmlEnvHook(config: ResolvedConfig): IndexHtmlTransformHook { } } +export function injectNonceAttributeTagHook( + config: ResolvedConfig, +): IndexHtmlTransformHook { + const processRelType = new Set(['stylesheet', 'modulepreload', 'preload']) + + return async (html, { filename }) => { + const nonce = config.html?.cspNonce + if (!nonce) return + + const s = new MagicString(html) + + await traverseHtml(html, filename, (node) => { + if (!nodeIsElement(node)) { + return + } + + const { nodeName, attrs, sourceCodeLocation } = node + + if ( + nodeName === 'script' || + nodeName === 'style' || + (nodeName === 'link' && + attrs.some( + (attr) => + attr.name === 'rel' && + parseRelAttr(attr.value).some((a) => processRelType.has(a)), + )) + ) { + // If we already have a nonce attribute, we don't need to add another one + if (attrs.some(({ name }) => name === 'nonce')) { + return + } + + const startTagEndOffset = sourceCodeLocation!.startTag!.endOffset + + // if the closing of the start tag includes a `/`, the offset should be 2 so the nonce + // is appended prior to the `/` + const appendOffset = html[startTagEndOffset - 2] === '/' ? 2 : 1 + + s.appendRight(startTagEndOffset - appendOffset, ` nonce="${nonce}"`) + } + }) + + return s.toString() + } +} + export function resolveHtmlTransforms( plugins: readonly Plugin[], logger: Logger, @@ -1182,6 +1266,42 @@ export function resolveHtmlTransforms( return [preHooks, normalHooks, postHooks] } +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head#see_also +const elementsAllowedInHead = new Set([ + 'title', + 'base', + 'link', + 'style', + 'meta', + 'script', + 'noscript', + 'template', +]) + +function headTagInsertCheck( + tags: HtmlTagDescriptor[], + ctx: IndexHtmlTransformContext, +) { + if (!tags.length) return + const { logger } = ctx.server?.config || {} + const disallowedTags = tags.filter( + (tagDescriptor) => !elementsAllowedInHead.has(tagDescriptor.tag), + ) + + if (disallowedTags.length) { + const dedupedTags = unique( + disallowedTags.map((tagDescriptor) => `<${tagDescriptor.tag}>`), + ) + logger?.warn( + colors.yellow( + colors.bold( + `[${dedupedTags.join(',')}] can not be used inside the Element, please check the 'injectTo' value`, + ), + ), + ) + } +} + export async function applyHtmlTransforms( html: string, hooks: IndexHtmlTransformHook[], @@ -1223,7 +1343,7 @@ export async function applyHtmlTransforms( ;(headPrependTags ??= []).push(tag) } } - + headTagInsertCheck([...(headTags || []), ...(headPrependTags || [])], ctx) if (headPrependTags) html = injectToHead(html, headPrependTags, true) if (headTags) html = injectToHead(html, headTags) if (bodyPrependTags) html = injectToBody(html, bodyPrependTags, true) diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index e5e3608f7c723c..754a800f0b55b2 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -8,9 +8,9 @@ import type { ImportSpecifier, } from 'es-module-lexer' import { init, parse as parseImports } from 'es-module-lexer' -import { parse as parseJS } from 'acorn' -import type { Node } from 'estree' -import { findStaticImports, parseStaticImport } from 'mlly' +import { parseAst } from 'rollup/parseAst' +import type { StaticImport } from 'mlly' +import { ESM_STATIC_IMPORT_RE, parseStaticImport } from 'mlly' import { makeLegalIdentifier } from '@rollup/pluginutils' import type { ViteDevServer } from '..' import { @@ -18,6 +18,7 @@ import { CLIENT_PUBLIC_PATH, DEP_VERSION_RE, FS_PREFIX, + SPECIAL_QUERY_RE, } from '../constants' import { debugHmr, @@ -62,11 +63,13 @@ import { withTrailingSlash, wrapId, } from '../../shared/utils' +import type { TransformPluginContext } from '../server/pluginContainer' import { throwOutdatedRequest } from './optimizedDeps' import { isCSSRequest, isDirectCSSRequest } from './css' import { browserExternalId } from './resolve' import { serializeDefine } from './define' import { WORKER_FILE_ID } from './worker' +import { getAliasPatternMatcher } from './preAlias' const debug = createDebugger('vite:import-analysis') @@ -116,11 +119,21 @@ function extractImportedBindings( } const exp = source.slice(importSpec.ss, importSpec.se) - const [match0] = findStaticImports(exp) - if (!match0) { + ESM_STATIC_IMPORT_RE.lastIndex = 0 + const match = ESM_STATIC_IMPORT_RE.exec(exp) + if (!match) { return } - const parsed = parseStaticImport(match0) + + const staticImport: StaticImport = { + type: 'static', + code: match[0], + start: match.index, + end: match.index + match[0].length, + imports: match.groups!.imports, + specifier: match.groups!.specifier, + } + const parsed = parseStaticImport(staticImport) if (!parsed) { return } @@ -171,6 +184,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const fsUtils = getFsUtils(config) const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH) const enablePartialAccept = config.experimental?.hmrPartialAccept + const matchAlias = getAliasPatternMatcher(config.resolve.alias) let server: ViteDevServer let _env: string | undefined @@ -250,7 +264,10 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { throwOutdatedRequest(importer) } - if (!imports.length && !(this as any)._addedImports) { + if ( + !imports.length && + !(this as unknown as TransformPluginContext)._addedImports + ) { importerModule.isSelfAccepting = false debug?.( `${timeFrom(msAtStart)} ${colors.dim( @@ -309,6 +326,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { } // fix#9534, prevent the importerModuleNode being stopped from propagating updates importerModule.isSelfAccepting = false + moduleGraph._hasResolveFailedErrorModules.add(importerModule) return this.error( `Failed to resolve import "${url}" from "${normalizePath( path.relative(process.cwd(), importerFile), @@ -486,7 +504,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { return } // skip ssr external - if (ssr) { + if (ssr && !matchAlias(specifier)) { if (shouldExternalizeForSSR(specifier, importer, config)) { return } @@ -740,11 +758,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { // update the module graph for HMR analysis. // node CSS imports does its own graph update in the css-analysis plugin so we // only handle js graph updates here. - if (!isCSSRequest(importer)) { + // note that we want to handle .css?raw and .css?url here + if (!isCSSRequest(importer) || SPECIAL_QUERY_RE.test(importer)) { // attached by pluginContainer.addWatchFile - const pluginImports = (this as any)._addedImports as - | Set - | undefined + const pluginImports = (this as unknown as TransformPluginContext) + ._addedImports if (pluginImports) { ;( await Promise.all( @@ -842,7 +860,7 @@ export function createParseErrorInfo( } } // prettier-ignore -const interopHelper = (m: any) => m?.__esModule ? m : { ...(typeof m === 'object' && !Array.isArray(m) ? m : {}), default: m } +const interopHelper = (m: any) => m?.__esModule ? m : { ...(typeof m === 'object' && !Array.isArray(m) || typeof m === 'function' ? m : {}), default: m } export function interopNamedImports( str: MagicString, @@ -926,12 +944,7 @@ export function transformCjsImport( importer: string, config: ResolvedConfig, ): string | undefined { - const node = ( - parseJS(importExp, { - ecmaVersion: 'latest', - sourceType: 'module', - }) as any - ).body[0] as Node + const node = parseAst(importExp).body[0] // `export * from '...'` may cause unexpected problem, so give it a warning if ( diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 8949d057f7d114..602466749b4cce 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -5,7 +5,7 @@ import type { ImportSpecifier, } from 'es-module-lexer' import { init, parse as parseImports } from 'es-module-lexer' -import type { OutputChunk, SourceMap } from 'rollup' +import type { SourceMap } from 'rollup' import type { RawSourceMap } from '@ampproject/remapping' import convertSourceMap from 'convert-source-map' import { @@ -37,7 +37,7 @@ export const preloadMarker = `__VITE_PRELOAD__` export const preloadBaseMarker = `__VITE_PRELOAD_BASE__` export const preloadHelperId = '\0vite/preload-helper.js' -const preloadMarkerWithQuote = new RegExp(`['"]${preloadMarker}['"]`, 'g') +const preloadMarkerRE = new RegExp(preloadMarker, 'g') const dynamicImportPrefixRE = /import\s*\(/ @@ -80,6 +80,13 @@ function preload( // @ts-expect-error __VITE_IS_MODERN__ will be replaced with boolean later if (__VITE_IS_MODERN__ && deps && deps.length > 0) { const links = document.getElementsByTagName('link') + const cspNonceMeta = document.querySelector( + 'meta[property=csp-nonce]', + ) + // `.nonce` should be used to get along with nonce hiding (https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce#accessing_nonces_and_nonce_hiding) + // Firefox 67-74 uses modern chunks and supports CSP nonce, but does not support `.nonce` + // in that case fallback to getAttribute + const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute('nonce') promise = Promise.all( deps.map((dep) => { @@ -116,6 +123,9 @@ function preload( link.crossOrigin = '' } link.href = dep + if (cspNonce) { + link.setAttribute('nonce', cspNonce) + } document.head.appendChild(link) if (isCss) { return new Promise((res, rej) => { @@ -225,6 +235,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { for (let index = 0; index < imports.length; index++) { const { + s: start, e: end, ss: expStart, se: expEnd, @@ -239,12 +250,19 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { str().remove(end + 1, expEnd) } - if (isDynamicImport && insertPreload) { + if ( + isDynamicImport && + insertPreload && + // Only preload static urls + (source[start] === '"' || + source[start] === "'" || + source[start] === '`') + ) { needPreloadHelper = true str().prependLeft(expStart, `${preloadMethod}(() => `) str().appendRight( expEnd, - `,${isModernFlag}?"${preloadMarker}":void 0${ + `,${isModernFlag}?${preloadMarker}:void 0${ optimizeModulePreloadRelativePaths || renderBuiltUrl ? ',import.meta.url' : '' @@ -371,15 +389,17 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { if (filename === ownerFilename) return if (analyzed.has(filename)) return analyzed.add(filename) - const chunk = bundle[filename] as OutputChunk | undefined + const chunk = bundle[filename] if (chunk) { deps.add(chunk.fileName) - chunk.imports.forEach(addDeps) - // Ensure that the css imported by current chunk is loaded after the dependencies. - // So the style of current chunk won't be overwritten unexpectedly. - chunk.viteMetadata!.importedCss.forEach((file) => { - deps.add(file) - }) + if (chunk.type === 'chunk') { + chunk.imports.forEach(addDeps) + // Ensure that the css imported by current chunk is loaded after the dependencies. + // So the style of current chunk won't be overwritten unexpectedly. + chunk.viteMetadata!.importedCss.forEach((file) => { + deps.add(file) + }) + } } else { const removedPureCssFiles = removedPureCssFilesCache.get(config)! @@ -401,15 +421,12 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { let markerStartPos = indexOfMatchInSlice( code, - preloadMarkerWithQuote, + preloadMarkerRE, end, ) // fix issue #3051 if (markerStartPos === -1 && imports.length === 1) { - markerStartPos = indexOfMatchInSlice( - code, - preloadMarkerWithQuote, - ) + markerStartPos = indexOfMatchInSlice(code, preloadMarkerRE) } if (markerStartPos > 0) { @@ -475,50 +492,48 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { s.update( markerStartPos, - markerStartPos + preloadMarker.length + 2, - `__vite__mapDeps([${renderedDeps.join(',')}])`, + markerStartPos + preloadMarker.length, + renderedDeps.length > 0 + ? `__vite__mapDeps([${renderedDeps.join(',')}])` + : `[]`, ) rewroteMarkerStartPos.add(markerStartPos) } } } - const fileDepsCode = `[${fileDeps - .map((fileDep) => - fileDep.runtime ? fileDep.url : JSON.stringify(fileDep.url), - ) - .join(',')}]` + if (fileDeps.length > 0) { + const fileDepsCode = `[${fileDeps + .map((fileDep) => + fileDep.runtime ? fileDep.url : JSON.stringify(fileDep.url), + ) + .join(',')}]` - const mapDepsCode = `\ -function __vite__mapDeps(indexes) { - if (!__vite__mapDeps.viteFileDeps) { - __vite__mapDeps.viteFileDeps = ${fileDepsCode} - } - return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) -}\n` - - // inject extra code at the top or next line of hashbang - if (code.startsWith('#!')) { - s.prependLeft(code.indexOf('\n') + 1, mapDepsCode) - } else { - s.prepend(mapDepsCode) + const mapDepsCode = `const __vite__fileDeps=${fileDepsCode},__vite__mapDeps=i=>i.map(i=>__vite__fileDeps[i]);\n` + + // inject extra code at the top or next line of hashbang + if (code.startsWith('#!')) { + s.prependLeft(code.indexOf('\n') + 1, mapDepsCode) + } else { + s.prepend(mapDepsCode) + } } // there may still be markers due to inlined dynamic imports, remove // all the markers regardless - let markerStartPos = indexOfMatchInSlice(code, preloadMarkerWithQuote) + let markerStartPos = indexOfMatchInSlice(code, preloadMarkerRE) while (markerStartPos >= 0) { if (!rewroteMarkerStartPos.has(markerStartPos)) { s.update( markerStartPos, - markerStartPos + preloadMarker.length + 2, + markerStartPos + preloadMarker.length, 'void 0', ) } markerStartPos = indexOfMatchInSlice( code, - preloadMarkerWithQuote, - markerStartPos + preloadMarker.length + 2, + preloadMarkerRE, + markerStartPos + preloadMarker.length, ) } diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 413c60f785a514..8f2475709003c0 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -4,22 +4,18 @@ import { stripLiteral } from 'strip-literal' import colors from 'picocolors' import type { ArrayExpression, - CallExpression, Expression, Literal, - MemberExpression, Node, - SequenceExpression, SpreadElement, TemplateLiteral, } from 'estree' -import { parseExpressionAt } from 'acorn' -import type { CustomPluginOptions, RollupError } from 'rollup' -import { findNodeAt } from 'acorn-walk' +import type { CustomPluginOptions, RollupAstNode, RollupError } from 'rollup' import MagicString from 'magic-string' import fg from 'fast-glob' import { stringifyQuery } from 'ufo' import type { GeneralImportGlobOptions } from 'types/importGlob' +import { parseAstAsync } from 'rollup/parseAst' import type { Plugin } from '../plugin' import type { ViteDevServer } from '../server' import type { ModuleNode } from '../server/moduleGraph' @@ -218,7 +214,7 @@ export async function parseImportGlob( resolveId: IdResolver, logger?: Logger, ): Promise { - let cleanCode + let cleanCode: string try { cleanCode = stripLiteral(code) } catch (e) { @@ -236,51 +232,30 @@ export async function parseImportGlob( return e } - let ast: CallExpression | SequenceExpression | MemberExpression - let lastTokenPos: number | undefined - - try { - ast = parseExpressionAt(code, start, { - ecmaVersion: 'latest', - sourceType: 'module', - ranges: true, - onToken: (token) => { - lastTokenPos = token.end - }, - }) as any - } catch (e) { - const _e = e as any - if (_e.message && _e.message.startsWith('Unterminated string constant')) - return undefined! - if (lastTokenPos == null || lastTokenPos <= start) throw _e - - // tailing comma in object or array will make the parser think it's a comma operation - // we try to parse again removing the comma - try { - const statement = code.slice(start, lastTokenPos).replace(/[,\s]*$/, '') - ast = parseExpressionAt( - ' '.repeat(start) + statement, // to keep the ast position - start, - { - ecmaVersion: 'latest', - sourceType: 'module', - ranges: true, - }, - ) as any - } catch { - throw _e - } + const end = + findCorrespondingCloseParenthesisPosition( + cleanCode, + start + match[0].length, + ) + 1 + if (end <= 0) { + throw err('Close parenthesis not found') } - const found = findNodeAt(ast as any, start, undefined, 'CallExpression') - if (!found) throw err(`Expect CallExpression, got ${ast.type}`) - ast = found.node as unknown as CallExpression + const statementCode = code.slice(start, end) + const rootAst = (await parseAstAsync(statementCode)).body[0] + if (rootAst.type !== 'ExpressionStatement') { + throw err(`Expect CallExpression, got ${rootAst.type}`) + } + const ast = rootAst.expression + if (ast.type !== 'CallExpression') { + throw err(`Expect CallExpression, got ${ast.type}`) + } if (ast.arguments.length < 1 || ast.arguments.length > 2) throw err(`Expected 1-2 arguments, but got ${ast.arguments.length}`) const arg1 = ast.arguments[0] as ArrayExpression | Literal | TemplateLiteral - const arg2 = ast.arguments[1] as Node | undefined + const arg2 = ast.arguments[1] as RollupAstNode | undefined const globs: string[] = [] @@ -321,14 +296,12 @@ export async function parseImportGlob( ) options = parseGlobOptions( - code.slice(arg2.range![0], arg2.range![1]), - arg2.range![0], + code.slice(start + arg2.start, start + arg2.end), + start + arg2.start, logger, ) } - const end = ast.range![1] - const globsResolved = await Promise.all( globs.map((glob) => toAbsoluteGlob(glob, root, importer, resolveId)), ) @@ -348,6 +321,34 @@ export async function parseImportGlob( return (await Promise.all(tasks)).filter(Boolean) } +function findCorrespondingCloseParenthesisPosition( + cleanCode: string, + openPos: number, +) { + const closePos = cleanCode.indexOf(')', openPos) + if (closePos < 0) return -1 + + if (!cleanCode.slice(openPos, closePos).includes('(')) return closePos + + let remainingParenthesisCount = 0 + const cleanCodeLen = cleanCode.length + for (let pos = openPos; pos < cleanCodeLen; pos++) { + switch (cleanCode[pos]) { + case '(': { + remainingParenthesisCount++ + break + } + case ')': { + remainingParenthesisCount-- + if (remainingParenthesisCount <= 0) { + return pos + } + } + } + } + return -1 +} + const importPrefix = '__vite_glob_' const { basename, dirname, relative, join } = posix diff --git a/packages/vite/src/node/plugins/manifest.ts b/packages/vite/src/node/plugins/manifest.ts index a82897ca6adb36..84602e78268866 100644 --- a/packages/vite/src/node/plugins/manifest.ts +++ b/packages/vite/src/node/plugins/manifest.ts @@ -21,6 +21,7 @@ export interface ManifestChunk { css?: string[] assets?: string[] isEntry?: boolean + name?: string isDynamicEntry?: boolean imports?: string[] dynamicImports?: string[] @@ -60,6 +61,7 @@ export function manifestPlugin(config: ResolvedConfig): Plugin { function createChunk(chunk: OutputChunk): ManifestChunk { const manifestChunk: ManifestChunk = { file: chunk.fileName, + name: chunk.name, } if (chunk.facadeModuleId) { diff --git a/packages/vite/src/node/plugins/optimizedDeps.ts b/packages/vite/src/node/plugins/optimizedDeps.ts index cdde20891b836b..6d6a8d22eb9468 100644 --- a/packages/vite/src/node/plugins/optimizedDeps.ts +++ b/packages/vite/src/node/plugins/optimizedDeps.ts @@ -10,6 +10,8 @@ import { cleanUrl } from '../../shared/utils' export const ERR_OPTIMIZE_DEPS_PROCESSING_ERROR = 'ERR_OPTIMIZE_DEPS_PROCESSING_ERROR' export const ERR_OUTDATED_OPTIMIZED_DEP = 'ERR_OUTDATED_OPTIMIZED_DEP' +export const ERR_FILE_NOT_FOUND_IN_OPTIMIZED_DEP_DIR = + 'ERR_FILE_NOT_FOUND_IN_OPTIMIZED_DEP_DIR' const debug = createDebugger('vite:optimize-deps') @@ -68,8 +70,12 @@ export function optimizedDepsPlugin(config: ResolvedConfig): Plugin { try { return await fsp.readFile(file, 'utf-8') } catch (e) { - // Outdated non-entry points (CHUNK), loaded after a rerun - throwOutdatedRequest(id) + const newMetadata = depsOptimizer.metadata + if (optimizedDepInfoFromFile(newMetadata, file)) { + // Outdated non-entry points (CHUNK), loaded after a rerun + throwOutdatedRequest(id) + } + throwFileNotFoundInOptimizedDep(id) } } }, @@ -97,3 +103,15 @@ export function throwOutdatedRequest(id: string): never { // send a 504 status code request timeout throw err } + +export function throwFileNotFoundInOptimizedDep(id: string): never { + const err: any = new Error( + `The file does not exist at "${id}" which is in the optimize deps directory. ` + + `The dependency might be incompatible with the dep optimizer. ` + + `Try adding it to \`optimizeDeps.exclude\`.`, + ) + err.code = ERR_FILE_NOT_FOUND_IN_OPTIMIZED_DEP_DIR + // This error will be caught by the transform middleware that will + // send a 404 status code not found + throw err +} diff --git a/packages/vite/src/node/plugins/preAlias.ts b/packages/vite/src/node/plugins/preAlias.ts index e88173061de122..eaefdb7e6eb65d 100644 --- a/packages/vite/src/node/plugins/preAlias.ts +++ b/packages/vite/src/node/plugins/preAlias.ts @@ -128,3 +128,11 @@ function getAliasPatterns( } return Object.entries(entries).map(([find]) => find) } + +export function getAliasPatternMatcher( + entries: (AliasOptions | undefined) & Alias[], +): (importee: string) => boolean { + const patterns = getAliasPatterns(entries) + return (importee: string) => + patterns.some((pattern) => matches(pattern, importee)) +} diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index eee6ba0f92d742..ccffd1c152972c 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -402,6 +402,7 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { if (isBuiltin(id)) { if (ssr) { if ( + targetWeb && ssrNoExternal === true && // if both noExternal and external are true, noExternal will take the higher priority and bundle it. // only if the id is explicitly listed in external, we will externalize it and skip this error. @@ -480,9 +481,12 @@ function resolveSubpathImports( const pkgData = findNearestPackageData(basedir, options.packageCache) if (!pkgData) return + let { file: idWithoutPostfix, postfix } = splitFileAndPostfix(id.slice(1)) + idWithoutPostfix = '#' + idWithoutPostfix + let importsPath = resolveExportsOrImports( pkgData.data, - id, + idWithoutPostfix, options, targetWeb, 'imports', @@ -496,7 +500,7 @@ function resolveSubpathImports( } } - return importsPath + return importsPath + postfix } function ensureVersionQuery( @@ -732,7 +736,19 @@ export function tryNodeResolve( basedir = root } - const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache) + let selfPkg = null + if (!isBuiltin(id) && !id.includes('\0') && bareImportRE.test(id)) { + // check if it's a self reference dep. + const selfPackageData = findNearestPackageData(basedir, packageCache) + selfPkg = + selfPackageData?.data.exports && selfPackageData?.data.name === pkgId + ? selfPackageData + : null + } + + const pkg = + selfPkg || + resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache) if (!pkg) { // if import can't be found, check if it's an optional peer dep. // if so, we can resolve to a special id that errors only when imported. diff --git a/packages/vite/src/node/plugins/splitVendorChunk.ts b/packages/vite/src/node/plugins/splitVendorChunk.ts index b2ecbcb6f684dc..39a50a02dbf28f 100644 --- a/packages/vite/src/node/plugins/splitVendorChunk.ts +++ b/packages/vite/src/node/plugins/splitVendorChunk.ts @@ -26,6 +26,9 @@ export const isCSSRequest = (request: string): boolean => // The cache needs to be reset on buildStart for watch mode to work correctly // Don't use this manualChunks strategy for ssr, lib mode, and 'umd' or 'iife' +/** + * @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration + */ export class SplitVendorChunkCache { cache: Map constructor() { @@ -36,6 +39,9 @@ export class SplitVendorChunkCache { } } +/** + * @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration + */ export function splitVendorChunk( options: { cache?: SplitVendorChunkCache } = {}, ): GetManualChunk { @@ -87,6 +93,9 @@ function staticImportedByEntry( return someImporterIs } +/** + * @deprecated use build.rollupOptions.output.manualChunks or framework specific configuration + */ export function splitVendorChunkPlugin(): Plugin { const caches: SplitVendorChunkCache[] = [] function createSplitVendorChunk(output: OutputOptions, config: UserConfig) { diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index f5adab8e1b43d5..407ea5f0009a9e 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -29,6 +29,7 @@ const wasmHelper = async (opts = {}, url: string) => { // correct MIME type for .wasm files, which unfortunately doesn't work for // a lot of static file servers, so we just work around it by getting the // raw buffer. + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- this function runs in browsers const response = await fetch(url) const contentType = response.headers.get('Content-Type') || '' if ( diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 9749f6cd006391..4094b581a52b63 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -1,11 +1,17 @@ import path from 'node:path' import MagicString from 'magic-string' -import type { EmittedAsset, OutputChunk } from 'rollup' +import type { OutputChunk } from 'rollup' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import type { ViteDevServer } from '../server' import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants' -import { getHash, injectQuery, urlRE } from '../utils' +import { + encodeURIPath, + getHash, + injectQuery, + prettifyUrl, + urlRE, +} from '../utils' import { createToImportMetaURLBasedRelativeRuntime, onRollupWarning, @@ -14,9 +20,11 @@ import { import { cleanUrl } from '../../shared/utils' import { fileToUrl } from './asset' +type WorkerBundleAsset = { fileName: string; source: string | Uint8Array } + interface WorkerCache { // save worker all emit chunk avoid rollup make the same asset unique. - assets: Map + assets: Map // worker bundle don't deps on any more worker runtime info an id only had a result. // save worker bundled file id to avoid repeated execution of bundles @@ -38,24 +46,32 @@ const workerCache = new WeakMap() function saveEmitWorkerAsset( config: ResolvedConfig, - asset: EmittedAsset, + asset: WorkerBundleAsset, ): void { - const fileName = asset.fileName! const workerMap = workerCache.get(config.mainConfig || config)! - workerMap.assets.set(fileName, asset) + workerMap.assets.set(asset.fileName, asset) } async function bundleWorkerEntry( config: ResolvedConfig, id: string, ): Promise { + const input = cleanUrl(id) + const newBundleChain = [...config.bundleChain, input] + if (config.bundleChain.includes(input)) { + throw new Error( + 'Circular worker imports detected. Vite does not support it. ' + + `Import chain: ${newBundleChain.map((id) => prettifyUrl(id, config.root)).join(' -> ')}`, + ) + } + // bundle the file as entry to support imports const { rollup } = await import('rollup') const { plugins, rollupOptions, format } = config.worker const bundle = await rollup({ ...rollupOptions, - input: cleanUrl(id), - plugins: await plugins(), + input, + plugins: await plugins(newBundleChain), onwarn(warning, warn) { onRollupWarning(warning, warn, config) }, @@ -96,7 +112,6 @@ async function bundleWorkerEntry( saveEmitWorkerAsset(config, { fileName: outputChunk.fileName, source: outputChunk.code, - type: 'asset', }) } }) @@ -121,7 +136,6 @@ function emitSourcemapForWorkerEntry( const mapFileName = chunk.fileName + '.map' saveEmitWorkerAsset(config, { fileName: mapFileName, - type: 'asset', source: data, }) } @@ -156,7 +170,6 @@ export async function workerFileToUrl( saveEmitWorkerAsset(config, { fileName, source: outputChunk.code, - type: 'asset', }) workerMap.bundle.set(id, fileName) } @@ -264,8 +277,6 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { const workerMatch = workerOrSharedWorkerRE.exec(id) if (!workerMatch) return - // stringified url or `new URL(...)` - let url: string const { format } = config.worker const workerConstructor = workerMatch[1] === 'sharedworker' ? 'SharedWorker' : 'Worker' @@ -279,8 +290,11 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { name: options?.name }` + let urlCode: string if (isBuild) { - if (inlineRE.test(id)) { + if (isWorker && this.getModuleInfo(cleanUrl(id))?.isEntry) { + urlCode = 'self.location.href' + } else if (inlineRE.test(id)) { const chunk = await bundleWorkerEntry(config, id) const encodedJs = `const encodedJs = "${Buffer.from( chunk.code, @@ -337,16 +351,17 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { map: { mappings: '' }, } } else { - url = await workerFileToUrl(config, id) + urlCode = JSON.stringify(await workerFileToUrl(config, id)) } } else { - url = await fileToUrl(cleanUrl(id), config, this) + let url = await fileToUrl(cleanUrl(id), config, this) url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`) + urlCode = JSON.stringify(url) } if (urlRE.test(id)) { return { - code: `export default ${JSON.stringify(url)}`, + code: `export default ${urlCode}`, map: { mappings: '' }, // Empty sourcemap to suppress Rollup warning } } @@ -354,7 +369,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { return { code: `export default function WorkerWrapper(options) { return new ${workerConstructor}( - ${JSON.stringify(url)}, + ${urlCode}, ${workerTypeOption} ); }`, @@ -402,7 +417,7 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { ) const replacementString = typeof replacement === 'string' - ? JSON.stringify(replacement).slice(1, -1) + ? JSON.stringify(encodeURIPath(replacement)).slice(1, -1) : `"+${replacement.runtime}+"` s.update(match.index, match.index + full.length, replacementString) } @@ -410,16 +425,42 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { return result() }, - generateBundle(opts) { + generateBundle(opts, bundle) { // @ts-expect-error asset emits are skipped in legacy bundle if (opts.__vite_skip_asset_emit__ || isWorker) { return } const workerMap = workerCache.get(config)! workerMap.assets.forEach((asset) => { - this.emitFile(asset) - workerMap.assets.delete(asset.fileName!) + const duplicateAsset = bundle[asset.fileName] + if (duplicateAsset) { + const content = + duplicateAsset.type === 'asset' + ? duplicateAsset.source + : duplicateAsset.code + // don't emit if the file name and the content is same + if (isSameContent(content, asset.source)) { + return + } + } + + this.emitFile({ + type: 'asset', + fileName: asset.fileName, + source: asset.source, + }) }) + workerMap.assets.clear() }, } } + +function isSameContent(a: string | Uint8Array, b: string | Uint8Array) { + if (typeof a === 'string') { + if (typeof b === 'string') { + return a === b + } + return Buffer.from(a).equals(b) + } + return Buffer.from(b).equals(a) +} diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 4460c71cf3e836..22a5de2ec67b06 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -165,22 +165,29 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { : slash(path.resolve(path.dirname(id), url)) } - let builtUrl: string - if (isBuild) { - builtUrl = await workerFileToUrl(config, file) + if ( + isBuild && + config.isWorker && + this.getModuleInfo(cleanUrl(file))?.isEntry + ) { + s.update(expStart, expEnd, 'self.location.href') } else { - builtUrl = await fileToUrl(cleanUrl(file), config, this) - builtUrl = injectQuery( - builtUrl, - `${WORKER_FILE_ID}&type=${workerType}`, + let builtUrl: string + if (isBuild) { + builtUrl = await workerFileToUrl(config, file) + } else { + builtUrl = await fileToUrl(cleanUrl(file), config, this) + builtUrl = injectQuery( + builtUrl, + `${WORKER_FILE_ID}&type=${workerType}`, + ) + } + s.update( + expStart, + expEnd, + `new URL(/* @vite-ignore */ ${JSON.stringify(builtUrl)}, import.meta.url)`, ) } - s.update( - expStart, - expEnd, - // add `'' +` to skip vite:asset-import-meta-url plugin - `new URL('' + ${JSON.stringify(builtUrl)}, import.meta.url)`, - ) } if (s) { diff --git a/packages/vite/src/node/preview.ts b/packages/vite/src/node/preview.ts index 74a4728529b55a..4d2e1e645bbcdc 100644 --- a/packages/vite/src/node/preview.ts +++ b/packages/vite/src/node/preview.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import sirv from 'sirv' +import compression from '@polka/compression' import connect from 'connect' import type { Connect } from 'dep-types/connect' import corsMiddleware from 'cors' @@ -19,7 +20,6 @@ import { setClientErrorHandler, } from './http' import { openBrowser } from './server/openBrowser' -import compression from './server/middlewares/compression' import { baseMiddleware } from './server/middlewares/base' import { htmlFallbackMiddleware } from './server/middlewares/htmlFallback' import { indexHtmlMiddleware } from './server/middlewares/indexHtml' diff --git a/packages/vite/src/node/server/__tests__/watcher.spec.ts b/packages/vite/src/node/server/__tests__/watcher.spec.ts index 90b68fcfd88d0f..df0e8c0641d0d7 100644 --- a/packages/vite/src/node/server/__tests__/watcher.spec.ts +++ b/packages/vite/src/node/server/__tests__/watcher.spec.ts @@ -1,9 +1,47 @@ -import { describe, expect, it } from 'vitest' +import { resolve } from 'node:path' +import { + type MockInstance, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest' +import chokidar from 'chokidar' import { createServer } from '../index' const stubGetWatchedCode = /getWatched\(\) \{.+?return \{\};.+?\}/s +let watchSpy: MockInstance< + Parameters, + ReturnType +> + +vi.mock('../../config', async () => { + const config: typeof import('../../config') = + await vi.importActual('../../config') + const resolveConfig = config.resolveConfig + vi.spyOn(config, 'resolveConfig').mockImplementation(async (...args) => { + const resolved: Awaited> = + await resolveConfig.call(config, ...args) + resolved.configFileDependencies.push( + resolve('fake/config/dependency.js').replace(/\\/g, '/'), + ) + return resolved + }) + return config +}) + describe('watcher configuration', () => { + beforeEach(() => { + watchSpy = vi.spyOn(chokidar, 'watch') + }) + + afterEach(() => { + watchSpy.mockRestore() + }) + it('when watcher is disabled, return noop watcher', async () => { const server = await createServer({ server: { @@ -21,4 +59,27 @@ describe('watcher configuration', () => { }) expect(server.watcher.getWatched.toString()).not.toMatch(stubGetWatchedCode) }) + + it('should watch the root directory, config file dependencies, dotenv files, and the public directory', async () => { + await createServer({ + server: { + watch: {}, + }, + publicDir: '__test_public__', + }) + expect(watchSpy).toHaveBeenLastCalledWith( + expect.arrayContaining( + [ + process.cwd(), + resolve('fake/config/dependency.js'), + resolve('.env'), + resolve('.env.local'), + resolve('.env.development'), + resolve('.env.development.local'), + resolve('__test_public__'), + ].map((file) => file.replace(/\\/g, '/')), + ), + expect.anything(), + ) + }) }) diff --git a/packages/vite/src/node/server/hmr.ts b/packages/vite/src/node/server/hmr.ts index 86bf9961bf9a72..040495673e8836 100644 --- a/packages/vite/src/node/server/hmr.ts +++ b/packages/vite/src/node/server/hmr.ts @@ -6,7 +6,7 @@ import colors from 'picocolors' import type { CustomPayload, HMRPayload, Update } from 'types/hmrPayload' import type { RollupError } from 'rollup' import { CLIENT_DIR } from '../constants' -import { createDebugger, normalizePath, unique } from '../utils' +import { createDebugger, normalizePath } from '../utils' import type { InferCustomEventPayload, ViteDevServer } from '..' import { isCSSRequest } from '../plugins/css' import { getAffectedGlobModules } from '../plugins/importMetaGlob' @@ -118,9 +118,9 @@ export function getShortName(file: string, root: string): string { } export async function handleHMRUpdate( + type: 'create' | 'delete' | 'update', file: string, server: ViteDevServer, - configOnly: boolean, ): Promise { const { hot, config, moduleGraph } = server const shortFile = getShortName(file, config.root) @@ -138,7 +138,9 @@ export async function handleHMRUpdate( debugHmr?.(`[config change] ${colors.dim(shortFile)}`) config.logger.info( colors.green( - `${path.relative(process.cwd(), file)} changed, restarting server...`, + `${normalizePath( + path.relative(process.cwd(), file), + )} changed, restarting server...`, ), { clear: true, timestamp: true }, ) @@ -150,10 +152,6 @@ export async function handleHMRUpdate( return } - if (configOnly) { - return - } - debugHmr?.(`[file change] ${colors.dim(shortFile)}`) // (dev only) the client itself cannot be hot updated. @@ -166,22 +164,34 @@ export async function handleHMRUpdate( return } - const mods = moduleGraph.getModulesByFile(file) + const mods = new Set(moduleGraph.getModulesByFile(file)) + if (type === 'create') { + for (const mod of moduleGraph._hasResolveFailedErrorModules) { + mods.add(mod) + } + } + if (type === 'create' || type === 'delete') { + for (const mod of getAffectedGlobModules(file, server)) { + mods.add(mod) + } + } // check if any plugin wants to perform custom HMR handling const timestamp = Date.now() const hmrContext: HmrContext = { file, timestamp, - modules: mods ? [...mods] : [], + modules: [...mods], read: () => readModifiedFile(file), server, } - for (const hook of config.getSortedPluginHooks('handleHotUpdate')) { - const filteredModules = await hook(hmrContext) - if (filteredModules) { - hmrContext.modules = filteredModules + if (type === 'update') { + for (const hook of config.getSortedPluginHooks('handleHotUpdate')) { + const filteredModules = await hook(hmrContext) + if (filteredModules) { + hmrContext.modules = filteredModules + } } } @@ -220,7 +230,8 @@ export function updateModules( const updates: Update[] = [] const invalidatedModules = new Set() const traversedModules = new Set() - let needFullReload: HasDeadEnd = false + // Modules could be empty if a root module is invalidated via import.meta.hot.invalidate() + let needFullReload: HasDeadEnd = modules.length === 0 for (const mod of modules) { const boundaries: PropagationBoundary[] = [] @@ -315,33 +326,6 @@ function getSSRInvalidatedImporters(module: ModuleNode) { ) } -export async function handleFileAddUnlink( - file: string, - server: ViteDevServer, - isUnlink: boolean, -): Promise { - const modules = [...(server.moduleGraph.getModulesByFile(file) || [])] - - if (isUnlink) { - for (const deletedMod of modules) { - deletedMod.importedModules.forEach((importedMod) => { - importedMod.importers.delete(deletedMod) - }) - } - } - - modules.push(...getAffectedGlobModules(file, server)) - - if (modules.length > 0) { - updateModules( - getShortName(file, server.config.root), - unique(modules), - Date.now(), - server, - ) - } -} - function areAllImportsAccepted( importedBindings: Set, acceptedExports: Set, @@ -552,6 +536,7 @@ export function handlePrunedModules( const t = Date.now() mods.forEach((mod) => { mod.lastHMRTimestamp = t + mod.lastHMRInvalidationReceived = false debugHmr?.(`[dispose] ${colors.dim(mod.file)}`) }) hot.send({ diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index d3029175ce9376..7590bafcc3a88e 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import { execSync } from 'node:child_process' import type * as net from 'node:net' import { get as httpGet } from 'node:http' import { get as httpsGet } from 'node:https' @@ -31,6 +32,7 @@ import { isParentDirectory, mergeConfig, normalizePath, + promiseWithResolvers, resolveHostname, resolveServerUrls, } from '../utils' @@ -45,7 +47,12 @@ import type { BindCLIShortcutsOptions } from '../shortcuts' import { CLIENT_DIR, DEFAULT_DEV_PORT } from '../constants' import type { Logger } from '../logger' import { printServerUrls } from '../logger' -import { createNoopWatcher, resolveChokidarOptions } from '../watch' +import { + createNoopWatcher, + getResolvedOutDirs, + resolveChokidarOptions, + resolveEmptyOutDir, +} from '../watch' import { initPublicFiles } from '../publicDir' import { getEnvFilesForMode } from '../env' import type { FetchResult } from '../../runtime/types' @@ -80,7 +87,6 @@ import { createHMRBroadcaster, createServerHMRChannel, getShortName, - handleFileAddUnlink, handleHMRUpdate, updateModules, } from './hmr' @@ -95,6 +101,11 @@ export interface ServerOptions extends CommonServerOptions { * Configure HMR-specific options (port, host, path & protocol) */ hmr?: HmrOptions | boolean + /** + * Do not start the websocket connection. + * @experimental + */ + ws?: false /** * Warm-up files to transform and cache the results in advance. This improves the * initial page load during server starts and prevents transform waterfalls. @@ -156,9 +167,10 @@ export interface ServerOptions extends CommonServerOptions { | ((sourcePath: string, sourcemapPath: string) => boolean) } -export interface ResolvedServerOptions extends ServerOptions { +export interface ResolvedServerOptions + extends Omit { fs: Required - middlewareMode: boolean + middlewareMode: NonNullable sourcemapIgnoreList: Exclude< ServerOptions['sourcemapIgnoreList'], false | undefined @@ -235,7 +247,6 @@ export interface ViteDevServer { watcher: FSWatcher /** * web socket server with `send(payload)` method - * @deprecated use `hot` instead */ ws: WebSocketServer /** @@ -243,6 +254,7 @@ export interface ViteDevServer { * * Always sends a message to at least a WebSocket client. Any third party can * add a channel to the broadcaster to process messages + * @deprecated will be replaced with the environment api in v6. */ hot: HMRBroadcaster /** @@ -342,6 +354,22 @@ export interface ViteDevServer { * Open browser */ openBrowser(): void + /** + * Calling `await server.waitForRequestsIdle(id)` will wait until all static imports + * are processed. If called from a load or transform plugin hook, the id needs to be + * passed as a parameter to avoid deadlocks. Calling this function after the first + * static imports section of the module graph has been processed will resolve immediately. + * @experimental + */ + waitForRequestsIdle: (ignoredId?: string) => Promise + /** + * @internal + */ + _registerRequestProcessing: (id: string, done: () => Promise) => void + /** + * @internal + */ + _onCrawlEnd(cb: () => void): void /** * @internal */ @@ -410,10 +438,25 @@ export async function _createServer( const httpsOptions = await resolveHttpsConfig(config.server.https) const { middlewareMode } = serverConfig - const resolvedWatchOptions = resolveChokidarOptions(config, { - disableGlobbing: true, - ...serverConfig.watch, - }) + const resolvedOutDirs = getResolvedOutDirs( + config.root, + config.build.outDir, + config.build.rollupOptions?.output, + ) + const emptyOutDir = resolveEmptyOutDir( + config.build.emptyOutDir, + config.root, + resolvedOutDirs, + ) + const resolvedWatchOptions = resolveChokidarOptions( + config, + { + disableGlobbing: true, + ...serverConfig.watch, + }, + resolvedOutDirs, + emptyOutDir, + ) const middlewares = connect() as Connect.Server const httpServer = middlewareMode @@ -428,6 +471,9 @@ export async function _createServer( config.server.hmr.channels.forEach((channel) => hot.addChannel(channel)) } + const publicFiles = await initPublicFilesPromise + const { publicDir } = config + if (httpServer) { setClientErrorHandler(httpServer, config.logger) } @@ -441,6 +487,9 @@ export async function _createServer( root, ...config.configFileDependencies, ...getEnvFilesForMode(config.mode, config.envDir), + // Watch the public directory explicitly because it might be outside + // of the root directory. + ...(publicDir && publicFiles ? [publicDir] : []), ], resolvedWatchOptions, ) as FSWatcher) @@ -457,6 +506,20 @@ export async function _createServer( const devHtmlTransformFn = createDevHtmlTransformFn(config) + const onCrawlEndCallbacks: (() => void)[] = [] + const crawlEndFinder = setupOnCrawlEnd(() => { + onCrawlEndCallbacks.forEach((cb) => cb()) + }) + function waitForRequestsIdle(ignoredId?: string): Promise { + return crawlEndFinder.waitForRequestsIdle(ignoredId) + } + function _registerRequestProcessing(id: string, done: () => Promise) { + crawlEndFinder.registerRequestProcessing(id, done) + } + function _onCrawlEnd(cb: () => void) { + onCrawlEndCallbacks.push(cb) + } + let server: ViteDevServer = { config, middlewares, @@ -479,7 +542,9 @@ export async function _createServer( return transformRequest(url, server, options) }, async warmupRequest(url, options) { - await transformRequest(url, server, options).catch((e) => { + try { + await transformRequest(url, server, options) + } catch (e) { if ( e?.code === ERR_OUTDATED_OPTIMIZED_DEP || e?.code === ERR_CLOSED_SERVER @@ -492,7 +557,7 @@ export async function _createServer( error: e, timestamp: true, }) - }) + } }, transformIndexHtml(url, html, originalUrl) { return devHtmlTransformFn(server, url, html, originalUrl) @@ -588,6 +653,7 @@ export async function _createServer( watcher.close(), hot.close(), container.close(), + crawlEndFinder?.cancel(), getDepsOptimizer(server.config)?.close(), getDepsOptimizer(server.config, true)?.close(), closeHttpServer(), @@ -636,6 +702,10 @@ export async function _createServer( return server._restartPromise }, + waitForRequestsIdle, + _registerRequestProcessing, + _onCrawlEnd, + _setInternalServer(_server: ViteDevServer) { // Rebind internal the server variable so functions reference the user // server instance after a restart @@ -645,10 +715,19 @@ export async function _createServer( _importGlobMap: new Map(), _forceOptimizeOnRestart: false, _pendingRequests: new Map(), - _fsDenyGlob: picomatch(config.server.fs.deny, { - matchBase: true, - nocase: true, - }), + _fsDenyGlob: picomatch( + // matchBase: true does not work as it's documented + // https://github.com/micromatch/picomatch/issues/89 + // convert patterns without `/` on our side for now + config.server.fs.deny.map((pattern) => + pattern.includes('/') ? pattern : `**/${pattern}`, + ), + { + matchBase: false, + nocase: true, + dot: true, + }, + ), _shortcutsOptions: undefined, } @@ -677,12 +756,13 @@ export async function _createServer( } } - const publicFiles = await initPublicFilesPromise - - const onHMRUpdate = async (file: string, configOnly: boolean) => { + const onHMRUpdate = async ( + type: 'create' | 'delete' | 'update', + file: string, + ) => { if (serverConfig.hmr !== false) { try { - await handleHMRUpdate(file, server, configOnly) + await handleHMRUpdate(type, file, server) } catch (err) { hot.send({ type: 'error', @@ -692,8 +772,6 @@ export async function _createServer( } } - const { publicDir } = config - const onFileAddUnlink = async (file: string, isUnlink: boolean) => { file = normalizePath(file) await container.watchChange(file, { event: isUnlink ? 'delete' : 'create' }) @@ -713,8 +791,8 @@ export async function _createServer( } } } - await handleFileAddUnlink(file, server, isUnlink) - await onHMRUpdate(file, true) + if (isUnlink) moduleGraph.onFileDelete(file) + await onHMRUpdate(isUnlink ? 'delete' : 'create', file) } watcher.on('change', async (file) => { @@ -722,7 +800,7 @@ export async function _createServer( await container.watchChange(file, { event: 'update' }) // invalidate module graph cache on file change moduleGraph.onFileChange(file) - await onHMRUpdate(file, false) + await onHMRUpdate('update', file) }) getFsUtils(config).initWatcher?.(watcher) @@ -736,7 +814,13 @@ export async function _createServer( hot.on('vite:invalidate', async ({ path, message }) => { const mod = moduleGraph.urlToModuleMap.get(path) - if (mod && mod.isSelfAccepting && mod.lastHMRTimestamp > 0) { + if ( + mod && + mod.isSelfAccepting && + mod.lastHMRTimestamp > 0 && + !mod.lastHMRInvalidationReceived + ) { + mod.lastHMRInvalidationReceived = true config.logger.info( colors.yellow(`hmr invalidate `) + colors.dim(path) + @@ -786,15 +870,13 @@ export async function _createServer( const { proxy } = serverConfig if (proxy) { const middlewareServer = - (isObject(serverConfig.middlewareMode) - ? serverConfig.middlewareMode.server - : null) || httpServer + (isObject(middlewareMode) ? middlewareMode.server : null) || httpServer middlewares.use(proxyMiddleware(middlewareServer, proxy, config)) } // base if (config.base !== '/') { - middlewares.use(baseMiddleware(config.rawBase, middlewareMode)) + middlewares.use(baseMiddleware(config.rawBase, !!middlewareMode)) } // open in editor support @@ -849,7 +931,7 @@ export async function _createServer( } // error handler - middlewares.use(errorMiddleware(server, middlewareMode)) + middlewares.use(errorMiddleware(server, !!middlewareMode)) // httpServer.listen can be called multiple times // when port when using next port number @@ -981,7 +1063,7 @@ export function resolveServerOptions( raw?.sourcemapIgnoreList === false ? () => false : raw?.sourcemapIgnoreList || isInNodeModules, - middlewareMode: !!raw?.middlewareMode, + middlewareMode: raw?.middlewareMode || false, } let allowDirs = server.fs?.allow const deny = server.fs?.deny || ['.env', '.env.*', '*.{crt,pem}'] @@ -990,6 +1072,26 @@ export function resolveServerOptions( allowDirs = [searchForWorkspaceRoot(root)] } + if (process.versions.pnp) { + try { + const enableGlobalCache = + execSync('yarn config get enableGlobalCache', { cwd: root }) + .toString() + .trim() === 'true' + const yarnCacheDir = execSync( + `yarn config get ${enableGlobalCache ? 'globalFolder' : 'cacheFolder'}`, + { cwd: root }, + ) + .toString() + .trim() + allowDirs.push(yarnCacheDir) + } catch (e) { + logger.warn(`Get yarn cache dir error: ${e.message}`, { + timestamp: true, + }) + } + } + allowDirs = allowDirs.map((i) => resolvedAllowDir(root, i)) // only push client dir when vite itself is outside-of-root @@ -1113,3 +1215,81 @@ export async function restartServerWithUrls( server.printUrls() } } + +const callCrawlEndIfIdleAfterMs = 50 + +interface CrawlEndFinder { + registerRequestProcessing: (id: string, done: () => Promise) => void + waitForRequestsIdle: (ignoredId?: string) => Promise + cancel: () => void +} + +function setupOnCrawlEnd(onCrawlEnd: () => void): CrawlEndFinder { + const registeredIds = new Set() + const seenIds = new Set() + const onCrawlEndPromiseWithResolvers = promiseWithResolvers() + + let timeoutHandle: NodeJS.Timeout | undefined + + let cancelled = false + function cancel() { + cancelled = true + } + + let crawlEndCalled = false + function callOnCrawlEnd() { + if (!cancelled && !crawlEndCalled) { + crawlEndCalled = true + onCrawlEnd() + } + onCrawlEndPromiseWithResolvers.resolve() + } + + function registerRequestProcessing( + id: string, + done: () => Promise, + ): void { + if (!seenIds.has(id)) { + seenIds.add(id) + registeredIds.add(id) + done() + .catch(() => {}) + .finally(() => markIdAsDone(id)) + } + } + + function waitForRequestsIdle(ignoredId?: string): Promise { + if (ignoredId) { + seenIds.add(ignoredId) + markIdAsDone(ignoredId) + } + return onCrawlEndPromiseWithResolvers.promise + } + + function markIdAsDone(id: string): void { + if (registeredIds.has(id)) { + registeredIds.delete(id) + checkIfCrawlEndAfterTimeout() + } + } + + 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 { + registerRequestProcessing, + waitForRequestsIdle, + cancel, + } +} diff --git a/packages/vite/src/node/server/middlewares/compression.ts b/packages/vite/src/node/server/middlewares/compression.ts deleted file mode 100644 index e7bc133bf3682c..00000000000000 --- a/packages/vite/src/node/server/middlewares/compression.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* eslint-disable */ -//@ts-nocheck -//TODO: replace this code with https://github.com/lukeed/polka/pull/148 once it's released - -// This is based on https://github.com/preactjs/wmr/blob/main/packages/wmr/src/lib/polkompress.js -// MIT Licensed https://github.com/preactjs/wmr/blob/main/LICENSE -import zlib from 'node:zlib' - -/* global Buffer */ - -const noop = () => {} - -const mimes = /text|javascript|\/json|xml/i -const threshold = 1024 -const level = -1 -let brotli = false -const gzip = true - -const getChunkSize = (chunk, enc) => (chunk ? Buffer.byteLength(chunk, enc) : 0) - -export default function compression() { - const brotliOpts = (typeof brotli === 'object' && brotli) || {} - const gzipOpts = (typeof gzip === 'object' && gzip) || {} - - // disable Brotli on Node<12.7 where it is unsupported: - if (!zlib.createBrotliCompress) brotli = false - - return function viteCompressionMiddleware(req, res, next = noop) { - const accept = req.headers['accept-encoding'] + '' - const encoding = ((brotli && accept.match(/\bbr\b/)) || - (gzip && accept.match(/\bgzip\b/)) || - [])[0] - - // skip if no response body or no supported encoding: - if (req.method === 'HEAD' || !encoding) return next() - - /** @type {zlib.Gzip | zlib.BrotliCompress} */ - let compress - let pendingStatus - /** @type {[string, function][]?} */ - let pendingListeners = [] - let started = false - let size = 0 - - function start() { - started = true - size = res.getHeader('Content-Length') | 0 || size - const compressible = mimes.test( - String(res.getHeader('Content-Type') || 'text/plain'), - ) - const cleartext = !res.getHeader('Content-Encoding') - const listeners = pendingListeners || [] - if (compressible && cleartext && size >= threshold) { - res.setHeader('Content-Encoding', encoding) - res.removeHeader('Content-Length') - if (encoding === 'br') { - const params = { - [zlib.constants.BROTLI_PARAM_QUALITY]: level, - [zlib.constants.BROTLI_PARAM_SIZE_HINT]: size, - } - compress = zlib.createBrotliCompress({ - params: Object.assign(params, brotliOpts), - }) - } else { - compress = zlib.createGzip(Object.assign({ level }, gzipOpts)) - } - // backpressure - compress.on( - 'data', - (chunk) => write.call(res, chunk) === false && compress.pause(), - ) - on.call(res, 'drain', () => compress.resume()) - compress.on('end', () => end.call(res)) - listeners.forEach((p) => compress.on.apply(compress, p)) - } else { - pendingListeners = null - listeners.forEach((p) => on.apply(res, p)) - } - - writeHead.call(res, pendingStatus || res.statusCode) - } - - const { end, write, on, writeHead } = res - - res.writeHead = function (status, reason, headers) { - if (typeof reason !== 'string') [headers, reason] = [reason, headers] - if (headers) for (let i in headers) res.setHeader(i, headers[i]) - pendingStatus = status - return this - } - - res.write = function (chunk, enc, cb) { - size += getChunkSize(chunk, enc) - if (!started) start() - if (!compress) return write.apply(this, arguments) - return compress.write.apply(compress, arguments) - } - - res.end = function (chunk, enc, cb) { - if (arguments.length > 0 && typeof chunk !== 'function') { - size += getChunkSize(chunk, enc) - } - if (!started) start() - if (!compress) return end.apply(this, arguments) - return compress.end.apply(compress, arguments) - } - - res.on = function (type, listener) { - if (!pendingListeners || type !== 'drain') on.call(this, type, listener) - else if (compress) compress.on(type, listener) - else pendingListeners.push([type, listener]) - return this - } - - next() - } -} diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index d9232b9d629e72..b5893dd072b972 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -15,6 +15,8 @@ import { getScriptInfo, htmlEnvHook, htmlProxyResult, + injectCspNonceMetaTagHook, + injectNonceAttributeTagHook, nodeIsElement, overwriteAttrValue, postImportMapHook, @@ -69,11 +71,13 @@ export function createDevHtmlTransformFn( ) const transformHooks = [ preImportMapHook(config), + injectCspNonceMetaTagHook(config), ...preHooks, htmlEnvHook(config), devHtmlHook, ...normalHooks, ...postHooks, + injectNonceAttributeTagHook(config), postImportMapHook(), ] return ( @@ -187,7 +191,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( const trailingSlash = htmlPath.endsWith('/') if (!trailingSlash && getFsUtils(config).existsSync(filename)) { proxyModulePath = htmlPath - proxyModuleUrl = joinUrlSegments(base, htmlPath) + proxyModuleUrl = proxyModulePath } else { // There are users of vite.transformIndexHtml calling it with url '/' // for SSR integrations #7993, filename is root for this case @@ -198,6 +202,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( proxyModulePath = `\0${validPath}` proxyModuleUrl = wrapId(proxyModulePath) } + proxyModuleUrl = joinUrlSegments(base, proxyModuleUrl) const s = new MagicString(html) let inlineModuleIndex = -1 diff --git a/packages/vite/src/node/server/middlewares/proxy.ts b/packages/vite/src/node/server/middlewares/proxy.ts index 2b80f2801d2f71..4ceb4d8d435426 100644 --- a/packages/vite/src/node/server/middlewares/proxy.ts +++ b/packages/vite/src/node/server/middlewares/proxy.ts @@ -29,6 +29,29 @@ export interface ProxyOptions extends HttpProxy.ServerOptions { ) => void | null | undefined | false | string } +const setOriginHeader = ( + proxyReq: http.ClientRequest, + options: HttpProxy.ServerOptions, +) => { + // Browsers may send Origin headers even with same-origin + // requests. It is common for WebSocket servers to check the Origin + // header, so if changeOrigin is true we change the Origin to match + // the target URL. + // https://github.com/http-party/node-http-proxy/issues/1669 + if (options.changeOrigin) { + const { target } = options + + if (proxyReq.getHeader('origin') && target) { + const changedOrigin = + typeof target === 'object' + ? `${target.protocol}//${target.host}` + : target + + proxyReq.setHeader('origin', changedOrigin) + } + } +} + export function proxyMiddleware( httpServer: HttpServer | null, options: NonNullable, @@ -89,7 +112,13 @@ export function proxyMiddleware( } }) + proxy.on('proxyReq', (proxyReq, req, res, options) => { + setOriginHeader(proxyReq, options) + }) + proxy.on('proxyReqWs', (proxyReq, req, socket, options, head) => { + setOriginHeader(proxyReq, options) + socket.on('error', (err) => { config.logger.error( `${colors.red(`ws proxy socket error:`)}\n${err.stack}`, diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index a0239aab7fcb4e..12a440d4c10774 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -27,6 +27,7 @@ import { isDirectRequest, } from '../../plugins/css' import { + ERR_FILE_NOT_FOUND_IN_OPTIMIZED_DEP_DIR, ERR_OPTIMIZE_DEPS_PROCESSING_ERROR, ERR_OUTDATED_OPTIMIZED_DEP, } from '../../plugins/optimizedDeps' @@ -253,6 +254,15 @@ export function transformMiddleware( // error but a normal part of the missing deps discovery flow return } + if (e?.code === ERR_FILE_NOT_FOUND_IN_OPTIMIZED_DEP_DIR) { + // Skip if response has already been sent + if (!res.writableEnded) { + res.statusCode = 404 + res.end() + } + server.config.logger.warn(colors.yellow(e.message)) + return + } if (e?.code === ERR_LOAD_URL) { // Let other middleware handle if we can't load the url via transformRequest return next() diff --git a/packages/vite/src/node/server/moduleGraph.ts b/packages/vite/src/node/server/moduleGraph.ts index ac139f06fd112a..442ece308dbaff 100644 --- a/packages/vite/src/node/server/moduleGraph.ts +++ b/packages/vite/src/node/server/moduleGraph.ts @@ -35,6 +35,13 @@ export class ModuleNode { ssrModule: Record | null = null ssrError: Error | null = null lastHMRTimestamp = 0 + /** + * `import.meta.hot.invalidate` is called by the client. + * If there's multiple clients, multiple `invalidate` request is received. + * This property is used to dedupe those request to avoid multiple updates happening. + * @internal + */ + lastHMRInvalidationReceived = false lastInvalidationTimestamp = 0 /** * If the module only needs to update its imports timestamp (e.g. within an HMR chain), @@ -108,6 +115,9 @@ export class ModuleGraph { Promise | ModuleNode >() + /** @internal */ + _hasResolveFailedErrorModules = new Set() + constructor( private resolveId: ( url: string, @@ -148,6 +158,17 @@ export class ModuleGraph { } } + onFileDelete(file: string): void { + const mods = this.getModulesByFile(file) + if (mods) { + mods.forEach((mod) => { + mod.importedModules.forEach((importedMod) => { + importedMod.importers.delete(mod) + }) + }) + } + } + invalidateModule( mod: ModuleNode, seen: Set = new Set(), @@ -185,6 +206,7 @@ export class ModuleGraph { if (isHmr) { mod.lastHMRTimestamp = timestamp + mod.lastHMRInvalidationReceived = false } else { // Save the timestamp for this invalidation, so we can avoid caching the result of possible already started // processing being done for this module @@ -218,6 +240,8 @@ export class ModuleGraph { ) } }) + + this._hasResolveFailedErrorModules.delete(mod) } invalidateAll(): void { diff --git a/packages/vite/src/node/server/openBrowser.ts b/packages/vite/src/node/server/openBrowser.ts index 666a86dd4d4ef4..740df255c70c4a 100644 --- a/packages/vite/src/node/server/openBrowser.ts +++ b/packages/vite/src/node/server/openBrowser.ts @@ -34,7 +34,7 @@ export function openBrowser( const browserArgs = process.env.BROWSER_ARGS ? process.env.BROWSER_ARGS.split(' ') : [] - startBrowserProcess(browser, browserArgs, url) + startBrowserProcess(browser, browserArgs, url, logger) } } @@ -72,6 +72,7 @@ async function startBrowserProcess( browser: string | undefined, browserArgs: string[], url: string, + logger: Logger, ) { // If we're on OS X, the user hasn't specifically // requested a different browser, we can try opening @@ -122,7 +123,17 @@ async function startBrowserProcess( const options: open.Options = browser ? { app: { name: browser, arguments: browserArgs } } : {} - open(url, options).catch(() => {}) // Prevent `unhandledRejection` error. + + new Promise((_, reject) => { + open(url, options) + .then((subprocess) => { + subprocess.on('error', reject) + }) + .catch(reject) + }).catch((err) => { + logger.error(err.stack || err.message) + }) + return true } catch (err) { return false diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 4ea3d0e6b51ea0..3251790d169864 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -52,6 +52,7 @@ import type { RollupError, RollupLog, PluginContext as RollupPluginContext, + TransformPluginContext as RollupTransformPluginContext, SourceDescription, SourceMap, TransformResult, @@ -59,7 +60,7 @@ import type { import type { RawSourceMap } from '@ampproject/remapping' import { TraceMap, originalPositionFor } from '@jridgewell/trace-mapping' import MagicString from 'magic-string' -import type { FSWatcher } from 'chokidar' +import type { FSWatcher } from 'dep-types/chokidar' import colors from 'picocolors' import type { Plugin } from '../plugin' import { @@ -76,7 +77,7 @@ import { timeFrom, } from '../utils' import { FS_PREFIX } from '../constants' -import type { ResolvedConfig } from '../config' +import type { PluginHookUtils, ResolvedConfig } from '../config' import { createPluginHookUtils, getHookHandler } from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' import { buildErrorMessage } from './middlewares/error' @@ -84,6 +85,22 @@ import type { ModuleGraph, ModuleNode } from './moduleGraph' const noop = () => {} +// same default value of "moduleInfo.meta" as in Rollup +const EMPTY_OBJECT = Object.freeze({}) + +const debugSourcemapCombineFilter = + process.env.DEBUG_VITE_SOURCEMAP_COMBINE_FILTER +const debugSourcemapCombine = createDebugger('vite:sourcemap-combine', { + onlyWhenFocused: true, +}) +const debugResolve = createDebugger('vite:resolve') +const debugPluginResolve = createDebugger('vite:plugin-resolve', { + onlyWhenFocused: 'vite:plugin', +}) +const debugPluginTransform = createDebugger('vite:plugin-transform', { + onlyWhenFocused: 'vite:plugin', +}) + export const ERR_CLOSED_SERVER = 'ERR_CLOSED_SERVER' export function throwClosedServerError(): never { @@ -103,120 +120,154 @@ export interface PluginContainerOptions { writeFile?: (name: string, source: string | Uint8Array) => void } -export interface PluginContainer { - options: InputOptions - getModuleInfo(id: string): ModuleInfo | null - buildStart(options: InputOptions): Promise - resolveId( - id: string, - importer?: string, - options?: { - attributes?: Record - custom?: CustomPluginOptions - skip?: Set - ssr?: boolean - /** - * @internal - */ - scan?: boolean - isEntry?: boolean - }, - ): Promise - transform( - code: string, - id: string, - options?: { - inMap?: SourceDescription['map'] - ssr?: boolean - }, - ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> - load( - id: string, - options?: { - ssr?: boolean - }, - ): Promise - watchChange( - id: string, - change: { event: 'create' | 'update' | 'delete' }, - ): Promise - close(): Promise -} - -type PluginContext = Omit< - RollupPluginContext, - // not documented - 'cache' -> - export async function createPluginContainer( config: ResolvedConfig, moduleGraph?: ModuleGraph, watcher?: FSWatcher, ): Promise { - const { - plugins, - logger, - root, - build: { rollupOptions }, - } = config - const { getSortedPluginHooks, getSortedPlugins } = - createPluginHookUtils(plugins) - - const seenResolves: Record = {} - const debugResolve = createDebugger('vite:resolve') - const debugPluginResolve = createDebugger('vite:plugin-resolve', { - onlyWhenFocused: 'vite:plugin', - }) - const debugPluginTransform = createDebugger('vite:plugin-transform', { - onlyWhenFocused: 'vite:plugin', - }) - const debugSourcemapCombineFilter = - process.env.DEBUG_VITE_SOURCEMAP_COMBINE_FILTER - const debugSourcemapCombine = createDebugger('vite:sourcemap-combine', { - onlyWhenFocused: true, - }) - - // --------------------------------------------------------------------------- - - const watchFiles = new Set() + const container = new PluginContainer(config, moduleGraph, watcher) + await container.resolveRollupOptions() + return container +} + +class PluginContainer { + private _pluginContextMap = new Map() + private _pluginContextMapSsr = new Map() + private _resolvedRollupOptions?: InputOptions + private _processesing = new Set>() + private _seenResolves: Record = {} + private _closed = false // _addedFiles from the `load()` hook gets saved here so it can be reused in the `transform()` hook - const moduleNodeToLoadAddedImports = new WeakMap< + private _moduleNodeToLoadAddedImports = new WeakMap< ModuleNode, Set | null >() - const minimalContext: MinimalPluginContext = { - meta: { - rollupVersion, - watchMode: true, - }, - debug: noop, - info: noop, - warn: noop, - // @ts-expect-error noop - error: noop, + getSortedPluginHooks: PluginHookUtils['getSortedPluginHooks'] + getSortedPlugins: PluginHookUtils['getSortedPlugins'] + + watchFiles = new Set() + minimalContext: MinimalPluginContext + + /** + * @internal use `createPluginContainer` instead + */ + constructor( + public config: ResolvedConfig, + public moduleGraph?: ModuleGraph, + public watcher?: FSWatcher, + public plugins = config.plugins, + ) { + this.minimalContext = { + meta: { + rollupVersion, + watchMode: true, + }, + debug: noop, + info: noop, + warn: noop, + // @ts-expect-error noop + error: noop, + } + const utils = createPluginHookUtils(plugins) + this.getSortedPlugins = utils.getSortedPlugins + this.getSortedPluginHooks = utils.getSortedPluginHooks } - function warnIncompatibleMethod(method: string, plugin: string) { - logger.warn( - colors.cyan(`[plugin:${plugin}] `) + - colors.yellow( - `context method ${colors.bold( - `${method}()`, - )} is not supported in serve mode. This plugin is likely not vite-compatible.`, - ), - ) + private _updateModuleLoadAddedImports( + id: string, + addedImports: Set | null, + ): void { + const module = this.moduleGraph?.getModuleById(id) + if (module) { + this._moduleNodeToLoadAddedImports.set(module, addedImports) + } + } + + private _getAddedImports(id: string): Set | null { + const module = this.moduleGraph?.getModuleById(id) + return module + ? this._moduleNodeToLoadAddedImports.get(module) || null + : null + } + + getModuleInfo(id: string): ModuleInfo | null { + const module = this.moduleGraph?.getModuleById(id) + if (!module) { + return null + } + if (!module.info) { + module.info = new Proxy( + { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, + // throw when an unsupported ModuleInfo property is accessed, + // so that incompatible plugins fail in a non-cryptic way. + { + get(info: any, key: string) { + if (key in info) { + return info[key] + } + // Don't throw an error when returning from an async function + if (key === 'then') { + return undefined + } + throw Error( + `[vite] The "${key}" property of ModuleInfo is not supported.`, + ) + }, + }, + ) + } + return module.info ?? null + } + + // keeps track of hook promises so that we can wait for them all to finish upon closing the server + private handleHookPromise(maybePromise: undefined | T | Promise) { + if (!(maybePromise as any)?.then) { + return maybePromise + } + const promise = maybePromise as Promise + this._processesing.add(promise) + return promise.finally(() => this._processesing.delete(promise)) + } + + get options(): InputOptions { + return this._resolvedRollupOptions! + } + + async resolveRollupOptions(): Promise { + if (!this._resolvedRollupOptions) { + let options = this.config.build.rollupOptions + for (const optionsHook of this.getSortedPluginHooks('options')) { + if (this._closed) { + throwClosedServerError() + } + options = + (await this.handleHookPromise( + optionsHook.call(this.minimalContext, options), + )) || options + } + this._resolvedRollupOptions = options + } + return this._resolvedRollupOptions + } + + private _getPluginContext(plugin: Plugin, ssr: boolean) { + const map = ssr ? this._pluginContextMapSsr : this._pluginContextMap + if (!map.has(plugin)) { + const ctx = new PluginContext(plugin, this, ssr) + map.set(plugin, ctx) + } + return map.get(plugin)! } // parallel, ignores returns - async function hookParallel( + private async hookParallel( hookName: H, context: (plugin: Plugin) => ThisType, args: (plugin: Plugin) => Parameters, ): Promise { const parallelPromises: Promise[] = [] - for (const plugin of getSortedPlugins(hookName)) { + for (const plugin of this.getSortedPlugins(hookName)) { // Don't throw here if closed, so buildEnd and closeBundle hooks can finish running const hook = plugin[hookName] if (!hook) continue @@ -233,208 +284,384 @@ export async function createPluginContainer( await Promise.all(parallelPromises) } - // throw when an unsupported ModuleInfo property is accessed, - // so that incompatible plugins fail in a non-cryptic way. - const ModuleInfoProxy: ProxyHandler = { - get(info: any, key: string) { - if (key in info) { - return info[key] - } - // Don't throw an error when returning from an async function - if (key === 'then') { - return undefined - } - throw Error( - `[vite] The "${key}" property of ModuleInfo is not supported.`, - ) - }, + async buildStart(_options?: InputOptions): Promise { + await this.handleHookPromise( + this.hookParallel( + 'buildStart', + (plugin) => this._getPluginContext(plugin, false), + () => [this.options as NormalizedInputOptions], + ), + ) } - // same default value of "moduleInfo.meta" as in Rollup - const EMPTY_OBJECT = Object.freeze({}) + async resolveId( + rawId: string, + importer: string | undefined = join(this.config.root, 'index.html'), + options?: { + attributes?: Record + custom?: CustomPluginOptions + skip?: Set + ssr?: boolean + /** + * @internal + */ + scan?: boolean + isEntry?: boolean + }, + ): Promise { + const skip = options?.skip + const ssr = options?.ssr + const scan = !!options?.scan + const ctx = new ResolveIdContext(this, !!ssr, skip, scan) + + const resolveStart = debugResolve ? performance.now() : 0 + let id: string | null = null + const partial: Partial = {} + + for (const plugin of this.getSortedPlugins('resolveId')) { + if (this._closed && !ssr) throwClosedServerError() + if (!plugin.resolveId) continue + if (skip?.has(plugin)) continue + + ctx._plugin = plugin + + const pluginResolveStart = debugPluginResolve ? performance.now() : 0 + const handler = getHookHandler(plugin.resolveId) + const result = await this.handleHookPromise( + handler.call(ctx as any, rawId, importer, { + attributes: options?.attributes ?? {}, + custom: options?.custom, + isEntry: !!options?.isEntry, + ssr, + scan, + }), + ) + if (!result) continue + + if (typeof result === 'string') { + id = result + } else { + id = result.id + Object.assign(partial, result) + } - function getModuleInfo(id: string) { - const module = moduleGraph?.getModuleById(id) - if (!module) { - return null - } - if (!module.info) { - module.info = new Proxy( - { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, - ModuleInfoProxy, + debugPluginResolve?.( + timeFrom(pluginResolveStart), + plugin.name, + prettifyUrl(id, this.config.root), ) + + // resolveId() is hookFirst - first non-null result is returned. + break } - return module.info - } - function updateModuleInfo(id: string, { meta }: { meta?: object | null }) { - if (meta) { - const moduleInfo = getModuleInfo(id) - if (moduleInfo) { - moduleInfo.meta = { ...moduleInfo.meta, ...meta } + if (debugResolve && rawId !== id && !rawId.startsWith(FS_PREFIX)) { + const key = rawId + id + // avoid spamming + if (!this._seenResolves[key]) { + this._seenResolves[key] = true + debugResolve( + `${timeFrom(resolveStart)} ${colors.cyan(rawId)} -> ${colors.dim( + id, + )}`, + ) } } - } - function updateModuleLoadAddedImports(id: string, ctx: Context) { - const module = moduleGraph?.getModuleById(id) - if (module) { - moduleNodeToLoadAddedImports.set(module, ctx._addedImports) + if (id) { + partial.id = isExternalUrl(id) ? id : normalizePath(id) + return partial as PartialResolvedId + } else { + return null } } - // we should create a new context for each async hook pipeline so that the - // active plugin in that pipeline can be tracked in a concurrency-safe manner. - // using a class to make creating new contexts more efficient - class Context implements PluginContext { - meta = minimalContext.meta - ssr = false - _scan = false - _activePlugin: Plugin | null - _activeId: string | null = null - _activeCode: string | null = null - _resolveSkips?: Set - _addedImports: Set | null = null - - constructor(initialPlugin?: Plugin) { - this._activePlugin = initialPlugin || null + async load( + id: string, + options?: { + ssr?: boolean + }, + ): Promise { + const ssr = options?.ssr + const ctx = new LoadPluginContext(this, !!ssr) + + for (const plugin of this.getSortedPlugins('load')) { + if (this._closed && !ssr) throwClosedServerError() + if (!plugin.load) continue + ctx._plugin = plugin + const handler = getHookHandler(plugin.load) + const result = await this.handleHookPromise( + handler.call(ctx as any, id, { ssr }), + ) + if (result != null) { + if (isObject(result)) { + ctx._updateModuleInfo(id, result) + } + this._updateModuleLoadAddedImports(id, ctx._addedImports) + return result + } } + this._updateModuleLoadAddedImports(id, ctx._addedImports) + return null + } - parse(code: string, opts: any) { - return rollupParseAst(code, opts) - } + async transform( + code: string, + id: string, + options?: { + ssr?: boolean + inMap?: SourceDescription['map'] + }, + ): Promise<{ code: string; map: SourceMap | { mappings: '' } | null }> { + const inMap = options?.inMap + const ssr = options?.ssr + + const ctx = new TransformPluginContext( + this, + id, + code, + inMap as SourceMap, + !!ssr, + ) + ctx._addedImports = this._getAddedImports(id) - async resolve( - id: string, - importer?: string, - options?: { - attributes?: Record - custom?: CustomPluginOptions - isEntry?: boolean - skipSelf?: boolean - }, - ) { - let skip: Set | undefined - if (options?.skipSelf !== false && this._activePlugin) { - skip = new Set(this._resolveSkips) - skip.add(this._activePlugin) + for (const plugin of this.getSortedPlugins('transform')) { + if (this._closed && !ssr) throwClosedServerError() + if (!plugin.transform) continue + + ctx._updateActiveInfo(plugin, id, code) + + const start = debugPluginTransform ? performance.now() : 0 + let result: TransformResult | string | undefined + const handler = getHookHandler(plugin.transform) + try { + result = await this.handleHookPromise( + handler.call(ctx as any, code, id, { ssr }), + ) + } catch (e) { + ctx.error(e) } - let out = await container.resolveId(id, importer, { - attributes: options?.attributes, - custom: options?.custom, - isEntry: !!options?.isEntry, - skip, - ssr: this.ssr, - scan: this._scan, - }) - if (typeof out === 'string') out = { id: out } - return out as ResolvedId | null + if (!result) continue + debugPluginTransform?.( + timeFrom(start), + plugin.name, + prettifyUrl(id, this.config.root), + ) + if (isObject(result)) { + if (result.code !== undefined) { + code = result.code + if (result.map) { + if (debugSourcemapCombine) { + // @ts-expect-error inject plugin name for debug purpose + result.map.name = plugin.name + } + ctx.sourcemapChain.push(result.map) + } + } + ctx._updateModuleInfo(id, result) + } else { + code = result + } + } + return { + code, + map: ctx._getCombinedSourcemap(), } + } - async load( - options: { - id: string - resolveDependencies?: boolean - } & Partial>, - ): Promise { - // We may not have added this to our module graph yet, so ensure it exists - await moduleGraph?.ensureEntryFromUrl(unwrapId(options.id), this.ssr) - // Not all options passed to this function make sense in the context of loading individual files, - // but we can at least update the module info properties we support - updateModuleInfo(options.id, options) - - const loadResult = await container.load(options.id, { ssr: this.ssr }) - const code = - typeof loadResult === 'object' ? loadResult?.code : loadResult - if (code != null) { - await container.transform(code, options.id, { ssr: this.ssr }) - } + async watchChange( + id: string, + change: { event: 'create' | 'update' | 'delete' }, + ): Promise { + await this.hookParallel( + 'watchChange', + (plugin) => this._getPluginContext(plugin, false), + () => [id, change], + ) + } - const moduleInfo = this.getModuleInfo(options.id) - // This shouldn't happen due to calling ensureEntryFromUrl, but 1) our types can't ensure that - // and 2) moduleGraph may not have been provided (though in the situations where that happens, - // we should never have plugins calling this.load) - if (!moduleInfo) - throw Error(`Failed to load module with id ${options.id}`) - return moduleInfo - } + async close(): Promise { + if (this._closed) return + this._closed = true + await Promise.allSettled(Array.from(this._processesing)) + await this.hookParallel( + 'buildEnd', + (plugin) => this._getPluginContext(plugin, false), + () => [], + ) + await this.hookParallel( + 'closeBundle', + (plugin) => this._getPluginContext(plugin, false), + () => [], + ) + } +} - getModuleInfo(id: string) { - return getModuleInfo(id) - } +class PluginContext implements Omit { + protected _scan = false + protected _resolveSkips?: Set + protected _activeId: string | null = null + protected _activeCode: string | null = null - getModuleIds() { - return moduleGraph - ? moduleGraph.idToModuleMap.keys() - : Array.prototype[Symbol.iterator]() - } + meta: RollupPluginContext['meta'] - addWatchFile(id: string) { - watchFiles.add(id) - ;(this._addedImports || (this._addedImports = new Set())).add(id) - if (watcher) ensureWatchedFile(watcher, id, root) - } + constructor( + public _plugin: Plugin, + public _container: PluginContainer, + public ssr: boolean, + ) { + this.meta = this._container.minimalContext.meta + } - getWatchFiles() { - return [...watchFiles] - } + parse(code: string, opts: any): ReturnType { + return rollupParseAst(code, opts) + } - emitFile(assetOrFile: EmittedFile) { - warnIncompatibleMethod(`emitFile`, this._activePlugin!.name) - return '' + getModuleInfo(id: string): ModuleInfo | null { + return this._container.getModuleInfo(id) + } + + async resolve( + id: string, + importer?: string, + options?: { + attributes?: Record + custom?: CustomPluginOptions + isEntry?: boolean + skipSelf?: boolean + }, + ): ReturnType { + let skip: Set | undefined + if (options?.skipSelf !== false && this._plugin) { + skip = new Set(this._resolveSkips) + skip.add(this._plugin) } + let out = await this._container.resolveId(id, importer, { + attributes: options?.attributes, + custom: options?.custom, + isEntry: !!options?.isEntry, + skip, + ssr: this.ssr, + scan: this._scan, + }) + if (typeof out === 'string') out = { id: out } + return out as ResolvedId | null + } - setAssetSource() { - warnIncompatibleMethod(`setAssetSource`, this._activePlugin!.name) + async load( + options: { + id: string + resolveDependencies?: boolean + } & Partial>, + ): Promise { + // We may not have added this to our module graph yet, so ensure it exists + await this._container.moduleGraph?.ensureEntryFromUrl( + unwrapId(options.id), + this.ssr, + ) + // Not all options passed to this function make sense in the context of loading individual files, + // but we can at least update the module info properties we support + this._updateModuleInfo(options.id, options) + + const loadResult = await this._container.load(options.id, { + ssr: this.ssr, + }) + const code = typeof loadResult === 'object' ? loadResult?.code : loadResult + if (code != null) { + await this._container.transform(code, options.id, { ssr: this.ssr }) } - getFileName() { - warnIncompatibleMethod(`getFileName`, this._activePlugin!.name) - return '' + const moduleInfo = this.getModuleInfo(options.id) + // This shouldn't happen due to calling ensureEntryFromUrl, but 1) our types can't ensure that + // and 2) moduleGraph may not have been provided (though in the situations where that happens, + // we should never have plugins calling this.load) + if (!moduleInfo) throw Error(`Failed to load module with id ${options.id}`) + return moduleInfo + } + + _updateModuleInfo(id: string, { meta }: { meta?: object | null }): void { + if (meta) { + const moduleInfo = this.getModuleInfo(id) + if (moduleInfo) { + moduleInfo.meta = { ...moduleInfo.meta, ...meta } + } } + } - warn( - e: string | RollupLog | (() => string | RollupLog), - position?: number | { column: number; line: number }, - ) { - const err = formatError(typeof e === 'function' ? e() : e, position, this) - const msg = buildErrorMessage( - err, - [colors.yellow(`warning: ${err.message}`)], - false, + getModuleIds(): IterableIterator { + return this._container.moduleGraph + ? this._container.moduleGraph.idToModuleMap.keys() + : Array.prototype[Symbol.iterator]() + } + + addWatchFile(id: string): void { + this._container.watchFiles.add(id) + if (this._container.watcher) + ensureWatchedFile( + this._container.watcher, + id, + this._container.config.root, ) - logger.warn(msg, { - clear: true, - timestamp: true, - }) - } + } - error( - e: string | RollupError, - position?: number | { column: number; line: number }, - ): never { - // error thrown here is caught by the transform middleware and passed on - // the the error middleware. - throw formatError(e, position, this) - } + getWatchFiles(): string[] { + return [...this._container.watchFiles] + } - debug = noop - info = noop + emitFile(assetOrFile: EmittedFile): string { + this._warnIncompatibleMethod(`emitFile`) + return '' } - function formatError( + setAssetSource(): void { + this._warnIncompatibleMethod(`setAssetSource`) + } + + getFileName(): string { + this._warnIncompatibleMethod(`getFileName`) + return '' + } + + warn( + e: string | RollupLog | (() => string | RollupLog), + position?: number | { column: number; line: number }, + ): void { + const err = this._formatError(typeof e === 'function' ? e() : e, position) + const msg = buildErrorMessage( + err, + [colors.yellow(`warning: ${err.message}`)], + false, + ) + this._container.config.logger.warn(msg, { + clear: true, + timestamp: true, + }) + } + + error( + e: string | RollupError, + position?: number | { column: number; line: number }, + ): never { + // error thrown here is caught by the transform middleware and passed on + // the the error middleware. + throw this._formatError(e, position) + } + + debug = noop + info = noop + + private _formatError( e: string | RollupError, position: number | { column: number; line: number } | undefined, - ctx: Context, - ) { + ): RollupError { const err = (typeof e === 'string' ? new Error(e) : e) as RollupError if (err.pluginCode) { return err // The plugin likely called `this.error` } - if (ctx._activePlugin) err.plugin = ctx._activePlugin.name - if (ctx._activeId && !err.id) err.id = ctx._activeId - if (ctx._activeCode) { - err.pluginCode = ctx._activeCode + if (this._plugin) err.plugin = this._plugin.name + if (this._activeId && !err.id) err.id = this._activeId + if (this._activeCode) { + err.pluginCode = this._activeCode // some rollup plugins, e.g. json, sets err.position instead of err.pos const pos = position ?? err.pos ?? (err as any).position @@ -442,9 +669,9 @@ export async function createPluginContainer( if (pos != null) { let errLocation try { - errLocation = numberToPos(ctx._activeCode, pos) + errLocation = numberToPos(this._activeCode, pos) } catch (err2) { - logger.error( + this._container.config.logger.error( colors.red( `Error in error handler:\n${err2.stack || err2.message}\n`, ), @@ -457,11 +684,11 @@ export async function createPluginContainer( file: err.id, ...errLocation, } - err.frame = err.frame || generateCodeFrame(ctx._activeCode, pos) + err.frame = err.frame || generateCodeFrame(this._activeCode, pos) } else if (err.loc) { // css preprocessors may report errors in an included file if (!err.frame) { - let code = ctx._activeCode + let code = this._activeCode if (err.loc.file) { err.id = normalizePath(err.loc.file) try { @@ -476,15 +703,16 @@ export async function createPluginContainer( line: (err as any).line, column: (err as any).column, } - err.frame = err.frame || generateCodeFrame(ctx._activeCode, err.loc) + err.frame = err.frame || generateCodeFrame(this._activeCode, err.loc) } + // TODO: move it to overrides if ( - ctx instanceof TransformContext && + this instanceof TransformPluginContext && typeof err.loc?.line === 'number' && typeof err.loc?.column === 'number' ) { - const rawSourceMap = ctx._getCombinedSourcemap() + const rawSourceMap = this._getCombinedSourcemap() if (rawSourceMap && 'version' in rawSourceMap) { const traced = new TraceMap(rawSourceMap as any) const { source, line, column } = originalPositionFor(traced, { @@ -524,313 +752,164 @@ export async function createPluginContainer( return err } - class TransformContext extends Context { - filename: string - originalCode: string - originalSourcemap: SourceMap | null = null - sourcemapChain: NonNullable[] = [] - combinedMap: SourceMap | { mappings: '' } | null = null - - constructor(id: string, code: string, inMap?: SourceMap | string) { - super() - this.filename = id - this.originalCode = code - if (inMap) { - if (debugSourcemapCombine) { - // @ts-expect-error inject name for debug purpose - inMap.name = '$inMap' - } - this.sourcemapChain.push(inMap) - } - // Inherit `_addedImports` from the `load()` hook - const node = moduleGraph?.getModuleById(id) - if (node) { - this._addedImports = moduleNodeToLoadAddedImports.get(node) ?? null - } - } + _warnIncompatibleMethod(method: string): void { + this._container.config.logger.warn( + colors.cyan(`[plugin:${this._plugin.name}] `) + + colors.yellow( + `context method ${colors.bold( + `${method}()`, + )} is not supported in serve mode. This plugin is likely not vite-compatible.`, + ), + ) + } +} - _getCombinedSourcemap() { - if ( - debugSourcemapCombine && - debugSourcemapCombineFilter && - this.filename.includes(debugSourcemapCombineFilter) - ) { - debugSourcemapCombine('----------', this.filename) - debugSourcemapCombine(this.combinedMap) - debugSourcemapCombine(this.sourcemapChain) - debugSourcemapCombine('----------') - } +class ResolveIdContext extends PluginContext { + constructor( + container: PluginContainer, + ssr: boolean, + skip: Set | undefined, + scan: boolean, + ) { + super(null!, container, ssr) + this._resolveSkips = skip + this._scan = scan + } +} - let combinedMap = this.combinedMap - // { mappings: '' } - if ( - combinedMap && - !('version' in combinedMap) && - combinedMap.mappings === '' - ) { - this.sourcemapChain.length = 0 - return combinedMap - } +class LoadPluginContext extends PluginContext { + _addedImports: Set | null = null - for (let m of this.sourcemapChain) { - if (typeof m === 'string') m = JSON.parse(m) - if (!('version' in (m as SourceMap))) { - // { mappings: '' } - if ((m as SourceMap).mappings === '') { - combinedMap = { mappings: '' } - break - } - // empty, nullified source map - combinedMap = null - break - } - if (!combinedMap) { - const sm = m as SourceMap - // sourcemap should not include `sources: [null]` (because `sources` should be string) nor - // `sources: ['']` (because `''` means the path of sourcemap) - // but MagicString generates this when `filename` option is not set. - // Rollup supports these and therefore we support this as well - if (sm.sources.length === 1 && !sm.sources[0]) { - combinedMap = { - ...sm, - sources: [this.filename], - sourcesContent: [this.originalCode], - } - } else { - combinedMap = sm - } - } else { - combinedMap = combineSourcemaps(cleanUrl(this.filename), [ - m as RawSourceMap, - combinedMap as RawSourceMap, - ]) as SourceMap - } - } - if (combinedMap !== this.combinedMap) { - this.combinedMap = combinedMap - this.sourcemapChain.length = 0 - } - return this.combinedMap - } - - getCombinedSourcemap() { - const map = this._getCombinedSourcemap() - if (!map || (!('version' in map) && map.mappings === '')) { - return new MagicString(this.originalCode).generateMap({ - includeContent: true, - hires: 'boundary', - source: cleanUrl(this.filename), - }) - } - return map - } + constructor(container: PluginContainer, ssr: boolean) { + super(null!, container, ssr) } - let closed = false - const processesing = new Set>() - // keeps track of hook promises so that we can wait for them all to finish upon closing the server - function handleHookPromise(maybePromise: undefined | T | Promise) { - if (!(maybePromise as any)?.then) { - return maybePromise + override addWatchFile(id: string): void { + if (!this._addedImports) { + this._addedImports = new Set() } - const promise = maybePromise as Promise - processesing.add(promise) - return promise.finally(() => processesing.delete(promise)) + this._addedImports.add(id) + super.addWatchFile(id) } +} - const container: PluginContainer = { - options: await (async () => { - let options = rollupOptions - for (const optionsHook of getSortedPluginHooks('options')) { - if (closed) throwClosedServerError() - options = - (await handleHookPromise( - optionsHook.call(minimalContext, options), - )) || options +class TransformPluginContext + extends LoadPluginContext + implements Omit +{ + filename: string + originalCode: string + originalSourcemap: SourceMap | null = null + sourcemapChain: NonNullable[] = [] + combinedMap: SourceMap | { mappings: '' } | null = null + + constructor( + container: PluginContainer, + id: string, + code: string, + inMap: SourceMap | string | undefined, + ssr: boolean, + ) { + super(container, ssr) + + this.filename = id + this.originalCode = code + if (inMap) { + if (debugSourcemapCombine) { + // @ts-expect-error inject name for debug purpose + inMap.name = '$inMap' } - return options - })(), - - getModuleInfo, + this.sourcemapChain.push(inMap) + } + } - async buildStart() { - await handleHookPromise( - hookParallel( - 'buildStart', - (plugin) => new Context(plugin), - () => [container.options as NormalizedInputOptions], - ), - ) - }, + _getCombinedSourcemap(): SourceMap { + if ( + debugSourcemapCombine && + debugSourcemapCombineFilter && + this.filename.includes(debugSourcemapCombineFilter) + ) { + debugSourcemapCombine('----------', this.filename) + debugSourcemapCombine(this.combinedMap) + debugSourcemapCombine(this.sourcemapChain) + debugSourcemapCombine('----------') + } - async resolveId(rawId, importer = join(root, 'index.html'), options) { - const skip = options?.skip - const ssr = options?.ssr - const scan = !!options?.scan - const ctx = new Context() - ctx.ssr = !!ssr - ctx._scan = scan - ctx._resolveSkips = skip - const resolveStart = debugResolve ? performance.now() : 0 - let id: string | null = null - const partial: Partial = {} - for (const plugin of getSortedPlugins('resolveId')) { - if (closed && !ssr) throwClosedServerError() - if (!plugin.resolveId) continue - if (skip?.has(plugin)) continue - - ctx._activePlugin = plugin - - const pluginResolveStart = debugPluginResolve ? performance.now() : 0 - const handler = getHookHandler(plugin.resolveId) - const result = await handleHookPromise( - handler.call(ctx as any, rawId, importer, { - attributes: options?.attributes ?? {}, - custom: options?.custom, - isEntry: !!options?.isEntry, - ssr, - scan, - }), - ) - if (!result) continue + let combinedMap = this.combinedMap + // { mappings: '' } + if ( + combinedMap && + !('version' in combinedMap) && + combinedMap.mappings === '' + ) { + this.sourcemapChain.length = 0 + return combinedMap as SourceMap + } - if (typeof result === 'string') { - id = result - } else { - id = result.id - Object.assign(partial, result) + for (let m of this.sourcemapChain) { + if (typeof m === 'string') m = JSON.parse(m) + if (!('version' in (m as SourceMap))) { + // { mappings: '' } + if ((m as SourceMap).mappings === '') { + combinedMap = { mappings: '' } + break } - - debugPluginResolve?.( - timeFrom(pluginResolveStart), - plugin.name, - prettifyUrl(id, root), - ) - - // resolveId() is hookFirst - first non-null result is returned. + // empty, nullified source map + combinedMap = null break } - - if (debugResolve && rawId !== id && !rawId.startsWith(FS_PREFIX)) { - const key = rawId + id - // avoid spamming - if (!seenResolves[key]) { - seenResolves[key] = true - debugResolve( - `${timeFrom(resolveStart)} ${colors.cyan(rawId)} -> ${colors.dim( - id, - )}`, - ) - } - } - - if (id) { - partial.id = isExternalUrl(id) ? id : normalizePath(id) - return partial as PartialResolvedId - } else { - return null - } - }, - - async load(id, options) { - const ssr = options?.ssr - const ctx = new Context() - ctx.ssr = !!ssr - for (const plugin of getSortedPlugins('load')) { - if (closed && !ssr) throwClosedServerError() - if (!plugin.load) continue - ctx._activePlugin = plugin - const handler = getHookHandler(plugin.load) - const result = await handleHookPromise( - handler.call(ctx as any, id, { ssr }), - ) - if (result != null) { - if (isObject(result)) { - updateModuleInfo(id, result) + if (!combinedMap) { + const sm = m as SourceMap + // sourcemap should not include `sources: [null]` (because `sources` should be string) nor + // `sources: ['']` (because `''` means the path of sourcemap) + // but MagicString generates this when `filename` option is not set. + // Rollup supports these and therefore we support this as well + if (sm.sources.length === 1 && !sm.sources[0]) { + combinedMap = { + ...sm, + sources: [this.filename], + sourcesContent: [this.originalCode], } - updateModuleLoadAddedImports(id, ctx) - return result - } - } - updateModuleLoadAddedImports(id, ctx) - return null - }, - - async transform(code, id, options) { - const inMap = options?.inMap - const ssr = options?.ssr - const ctx = new TransformContext(id, code, inMap as SourceMap) - ctx.ssr = !!ssr - for (const plugin of getSortedPlugins('transform')) { - if (closed && !ssr) throwClosedServerError() - if (!plugin.transform) continue - ctx._activePlugin = plugin - ctx._activeId = id - ctx._activeCode = code - const start = debugPluginTransform ? performance.now() : 0 - let result: TransformResult | string | undefined - const handler = getHookHandler(plugin.transform) - try { - result = await handleHookPromise( - handler.call(ctx as any, code, id, { ssr }), - ) - } catch (e) { - ctx.error(e) - } - if (!result) continue - debugPluginTransform?.( - timeFrom(start), - plugin.name, - prettifyUrl(id, root), - ) - if (isObject(result)) { - if (result.code !== undefined) { - code = result.code - if (result.map) { - if (debugSourcemapCombine) { - // @ts-expect-error inject plugin name for debug purpose - result.map.name = plugin.name - } - ctx.sourcemapChain.push(result.map) - } - } - updateModuleInfo(id, result) } else { - code = result + combinedMap = sm } + } else { + combinedMap = combineSourcemaps(cleanUrl(this.filename), [ + m as RawSourceMap, + combinedMap as RawSourceMap, + ]) as SourceMap } - return { - code, - map: ctx._getCombinedSourcemap(), - } - }, + } + if (combinedMap !== this.combinedMap) { + this.combinedMap = combinedMap + this.sourcemapChain.length = 0 + } + return this.combinedMap as SourceMap + } - async watchChange(id, change) { - const ctx = new Context() - await hookParallel( - 'watchChange', - () => ctx, - () => [id, change], - ) - }, + getCombinedSourcemap(): SourceMap { + const map = this._getCombinedSourcemap() as SourceMap | { mappings: '' } + if (!map || (!('version' in map) && map.mappings === '')) { + return new MagicString(this.originalCode).generateMap({ + includeContent: true, + hires: 'boundary', + source: cleanUrl(this.filename), + }) + } + return map + } - async close() { - if (closed) return - closed = true - await Promise.allSettled(Array.from(processesing)) - const ctx = new Context() - await hookParallel( - 'buildEnd', - () => ctx, - () => [], - ) - await hookParallel( - 'closeBundle', - () => ctx, - () => [], - ) - }, + _updateActiveInfo(plugin: Plugin, id: string, code: string): void { + this._plugin = plugin + this._activeId = id + this._activeCode = code } +} - return container +// We only expose the types but not the implementations +export type { + PluginContainer, + PluginContext, + TransformPluginContext, + TransformResult, } diff --git a/packages/vite/src/node/server/sourcemap.ts b/packages/vite/src/node/server/sourcemap.ts index eafde8c25b67d1..684dff128e597d 100644 --- a/packages/vite/src/node/server/sourcemap.ts +++ b/packages/vite/src/node/server/sourcemap.ts @@ -1,8 +1,10 @@ import path from 'node:path' import fsp from 'node:fs/promises' +import convertSourceMap from 'convert-source-map' import type { ExistingRawSourceMap, SourceMap } from 'rollup' import type { Logger } from '../logger' -import { createDebugger } from '../utils' +import { blankReplacer, createDebugger } from '../utils' +import { cleanUrl } from '../../shared/utils' const debug = createDebugger('vite:sourcemap', { onlyWhenFocused: true, @@ -43,7 +45,7 @@ export async function injectSourcesContent( for (let index = 0; index < map.sources.length; index++) { const sourcePath = map.sources[index] if ( - !sourcesContent[index] && + sourcesContent[index] == null && sourcePath && !virtualSourceRE.test(sourcePath) ) { @@ -52,7 +54,7 @@ export async function injectSourcesContent( // inject content from source file when sourcesContent is null sourceRootPromise ??= computeSourceRoute(map, file) const sourceRoot = await sourceRootPromise - let resolvedSourcePath = decodeURI(sourcePath) + let resolvedSourcePath = cleanUrl(decodeURI(sourcePath)) if (sourceRoot) { resolvedSourcePath = path.resolve(sourceRoot, resolvedSourcePath) } @@ -143,3 +145,32 @@ export function applySourcemapIgnoreList( if (!map.x_google_ignoreList) map.x_google_ignoreList = x_google_ignoreList } } + +export async function extractSourcemapFromFile( + code: string, + filePath: string, +): Promise<{ code: string; map: SourceMap } | undefined> { + const map = ( + convertSourceMap.fromSource(code) || + (await convertSourceMap.fromMapFileSource( + code, + createConvertSourceMapReadMap(filePath), + )) + )?.toObject() + + if (map) { + return { + code: code.replace(convertSourceMap.mapFileCommentRegex, blankReplacer), + map, + } + } +} + +function createConvertSourceMapReadMap(originalFileName: string) { + return (filename: string) => { + return fsp.readFile( + path.resolve(path.dirname(originalFileName), filename), + 'utf-8', + ) + } +} diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index 2813128af5b1dd..94e8c124041077 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -2,14 +2,12 @@ import fsp from 'node:fs/promises' import path from 'node:path' import { performance } from 'node:perf_hooks' import getEtag from 'etag' -import convertSourceMap from 'convert-source-map' import MagicString from 'magic-string' import { init, parse as parseImports } from 'es-module-lexer' import type { PartialResolvedId, SourceDescription, SourceMap } from 'rollup' import colors from 'picocolors' import type { ModuleNode, ViteDevServer } from '..' import { - blankReplacer, createDebugger, ensureWatchedFile, injectQuery, @@ -24,7 +22,11 @@ import { checkPublicFile } from '../publicDir' import { isDepsOptimizerEnabled } from '../config' import { getDepsOptimizer, initDevSsrDepsOptimizer } from '../optimizer' import { cleanUrl, unwrapId } from '../../shared/utils' -import { applySourcemapIgnoreList, injectSourcesContent } from './sourcemap' +import { + applySourcemapIgnoreList, + extractSourcemapFromFile, + injectSourcesContent, +} from './sourcemap' import { isFileServingAllowed } from './middlewares/static' import { throwClosedServerError } from './pluginContainer' @@ -181,7 +183,15 @@ async function doTransform( resolved, ) - getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, () => result) + if (!ssr) { + // Only register client requests, server.waitForRequestsIdle should + // have been called server.waitForClientRequestsIdle. We can rename + // it as part of the environment API work + const depsOptimizer = getDepsOptimizer(config, ssr) + if (!depsOptimizer?.isOptimizedDepFile(id)) { + server._registerRequestProcessing(id, () => result) + } + } return result } @@ -260,21 +270,19 @@ async function loadAndTransform( throw e } } - ensureWatchedFile(server.watcher, file, config.root) + if (code != null) { + ensureWatchedFile(server.watcher, file, config.root) + } } if (code) { try { - map = ( - convertSourceMap.fromSource(code) || - (await convertSourceMap.fromMapFileSource( - code, - createConvertSourceMapReadMap(file), - )) - )?.toObject() - - code = code.replace(convertSourceMap.mapFileCommentRegex, blankReplacer) + const extracted = await extractSourcemapFromFile(code, file) + if (extracted) { + code = extracted.code + map = extracted.map + } } catch (e) { - logger.warn(`Failed to load source map for ${url}.`, { + logger.warn(`Failed to load source map for ${file}.\n${e}`, { timestamp: true, }) } @@ -403,15 +411,6 @@ async function loadAndTransform( return result } -function createConvertSourceMapReadMap(originalFileName: string) { - return (filename: string) => { - return fsp.readFile( - path.resolve(path.dirname(originalFileName), filename), - 'utf-8', - ) - } -} - /** * When a module is soft-invalidated, we can preserve its previous `transformResult` and * return similar code to before: diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 6b70d1fbea5e77..d0bffcdce4f8a0 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -85,11 +85,31 @@ const wsServerEvents = [ 'message', ] +function noop() { + // noop +} + export function createWebSocketServer( server: HttpServer | null, config: ResolvedConfig, httpsOptions?: HttpsServerOptions, ): WebSocketServer { + if (config.server.ws === false) { + return { + name: 'ws', + get clients() { + return new Set() + }, + async close() { + // noop + }, + on: noop as any as WebSocketServer['on'], + off: noop as any as WebSocketServer['off'], + listen: noop, + send: noop, + } + } + let wss: WebSocketServerRaw_ let wsHttpServer: Server | undefined = undefined diff --git a/packages/vite/src/node/ssr/__tests__/fixtures/modules/import-meta.js b/packages/vite/src/node/ssr/__tests__/fixtures/modules/import-meta.js new file mode 100644 index 00000000000000..cdaa5035d7ef4e --- /dev/null +++ b/packages/vite/src/node/ssr/__tests__/fixtures/modules/import-meta.js @@ -0,0 +1,2 @@ +export const dirname = import.meta.dirname +export const filename = import.meta.filename diff --git a/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts index 192b0b8cd3326f..b3f3c2364ef04b 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrLoadModule.spec.ts @@ -52,3 +52,13 @@ test('error has same instance', async () => { expect(e[s]).toBe(true) } }) + +test('import.meta.filename/dirname returns same value with Node', async () => { + const server = await createDevServer() + const moduleRelativePath = '/fixtures/modules/import-meta.js' + const filename = path.resolve(root, '.' + moduleRelativePath) + + const viteValue = await server.ssrLoadModule(moduleRelativePath) + expect(viteValue.dirname).toBe(path.dirname(filename)) + expect(viteValue.filename).toBe(filename) +}) diff --git a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts index 4935c13c9d6b2f..9d1e5403c67845 100644 --- a/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts +++ b/packages/vite/src/node/ssr/__tests__/ssrTransform.spec.ts @@ -104,9 +104,9 @@ test('export * from', async () => { ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue"); __vite_ssr_exportAll__(__vite_ssr_import_0__); + const __vite_ssr_import_1__ = await __vite_ssr_import__("react"); __vite_ssr_exportAll__(__vite_ssr_import_1__); - " `) }) @@ -249,6 +249,59 @@ test('do not rewrite when function declaration is in scope', async () => { expect(result?.deps).toEqual(['vue']) }) +// #16452 +test('do not rewrite when function expression is in scope', async () => { + const result = await ssrTransformSimple( + `import {fn} from './vue';var a = function() { return function fn() { console.log(fn) } }`, + ) + expect(result?.code).toMatchInlineSnapshot(` + "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["fn"]}); + var a = function() { return function fn() { console.log(fn) } }" + `) +}) + +// #16452 +test('do not rewrite when function expression is in global scope', async () => { + const result = await ssrTransformSimple( + `import {fn} from './vue';foo(function fn(a = fn) { console.log(fn) })`, + ) + expect(result?.code).toMatchInlineSnapshot(` + "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["fn"]}); + foo(function fn(a = fn) { console.log(fn) })" + `) +}) + +test('do not rewrite when class declaration is in scope', async () => { + const result = await ssrTransformSimple( + `import { cls } from 'vue';function A(){ class cls {} return { cls }; }`, + ) + expect(result?.code).toMatchInlineSnapshot(` + "const __vite_ssr_import_0__ = await __vite_ssr_import__("vue", {"importedNames":["cls"]}); + function A(){ class cls {} return { cls }; }" + `) + expect(result?.deps).toEqual(['vue']) +}) + +test('do not rewrite when class expression is in scope', async () => { + const result = await ssrTransformSimple( + `import { cls } from './vue';var a = function() { return class cls { constructor() { console.log(cls) } } }`, + ) + expect(result?.code).toMatchInlineSnapshot(` + "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["cls"]}); + var a = function() { return class cls { constructor() { console.log(cls) } } }" + `) +}) + +test('do not rewrite when class expression is in global scope', async () => { + const result = await ssrTransformSimple( + `import { cls } from './vue';foo(class cls { constructor() { console.log(cls) } })`, + ) + expect(result?.code).toMatchInlineSnapshot(` + "const __vite_ssr_import_0__ = await __vite_ssr_import__("./vue", {"importedNames":["cls"]}); + foo(class cls { constructor() { console.log(cls) } })" + `) +}) + test('do not rewrite catch clause', async () => { const result = await ssrTransformSimple( `import {error} from './dependency';try {} catch(error) {}`, @@ -964,14 +1017,14 @@ console.log(foo + 2) `), ).toMatchInlineSnapshot(` "const __vite_ssr_import_0__ = await __vite_ssr_import__("./foo", {"importedNames":["foo"]}); - const __vite_ssr_import_1__ = await __vite_ssr_import__("./a"); - __vite_ssr_exportAll__(__vite_ssr_import_1__); - const __vite_ssr_import_2__ = await __vite_ssr_import__("./b"); - __vite_ssr_exportAll__(__vite_ssr_import_2__); console.log(__vite_ssr_import_0__.foo + 1) + const __vite_ssr_import_1__ = await __vite_ssr_import__("./a"); + __vite_ssr_exportAll__(__vite_ssr_import_1__); + const __vite_ssr_import_2__ = await __vite_ssr_import__("./b"); + __vite_ssr_exportAll__(__vite_ssr_import_2__); console.log(__vite_ssr_import_0__.foo + 2) " diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index ae66d3af596072..60c0cb0a416b57 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -1,6 +1,5 @@ import { pathToFileURL } from 'node:url' import type { ModuleNode, TransformResult, ViteDevServer } from '..' -import type { PackageCache } from '../packages' import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' import { tryNodeResolve } from '../plugins/resolve' import { isBuiltin, isExternalUrl, isFilePathESM } from '../utils' @@ -9,14 +8,8 @@ import { unwrapId } from '../../shared/utils' import { SOURCEMAPPING_URL, VITE_RUNTIME_SOURCEMAPPING_SOURCE, - VITE_RUNTIME_SOURCEMAPPING_URL, } from '../../shared/constants' - -interface NodeImportResolveOptions - extends InternalResolveOptionsWithOverrideConditions { - legacyProxySsrExternalModules?: boolean - packageCache?: PackageCache -} +import { genSourceMapUrl } from '../server/sourcemap' export interface FetchModuleOptions { inlineSourceMap?: boolean @@ -51,7 +44,7 @@ export async function fetchModule( } = server.config const overrideConditions = ssr.resolve?.externalConditions || [] - const resolveOptions: NodeImportResolveOptions = { + const resolveOptions: InternalResolveOptionsWithOverrideConditions = { mainFields: ['main'], conditions: [], overrideConditions: [...overrideConditions, 'production', 'development'], @@ -62,8 +55,6 @@ export async function fetchModule( isProduction, root, ssrConfig: ssr, - legacyProxySsrExternalModules: - server.config.legacy?.proxySsrExternalModules, packageCache: server.config.packageCache, } @@ -148,13 +139,10 @@ function inlineSourceMap( if (OTHER_SOURCE_MAP_REGEXP.test(code)) code = code.replace(OTHER_SOURCE_MAP_REGEXP, '') - const sourceMap = Buffer.from( - JSON.stringify(processSourceMap?.(map) || map), - 'utf-8', - ).toString('base64') + const sourceMap = processSourceMap?.(map) || map result.code = `${code.trimEnd()}\n//# sourceURL=${ mod.id - }\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${VITE_RUNTIME_SOURCEMAPPING_URL};base64,${sourceMap}\n` + }\n${VITE_RUNTIME_SOURCEMAPPING_SOURCE}\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(sourceMap)}\n` return result } diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/circular/circular-index.js b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/circular/circular-index.js index 9fdf137a639c8b..cf87d0e9c4e9f1 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/circular/circular-index.js +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/circular/circular-index.js @@ -3,6 +3,5 @@ export { b } from './circular-b' // since there is no .accept, it does full reload import.meta.hot.on('vite:beforeFullReload', () => { - // eslint-disable-next-line no-console console.log('full reload') }) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/cjs-external-non-existing.js b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/cjs-external-non-existing.js index 2b67706ca1dcfb..905b2492e757d3 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/cjs-external-non-existing.js +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/cjs-external-non-existing.js @@ -1,4 +1,3 @@ import { nonExisting } from '@vitejs/cjs-external' -// eslint-disable-next-line no-console console.log(nonExisting) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-external-non-existing.js b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-external-non-existing.js index 7a1d8a07ebc60a..3d5265ce521c06 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-external-non-existing.js +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/esm-external-non-existing.js @@ -1,4 +1,3 @@ import { nonExisting } from '@vitejs/esm-external' -// eslint-disable-next-line no-console console.log(nonExisting) diff --git a/packages/vite/src/node/ssr/runtime/__tests__/fixtures/has-error-deep.ts b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/has-error-deep.ts new file mode 100644 index 00000000000000..8da094a3fa4800 --- /dev/null +++ b/packages/vite/src/node/ssr/runtime/__tests__/fixtures/has-error-deep.ts @@ -0,0 +1,7 @@ +function crash(message: string) { + throw new Error(message) +} + +export function main(): void { + crash('crash') +} diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts index c482a0464827f4..fd8973235af0b6 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-source-maps.spec.ts @@ -21,6 +21,11 @@ describe('vite-runtime initialization', async () => { const serializeStack = (runtime: ViteRuntime, err: Error) => { return err.stack!.split('\n')[1].replace(runtime.options.root, '') } + const serializeStackDeep = (runtime: ViteRuntime, err: Error) => { + return err + .stack!.split('\n') + .map((s) => s.replace(runtime.options.root, '')) + } it('source maps are correctly applied to stack traces', async ({ runtime, @@ -59,4 +64,16 @@ describe('vite-runtime initialization', async () => { ' at Module.throwError (/fixtures/throws-error-method.ts:11:9)', ) }) + + it('deep stacktrace', async ({ runtime }) => { + const methodError = await getError(async () => { + const mod = await runtime.executeUrl('/fixtures/has-error-deep.ts') + mod.main() + }) + expect(serializeStackDeep(runtime, methodError).slice(0, 3)).toEqual([ + 'Error: crash', + ' at crash (/fixtures/has-error-deep.ts:2:9)', + ' at Module.main (/fixtures/has-error-deep.ts:6:3)', + ]) + }) }) diff --git a/packages/vite/src/node/ssr/ssrFetchModule.ts b/packages/vite/src/node/ssr/ssrFetchModule.ts index 27f9213963918f..d0e1c98cca2569 100644 --- a/packages/vite/src/node/ssr/ssrFetchModule.ts +++ b/packages/vite/src/node/ssr/ssrFetchModule.ts @@ -1,15 +1,8 @@ import type { ViteDevServer } from '../server' import type { FetchResult } from '../../runtime/types' +import { asyncFunctionDeclarationPaddingLineCount } from '../../shared/utils' import { fetchModule } from './fetchModule' -// eslint-disable-next-line @typescript-eslint/no-empty-function -const AsyncFunction = async function () {}.constructor as typeof Function -const fnDeclarationLineCount = (() => { - const body = '/*code*/' - const source = new AsyncFunction('a', 'b', body).toString() - return source.slice(0, source.indexOf(body)).split('\n').length - 1 -})() - export function ssrFetchModule( server: ViteDevServer, id: string, @@ -19,9 +12,8 @@ export function ssrFetchModule( processSourceMap(map) { // this assumes that "new AsyncFunction" is used to create the module return Object.assign({}, map, { - // currently we need to offset the line - // https://github.com/nodejs/node/issues/43047#issuecomment-1180632750 - mappings: ';'.repeat(fnDeclarationLineCount) + map.mappings, + mappings: + ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + map.mappings, }) }, }) diff --git a/packages/vite/src/node/ssr/ssrModuleLoader.ts b/packages/vite/src/node/ssr/ssrModuleLoader.ts index 42f023adb4a82a..1a539d2ff58943 100644 --- a/packages/vite/src/node/ssr/ssrModuleLoader.ts +++ b/packages/vite/src/node/ssr/ssrModuleLoader.ts @@ -7,8 +7,17 @@ import { transformRequest } from '../server/transformRequest' import type { InternalResolveOptionsWithOverrideConditions } from '../plugins/resolve' import { tryNodeResolve } from '../plugins/resolve' import { genSourceMapUrl } from '../server/sourcemap' -import type { PackageCache } from '../packages' -import { unwrapId } from '../../shared/utils' +import { + AsyncFunction, + asyncFunctionDeclarationPaddingLineCount, + isWindows, + unwrapId, +} from '../../shared/utils' +import { + type SSRImportBaseMetadata, + analyzeImportedModDifference, +} from '../../shared/ssrTransform' +import { SOURCEMAPPING_URL } from '../../shared/constants' import { ssrDynamicImportKey, ssrExportAllKey, @@ -27,31 +36,6 @@ type SSRModule = Record interface NodeImportResolveOptions extends InternalResolveOptionsWithOverrideConditions { legacyProxySsrExternalModules?: boolean - packageCache?: PackageCache -} - -interface SSRImportMetadata { - isDynamicImport?: boolean - /** - * Imported names before being transformed to `ssrImportKey` - * - * import foo, { bar as baz, qux } from 'hello' - * => ['default', 'bar', 'qux'] - * - * import * as namespace from 'world - * => undefined - */ - importedNames?: string[] -} - -// eslint-disable-next-line @typescript-eslint/no-empty-function -const AsyncFunction = async function () {}.constructor as typeof Function -let fnDeclarationLineCount = 0 -{ - const body = '/*code*/' - const source = new AsyncFunction('a', 'b', body).toString() - fnDeclarationLineCount = - source.slice(0, source.indexOf(body)).split('\n').length - 1 } const pendingModules = new Map>() @@ -128,7 +112,12 @@ async function instantiateModule( // referenced before it's been instantiated. mod.ssrModule = ssrModule + // replace '/' with '\\' on Windows to match Node.js + const osNormalizedFilename = isWindows ? path.resolve(mod.file!) : mod.file! + const ssrImportMeta = { + dirname: path.dirname(osNormalizedFilename), + filename: osNormalizedFilename, // The filesystem URL, matching native Node.js modules url: pathToFileURL(mod.file!).toString(), } @@ -165,7 +154,7 @@ async function instantiateModule( // account for multiple pending deps and duplicate imports. const pendingDeps: string[] = [] - const ssrImport = async (dep: string, metadata?: SSRImportMetadata) => { + const ssrImport = async (dep: string, metadata?: SSRImportBaseMetadata) => { try { if (dep[0] !== '.' && dep[0] !== '/') { return await nodeImport(dep, mod.file!, resolveOptions, metadata) @@ -227,12 +216,11 @@ async function instantiateModule( let sourceMapSuffix = '' if (result.map && 'version' in result.map) { const moduleSourceMap = Object.assign({}, result.map, { - // currently we need to offset the line - // https://github.com/nodejs/node/issues/43047#issuecomment-1180632750 - mappings: ';'.repeat(fnDeclarationLineCount) + result.map.mappings, + mappings: + ';'.repeat(asyncFunctionDeclarationPaddingLineCount) + + result.map.mappings, }) - sourceMapSuffix = - '\n//# sourceMappingURL=' + genSourceMapUrl(moduleSourceMap) + sourceMapSuffix = `\n//# ${SOURCEMAPPING_URL}=${genSourceMapUrl(moduleSourceMap)}` } try { @@ -289,7 +277,7 @@ async function nodeImport( id: string, importer: string, resolveOptions: NodeImportResolveOptions, - metadata?: SSRImportMetadata, + metadata?: SSRImportBaseMetadata, ) { let url: string let filePath: string | undefined @@ -322,12 +310,13 @@ async function nodeImport( } else if (filePath) { analyzeImportedModDifference( mod, - filePath, id, + isFilePathESM(filePath, resolveOptions.packageCache) + ? 'module' + : undefined, metadata, - resolveOptions.packageCache, ) - return proxyGuardOnlyEsm(mod, id) + return mod } else { return mod } @@ -358,63 +347,3 @@ function proxyESM(mod: any) { function isPrimitive(value: any) { return !value || (typeof value !== 'object' && typeof value !== 'function') } - -/** - * Vite converts `import { } from 'foo'` to `const _ = __vite_ssr_import__('foo')`. - * Top-level imports and dynamic imports work slightly differently in Node.js. - * This function normalizes the differences so it matches prod behaviour. - */ -function analyzeImportedModDifference( - mod: any, - filePath: string, - rawId: string, - metadata?: SSRImportMetadata, - packageCache?: PackageCache, -) { - // No normalization needed if the user already dynamic imports this module - if (metadata?.isDynamicImport) return - // If file path is ESM, everything should be fine - if (isFilePathESM(filePath, packageCache)) return - - // For non-ESM, named imports is done via static analysis with cjs-module-lexer in Node.js. - // If the user named imports a specifier that can't be analyzed, error. - if (metadata?.importedNames?.length) { - const missingBindings = metadata.importedNames.filter((s) => !(s in mod)) - if (missingBindings.length) { - const lastBinding = missingBindings[missingBindings.length - 1] - // Copied from Node.js - throw new SyntaxError(`\ -[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports. -CommonJS modules can always be imported via the default export, for example using: - -import pkg from '${rawId}'; -const {${missingBindings.join(', ')}} = pkg; -`) - } - } -} - -/** - * Guard invalid named exports only, similar to how Node.js errors for top-level imports. - * But since we transform as dynamic imports, we need to emulate the error manually. - */ -function proxyGuardOnlyEsm( - mod: any, - rawId: string, - metadata?: SSRImportMetadata, -) { - // If the module doesn't import anything explicitly, e.g. `import 'foo'` or - // `import * as foo from 'foo'`, we can skip the proxy guard. - if (!metadata?.importedNames?.length) return mod - - return new Proxy(mod, { - get(mod, prop) { - if (prop !== 'then' && !(prop in mod)) { - throw new SyntaxError( - `[vite] The requested module '${rawId}' does not provide an export named '${prop.toString()}'`, - ) - } - return mod[prop] - }, - }) -} diff --git a/packages/vite/src/node/ssr/ssrTransform.ts b/packages/vite/src/node/ssr/ssrTransform.ts index 3668a4a525a267..61849d0e3ca4f2 100644 --- a/packages/vite/src/node/ssr/ssrTransform.ts +++ b/packages/vite/src/node/ssr/ssrTransform.ts @@ -16,6 +16,7 @@ import { parseAstAsync as rollupParseAstAsync } from 'rollup/parseAst' import type { TransformResult } from '../server/transformRequest' import { combineSourcemaps, isDefined } from '../utils' import { isJSONRequest } from '../plugins/json' +import type { DefineImportMetadata } from '../../shared/ssrTransform' type Node = _Node & { start: number @@ -28,19 +29,6 @@ interface TransformOptions { } } -interface DefineImportMetadata { - /** - * Imported names of an import statement, e.g. - * - * import foo, { bar as baz, qux } from 'hello' - * => ['default', 'bar', 'qux'] - * - * import * as namespace from 'world - * => undefined - */ - importedNames?: string[] -} - export const ssrModuleExportsKey = `__vite_ssr_exports__` export const ssrImportKey = `__vite_ssr_import__` export const ssrDynamicImportKey = `__vite_ssr_dynamic_import__` @@ -106,7 +94,11 @@ async function ssrTransformScript( // hoist at the start of the file, after the hashbang const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0 - function defineImport(source: string, metadata?: DefineImportMetadata) { + function defineImport( + index: number, + source: string, + metadata?: DefineImportMetadata, + ) { deps.add(source) const importId = `__vite_ssr_import_${uid++}__` @@ -122,7 +114,7 @@ async function ssrTransformScript( // There will be an error if the module is called before it is imported, // so the module import statement is hoisted to the top s.appendLeft( - hoistIndex, + index, `const ${importId} = await ${ssrImportKey}(${JSON.stringify( source, )}${metadataStr});\n`, @@ -144,7 +136,7 @@ async function ssrTransformScript( // import { baz } from 'foo' --> baz -> __import_foo__.baz // import * as ok from 'foo' --> ok -> __import_foo__ if (node.type === 'ImportDeclaration') { - const importId = defineImport(node.source.value as string, { + const importId = defineImport(hoistIndex, node.source.value as string, { importedNames: node.specifiers .map((s) => { if (s.type === 'ImportSpecifier') return s.imported.name @@ -194,13 +186,16 @@ async function ssrTransformScript( s.remove(node.start, node.end) if (node.source) { // export { foo, bar } from './foo' - const importId = defineImport(node.source.value as string, { - importedNames: node.specifiers.map((s) => s.local.name), - }) - // hoist re-exports near the defined import so they are immediately exported + const importId = defineImport( + node.start, + node.source.value as string, + { + importedNames: node.specifiers.map((s) => s.local.name), + }, + ) for (const spec of node.specifiers) { defineExport( - hoistIndex, + node.start, spec.exported.name, `${importId}.${spec.local.name}`, ) @@ -246,12 +241,11 @@ async function ssrTransformScript( // export * from './foo' if (node.type === 'ExportAllDeclaration') { s.remove(node.start, node.end) - const importId = defineImport(node.source.value as string) - // hoist re-exports near the defined import so they are immediately exported + const importId = defineImport(node.start, node.source.value as string) if (node.exported) { - defineExport(hoistIndex, node.exported.name, `${importId}`) + defineExport(node.start, node.exported.name, `${importId}`) } else { - s.appendLeft(hoistIndex, `${ssrExportAllKey}(${importId});\n`) + s.appendLeft(node.start, `${ssrExportAllKey}(${importId});\n`) } } } @@ -445,9 +439,14 @@ function walk( if (node.type === 'FunctionDeclaration') { const parentScope = findParentScope(parentStack) if (parentScope) { - setScope(parentScope, node.id!.name) + setScope(parentScope, node.id.name) } } + // If it is a function expression, its name (if exist) could also be + // shadowing an import. So add its own name to the scope + if (node.type === 'FunctionExpression' && node.id) { + setScope(node, node.id.name) + } // walk function expressions and add its arguments to known identifiers // so that we don't prefix them node.params.forEach((p) => { @@ -480,6 +479,15 @@ function walk( }, }) }) + } else if (node.type === 'ClassDeclaration') { + // A class declaration name could shadow an import, so add its name to the parent scope + const parentScope = findParentScope(parentStack) + if (parentScope) { + setScope(parentScope, node.id.name) + } + } else if (node.type === 'ClassExpression' && node.id) { + // A class expression name could shadow an import, so add its name to the scope + setScope(node, node.id.name) } else if (node.type === 'Property' && parent!.type === 'ObjectPattern') { // mark property in destructuring pattern setIsNodeInPattern(node) diff --git a/packages/vite/src/node/utils.ts b/packages/vite/src/node/utils.ts index c6ef88a4cb72fc..048508a1bad7b5 100644 --- a/packages/vite/src/node/utils.ts +++ b/packages/vite/src/node/utils.ts @@ -9,7 +9,7 @@ import { promises as dns } from 'node:dns' import { performance } from 'node:perf_hooks' import type { AddressInfo, Server } from 'node:net' import fsp from 'node:fs/promises' -import type { FSWatcher } from 'chokidar' +import type { FSWatcher } from 'dep-types/chokidar' import remapping from '@ampproject/remapping' import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping' import colors from 'picocolors' @@ -196,6 +196,7 @@ function testCaseInsensitiveFS() { } export const urlCanParse = + // eslint-disable-next-line n/no-unsupported-features/node-builtins URL.canParse ?? // URL.canParse is supported from Node.js 18.17.0+, 20.0.0+ ((path: string, base?: string | undefined): boolean => { @@ -433,7 +434,7 @@ export function isFilePathESM( } } -const splitRE = /\r?\n/ +export const splitRE = /\r?\n/g const range: number = 2 @@ -731,6 +732,8 @@ function joinSrcset(ret: ImageCandidate[]) { .join(', ') } +// NOTE: The returned `url` should perhaps be decoded so all handled URLs within Vite are consistently decoded. +// However, this may also require a refactor for `cssReplacer` to accept decoded URLs instead. function splitSrcSetDescriptor(srcs: string): ImageCandidate[] { return splitSrcSet(srcs) .map((s) => { @@ -770,10 +773,17 @@ export function processSrcSetSync( } const cleanSrcSetRE = - /(?:url|image|gradient|cross-fade)\([^)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|data:\w+\/[\w.+\-]+;base64,[\w+/=]+/g + /(?:url|image|gradient|cross-fade)\([^)]*\)|"([^"]|(?<=\\)")*"|'([^']|(?<=\\)')*'|data:\w+\/[\w.+\-]+;base64,[\w+/=]+|\?\S+,/g function splitSrcSet(srcs: string) { const parts: string[] = [] - // There could be a ',' inside of url(data:...), linear-gradient(...), "data:..." or data:... + /** + * There could be a ',' inside of: + * - url(data:...) + * - linear-gradient(...) + * - "data:..." + * - data:... + * - query parameter ?... + */ const cleanedSrcs = srcs.replace(cleanSrcSetRE, blankReplacer) let startIndex = 0 let splitIndex: number @@ -1100,7 +1110,7 @@ function mergeConfigRecursively( } if (Array.isArray(existing) || Array.isArray(value)) { - merged[key] = [...arraify(existing ?? []), ...arraify(value ?? [])] + merged[key] = [...arraify(existing), ...arraify(value)] continue } if (isObject(existing) && isObject(value)) { @@ -1407,3 +1417,24 @@ export function displayTime(time: number): string { // display: {X}m {Y}s return `${mins}m${seconds < 1 ? '' : ` ${seconds.toFixed(0)}s`}` } + +/** + * Encodes the URI path portion (ignores part after ? or #) + */ +export function encodeURIPath(uri: string): string { + if (uri.startsWith('data:')) return uri + const filePath = cleanUrl(uri) + const postfix = filePath !== uri ? uri.slice(filePath.length) : '' + return encodeURI(filePath) + postfix +} + +/** + * Like `encodeURIPath`, but only replacing `%` as `%25`. This is useful for environments + * that can handle un-encoded URIs, where `%` is the only ambiguous character. + */ +export function partialEncodeURIPath(uri: string): string { + if (uri.startsWith('data:')) return uri + const filePath = cleanUrl(uri) + const postfix = filePath !== uri ? uri.slice(filePath.length) : '' + return filePath.replaceAll('%', '%25') + postfix +} diff --git a/packages/vite/src/node/watch.ts b/packages/vite/src/node/watch.ts index 9c9972bdd3a471..d8eb128f1613a5 100644 --- a/packages/vite/src/node/watch.ts +++ b/packages/vite/src/node/watch.ts @@ -2,12 +2,58 @@ import { EventEmitter } from 'node:events' import path from 'node:path' import glob from 'fast-glob' import type { FSWatcher, WatchOptions } from 'dep-types/chokidar' -import { arraify } from './utils' -import type { ResolvedConfig } from '.' +import type { OutputOptions } from 'rollup' +import * as colors from 'picocolors' +import { withTrailingSlash } from '../shared/utils' +import { arraify, normalizePath } from './utils' +import type { ResolvedConfig } from './config' +import type { Logger } from './logger' + +export function getResolvedOutDirs( + root: string, + outDir: string, + outputOptions: OutputOptions[] | OutputOptions | undefined, +): Set { + const resolvedOutDir = path.resolve(root, outDir) + if (!outputOptions) return new Set([resolvedOutDir]) + + return new Set( + arraify(outputOptions).map(({ dir }) => + dir ? path.resolve(root, dir) : resolvedOutDir, + ), + ) +} + +export function resolveEmptyOutDir( + emptyOutDir: boolean | null, + root: string, + outDirs: Set, + logger?: Logger, +): boolean { + if (emptyOutDir != null) return emptyOutDir + + for (const outDir of outDirs) { + if (!normalizePath(outDir).startsWith(withTrailingSlash(root))) { + // warn if outDir is outside of root + logger?.warn( + colors.yellow( + `\n${colors.bold(`(!)`)} outDir ${colors.white( + colors.dim(outDir), + )} is not inside project root and will not be emptied.\n` + + `Use --emptyOutDir to override.\n`, + ), + ) + return false + } + } + return true +} export function resolveChokidarOptions( config: ResolvedConfig, options: WatchOptions | undefined, + resolvedOutDirs: Set, + emptyOutDir: boolean, ): WatchOptions { const { ignored: ignoredList, ...otherOptions } = options ?? {} const ignored: WatchOptions['ignored'] = [ @@ -17,9 +63,9 @@ export function resolveChokidarOptions( glob.escapePath(config.cacheDir) + '/**', ...arraify(ignoredList || []), ] - if (config.build.outDir) { + if (emptyOutDir) { ignored.push( - glob.escapePath(path.resolve(config.root, config.build.outDir)) + '/**', + ...[...resolvedOutDirs].map((outDir) => glob.escapePath(outDir) + '/**'), ) } diff --git a/packages/vite/src/runtime/esmRunner.ts b/packages/vite/src/runtime/esmRunner.ts index 47078c370fb852..5d4c481c39e85a 100644 --- a/packages/vite/src/runtime/esmRunner.ts +++ b/packages/vite/src/runtime/esmRunner.ts @@ -1,3 +1,4 @@ +import { AsyncFunction } from '../shared/utils' import { ssrDynamicImportKey, ssrExportAllKey, @@ -7,9 +8,6 @@ import { } from './constants' import type { ViteModuleRunner, ViteRuntimeModuleContext } from './types' -// eslint-disable-next-line @typescript-eslint/no-empty-function -const AsyncFunction = async function () {}.constructor as typeof Function - export class ESModulesRunner implements ViteModuleRunner { async runViteModule( context: ViteRuntimeModuleContext, diff --git a/packages/vite/src/runtime/hmrHandler.ts b/packages/vite/src/runtime/hmrHandler.ts index 0b8363eac52dea..b0b9fdd5fd6f32 100644 --- a/packages/vite/src/runtime/hmrHandler.ts +++ b/packages/vite/src/runtime/hmrHandler.ts @@ -67,7 +67,7 @@ export async function handleHMRPayload( } case 'prune': await hmrClient.notifyListeners('vite:beforePrune', payload) - hmrClient.prunePaths(payload.paths) + await hmrClient.prunePaths(payload.paths) break case 'error': { await hmrClient.notifyListeners('vite:error', payload) diff --git a/packages/vite/src/runtime/moduleCache.ts b/packages/vite/src/runtime/moduleCache.ts index 750e933e0e303c..3681e3db1ed78d 100644 --- a/packages/vite/src/runtime/moduleCache.ts +++ b/packages/vite/src/runtime/moduleCache.ts @@ -1,11 +1,11 @@ -import { isWindows, withTrailingSlash } from '../shared/utils' -import { VITE_RUNTIME_SOURCEMAPPING_URL } from '../shared/constants' +import { isWindows, slash, withTrailingSlash } from '../shared/utils' +import { SOURCEMAPPING_URL } from '../shared/constants' import { decodeBase64 } from './utils' import { DecodedMap } from './sourcemap/decoder' import type { ModuleCache } from './types' const VITE_RUNTIME_SOURCEMAPPING_REGEXP = new RegExp( - `//# ${VITE_RUNTIME_SOURCEMAPPING_URL};base64,(.+)`, + `//# ${SOURCEMAPPING_URL}=data:application/json;base64,(.+)`, ) export class ModuleCacheMap extends Map { @@ -180,8 +180,7 @@ function normalizeModuleId(file: string, root: string): string { if (prefixedBuiltins.has(file)) return file // unix style, but Windows path still starts with the drive letter to check the root - let unixFile = file - .replace(/\\/g, '/') + let unixFile = slash(file) .replace(/^\/@fs\//, isWindows ? '' : '/') .replace(/^node:/, '') .replace(/^\/+/, '/') diff --git a/packages/vite/src/runtime/runtime.ts b/packages/vite/src/runtime/runtime.ts index 008dd214a37ce5..b7f08fed3d3a1b 100644 --- a/packages/vite/src/runtime/runtime.ts +++ b/packages/vite/src/runtime/runtime.ts @@ -4,9 +4,11 @@ import { cleanUrl, isPrimitive, isWindows, + slash, unwrapId, wrapId, } from '../shared/utils' +import { analyzeImportedModDifference } from '../shared/ssrTransform' import { ModuleCacheMap } from './moduleCache' import type { FetchResult, @@ -160,7 +162,7 @@ export class ViteRuntime { // 8 is the length of "file:///" url = url.slice(isWindows ? 8 : 7) } - url = url.replace(/\\/g, '/') + url = slash(url) const _root = this.options.root const root = _root[_root.length - 1] === '/' ? _root : `${_root}/` // strip root from the URL because fetchModule prefers a public served url path @@ -188,7 +190,7 @@ export class ViteRuntime { const { id, type } = fetchResult if (type !== 'module' && type !== 'commonjs') return exports analyzeImportedModDifference(exports, id, type, metadata) - return proxyGuardOnlyEsm(exports, id, metadata) + return exports } private async cachedRequest( @@ -427,62 +429,3 @@ function exportAll(exports: any, sourceModule: any) { } } } - -/** - * Vite converts `import { } from 'foo'` to `const _ = __vite_ssr_import__('foo')`. - * Top-level imports and dynamic imports work slightly differently in Node.js. - * This function normalizes the differences so it matches prod behaviour. - */ -function analyzeImportedModDifference( - mod: any, - rawId: string, - moduleType: string | undefined, - metadata?: SSRImportMetadata, -) { - // No normalization needed if the user already dynamic imports this module - if (metadata?.isDynamicImport) return - // If file path is ESM, everything should be fine - if (moduleType === 'module') return - - // For non-ESM, named imports is done via static analysis with cjs-module-lexer in Node.js. - // If the user named imports a specifier that can't be analyzed, error. - if (metadata?.importedNames?.length) { - const missingBindings = metadata.importedNames.filter((s) => !(s in mod)) - if (missingBindings.length) { - const lastBinding = missingBindings[missingBindings.length - 1] - // Copied from Node.js - throw new SyntaxError(`\ -[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports. -CommonJS modules can always be imported via the default export, for example using: - -import pkg from '${rawId}'; -const {${missingBindings.join(', ')}} = pkg; -`) - } - } -} - -/** - * Guard invalid named exports only, similar to how Node.js errors for top-level imports. - * But since we transform as dynamic imports, we need to emulate the error manually. - */ -function proxyGuardOnlyEsm( - mod: any, - rawId: string, - metadata?: SSRImportMetadata, -) { - // If the module doesn't import anything explicitly, e.g. `import 'foo'` or - // `import * as foo from 'foo'`, we can skip the proxy guard. - if (!metadata?.importedNames?.length) return mod - - return new Proxy(mod, { - get(mod, prop) { - if (prop !== 'then' && !(prop in mod)) { - throw new SyntaxError( - `[vite] The requested module '${rawId}' does not provide an export named '${prop.toString()}'`, - ) - } - return mod[prop] - }, - }) -} diff --git a/packages/vite/src/runtime/sourcemap/index.ts b/packages/vite/src/runtime/sourcemap/index.ts index 8329c27013eb9a..648c5e52717fc2 100644 --- a/packages/vite/src/runtime/sourcemap/index.ts +++ b/packages/vite/src/runtime/sourcemap/index.ts @@ -8,6 +8,7 @@ export function enableSourceMapSupport(runtime: ViteRuntime): () => void { `Cannot use "sourcemapInterceptor: 'node'" because global "process" variable is not available.`, ) } + /* eslint-disable n/no-unsupported-features/node-builtins -- process.setSourceMapsEnabled and process.sourceMapsEnabled */ if (typeof process.setSourceMapsEnabled !== 'function') { throw new TypeError( `Cannot use "sourcemapInterceptor: 'node'" because "process.setSourceMapsEnabled" function is not available. Please use Node >= 16.6.0.`, @@ -16,6 +17,7 @@ export function enableSourceMapSupport(runtime: ViteRuntime): () => void { const isEnabledAlready = process.sourceMapsEnabled ?? false process.setSourceMapsEnabled(true) return () => !isEnabledAlready && process.setSourceMapsEnabled(false) + /* eslint-enable n/no-unsupported-features/node-builtins */ } return interceptStackTrace( runtime, diff --git a/packages/vite/src/runtime/sourcemap/interceptor.ts b/packages/vite/src/runtime/sourcemap/interceptor.ts index 043287b5839168..58d324e79b943c 100644 --- a/packages/vite/src/runtime/sourcemap/interceptor.ts +++ b/packages/vite/src/runtime/sourcemap/interceptor.ts @@ -2,6 +2,7 @@ import type { OriginalMapping } from '@jridgewell/trace-mapping' import type { ViteRuntime } from '../runtime' import { posixDirname, posixResolve } from '../utils' import type { ModuleCacheMap } from '../moduleCache' +import { slash } from '../../shared/utils' import { DecodedMap, getOriginalPosition } from './decoder' interface RetrieveFileHandler { @@ -37,11 +38,11 @@ const createExecHandlers = any>( } const retrieveFileFromHandlers = createExecHandlers(retrieveFileHandlers) -const retrievSourceMapFromHandlers = createExecHandlers( +const retrieveSourceMapFromHandlers = createExecHandlers( retrieveSourceMapHandlers, ) -let overriden = false +let overridden = false const originalPrepare = Error.prepareStackTrace function resetInterceptor(runtime: ViteRuntime, options: InterceptorOptions) { @@ -51,7 +52,7 @@ function resetInterceptor(runtime: ViteRuntime, options: InterceptorOptions) { retrieveSourceMapHandlers.delete(options.retrieveSourceMap) if (moduleGraphs.size === 0) { Error.prepareStackTrace = originalPrepare - overriden = false + overridden = false } } @@ -59,9 +60,9 @@ export function interceptStackTrace( runtime: ViteRuntime, options: InterceptorOptions = {}, ): () => void { - if (!overriden) { + if (!overridden) { Error.prepareStackTrace = prepareStackTrace - overriden = true + overridden = true } moduleGraphs.add(runtime.moduleCache) if (options.retrieveFile) retrieveFileHandlers.add(options.retrieveFile) @@ -88,24 +89,21 @@ interface CachedMapEntry { // Support URLs relative to a directory, but be careful about a protocol prefix function supportRelativeURL(file: string, url: string) { if (!file) return url - const dir = posixDirname(file.replace(/\\/g, '/')) + const dir = posixDirname(slash(file)) const match = /^\w+:\/\/[^/]*/.exec(dir) let protocol = match ? match[0] : '' const startPath = dir.slice(protocol.length) if (protocol && /^\/\w:/.test(startPath)) { // handle file:///C:/ paths protocol += '/' - return ( - protocol + - posixResolve(dir.slice(protocol.length), url).replace(/\\/g, '/') - ) + return protocol + slash(posixResolve(startPath, url)) } - return protocol + posixResolve(dir.slice(protocol.length), url) + return protocol + posixResolve(startPath, url) } function getRuntimeSourceMap(position: OriginalMapping): CachedMapEntry | null { for (const moduleCache of moduleGraphs) { - const sourceMap = moduleCache.getSourceMap(position.source as string) + const sourceMap = moduleCache.getSourceMap(position.source!) if (sourceMap) { return { url: position.source, @@ -145,7 +143,7 @@ function retrieveSourceMapURL(source: string) { const reSourceMap = /^data:application\/json[^,]+base64,/ function retrieveSourceMap(source: string) { - const urlAndMap = retrievSourceMapFromHandlers(source) + const urlAndMap = retrieveSourceMapFromHandlers(source) if (urlAndMap) return urlAndMap let sourceMappingURL = retrieveSourceMapURL(source) diff --git a/packages/vite/src/runtime/types.ts b/packages/vite/src/runtime/types.ts index 56707005f8db50..730ed59630e26d 100644 --- a/packages/vite/src/runtime/types.ts +++ b/packages/vite/src/runtime/types.ts @@ -1,6 +1,10 @@ import type { ViteHotContext } from 'types/hot' import type { HMRPayload } from 'types/hmrPayload' import type { HMRConnection, HMRLogger } from '../shared/hmr' +import type { + DefineImportMetadata, + SSRImportBaseMetadata, +} from '../shared/ssrTransform' import type { ModuleCacheMap } from './moduleCache' import type { ssrDynamicImportKey, @@ -12,17 +16,9 @@ import type { import type { DecodedMap } from './sourcemap/decoder' import type { InterceptorOptions } from './sourcemap/interceptor' -export interface DefineImportMetadata { - /** - * Imported names before being transformed to `ssrImportKey` - * - * import foo, { bar as baz, qux } from 'hello' - * => ['default', 'bar', 'qux'] - * - * import * as namespace from 'world - * => undefined - */ - importedNames?: string[] +export type { DefineImportMetadata } +export interface SSRImportMetadata extends SSRImportBaseMetadata { + entrypoint?: boolean } export interface HMRRuntimeConnection extends HMRConnection { @@ -33,11 +29,6 @@ export interface HMRRuntimeConnection extends HMRConnection { onUpdate(callback: (payload: HMRPayload) => void): void } -export interface SSRImportMetadata extends DefineImportMetadata { - isDynamicImport?: boolean - entrypoint?: boolean -} - export interface ViteRuntimeImportMeta extends ImportMeta { url: string env: ImportMetaEnv diff --git a/packages/vite/src/shared/constants.ts b/packages/vite/src/shared/constants.ts index d60bbc9a1f8287..7c0e685d5abf6b 100644 --- a/packages/vite/src/shared/constants.ts +++ b/packages/vite/src/shared/constants.ts @@ -21,4 +21,3 @@ SOURCEMAPPING_URL += 'ppingURL' export const VITE_RUNTIME_SOURCEMAPPING_SOURCE = '//# sourceMappingSource=vite-runtime' -export const VITE_RUNTIME_SOURCEMAPPING_URL = `${SOURCEMAPPING_URL}=data:application/json;charset=utf-8` diff --git a/packages/vite/src/shared/hmr.ts b/packages/vite/src/shared/hmr.ts index 05f2f742c4f247..0f2cb23b4ad71f 100644 --- a/packages/vite/src/shared/hmr.ts +++ b/packages/vite/src/shared/hmr.ts @@ -232,8 +232,13 @@ export class HMRClient { // After an HMR update, some modules are no longer imported on the page // but they may have left behind side effects that need to be cleaned up // (.e.g style injections) - // TODO Trigger their dispose callbacks. - public prunePaths(paths: string[]): void { + public async prunePaths(paths: string[]): Promise { + await Promise.all( + paths.map((path) => { + const disposer = this.disposeMap.get(path) + if (disposer) return disposer(this.dataMap.get(path)) + }), + ) paths.forEach((path) => { const fn = this.pruneMap.get(path) if (fn) { diff --git a/packages/vite/src/shared/ssrTransform.ts b/packages/vite/src/shared/ssrTransform.ts new file mode 100644 index 00000000000000..9bf1f667f60fd9 --- /dev/null +++ b/packages/vite/src/shared/ssrTransform.ts @@ -0,0 +1,59 @@ +export interface DefineImportMetadata { + /** + * Imported names before being transformed to `ssrImportKey` + * + * import foo, { bar as baz, qux } from 'hello' + * => ['default', 'bar', 'qux'] + * + * import * as namespace from 'world + * => undefined + */ + importedNames?: string[] +} + +export interface SSRImportBaseMetadata extends DefineImportMetadata { + isDynamicImport?: boolean +} + +/** + * Vite converts `import { } from 'foo'` to `const _ = __vite_ssr_import__('foo')`. + * Top-level imports and dynamic imports work slightly differently in Node.js. + * This function normalizes the differences so it matches prod behaviour. + */ +export function analyzeImportedModDifference( + mod: any, + rawId: string, + moduleType: string | undefined, + metadata?: SSRImportBaseMetadata, +): void { + // No normalization needed if the user already dynamic imports this module + if (metadata?.isDynamicImport) return + + // If the user named imports a specifier that can't be analyzed, error. + // If the module doesn't import anything explicitly, e.g. `import 'foo'` or + // `import * as foo from 'foo'`, we can skip. + if (metadata?.importedNames?.length) { + const missingBindings = metadata.importedNames.filter((s) => !(s in mod)) + if (missingBindings.length) { + const lastBinding = missingBindings[missingBindings.length - 1] + + // For invalid named exports only, similar to how Node.js errors for top-level imports. + // But since we transform as dynamic imports, we need to emulate the error manually. + if (moduleType === 'module') { + throw new SyntaxError( + `[vite] The requested module '${rawId}' does not provide an export named '${lastBinding}'`, + ) + } else { + // For non-ESM, named imports is done via static analysis with cjs-module-lexer in Node.js. + // Copied from Node.js + throw new SyntaxError(`\ +[vite] Named export '${lastBinding}' not found. The requested module '${rawId}' is a CommonJS module, which may not support all module.exports as named exports. +CommonJS modules can always be imported via the default export, for example using: + +import pkg from '${rawId}'; +const {${missingBindings.join(', ')}} = pkg; +`) + } + } + } +} diff --git a/packages/vite/src/shared/utils.ts b/packages/vite/src/shared/utils.ts index e3b9800edbc6bf..3b3507a8280c82 100644 --- a/packages/vite/src/shared/utils.ts +++ b/packages/vite/src/shared/utils.ts @@ -43,3 +43,14 @@ export function withTrailingSlash(path: string): string { } return path } + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const AsyncFunction = async function () {}.constructor as typeof Function + +// https://github.com/nodejs/node/issues/43047#issuecomment-1564068099 +export const asyncFunctionDeclarationPaddingLineCount = + /** #__PURE__ */ (() => { + const body = '/*code*/' + const source = new AsyncFunction('a', 'b', body).toString() + return source.slice(0, source.indexOf(body)).split('\n').length - 1 + })() diff --git a/packages/vite/types/customEvent.d.ts b/packages/vite/types/customEvent.d.ts index a72c1b85fc1ad0..9202738f8227fb 100644 --- a/packages/vite/types/customEvent.d.ts +++ b/packages/vite/types/customEvent.d.ts @@ -23,6 +23,7 @@ export interface WebSocketConnectionPayload { * This might be removed in the future if we didn't find reasonable use cases. * If you find this useful, please open an issue with details so we can discuss and make it stable API. */ + // eslint-disable-next-line n/no-unsupported-features/node-builtins webSocket: WebSocket } @@ -31,5 +32,8 @@ export interface InvalidatePayload { message: string | undefined } +/** + * provides types for built-in Vite events + */ export type InferCustomEventPayload = T extends keyof CustomEventMap ? CustomEventMap[T] : any diff --git a/patches/acorn@8.11.3.patch b/patches/acorn@8.11.3.patch new file mode 100644 index 00000000000000..af0b6b8724d08b --- /dev/null +++ b/patches/acorn@8.11.3.patch @@ -0,0 +1,12 @@ +diff --git a/package.json b/package.json +index 1b8dc76afc3cf5890cc3693c2975577fd3117dd6..9ac3a4d813fda1be476bd896a8f6168b3a459e41 100644 +--- a/package.json ++++ b/package.json +@@ -46,5 +46,6 @@ + }, + "bin": { + "acorn": "./bin/acorn" +- } ++ }, ++ "sideEffects": false + } diff --git a/patches/http-proxy@1.18.1.patch b/patches/http-proxy@1.18.1.patch new file mode 100644 index 00000000000000..04e179137026a3 --- /dev/null +++ b/patches/http-proxy@1.18.1.patch @@ -0,0 +1,46 @@ +diff --git a/lib/http-proxy/common.js b/lib/http-proxy/common.js +index 6513e81d80d5250ea249ea833f819ece67897c7e..486d4c896d65a3bb7cf63307af68facb3ddb886b 100644 +--- a/lib/http-proxy/common.js ++++ b/lib/http-proxy/common.js +@@ -1,6 +1,5 @@ + var common = exports, + url = require('url'), +- extend = require('util')._extend, + required = require('requires-port'); + + var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i, +@@ -40,10 +39,10 @@ common.setupOutgoing = function(outgoing, options, req, forward) { + ); + + outgoing.method = options.method || req.method; +- outgoing.headers = extend({}, req.headers); ++ outgoing.headers = Object.assign({}, req.headers); + + if (options.headers){ +- extend(outgoing.headers, options.headers); ++ Object.assign(outgoing.headers, options.headers); + } + + if (options.auth) { +diff --git a/lib/http-proxy/index.js b/lib/http-proxy/index.js +index 977a4b3622b9eaac27689f06347ea4c5173a96cd..88b2d0fcfa03c3aafa47c7e6d38e64412c45a7cc 100644 +--- a/lib/http-proxy/index.js ++++ b/lib/http-proxy/index.js +@@ -1,5 +1,4 @@ + var httpProxy = module.exports, +- extend = require('util')._extend, + parse_url = require('url').parse, + EE3 = require('eventemitter3'), + http = require('http'), +@@ -47,9 +46,9 @@ function createRightProxy(type) { + args[cntr] !== res + ) { + //Copy global options +- requestOptions = extend({}, options); ++ requestOptions = Object.assign({}, options); + //Overwrite with request options +- extend(requestOptions, args[cntr]); ++ Object.assign(requestOptions, args[cntr]); + + cntr--; + } diff --git a/patches/postcss-import@16.0.1.patch b/patches/postcss-import@16.0.1.patch deleted file mode 100644 index a0bee8d11f1201..00000000000000 --- a/patches/postcss-import@16.0.1.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/lib/parse-styles.js b/lib/parse-styles.js -index 68a1fef604fad82ac367f6bd63e845027bf86089..4617545e86301f58bfcd649f66c0536e29bda50f 100644 ---- a/lib/parse-styles.js -+++ b/lib/parse-styles.js -@@ -223,19 +223,20 @@ function isProcessableURL(uri) { - return false - } - -- // check for fragment or query -- try { -- // needs a base to parse properly -- const url = new URL(uri, "https://example.com") -- -- if (url.hash) { -- return false -- } -- -- if (url.search) { -- return false -- } -- } catch {} // Ignore -+ // PATCH: comment out this part to support resolving imports field -+ // // check for fragment or query -+ // try { -+ // // needs a base to parse properly -+ // const url = new URL(uri, "https://example.com") -+ -+ // if (url.hash) { -+ // return false -+ // } -+ -+ // if (url.search) { -+ // return false -+ // } -+ // } catch {} // Ignore - - return true - } diff --git a/playground/alias/package.json b/playground/alias/package.json index f24caa882aa1c3..692d2a3af3ea44 100644 --- a/playground/alias/package.json +++ b/playground/alias/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "aliased-module": "file:./dir/module", - "vue": "^3.4.20", - "@vue/shared": "^3.4.20" + "vue": "^3.4.27", + "@vue/shared": "^3.4.27" }, "devDependencies": { "@vitejs/test-resolve-linked": "workspace:*" diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts index 5933db4d76c08d..6f193cc43cd9ca 100644 --- a/playground/assets/__tests__/assets.spec.ts +++ b/playground/assets/__tests__/assets.spec.ts @@ -14,6 +14,7 @@ import { page, readFile, readManifest, + serverLogs, untilUpdated, viteTestUrl, watcher, @@ -282,6 +283,26 @@ describe('css url() references', () => { }) describe('image', () => { + test('src', async () => { + const img = await page.$('.img-src') + const src = await img.getAttribute('src') + expect(src).toMatch( + isBuild + ? /\/foo\/bar\/assets\/html-only-asset-[-\w]{8}\.jpg/ + : /\/foo\/bar\/nested\/html-only-asset.jpg/, + ) + }) + + test('src inline', async () => { + const img = await page.$('.img-src-inline') + const src = await img.getAttribute('src') + expect(src).toMatch( + isBuild + ? /^data:image\/svg\+xml,%3csvg/ + : /\/foo\/bar\/nested\/inlined.svg/, + ) + }) + test('srcset', async () => { const img = await page.$('.img-src-set') const srcset = await img.getAttribute('srcset') @@ -373,7 +394,7 @@ describe('unicode url', () => { expect(await page.textContent('.unicode-url')).toMatch( isBuild ? `data:text/javascript;base64,${Buffer.from(src).toString('base64')}` - : `/foo/bar/テスト-測試-white space.js`, + : encodeURI(`/foo/bar/テスト-測試-white space.js`), ) }) }) @@ -444,7 +465,7 @@ test('new URL(`./${1 === 0 ? static : dynamic}?abc`, import.meta.url)', async () ) }) -test('new URL(`non-existent`, import.meta.url)', async () => { +test("new URL(/* @vite-ignore */ 'non-existent', import.meta.url)", async () => { // the inlined script tag is extracted in a separate file const importMetaUrl = new URL( isBuild ? '/foo/bar/assets/index.js' : '/foo/bar/index.html', @@ -453,6 +474,9 @@ test('new URL(`non-existent`, import.meta.url)', async () => { expect(await page.textContent('.non-existent-import-meta-url')).toMatch( new URL('non-existent', importMetaUrl).pathname, ) + expect(serverLogs).not.toContainEqual( + expect.stringContaining("doesn't exist at build time"), + ) }) test.runIf(isBuild)('manifest', async () => { diff --git a/playground/assets/asset/percent%.png b/playground/assets/asset/percent%.png new file mode 100644 index 00000000000000..628bbd479129c0 Binary files /dev/null and b/playground/assets/asset/percent%.png differ diff --git a/playground/assets/index.html b/playground/assets/index.html index dd013828cc96b0..2fd63f46f63960 100644 --- a/playground/assets/index.html +++ b/playground/assets/index.html @@ -138,6 +138,17 @@

Unicode URL

+

Filename including single quote

+
+ +
+ +

Filename including percent

+
+ + +
+

encodeURI for the address

Image Src Set

HTML only asset

- + +
+ +

HTML inline asset

+
+

SVG Fragments

@@ -288,7 +304,7 @@

new URL(`./${1 === 0 ? static : dynamic}?abc`, import.meta.url)

-

new URL(`non-existent`, import.meta.url)

+

new URL(/* @vite-ignore */ 'non-existent', import.meta.url)

@@ -440,6 +456,13 @@

assets in noscript

import unicodeUrl from './テスト-測試-white space.js?url' text('.unicode-url', unicodeUrl) + import filenameIncludingSingleQuoteUrl from "./nested/with-single'quote.png" + text('.filename-including-single-quote', filenameIncludingSingleQuoteUrl) + + // TODO: is not supported yet (https://github.com/vitejs/vite/pull/16243) + // import percentUrl from './asset/percent%25.png?url' + // text('.percent-url', percentUrl) + import cssUrl from './css/icons.css?url' text('.url-css', cssUrl) @@ -486,7 +509,10 @@

assets in noscript

import someString from './static/foo.txt?raw' document.querySelector('.raw-query').textContent = someString - const metaUrlNonExistent = new URL('non-existent', import.meta.url).pathname + const metaUrlNonExistent = new URL( + /* @vite-ignore */ 'non-existent', + import.meta.url, + ).pathname text('.non-existent-import-meta-url', metaUrlNonExistent) /** diff --git a/playground/assets/nested/inlined.svg b/playground/assets/nested/inlined.svg new file mode 100644 index 00000000000000..e00a25209ebd4e --- /dev/null +++ b/playground/assets/nested/inlined.svg @@ -0,0 +1,12 @@ + + + diff --git a/playground/assets/nested/with-single'quote.png b/playground/assets/nested/with-single'quote.png new file mode 100644 index 00000000000000..cb1c88d48c090a Binary files /dev/null and b/playground/assets/nested/with-single'quote.png differ diff --git a/playground/backend-integration/__tests__/backend-integration.spec.ts b/playground/backend-integration/__tests__/backend-integration.spec.ts index 563e03b5f4e7c9..669239af237846 100644 --- a/playground/backend-integration/__tests__/backend-integration.spec.ts +++ b/playground/backend-integration/__tests__/backend-integration.spec.ts @@ -6,8 +6,10 @@ import { getColor, isBuild, isServe, + listAssets, page, readManifest, + serverLogs, untilBrowserLogAfter, untilUpdated, } from '~utils' @@ -39,6 +41,7 @@ describe.runIf(isBuild)('build', () => { const scssAssetEntry = manifest['nested/blue.scss'] const imgAssetEntry = manifest['../images/logo.png'] const dirFooAssetEntry = manifest['../../dir/foo.css'] + const iconEntrypointEntry = manifest['icon.png'] expect(htmlEntry.css.length).toEqual(1) expect(htmlEntry.assets.length).toEqual(1) expect(cssAssetEntry?.file).not.toBeUndefined() @@ -53,6 +56,7 @@ describe.runIf(isBuild)('build', () => { expect(dirFooAssetEntry).not.toBeUndefined() // '\\' should not be used even on windows // use the entry name expect(dirFooAssetEntry.file).toMatch('assets/bar-') + expect(iconEntrypointEntry?.file).not.toBeUndefined() }) test('CSS imported from JS entry should have a non-nested chunk name', () => { @@ -61,6 +65,17 @@ describe.runIf(isBuild)('build', () => { expect(mainTsEntryCss.length).toBe(1) expect(mainTsEntryCss[0].replace('assets/', '')).not.toContain('/') }) + + test('entrypoint assets should not generate empty JS file', () => { + expect(serverLogs).not.toContainEqual( + 'Generated an empty chunk: "icon.png".', + ) + + const assets = listAssets('dev') + expect(assets).not.toContainEqual( + expect.stringMatching(/icon.png-[-\w]{8}\.js$/), + ) + }) }) describe.runIf(isServe)('serve', () => { diff --git a/playground/backend-integration/frontend/entrypoints/icon.png b/playground/backend-integration/frontend/entrypoints/icon.png new file mode 100644 index 00000000000000..4388bfdca3d4d7 Binary files /dev/null and b/playground/backend-integration/frontend/entrypoints/icon.png differ diff --git a/playground/backend-integration/package.json b/playground/backend-integration/package.json index 8318b694028b76..34231efc1f63f1 100644 --- a/playground/backend-integration/package.json +++ b/playground/backend-integration/package.json @@ -10,8 +10,8 @@ "preview": "vite preview" }, "devDependencies": { - "sass": "^1.71.1", - "tailwindcss": "^3.4.1", + "sass": "^1.77.2", + "tailwindcss": "^3.4.3", "fast-glob": "^3.3.2" } } diff --git a/playground/cli-module/__tests__/serve.ts b/playground/cli-module/__tests__/serve.ts index 4a76a6f810b3a9..b19bdb62701a7d 100644 --- a/playground/cli-module/__tests__/serve.ts +++ b/playground/cli-module/__tests__/serve.ts @@ -70,6 +70,7 @@ export async function serve() { const serverProcess = execaCommand(serverCommand, { cwd: rootDir, stdio: 'pipe', + forceKillAfterDelay: 3000, }) collectStreams('server', serverProcess) diff --git a/playground/cli/__tests__/serve.ts b/playground/cli/__tests__/serve.ts index 7cf2eca021ef1d..5da61aec991109 100644 --- a/playground/cli/__tests__/serve.ts +++ b/playground/cli/__tests__/serve.ts @@ -73,6 +73,7 @@ export async function serve() { const serverProcess = execaCommand(serverCommand, { cwd: rootDir, stdio: 'pipe', + forceKillAfterDelay: 3000, }) collectStreams('server', serverProcess) diff --git a/playground/config/packages/siblings/package.json b/playground/config/packages/siblings/package.json index 14c8726811acb4..ecba583cc41808 100644 --- a/playground/config/packages/siblings/package.json +++ b/playground/config/packages/siblings/package.json @@ -2,7 +2,7 @@ "name": "@vite/test-config-sibling", "type": "module", "devDependencies": { - "@types/lodash": "^4.14.202", + "@types/lodash": "^4.17.4", "lodash": "^4.17.21" } } diff --git a/playground/csp/__tests__/csp.spec.ts b/playground/csp/__tests__/csp.spec.ts new file mode 100644 index 00000000000000..93aa7cbbc1c278 --- /dev/null +++ b/playground/csp/__tests__/csp.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from 'vitest' +import { expectWithRetry, getColor, page } from '~utils' + +test('linked css', async () => { + expect(await getColor('.linked')).toBe('blue') +}) + +test('inline style tag', async () => { + expect(await getColor('.inline')).toBe('green') +}) + +test('imported css', async () => { + expect(await getColor('.from-js')).toBe('blue') +}) + +test('dynamic css', async () => { + expect(await getColor('.dynamic')).toBe('red') +}) + +test('script tag', async () => { + await expectWithRetry(() => page.textContent('.js')).toBe('js: ok') +}) + +test('dynamic js', async () => { + await expectWithRetry(() => page.textContent('.dynamic-js')).toBe( + 'dynamic-js: ok', + ) +}) + +test('inline js', async () => { + await expectWithRetry(() => page.textContent('.inline-js')).toBe( + 'inline-js: ok', + ) +}) + +test('nonce attributes are not repeated', async () => { + const htmlSource = await page.content() + expect(htmlSource).not.toContain(/nonce=""[^>]*nonce=""/) + await expectWithRetry(() => page.textContent('.double-nonce-js')).toBe( + 'double-nonce-js: ok', + ) +}) + +test('meta[property=csp-nonce] is injected', async () => { + const meta = await page.$('meta[property=csp-nonce]') + expect(await (await meta.getProperty('nonce')).jsonValue()).not.toBe('') +}) diff --git a/playground/csp/dynamic.css b/playground/csp/dynamic.css new file mode 100644 index 00000000000000..ca5140e1c23d94 --- /dev/null +++ b/playground/csp/dynamic.css @@ -0,0 +1,3 @@ +.dynamic { + color: red; +} diff --git a/playground/csp/dynamic.js b/playground/csp/dynamic.js new file mode 100644 index 00000000000000..3d3e3a413e5677 --- /dev/null +++ b/playground/csp/dynamic.js @@ -0,0 +1,3 @@ +import './dynamic.css' + +document.querySelector('.dynamic-js').textContent = 'dynamic-js: ok' diff --git a/playground/csp/from-js.css b/playground/csp/from-js.css new file mode 100644 index 00000000000000..fb48429dc60ab4 --- /dev/null +++ b/playground/csp/from-js.css @@ -0,0 +1,3 @@ +.from-js { + color: blue; +} diff --git a/playground/csp/index.html b/playground/csp/index.html new file mode 100644 index 00000000000000..45a508e76a2cd3 --- /dev/null +++ b/playground/csp/index.html @@ -0,0 +1,23 @@ + + + +

direct

+

inline

+

from-js

+

dynamic

+

js: error

+

dynamic-js: error

+

inline-js: error

+

double-nonce-js: error

+ + diff --git a/playground/csp/index.js b/playground/csp/index.js new file mode 100644 index 00000000000000..465359baca8297 --- /dev/null +++ b/playground/csp/index.js @@ -0,0 +1,5 @@ +import './from-js.css' + +document.querySelector('.js').textContent = 'js: ok' + +import('./dynamic.js') diff --git a/playground/csp/linked.css b/playground/csp/linked.css new file mode 100644 index 00000000000000..51636e6cfad81f --- /dev/null +++ b/playground/csp/linked.css @@ -0,0 +1,3 @@ +.linked { + color: blue; +} diff --git a/playground/csp/package.json b/playground/csp/package.json new file mode 100644 index 00000000000000..e8a834d93abd25 --- /dev/null +++ b/playground/csp/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-csp", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + } +} diff --git a/playground/csp/vite.config.js b/playground/csp/vite.config.js new file mode 100644 index 00000000000000..84d6d92ba0d0bb --- /dev/null +++ b/playground/csp/vite.config.js @@ -0,0 +1,67 @@ +import fs from 'node:fs/promises' +import url from 'node:url' +import path from 'node:path' +import crypto from 'node:crypto' +import { defineConfig } from 'vite' + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) + +const noncePlaceholder = '#$NONCE$#' +const createNonce = () => crypto.randomBytes(16).toString('base64') + +/** + * @param {import('node:http').ServerResponse} res + * @param {string} nonce + */ +const setNonceHeader = (res, nonce) => { + res.setHeader( + 'Content-Security-Policy', + `default-src 'nonce-${nonce}'; connect-src 'self'`, + ) +} + +/** + * @param {string} file + * @param {(input: string, originalUrl: string) => Promise} transform + * @returns {import('vite').Connect.NextHandleFunction} + */ +const createMiddleware = (file, transform) => async (req, res) => { + const nonce = createNonce() + setNonceHeader(res, nonce) + const content = await fs.readFile(path.join(__dirname, file), 'utf-8') + const transformedContent = await transform(content, req.originalUrl) + res.setHeader('Content-Type', 'text/html') + res.end(transformedContent.replaceAll(noncePlaceholder, nonce)) +} + +export default defineConfig({ + plugins: [ + { + name: 'nonce-inject', + config() { + return { + appType: 'custom', + html: { + cspNonce: noncePlaceholder, + }, + } + }, + configureServer({ transformIndexHtml, middlewares }) { + return () => { + middlewares.use( + createMiddleware('./index.html', (input, originalUrl) => + transformIndexHtml(originalUrl, input), + ), + ) + } + }, + configurePreviewServer({ middlewares }) { + return () => { + middlewares.use( + createMiddleware('./dist/index.html', async (input) => input), + ) + } + }, + }, + ], +}) diff --git a/playground/css-codesplit/__tests__/css-codesplit.spec.ts b/playground/css-codesplit/__tests__/css-codesplit.spec.ts index 2f7d5ab5fc5fba..cc54d865a6795e 100644 --- a/playground/css-codesplit/__tests__/css-codesplit.spec.ts +++ b/playground/css-codesplit/__tests__/css-codesplit.spec.ts @@ -3,6 +3,7 @@ import { findAssetFile, getColor, isBuild, + listAssets, page, readManifest, untilUpdated, @@ -12,6 +13,7 @@ test('should load all stylesheets', async () => { expect(await getColor('h1')).toBe('red') expect(await getColor('h2')).toBe('blue') expect(await getColor('.dynamic')).toBe('green') + expect(await getColor('.async-js')).toBe('blue') expect(await getColor('.chunk')).toBe('magenta') }) @@ -40,7 +42,12 @@ describe.runIf(isBuild)('build', () => { expect(findAssetFile(/style-.*\.js$/)).toBe('') expect(findAssetFile('main.*.js$')).toMatch(`/* empty css`) expect(findAssetFile('other.*.js$')).toMatch(`/* empty css`) - expect(findAssetFile(/async.*\.js$/)).toBe('') + expect(findAssetFile(/async-[-\w]{8}\.js$/)).toBe('') + + const assets = listAssets() + expect(assets).not.toContainEqual( + expect.stringMatching(/async-js-[-\w]{8}\.js$/), + ) }) test('should remove empty chunk, HTML without JS', async () => { diff --git a/playground/css-codesplit/async-js.css b/playground/css-codesplit/async-js.css new file mode 100644 index 00000000000000..ed61a7f513c277 --- /dev/null +++ b/playground/css-codesplit/async-js.css @@ -0,0 +1,3 @@ +.async-js { + color: blue; +} diff --git a/playground/css-codesplit/async-js.js b/playground/css-codesplit/async-js.js new file mode 100644 index 00000000000000..2ce31a1e741d2d --- /dev/null +++ b/playground/css-codesplit/async-js.js @@ -0,0 +1,2 @@ +// a JS file that becomes an empty file but imports CSS files +import './async-js.css' diff --git a/playground/css-codesplit/index.html b/playground/css-codesplit/index.html index 7d2a4991f20e0a..38885fa7ccb5ed 100644 --- a/playground/css-codesplit/index.html +++ b/playground/css-codesplit/index.html @@ -2,6 +2,7 @@

This should be red

This should be blue

This should be green

+

This should be blue

This should not be yellow

This should be yellow

diff --git a/playground/css-codesplit/main.js b/playground/css-codesplit/main.js index e548142add8786..ec266fa003156d 100644 --- a/playground/css-codesplit/main.js +++ b/playground/css-codesplit/main.js @@ -9,6 +9,7 @@ import chunkCssUrl from './chunk.css?url' globalThis.__test_chunkCssUrl = chunkCssUrl import('./async.css') +import('./async-js') import('./inline.css?inline').then((css) => { document.querySelector('.dynamic-inline').textContent = css.default diff --git a/playground/css-lightningcss-proxy/package.json b/playground/css-lightningcss-proxy/package.json index eda58ad563d946..ad6b503221a9f6 100644 --- a/playground/css-lightningcss-proxy/package.json +++ b/playground/css-lightningcss-proxy/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "devDependencies": { - "lightningcss": "^1.24.0", - "express": "^4.18.2" + "lightningcss": "^1.25.1", + "express": "^4.19.2" } } diff --git a/playground/css-lightningcss/package.json b/playground/css-lightningcss/package.json index 1ebca75d1ab941..6844ef9ffb0959 100644 --- a/playground/css-lightningcss/package.json +++ b/playground/css-lightningcss/package.json @@ -9,6 +9,6 @@ "preview": "vite preview" }, "devDependencies": { - "lightningcss": "^1.24.0" + "lightningcss": "^1.25.1" } } diff --git a/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts b/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts new file mode 100644 index 00000000000000..5110ef3a77ff7b --- /dev/null +++ b/playground/css-no-codesplit/__tests__/css-no-codesplit.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest' +import { expectWithRetry, getColor, isBuild, listAssets } from '~utils' + +test('should load all stylesheets', async () => { + expect(await getColor('.shared-linked')).toBe('blue') + await expectWithRetry(() => getColor('.async-js')).toBe('blue') +}) + +describe.runIf(isBuild)('build', () => { + test('should remove empty chunk', async () => { + const assets = listAssets() + expect(assets).not.toContainEqual( + expect.stringMatching(/shared-linked-.*\.js$/), + ) + expect(assets).not.toContainEqual(expect.stringMatching(/async-js-.*\.js$/)) + }) +}) diff --git a/playground/css-no-codesplit/async-js.css b/playground/css-no-codesplit/async-js.css new file mode 100644 index 00000000000000..ed61a7f513c277 --- /dev/null +++ b/playground/css-no-codesplit/async-js.css @@ -0,0 +1,3 @@ +.async-js { + color: blue; +} diff --git a/playground/css-no-codesplit/async-js.js b/playground/css-no-codesplit/async-js.js new file mode 100644 index 00000000000000..2ce31a1e741d2d --- /dev/null +++ b/playground/css-no-codesplit/async-js.js @@ -0,0 +1,2 @@ +// a JS file that becomes an empty file but imports CSS files +import './async-js.css' diff --git a/playground/css-no-codesplit/index.html b/playground/css-no-codesplit/index.html new file mode 100644 index 00000000000000..e7673c84e45933 --- /dev/null +++ b/playground/css-no-codesplit/index.html @@ -0,0 +1,5 @@ + + + +

shared linked: this should be blue

+

async JS importing CSS: this should be blue

diff --git a/playground/css-no-codesplit/index.js b/playground/css-no-codesplit/index.js new file mode 100644 index 00000000000000..44b33fda36a9cd --- /dev/null +++ b/playground/css-no-codesplit/index.js @@ -0,0 +1 @@ +import('./async-js') diff --git a/playground/css-no-codesplit/package.json b/playground/css-no-codesplit/package.json new file mode 100644 index 00000000000000..61d806d3d264fa --- /dev/null +++ b/playground/css-no-codesplit/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-css-no-codesplit", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "preview": "vite preview" + } +} diff --git a/playground/css-no-codesplit/shared-linked.css b/playground/css-no-codesplit/shared-linked.css new file mode 100644 index 00000000000000..51857a50efca1f --- /dev/null +++ b/playground/css-no-codesplit/shared-linked.css @@ -0,0 +1,3 @@ +.shared-linked { + color: blue; +} diff --git a/playground/css-no-codesplit/sub.html b/playground/css-no-codesplit/sub.html new file mode 100644 index 00000000000000..f535a771d06482 --- /dev/null +++ b/playground/css-no-codesplit/sub.html @@ -0,0 +1 @@ + diff --git a/playground/css-no-codesplit/vite.config.js b/playground/css-no-codesplit/vite.config.js new file mode 100644 index 00000000000000..f48d875832b928 --- /dev/null +++ b/playground/css-no-codesplit/vite.config.js @@ -0,0 +1,14 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + cssCodeSplit: false, + rollupOptions: { + input: { + index: resolve(__dirname, './index.html'), + sub: resolve(__dirname, './sub.html'), + }, + }, + }, +}) diff --git a/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts index 6706b5dcaae510..6c6472c848823d 100644 --- a/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts +++ b/playground/css-sourcemap/__tests__/css-sourcemap.spec.ts @@ -138,6 +138,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE", "sources": [ "/root/imported.sass", @@ -158,6 +159,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE", "sources": [ "/root/imported.module.sass", @@ -178,6 +180,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE", "sources": [ "/root/imported.less", @@ -200,6 +203,7 @@ describe.runIf(isServe)('serve', () => { const map = extractSourcemap(css) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { + "ignoreList": [], "mappings": "AACE;EACE,cAAM", "sources": [ "/root/imported.styl", diff --git a/playground/css-sourcemap/package.json b/playground/css-sourcemap/package.json index 143a60950f7436..11c26f182ae79d 100644 --- a/playground/css-sourcemap/package.json +++ b/playground/css-sourcemap/package.json @@ -11,9 +11,9 @@ }, "devDependencies": { "less": "^4.2.0", - "magic-string": "^0.30.7", - "sass": "^1.71.1", - "stylus": "^0.62.0", + "magic-string": "^0.30.10", + "sass": "^1.77.2", + "stylus": "^0.63.0", "sugarss": "^4.0.1" } } diff --git a/playground/css/__tests__/css.spec.ts b/playground/css/__tests__/css.spec.ts index 89226a8fbd5ba1..cb7af939bbd152 100644 --- a/playground/css/__tests__/css.spec.ts +++ b/playground/css/__tests__/css.spec.ts @@ -448,6 +448,16 @@ test('?raw', async () => { expect(await rawImportCss.textContent()).toBe( readFileSync(require.resolve('../raw-imported.css'), 'utf-8'), ) + + if (!isBuild) { + editFile('raw-imported.css', (code) => + code.replace('color: yellow', 'color: blue'), + ) + await untilUpdated( + () => page.textContent('.raw-imported-css'), + 'color: blue', + ) + } }) test('import css in less', async () => { @@ -533,3 +543,8 @@ test.runIf(isBuild)('manual chunk path', async () => { findAssetFile(/dir\/dir2\/manual-chunk-[-\w]{8}\.css$/), ).not.toBeUndefined() }) + +test.runIf(isBuild)('CSS modules should be treeshaken if not used', () => { + const css = findAssetFile(/\.css$/, undefined, undefined, true) + expect(css).not.toContain('treeshake-module-b') +}) diff --git a/playground/css/index.html b/playground/css/index.html index 508744160526de..a0e92b205e79f6 100644 --- a/playground/css/index.html +++ b/playground/css/index.html @@ -105,6 +105,8 @@

CSS

Imported SASS module:


 
+  

CSS modules should treeshake in build

+

Imported compose/from CSS/SASS module:

CSS modules composes path resolving: this should be turquoise diff --git a/playground/css/main.js b/playground/css/main.js index 8b3eb488fe813b..05a9c426f3419c 100644 --- a/playground/css/main.js +++ b/playground/css/main.js @@ -20,6 +20,11 @@ import sassMod from './mod.module.scss' document.querySelector('.modules-sass').classList.add(sassMod['apply-color']) text('.modules-sass-code', JSON.stringify(sassMod, null, 2)) +import { a as treeshakeMod } from './treeshake-module/index.js' +document + .querySelector('.modules-treeshake') + .classList.add(treeshakeMod()['treeshake-module-a']) + import composesPathResolvingMod from './composes-path-resolving.module.css' document .querySelector('.path-resolved-modules-css') diff --git a/playground/css/package.json b/playground/css/package.json index b2aec5504ae22c..ef7a2eb0688474 100644 --- a/playground/css/package.json +++ b/playground/css/package.json @@ -24,8 +24,8 @@ "fast-glob": "^3.3.2", "less": "^4.2.0", "postcss-nested": "^6.0.1", - "sass": "^1.71.1", - "stylus": "^0.62.0", + "sass": "^1.77.2", + "stylus": "^0.63.0", "sugarss": "^4.0.1" }, "imports": { diff --git a/playground/css/treeshake-module/a.js b/playground/css/treeshake-module/a.js new file mode 100644 index 00000000000000..7272fa1dc1d9c1 --- /dev/null +++ b/playground/css/treeshake-module/a.js @@ -0,0 +1,5 @@ +import style from './a.module.css' + +export function a() { + return style +} diff --git a/playground/css/treeshake-module/a.module.css b/playground/css/treeshake-module/a.module.css new file mode 100644 index 00000000000000..72ab1a9fdb001a --- /dev/null +++ b/playground/css/treeshake-module/a.module.css @@ -0,0 +1,3 @@ +.treeshake-module-a { + color: red; +} diff --git a/playground/css/treeshake-module/b.js b/playground/css/treeshake-module/b.js new file mode 100644 index 00000000000000..b3db996f7f64cd --- /dev/null +++ b/playground/css/treeshake-module/b.js @@ -0,0 +1,5 @@ +import style from './b.module.css' + +export function b() { + return style +} diff --git a/playground/css/treeshake-module/b.module.css b/playground/css/treeshake-module/b.module.css new file mode 100644 index 00000000000000..5ad402ef7353e8 --- /dev/null +++ b/playground/css/treeshake-module/b.module.css @@ -0,0 +1,3 @@ +.treeshake-module-b { + color: red; +} diff --git a/playground/css/treeshake-module/index.js b/playground/css/treeshake-module/index.js new file mode 100644 index 00000000000000..67332c5a21eb3d --- /dev/null +++ b/playground/css/treeshake-module/index.js @@ -0,0 +1,2 @@ +export { a } from './a.js' +export { b } from './b.js' diff --git a/playground/dynamic-import/__tests__/dynamic-import.spec.ts b/playground/dynamic-import/__tests__/dynamic-import.spec.ts index 3892251bfd2e41..56f0fbc294661f 100644 --- a/playground/dynamic-import/__tests__/dynamic-import.spec.ts +++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts @@ -1,5 +1,12 @@ import { expect, test } from 'vitest' -import { getColor, isBuild, page, serverLogs, untilUpdated } from '~utils' +import { + findAssetFile, + getColor, + isBuild, + page, + serverLogs, + untilUpdated, +} from '~utils' test('should load literal dynamic import', async () => { await page.click('.baz') @@ -170,3 +177,9 @@ test.runIf(isBuild)( ) }, ) + +test.runIf(isBuild)('should not preload for non-analyzable urls', () => { + const js = findAssetFile(/index-[-\w]{8}\.js$/) + // should match e.g. await import(e.jss);o(".view",p===i) + expect(js).to.match(/\.jss\);/) +}) diff --git a/playground/extensions/package.json b/playground/extensions/package.json index 1c07fb97ff3c49..68cb75360d3840 100644 --- a/playground/extensions/package.json +++ b/playground/extensions/package.json @@ -10,6 +10,6 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.4.20" + "vue": "^3.4.27" } } diff --git a/playground/external/dep-that-imports/package.json b/playground/external/dep-that-imports/package.json index bbf185932fcfd1..bae6b993a48c61 100644 --- a/playground/external/dep-that-imports/package.json +++ b/playground/external/dep-that-imports/package.json @@ -5,6 +5,6 @@ "dependencies": { "slash3": "npm:slash@^3.0.0", "slash5": "npm:slash@^5.1.0", - "vue": "^3.4.20" + "vue": "^3.4.27" } } diff --git a/playground/external/dep-that-requires/package.json b/playground/external/dep-that-requires/package.json index 9655cb314df0d5..7c56ad60573d2e 100644 --- a/playground/external/dep-that-requires/package.json +++ b/playground/external/dep-that-requires/package.json @@ -5,6 +5,6 @@ "dependencies": { "slash3": "npm:slash@^3.0.0", "slash5": "npm:slash@^5.1.0", - "vue": "^3.4.20" + "vue": "^3.4.27" } } diff --git a/playground/external/package.json b/playground/external/package.json index dcc8140c248d7e..934e0dc6ca0202 100644 --- a/playground/external/package.json +++ b/playground/external/package.json @@ -17,7 +17,7 @@ "slash3": "npm:slash@^3.0.0", "slash5": "npm:slash@^5.1.0", "vite": "workspace:*", - "vue": "^3.4.20", + "vue": "^3.4.27", "vue32": "npm:vue@~3.2.0" } } diff --git a/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts new file mode 100644 index 00000000000000..fb60922e86e1ae --- /dev/null +++ b/playground/fs-serve/__tests__/deny/fs-serve-deny.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from 'vitest' +import { isServe, page, viteTestUrl } from '~utils' + +describe.runIf(isServe)('main', () => { + test('**/deny/** should deny src/deny/deny.txt', async () => { + const res = await page.request.fetch( + new URL('/src/deny/deny.txt', viteTestUrl).href, + ) + expect(res.status()).toBe(403) + }) + test('**/deny/** should deny src/deny/.deny', async () => { + const res = await page.request.fetch( + new URL('/src/deny/.deny', viteTestUrl).href, + ) + expect(res.status()).toBe(403) + }) +}) diff --git a/playground/fs-serve/package.json b/playground/fs-serve/package.json index b66b79268d8601..f71a082b890c6a 100644 --- a/playground/fs-serve/package.json +++ b/playground/fs-serve/package.json @@ -10,6 +10,9 @@ "preview": "vite preview root", "dev:base": "vite root --config ./root/vite.config-base.js", "build:base": "vite build root --config ./root/vite.config-base.js", - "preview:base": "vite preview root --config ./root/vite.config-base.js" + "preview:base": "vite preview root --config ./root/vite.config-base.js", + "dev:deny": "vite root --config ./root/vite.config-deny.js", + "build:deny": "vite build root --config ./root/vite.config-deny.js", + "preview:deny": "vite preview root --config ./root/vite.config-deny.js" } } diff --git a/playground/fs-serve/root/src/deny/.deny b/playground/fs-serve/root/src/deny/.deny new file mode 100644 index 00000000000000..73bd3960853c61 --- /dev/null +++ b/playground/fs-serve/root/src/deny/.deny @@ -0,0 +1 @@ +.deny diff --git a/playground/fs-serve/root/src/deny/deny.txt b/playground/fs-serve/root/src/deny/deny.txt new file mode 100644 index 00000000000000..f9df83416f8a72 --- /dev/null +++ b/playground/fs-serve/root/src/deny/deny.txt @@ -0,0 +1 @@ +deny diff --git a/playground/fs-serve/root/vite.config-deny.js b/playground/fs-serve/root/vite.config-deny.js new file mode 100644 index 00000000000000..27501c55f38180 --- /dev/null +++ b/playground/fs-serve/root/vite.config-deny.js @@ -0,0 +1,22 @@ +import path from 'node:path' +import { defineConfig } from 'vite' + +export default defineConfig({ + build: { + rollupOptions: { + input: { + main: path.resolve(__dirname, 'src/index.html'), + }, + }, + }, + server: { + fs: { + strict: true, + allow: [path.resolve(__dirname, 'src')], + deny: ['**/deny/**'], + }, + }, + define: { + ROOT: JSON.stringify(path.dirname(__dirname).replace(/\\/g, '/')), + }, +}) diff --git a/playground/hasWindowsUnicodeFsBug.js b/playground/hasWindowsUnicodeFsBug.js deleted file mode 100644 index c46dd2a5545392..00000000000000 --- a/playground/hasWindowsUnicodeFsBug.js +++ /dev/null @@ -1,10 +0,0 @@ -import os from 'node:os' - -const isWindows = os.platform() === 'win32' -const nodeVersionArray = process.versions.node.split('.') -// ignore some files due to https://github.com/nodejs/node/issues/48673 -// node <=21.0.0 and ^20.4.0 has the bug -export const hasWindowsUnicodeFsBug = - isWindows && - (+nodeVersionArray[0] > 20 || - (+nodeVersionArray[0] === 20 && +nodeVersionArray[1] >= 4)) diff --git a/playground/hmr-ssr/__tests__/hmr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts similarity index 95% rename from playground/hmr-ssr/__tests__/hmr.spec.ts rename to playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index 0998ee8ddb406f..6a2b3763b3ffec 100644 --- a/playground/hmr-ssr/__tests__/hmr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -7,7 +7,14 @@ import type { InlineConfig, Logger, ViteDevServer } from 'vite' import { createServer, createViteRuntime } from 'vite' import type { ViteRuntime } from 'vite/runtime' import type { RollupError } from 'rollup' -import { page, promiseWithResolvers, slash, untilUpdated } from '~utils' +import { + addFile, + page, + promiseWithResolvers, + readFile, + slash, + untilUpdated, +} from '~utils' let server: ViteDevServer const clientLogs: string[] = [] @@ -246,7 +253,7 @@ describe('hmr works correctly', () => { }) // TODO - // test.skipIf(hasWindowsUnicodeFsBug)('full-reload encodeURI path', async () => { + // test('full-reload encodeURI path', async () => { // await page.goto( // viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', // ) @@ -737,31 +744,19 @@ test.todo('should hmr when file is deleted and restored', async () => { ) await untilUpdated(() => hmr('.file-delete-restore'), 'parent:child1') + // delete the file editFile(parentFile, (code) => code.replace( "export { value as childValue } from './child'", "export const childValue = 'not-child'", ), ) + const originalChildFileCode = readFile(childFile) removeFile(childFile) await untilUpdated(() => hmr('.file-delete-restore'), 'parent:not-child') - createFile( - childFile, - ` -import { rerender } from './runtime' - -export const value = 'child' - -if (import.meta.hot) { - import.meta.hot.accept((newMod) => { - if (!newMod) return - - rerender({ child: newMod.value }) - }) -} -`, - ) + // restore the file + createFile(childFile, originalChildFileCode) editFile(parentFile, (code) => code.replace( "export const childValue = 'not-child'", @@ -822,6 +817,45 @@ test.todo('delete file should not break hmr', async () => { ) }) +test.todo( + 'deleted file should trigger dispose and prune callbacks', + async () => { + await setupViteRuntime('/hmr.ts') + + const parentFile = 'file-delete-restore/parent.js' + const childFile = 'file-delete-restore/child.js' + + // delete the file + editFile(parentFile, (code) => + code.replace( + "export { value as childValue } from './child'", + "export const childValue = 'not-child'", + ), + ) + const originalChildFileCode = readFile(childFile) + removeFile(childFile) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:not-child', + ) + expect(clientLogs).to.include('file-delete-restore/child.js is disposed') + expect(clientLogs).to.include('file-delete-restore/child.js is pruned') + + // restore the file + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) + }, +) + test('import.meta.hot?.accept', async () => { await setupViteRuntime('/hmr.ts') await untilConsoleLogAfter( diff --git a/playground/hmr/__tests__/hmr.spec.ts b/playground/hmr/__tests__/hmr.spec.ts index 0d95557aa65fb3..d4281ec1bbe5ae 100644 --- a/playground/hmr/__tests__/hmr.spec.ts +++ b/playground/hmr/__tests__/hmr.spec.ts @@ -1,13 +1,15 @@ import { beforeAll, describe, expect, it, test } from 'vitest' -import { hasWindowsUnicodeFsBug } from '../../hasWindowsUnicodeFsBug' +import type { Page } from 'playwright-chromium' import { addFile, + browser, browserLogs, editFile, getBg, getColor, isBuild, page, + readFile, removeFile, serverLogs, untilBrowserLogAfter, @@ -152,7 +154,7 @@ if (!isBuild) { }) test('invalidate', async () => { - const el = await page.$('.invalidation') + const el = await page.$('.invalidation-parent') await untilBrowserLogAfter( () => editFile('invalidation/child.js', (code) => @@ -174,6 +176,47 @@ if (!isBuild) { await untilUpdated(() => el.textContent(), 'child updated') }) + test('invalidate works with multiple tabs', async () => { + let page2: Page + try { + page2 = await browser.newPage() + await page2.goto(viteTestUrl) + + const el = await page.$('.invalidation-parent') + await untilBrowserLogAfter( + () => + editFile('invalidation/child.js', (code) => + code.replace('child', 'child updated'), + ), + [ + '>>> vite:beforeUpdate -- update', + '>>> vite:invalidate -- /invalidation/child.js', + '[vite] invalidate /invalidation/child.js', + '[vite] hot updated: /invalidation/child.js', + '>>> vite:afterUpdate -- update', + // if invalidate dedupe doesn't work correctly, this beforeUpdate will be called twice + '>>> vite:beforeUpdate -- update', + '(invalidation) parent is executing', + '[vite] hot updated: /invalidation/parent.js', + '>>> vite:afterUpdate -- update', + ], + true, + ) + await untilUpdated(() => el.textContent(), 'child updated') + } finally { + await page2.close() + } + }) + + test('invalidate on root triggers page reload', async () => { + editFile('invalidation/root.js', (code) => code.replace('Init', 'Updated')) + await page.waitForEvent('load') + await untilUpdated( + async () => (await page.$('.invalidation-root')).textContent(), + 'Updated', + ) + }) + test('soft invalidate', async () => { const el = await page.$('.soft-invalidation') expect(await el.textContent()).toBe( @@ -218,24 +261,21 @@ if (!isBuild) { await untilUpdated(() => el.textContent(), '3') }) - test.skipIf(hasWindowsUnicodeFsBug)( - 'full-reload encodeURI path', - async () => { - await page.goto( - viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', - ) - const el = await page.$('#app') - expect(await el.textContent()).toBe('title') - editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) => - code.replace('title', 'title2'), - ) - await page.waitForEvent('load') - await untilUpdated( - async () => (await page.$('#app')).textContent(), - 'title2', - ) - }, - ) + test('full-reload encodeURI path', async () => { + await page.goto( + viteTestUrl + '/unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', + ) + const el = await page.$('#app') + expect(await el.textContent()).toBe('title') + editFile('unicode-path/中文-にほんご-한글-🌕🌖🌗/index.html', (code) => + code.replace('title', 'title2'), + ) + await page.waitForEvent('load') + await untilUpdated( + async () => (await page.$('#app')).textContent(), + 'title2', + ) + }) test('CSS update preserves query params', async () => { await page.goto(viteTestUrl) @@ -784,40 +824,31 @@ if (!isBuild) { 'parent:child1', ) + // delete the file editFile(parentFile, (code) => code.replace( "export { value as childValue } from './child'", "export const childValue = 'not-child'", ), ) + const originalChildFileCode = readFile(childFile) removeFile(childFile) await untilUpdated( () => page.textContent('.file-delete-restore'), 'parent:not-child', ) - addFile( - childFile, - ` -import { rerender } from './runtime' - -export const value = 'child' - -if (import.meta.hot) { - import.meta.hot.accept((newMod) => { - if (!newMod) return - - rerender({ child: newMod.value }) - }) -} -`, - ) - editFile(parentFile, (code) => - code.replace( - "export const childValue = 'not-child'", - "export { value as childValue } from './child'", - ), - ) + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await loadPromise + }, [/connected/]) await untilUpdated( () => page.textContent('.file-delete-restore'), 'parent:child', @@ -875,6 +906,42 @@ if (import.meta.hot) { ) }) + test('deleted file should trigger dispose and prune callbacks', async () => { + await page.goto(viteTestUrl) + + const parentFile = 'file-delete-restore/parent.js' + const childFile = 'file-delete-restore/child.js' + + // delete the file + editFile(parentFile, (code) => + code.replace( + "export { value as childValue } from './child'", + "export const childValue = 'not-child'", + ), + ) + const originalChildFileCode = readFile(childFile) + removeFile(childFile) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:not-child', + ) + expect(browserLogs).to.include('file-delete-restore/child.js is disposed') + expect(browserLogs).to.include('file-delete-restore/child.js is pruned') + + // restore the file + addFile(childFile, originalChildFileCode) + editFile(parentFile, (code) => + code.replace( + "export const childValue = 'not-child'", + "export { value as childValue } from './child'", + ), + ) + await untilUpdated( + () => page.textContent('.file-delete-restore'), + 'parent:child', + ) + }) + test('import.meta.hot?.accept', async () => { const el = await page.$('.optional-chaining') await untilBrowserLogAfter( @@ -938,4 +1005,23 @@ if (import.meta.hot) { editFile('css-deps/dep.js', (code) => code.replace(`red`, `green`)) await untilUpdated(() => getColor('.css-deps'), 'green') }) + + test('hmr should happen after missing file is created', async () => { + const file = 'missing-file/a.js' + const code = 'console.log("a.js")' + + await untilBrowserLogAfter( + () => + page.goto(viteTestUrl + '/missing-file/index.html', { + waitUntil: 'load', + }), + /connected/, // wait for HMR connection + ) + + await untilBrowserLogAfter(async () => { + const loadPromise = page.waitForEvent('load') + addFile(file, code) + await loadPromise + }, [/connected/, 'a.js']) + }) } diff --git a/playground/hmr/file-delete-restore/child.js b/playground/hmr/file-delete-restore/child.js index 704c7d8c7e98cc..7031ef7db067c3 100644 --- a/playground/hmr/file-delete-restore/child.js +++ b/playground/hmr/file-delete-restore/child.js @@ -8,4 +8,12 @@ if (import.meta.hot) { rerender({ child: newMod.value }) }) + + import.meta.hot.dispose(() => { + console.log('file-delete-restore/child.js is disposed') + }) + + import.meta.hot.prune(() => { + console.log('file-delete-restore/child.js is pruned') + }) } diff --git a/playground/hmr/hmr.ts b/playground/hmr/hmr.ts index 5e572f83b703aa..1f764da0861d6f 100644 --- a/playground/hmr/hmr.ts +++ b/playground/hmr/hmr.ts @@ -1,7 +1,6 @@ import { virtual } from 'virtual:file' import { foo as depFoo, nestedFoo } from './hmrDep' import './importing-updated' -import './invalidation/parent' import './file-delete-restore' import './optional-chaining/parent' import './intermediate-file-delete' diff --git a/playground/hmr/index.html b/playground/hmr/index.html index 221a3bf39e1705..d5adaab6bd5629 100644 --- a/playground/hmr/index.html +++ b/playground/hmr/index.html @@ -7,6 +7,7 @@

+