diff --git a/.changeset/nasty-baboons-joke.md b/.changeset/nasty-baboons-joke.md new file mode 100644 index 000000000..46f1ac302 --- /dev/null +++ b/.changeset/nasty-baboons-joke.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/vite-plugin-svelte': patch +--- + +fix: watch preprocessor dependencies and trigger hmr on change diff --git a/packages/playground/svelte-preprocess/.gitignore b/packages/playground/svelte-preprocess/.gitignore new file mode 100644 index 000000000..0895721cc --- /dev/null +++ b/packages/playground/svelte-preprocess/.gitignore @@ -0,0 +1,5 @@ +.vscode +.idea +node_modules +dist +dist-ssr diff --git a/packages/playground/svelte-preprocess/README.md b/packages/playground/svelte-preprocess/README.md new file mode 100644 index 000000000..2dd7e4918 --- /dev/null +++ b/packages/playground/svelte-preprocess/README.md @@ -0,0 +1,50 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- SvelteKit is still a work-in-progress. +- It currently does not support the pure-SPA use case. +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-app` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store'; +export default writable(0); +``` diff --git a/packages/playground/svelte-preprocess/__tests__/svelte-preprocess.spec.ts b/packages/playground/svelte-preprocess/__tests__/svelte-preprocess.spec.ts new file mode 100644 index 000000000..8e394dd4c --- /dev/null +++ b/packages/playground/svelte-preprocess/__tests__/svelte-preprocess.spec.ts @@ -0,0 +1,143 @@ +import { + isBuild, + getEl, + getText, + editFileAndWaitForHmrComplete, + untilUpdated, + sleep, + getColor, + addFile, + removeFile +} from '../../testUtils'; + +test('should render App', async () => { + expect(await getText('h1')).toBe(`I'm blue`); + expect(await getColor('h1')).toBe('blue'); + expect(await getText('h2')).toBe(`I'm red`); + expect(await getColor('h2')).toBe('red'); + expect(await getText('p')).toBe(`I'm green`); + expect(await getColor('p')).toBe('green'); + expect(await getText('span')).toBe(`I'm orangered`); + expect(await getColor('span')).toBe('orangered'); +}); + +test('should not have failed requests', async () => { + browserLogs.forEach((msg) => { + expect(msg).not.toMatch('404'); + }); +}); + +if (!isBuild) { + describe('hmr', () => { + test('should apply updates when editing App.svelte', async () => { + expect(await getText('span')).toBe(`I'm orangered`); + await editFileAndWaitForHmrComplete('src/App.svelte', (c) => + c.replace(`I'm orangered`, `I'm replaced`) + ); + expect(await getText('span')).toBe(`I'm replaced`); + expect(await getColor('span')).toBe('orangered'); + await editFileAndWaitForHmrComplete( + 'src/App.svelte', + (c) => c.replace(`color: orangered`, `color: magenta`), + '/src/App.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('span')).toBe('magenta'); + }); + + test('should apply updates when editing MultiFile.html', async () => { + expect(await getText('h1')).toBe(`I'm blue`); + expect(await getText('h2')).toBe(`I'm red`); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/MultiFile.html', + (c) => c.replace(`I'm blue`, `I'm replaced`).replace(`I'm red`, `I'm replaced too`), + '/src/lib/multifile/MultiFile.svelte' + ); + expect(await getText('h1')).toBe(`I'm replaced`); + expect(await getText('h2')).toBe(`I'm replaced too`); + }); + + test('should apply updates when editing MultiFile.scss', async () => { + expect(await getColor('h1')).toBe('blue'); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/MultiFile.scss', + (c) => c.replace(`color: blue`, `color: magenta`), + '/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('h1')).toBe('magenta'); + }); + + test('should apply updates when editing _someImport.scss', async () => { + expect(await getColor('h2')).toBe('red'); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/_someImport.scss', + (c) => c.replace(`color: red`, `color: magenta`), + '/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('h2')).toBe('magenta'); + }); + + test('should remove styles that are no longer imported', async () => { + expect(await getColor('h2')).toBe('magenta'); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/MultiFile.scss', + (c) => c.replace(`@import 'someImport';`, `/*@import 'someImport';*/`), + '/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('h2')).toBe('black'); + }); + + test('should apply styles from new dependency', async () => { + expect(await getColor('h2')).toBe('black'); + await addFile('src/lib/multifile/_foo.scss', 'h2 { color: maroon; }'); + expect(await getColor('h2')).toBe('black'); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/MultiFile.scss', + (c) => c.replace(`/*@import 'someImport';*/`, `/*@import 'someImport';*/\n@import 'foo';`), + '/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('h2')).toBe('maroon'); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/_foo.scss', + (c) => c.replace(`maroon`, `green`), + '/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('h2')).toBe('green'); + }); + + test('should apply updates when editing MultiFile.ts', async () => { + expect(await getText('p')).toBe(`I'm green`); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/MultiFile.ts', + (c) => c.replace(`'green'`, `'a replaced value'`), + '/src/lib/multifile/MultiFile.svelte' + ); + expect(await getText('p')).toBe(`I'm a replaced value`); + }); + + test('should apply updates when editing someother.css', async () => { + expect(await getColor('p')).toBe('green'); + await editFileAndWaitForHmrComplete('src/lib/multifile/someother.css', (c) => + c.replace(`color: green`, `color: magenta`) + ); + expect(await getColor('p')).toBe('magenta'); + }); + + test('should show error on deleting dependency', async () => { + expect(await getColor('h2')).toBe('green'); + await removeFile('src/lib/multifile/_foo.scss'); + expect(await getColor('h2')).toBe('green'); + const errorOverlay = await page.waitForSelector('vite-error-overlay'); + expect(errorOverlay).toBeTruthy(); + await errorOverlay.click({ position: { x: 1, y: 1 } }); + await sleep(50); + const errorOverlay2 = await getEl('vite-error-overlay'); + expect(errorOverlay2).toBeFalsy(); + await editFileAndWaitForHmrComplete( + 'src/lib/multifile/MultiFile.scss', + (c) => c.replace(`@import 'foo';`, ``), + '/src/lib/multifile/MultiFile.svelte?svelte&type=style&lang.css' + ); + expect(await getColor('h2')).toBe('black'); + }); + }); +} diff --git a/packages/playground/svelte-preprocess/index.html b/packages/playground/svelte-preprocess/index.html new file mode 100644 index 000000000..aca29d90a --- /dev/null +++ b/packages/playground/svelte-preprocess/index.html @@ -0,0 +1,13 @@ + + +
+ + + +I'm {foo}
diff --git a/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.scss b/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.scss new file mode 100644 index 000000000..4063ca6fc --- /dev/null +++ b/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.scss @@ -0,0 +1,4 @@ +@import 'someImport'; +h1 { + color: blue; +} diff --git a/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.svelte b/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.svelte new file mode 100644 index 000000000..abc83b119 --- /dev/null +++ b/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.svelte @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.ts b/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.ts new file mode 100644 index 000000000..f4b1a4b5b --- /dev/null +++ b/packages/playground/svelte-preprocess/src/lib/multifile/MultiFile.ts @@ -0,0 +1,3 @@ +// eslint-disable-next-line prefer-const +import './someother.css'; +export const foo: string = 'green'; diff --git a/packages/playground/svelte-preprocess/src/lib/multifile/_someImport.scss b/packages/playground/svelte-preprocess/src/lib/multifile/_someImport.scss new file mode 100644 index 000000000..57c1519a1 --- /dev/null +++ b/packages/playground/svelte-preprocess/src/lib/multifile/_someImport.scss @@ -0,0 +1,3 @@ +h2 { + color: red; +} diff --git a/packages/playground/svelte-preprocess/src/lib/multifile/someother.css b/packages/playground/svelte-preprocess/src/lib/multifile/someother.css new file mode 100644 index 000000000..452b9b9cf --- /dev/null +++ b/packages/playground/svelte-preprocess/src/lib/multifile/someother.css @@ -0,0 +1,3 @@ +p { + color: green; +} diff --git a/packages/playground/svelte-preprocess/src/main.ts b/packages/playground/svelte-preprocess/src/main.ts new file mode 100644 index 000000000..448d14965 --- /dev/null +++ b/packages/playground/svelte-preprocess/src/main.ts @@ -0,0 +1,7 @@ +import App from './App.svelte'; + +const app = new App({ + target: document.getElementById('app') +}); + +export default app; diff --git a/packages/playground/svelte-preprocess/svelte.config.cjs b/packages/playground/svelte-preprocess/svelte.config.cjs new file mode 100644 index 000000000..0b32783a0 --- /dev/null +++ b/packages/playground/svelte-preprocess/svelte.config.cjs @@ -0,0 +1,7 @@ +const sveltePreprocess = require('svelte-preprocess') + +module.exports = { + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: sveltePreprocess() +} diff --git a/packages/playground/svelte-preprocess/tsconfig.json b/packages/playground/svelte-preprocess/tsconfig.json new file mode 100644 index 000000000..c882897e0 --- /dev/null +++ b/packages/playground/svelte-preprocess/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "target": "esnext", + "module": "esnext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/packages/playground/svelte-preprocess/vite.config.js b/packages/playground/svelte-preprocess/vite.config.js new file mode 100644 index 000000000..ce40e8795 --- /dev/null +++ b/packages/playground/svelte-preprocess/vite.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require('vite'); +const svelte = require('@sveltejs/vite-plugin-svelte'); + +module.exports = defineConfig(({ command, mode }) => { + const isProduction = mode === 'production'; + return { + plugins: [svelte()], + build: { + minify: isProduction + } + }; +}); diff --git a/packages/playground/testUtils.ts b/packages/playground/testUtils.ts index efdfc245e..82dd0c882 100644 --- a/packages/playground/testUtils.ts +++ b/packages/playground/testUtils.ts @@ -140,13 +140,16 @@ export async function hmrUpdateComplete(file, timeout) { }); } -export async function editFileAndWaitForHmrComplete(file, replacer) { +export async function editFileAndWaitForHmrComplete(file, replacer, fileUpdateToWaitFor?) { const newContent = await editFile(file, replacer); + if (!fileUpdateToWaitFor) { + fileUpdateToWaitFor = file; + } try { - await hmrUpdateComplete(file, 10000); + await hmrUpdateComplete(fileUpdateToWaitFor, 10000); } catch (e) { console.log(`retrying hmr update for ${file}`); await editFile(file, () => newContent); - await hmrUpdateComplete(file, 5000); + await hmrUpdateComplete(fileUpdateToWaitFor, 5000); } } diff --git a/packages/playground/vite-ssr/__tests__/serve.js b/packages/playground/vite-ssr/__tests__/serve.js index 000416e42..3dce14a42 100644 --- a/packages/playground/vite-ssr/__tests__/serve.js +++ b/packages/playground/vite-ssr/__tests__/serve.js @@ -49,21 +49,21 @@ exports.serve = async function serve(root, isProd) { port: port, async close() { let err; - if(server) { + if (server) { err = await new Promise((resolve) => { - server.close(resolve) - }) + server.close(resolve); + }); } if (vite) { try { - await vite.close() + await vite.close(); } catch (e) { - if(!err) { + if (!err) { err = e; } } } - if(err) { + if (err) { throw err; } } diff --git a/packages/vite-plugin-svelte/src/index.ts b/packages/vite-plugin-svelte/src/index.ts index 79270c170..7cbd5db4d 100644 --- a/packages/vite-plugin-svelte/src/index.ts +++ b/packages/vite-plugin-svelte/src/index.ts @@ -6,8 +6,8 @@ import * as relative from 'require-relative'; import { handleHotUpdate } from './handleHotUpdate'; import { log } from './utils/log'; -import { createCompileSvelte } from './utils/compile'; -import { buildIdParser, IdParser } from './utils/id'; +import { CompileData, createCompileSvelte } from './utils/compile'; +import { buildIdParser, IdParser, SvelteRequest } from './utils/id'; import { validateInlineOptions, Options, @@ -18,6 +18,7 @@ import { import { VitePluginSvelteCache } from './utils/VitePluginSvelteCache'; import { SVELTE_IMPORTS, SVELTE_RESOLVE_MAIN_FIELDS } from './utils/constants'; +import { setupWatchers } from './utils/watch'; export { Options, @@ -52,7 +53,13 @@ export default function vitePluginSvelte(inlineOptions?: Partial