diff --git a/docs/config/index.md b/docs/config/index.md index c141e7be88712f..3a7f355452c814 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -485,6 +485,23 @@ export default defineConfig(async ({ command, mode }) => { When running Vite on Windows Subsystem for Linux (WSL) 2, if the project folder resides in a Windows filesystem, you'll need to set this option to `{ usePolling: true }`. This is due to [a WSL2 limitation](https://github.com/microsoft/WSL/issues/4739) with the Windows filesystem. + The Vite server watcher skips `.git/` and `node_modules/` directories by default. If you want to watch a package inside `node_modules/`, you can pass a negated glob pattern to `server.watch.ignored`. That is: + + ```js + export default defineConfig({ + server: { + watch: { + ignored: ['!**/node_modules/your-package-name/**'] + } + }, + // The watched package must be excluded from optimization, + // so that it can appear in the dependency graph and trigger hot reload. + optimizeDeps: { + exclude: ['your-package-name'] + } + }) + ``` + ### server.middlewareMode - **Type:** `'ssr' | 'html'` @@ -657,6 +674,17 @@ export default defineConfig({ If disabled, all CSS in the entire project will be extracted into a single CSS file. +### build.cssTarget + +- **Type:** `string | string[]` +- **Default:** the same as [`build.target`](/config/#build-target) + + This options allows users to set a different browser target for CSS minification from the one used for JavaScript transpilation. + + It should only be used when you are targeting a non-mainstream browser. + One example is Android WeChat WebView, which supports most modern JavaScript features but not the [`#RGBA` hexadecimal color notation in CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb_colors). + In this case, you need to set `build.cssTarget` to `chrome61` to prevent vite from transform `rgba()` colors into `#RGBA` hexadecimal notations. + ### build.sourcemap - **Type:** `boolean | 'inline' | 'hidden'` @@ -730,7 +758,7 @@ export default defineConfig({ - **Type:** `boolean` - **Default:** `true` if `outDir` is inside `root` - By default, Vite will empty the `outDir` on build if it is inside project root. It will emit a warning if `outDir` is outside of root to avoid accidentially removing important files. You can explicitly set this option to suppress the warning. This is also available via command line as `--emptyOutDir`. + By default, Vite will empty the `outDir` on build if it is inside project root. It will emit a warning if `outDir` is outside of root to avoid accidentally removing important files. You can explicitly set this option to suppress the warning. This is also available via command line as `--emptyOutDir`. ### build.brotliSize diff --git a/docs/guide/index.md b/docs/guide/index.md index dae44ce09b3910..43421f6b52b79a 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -20,6 +20,21 @@ You can learn more about the rationale behind the project in the [Why Vite](./wh - The default build targets browsers that support both [native ESM via script tags](https://caniuse.com/es6-module) and [native ESM dynamic import](https://caniuse.com/es6-module-dynamic-import). Legacy browsers can be supported via the official [@vitejs/plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) - see the [Building for Production](./build) section for more details. +## Trying Vite Online + +You can try Vite online on [StackBlitz](https://vite.new/). It runs the Vite-based build setup directly in the browser, so it is almost identical to the local setup but doesn't require installing anything on your machine. You can navigate to `vite.new/{template}` to select which framework to use. + +The supported template presets are: + +| JavaScript | TypeScript | +| :---: | :---: | +| [vanilla](https://vite.new/vanilla) | [vanilla-ts](https://vite.new/vanilla-ts) | +| [vue](https://vite.new/vue) | [vue-ts](https://vite.new/vue-ts) | +| [react](https://vite.new/react) | [react-ts](https://vite.new/react-ts) | +| [preact](https://vite.new/preact) | [preact-ts](https://vite.new/preact-ts) | +| [lit](https://vite.new/lit) | [lit-ts](https://vite.new/lit-ts) | +| [svelte](https://vite.new/svelte) | [svelte-ts](https://vite.new/svelte-ts) | + ## Scaffolding Your First Vite Project ::: tip Compatibility Note @@ -59,22 +74,7 @@ npm init vite@latest my-vue-app -- --template vue yarn create vite my-vue-app --template vue ``` -Supported template presets include: - -- `vanilla` -- `vanilla-ts` -- `vue` -- `vue-ts` -- `react` -- `react-ts` -- `preact` -- `preact-ts` -- `lit` -- `lit-ts` -- `svelte` -- `svelte-ts` - -See [create-vite](https://github.com/vitejs/vite/tree/main/packages/create-vite) for more details on each template. +See [create-vite](https://github.com/vitejs/vite/tree/main/packages/create-vite) for more details on each supported template: `vanilla`, `vanilla-ts`, `vue`, `vue-ts`, `react`, `react-ts`, `preact`, `preact-ts`, `lit`, `lit-ts`, `svelte`, `svelte-ts`. ## Community Templates diff --git a/packages/playground/css/__tests__/css.spec.ts b/packages/playground/css/__tests__/css.spec.ts index ad5d9f344813e4..010f028a14ea82 100644 --- a/packages/playground/css/__tests__/css.spec.ts +++ b/packages/playground/css/__tests__/css.spec.ts @@ -338,3 +338,14 @@ test('inlined', async () => { // should not insert css expect(await getColor('.inlined')).toBe('black') }) + +test('minify css', async () => { + if (!isBuild) { + return + } + + // should keep the rgba() syntax + const cssFile = findAssetFile(/index\.\w+\.css$/) + expect(cssFile).toMatch('rgba(') + expect(cssFile).not.toMatch('#ffff00b3') +}) diff --git a/packages/playground/css/main.js b/packages/playground/css/main.js index cd2d6fda9ac0d5..24a278c8687940 100644 --- a/packages/playground/css/main.js +++ b/packages/playground/css/main.js @@ -1,3 +1,5 @@ +import './minify.css' + import css from './imported.css' text('.imported-css', css) diff --git a/packages/playground/css/minify.css b/packages/playground/css/minify.css new file mode 100644 index 00000000000000..ada062407cdb38 --- /dev/null +++ b/packages/playground/css/minify.css @@ -0,0 +1,3 @@ +.test-minify { + color: rgba(255, 255, 0, 0.7); +} diff --git a/packages/playground/css/vite.config.js b/packages/playground/css/vite.config.js index d49dcf4207834d..e4dc8d5a9f265f 100644 --- a/packages/playground/css/vite.config.js +++ b/packages/playground/css/vite.config.js @@ -3,6 +3,9 @@ const path = require('path') * @type {import('vite').UserConfig} */ module.exports = { + build: { + cssTarget: 'chrome61' + }, resolve: { alias: { '@': __dirname diff --git a/packages/playground/ssr-html/__tests__/serve.js b/packages/playground/ssr-html/__tests__/serve.js new file mode 100644 index 00000000000000..5ba5724f2b7a94 --- /dev/null +++ b/packages/playground/ssr-html/__tests__/serve.js @@ -0,0 +1,36 @@ +// @ts-check +// this is automtically detected by scripts/jestPerTestSetup.ts and will replace +// the default e2e test serve behavior + +const path = require('path') + +const port = (exports.port = 9530) + +/** + * @param {string} root + * @param {boolean} isProd + */ +exports.serve = async function serve(root, isProd) { + const { createServer } = require(path.resolve(root, 'server.js')) + const { app, vite } = await createServer(root, isProd) + + return new Promise((resolve, reject) => { + try { + const server = app.listen(port, () => { + resolve({ + // for test teardown + async close() { + await new Promise((resolve) => { + server.close(resolve) + }) + if (vite) { + await vite.close() + } + } + }) + }) + } catch (e) { + reject(e) + } + }) +} diff --git a/packages/playground/ssr-html/__tests__/ssr-html.spec.ts b/packages/playground/ssr-html/__tests__/ssr-html.spec.ts new file mode 100644 index 00000000000000..e34b8a91fc3421 --- /dev/null +++ b/packages/playground/ssr-html/__tests__/ssr-html.spec.ts @@ -0,0 +1,39 @@ +import { port } from './serve' +import fetch from 'node-fetch' + +const url = `http://localhost:${port}` + +describe('injected inline scripts', () => { + test('no injected inline scripts are present', async () => { + await page.goto(url) + const inlineScripts = await page.$$eval('script', (nodes) => + nodes.filter((n) => !n.getAttribute('src') && n.innerHTML) + ) + expect(inlineScripts).toHaveLength(0) + }) + + test('injected script proxied correctly', async () => { + await page.goto(url) + const proxiedScripts = await page.$$eval('script', (nodes) => + nodes + .filter((n) => { + const src = n.getAttribute('src') + if (!src) return false + return src.includes('?html-proxy&index') + }) + .map((n) => n.getAttribute('src')) + ) + + // assert at least 1 proxied script exists + expect(proxiedScripts).not.toHaveLength(0) + + const scriptContents = await Promise.all( + proxiedScripts.map((src) => fetch(url + src).then((res) => res.text())) + ) + + // all proxied scripts return code + for (const code of scriptContents) { + expect(code).toBeTruthy() + } + }) +}) diff --git a/packages/playground/ssr-html/index.html b/packages/playground/ssr-html/index.html new file mode 100644 index 00000000000000..c37dcc7e366ae8 --- /dev/null +++ b/packages/playground/ssr-html/index.html @@ -0,0 +1,11 @@ + + + + + + SSR HTML + + +

SSR Dynamic HTML

+ + diff --git a/packages/playground/ssr-html/package.json b/packages/playground/ssr-html/package.json new file mode 100644 index 00000000000000..a14756422a8b28 --- /dev/null +++ b/packages/playground/ssr-html/package.json @@ -0,0 +1,15 @@ +{ + "name": "test-ssr-html", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "node server", + "serve": "cross-env NODE_ENV=production node server", + "debug": "node --inspect-brk server" + }, + "dependencies": {}, + "devDependencies": { + "cross-env": "^7.0.3", + "express": "^4.17.1" + } +} diff --git a/packages/playground/ssr-html/server.js b/packages/playground/ssr-html/server.js new file mode 100644 index 00000000000000..ad115f1be01163 --- /dev/null +++ b/packages/playground/ssr-html/server.js @@ -0,0 +1,75 @@ +// @ts-check +const fs = require('fs') +const path = require('path') +const express = require('express') + +const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD + +const DYNAMIC_SCRIPTS = ` + + +` + +async function createServer( + root = process.cwd(), + isProd = process.env.NODE_ENV === 'production' +) { + const resolve = (p) => path.resolve(__dirname, p) + + const app = express() + + /** + * @type {import('vite').ViteDevServer} + */ + let vite + vite = await require('vite').createServer({ + root, + logLevel: isTest ? 'error' : 'info', + server: { + middlewareMode: 'ssr', + watch: { + // During tests we edit the files too fast and sometimes chokidar + // misses change events, so enforce polling for consistency + usePolling: true, + interval: 100 + } + } + }) + // use vite's connect instance as middleware + app.use(vite.middlewares) + + app.use('*', async (req, res) => { + try { + let [url] = req.originalUrl.split('?') + if (url.endsWith('/')) url += 'index.html' + + const htmlLoc = resolve(`.${url}`) + let html = fs.readFileSync(htmlLoc, 'utf8') + html = html.replace('', `${DYNAMIC_SCRIPTS}`) + html = await vite.transformIndexHtml(url, html) + + res.status(200).set({ 'Content-Type': 'text/html' }).end(html) + } catch (e) { + vite && vite.ssrFixStacktrace(e) + console.log(e.stack) + res.status(500).end(e.stack) + } + }) + + return { app, vite } +} + +if (!isTest) { + createServer().then(({ app }) => + app.listen(3000, () => { + console.log('http://localhost:3000') + }) + ) +} + +// for test use +exports.createServer = createServer diff --git a/packages/playground/ssr-html/src/app.js b/packages/playground/ssr-html/src/app.js new file mode 100644 index 00000000000000..5b0175bb863d70 --- /dev/null +++ b/packages/playground/ssr-html/src/app.js @@ -0,0 +1,3 @@ +const p = document.createElement('p') +p.innerHTML = '✅ Dynamically injected script from file' +document.body.appendChild(p) diff --git a/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts b/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts index b92da5fc22bd63..f0ab340bc0ae4b 100644 --- a/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts +++ b/packages/playground/ssr-vue/__tests__/ssr-vue.spec.ts @@ -142,3 +142,8 @@ test('client navigation', async () => { editFile('src/pages/About.vue', (code) => code.replace('About', 'changed')) await untilUpdated(() => page.textContent('h1'), 'changed') }) + +test('import.meta.url', async () => { + await page.goto(url) + expect(await page.textContent('.protocol')).toEqual('file:') +}) \ No newline at end of file diff --git a/packages/playground/ssr-vue/src/pages/Home.vue b/packages/playground/ssr-vue/src/pages/Home.vue index b0003df3e9d0c9..05d39cf69ff1ae 100644 --- a/packages/playground/ssr-vue/src/pages/Home.vue +++ b/packages/playground/ssr-vue/src/pages/Home.vue @@ -7,6 +7,8 @@

msg from virtual module: {{ foo.msg }}

this will be styled with a font-face

+

{{ state.url }}

+

{{ state.protocol }}

@@ -21,7 +23,13 @@ const Foo = defineAsyncComponent(() => function load(file) { return defineAsyncComponent(() => import(`../components/${file}.vue`)) } -const state = reactive({ count: 0 }) +const url = import.meta.env.SSR ? import.meta.url : document.querySelector('.import-meta-url').textContent +const protocol = new URL(url).protocol +const state = reactive({ + count: 0, + protocol, + url +})