diff --git a/examples/svelte/.gitignore b/examples/svelte/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/examples/svelte/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/svelte/README.md b/examples/svelte/README.md new file mode 100644 index 0000000..e6cd94f --- /dev/null +++ b/examples/svelte/README.md @@ -0,0 +1,47 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VS Code](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?** + +- 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. + +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-vite` 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/examples/svelte/farm.config.ts b/examples/svelte/farm.config.ts new file mode 100644 index 0000000..2c6191c --- /dev/null +++ b/examples/svelte/farm.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from '@farmfe/core' +import { svelte } from '@farmfe/js-plugin-svelte' +import fs from 'fs' +export default defineConfig({ + plugins: [svelte(), base()], + compilation: { + persistentCache: false, + progress: false, + } +}) + +function base() { + return { + name: 'farm-load-vue-module-type', + priority: -100, + load: { + filters: { + resolvedPaths: ['.svelte'], + }, + executor: async (param) => { + const content = await fs.readFile(param.resolvedPath, 'utf-8') + + return { + content, + moduleType: 'js', + } + }, + }, + } +} diff --git a/examples/svelte/index.html b/examples/svelte/index.html new file mode 100644 index 0000000..b6c5f0a --- /dev/null +++ b/examples/svelte/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Svelte + TS + + +
+ + + diff --git a/examples/svelte/package.json b/examples/svelte/package.json new file mode 100644 index 0000000..b990ac1 --- /dev/null +++ b/examples/svelte/package.json @@ -0,0 +1,23 @@ +{ + "name": "svelte", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "farm dev", + "build": "farm build", + "preview": "farm preview", + "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" + }, + "devDependencies": { + "@farmfe/core": "/Users/adny/rust/farm/packages/core", + "@farmfe/cli": "/Users/adny/rust/farm/packages/cli", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@farmfe/js-plugin-svelte": "workspace:*", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.15.0", + "svelte-check": "^4.1.1", + "typescript": "~5.6.2", + "vite": "^6.0.5" + } +} diff --git a/examples/svelte/public/vite.svg b/examples/svelte/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/svelte/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/svelte/src/App.svelte b/examples/svelte/src/App.svelte new file mode 100644 index 0000000..f75b68a --- /dev/null +++ b/examples/svelte/src/App.svelte @@ -0,0 +1,47 @@ + + +
+
+ + + + + + +
+

Vite + Svelte

+ +
+ +
+ +

+ Check out SvelteKit, the official Svelte app framework powered by Vite! +

+ +

+ Click on the Vite and Svelte logos to learn more +

+
+ + diff --git a/examples/svelte/src/app.css b/examples/svelte/src/app.css new file mode 100644 index 0000000..617f5e9 --- /dev/null +++ b/examples/svelte/src/app.css @@ -0,0 +1,79 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/svelte/src/assets/svelte.svg b/examples/svelte/src/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/examples/svelte/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/svelte/src/lib/Counter.svelte b/examples/svelte/src/lib/Counter.svelte new file mode 100644 index 0000000..37d75ce --- /dev/null +++ b/examples/svelte/src/lib/Counter.svelte @@ -0,0 +1,10 @@ + + + diff --git a/examples/svelte/src/main.ts b/examples/svelte/src/main.ts new file mode 100644 index 0000000..664a057 --- /dev/null +++ b/examples/svelte/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from 'svelte' +import './app.css' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/examples/svelte/src/vite-env.d.ts b/examples/svelte/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/examples/svelte/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/svelte/svelte.config.js b/examples/svelte/svelte.config.js new file mode 100644 index 0000000..b0683fd --- /dev/null +++ b/examples/svelte/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/examples/svelte/tsconfig.app.json b/examples/svelte/tsconfig.app.json new file mode 100644 index 0000000..55a2f9b --- /dev/null +++ b/examples/svelte/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * 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, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/examples/svelte/tsconfig.json b/examples/svelte/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/examples/svelte/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/examples/svelte/tsconfig.node.json b/examples/svelte/tsconfig.node.json new file mode 100644 index 0000000..db0becc --- /dev/null +++ b/examples/svelte/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/svelte/vite.config.ts b/examples/svelte/vite.config.ts new file mode 100644 index 0000000..d32eba1 --- /dev/null +++ b/examples/svelte/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/js-plugins/svelte/CHANGELOG.md b/js-plugins/svelte/CHANGELOG.md new file mode 100644 index 0000000..7e8f973 --- /dev/null +++ b/js-plugins/svelte/CHANGELOG.md @@ -0,0 +1,1237 @@ +# @sveltejs/vite-plugin-svelte + +## 5.0.3 +### Patch Changes + + +- fix errorhandling to work with errors that don't have a code property ([#1054](https://github.com/sveltejs/vite-plugin-svelte/pull/1054)) + +## 5.0.2 +### Patch Changes + + +- adapt internal handling of warning and error `code` property to changes in svelte5 ([#1044](https://github.com/sveltejs/vite-plugin-svelte/pull/1044)) + +## 5.0.1 +### Patch Changes + + +- Fix peer dependencies warning ([#1038](https://github.com/sveltejs/vite-plugin-svelte/pull/1038)) + +## 5.0.0 +### Major Changes + + +- Handle Vite 6 breaking change and remove Vite 5 handling ([#1020](https://github.com/sveltejs/vite-plugin-svelte/pull/1020)) + + +- Support Vite 6 ([#1026](https://github.com/sveltejs/vite-plugin-svelte/pull/1026)) + + +### Minor Changes + + +- Add `esm-env` to `ssr.noExternal` by default to resolve its conditions with Vite ([#1020](https://github.com/sveltejs/vite-plugin-svelte/pull/1020)) + + +- Support `?inline` query on Svelte style virtual modules ([#1024](https://github.com/sveltejs/vite-plugin-svelte/pull/1024)) + + +### Patch Changes + + +- remove vite6 beta from peer range ([#1035](https://github.com/sveltejs/vite-plugin-svelte/pull/1035)) + + +- Allow script tags to span multiple lines ([`0db95a9`](https://github.com/sveltejs/vite-plugin-svelte/commit/0db95a9cbcd281b99b8b817c8eda8d9ff8fa2db2)) + +- Updated dependencies [[`4fefbc2`](https://github.com/sveltejs/vite-plugin-svelte/commit/4fefbc24718953161ac7f86750df2dd539ca7978), [`e262266`](https://github.com/sveltejs/vite-plugin-svelte/commit/e2622664d9871558e03974524467968c7f906098)]: + - @sveltejs/vite-plugin-svelte-inspector@4.0.0 + +## 5.0.0-next.0 +### Major Changes + + +- Handle Vite 6 breaking change and remove Vite 5 handling ([#1020](https://github.com/sveltejs/vite-plugin-svelte/pull/1020)) + + +- Support Vite 6 ([#1026](https://github.com/sveltejs/vite-plugin-svelte/pull/1026)) + + +### Minor Changes + + +- Add `esm-env` to `ssr.noExternal` by default to resolve its conditions with Vite ([#1020](https://github.com/sveltejs/vite-plugin-svelte/pull/1020)) + + +- Support `?inline` query on Svelte style virtual modules ([#1024](https://github.com/sveltejs/vite-plugin-svelte/pull/1024)) + + +### Patch Changes + + +- Allow script tags to span multiple lines ([`0db95a9`](https://github.com/sveltejs/vite-plugin-svelte/commit/0db95a9cbcd281b99b8b817c8eda8d9ff8fa2db2)) + +- Updated dependencies [[`e262266`](https://github.com/sveltejs/vite-plugin-svelte/commit/e2622664d9871558e03974524467968c7f906098)]: + - @sveltejs/vite-plugin-svelte-inspector@4.0.0-next.0 + +## 4.0.1 +### Patch Changes + + +- removed references to compiler options no longer available in svelte5 ([#1010](https://github.com/sveltejs/vite-plugin-svelte/pull/1010)) + +## 4.0.0 +### Major Changes + + +- only prebundle files with default filenames (.svelte for components, .svelte.(js|ts) for modules) ([#901](https://github.com/sveltejs/vite-plugin-svelte/pull/901)) + + +- remove support for Svelte 4 ([#892](https://github.com/sveltejs/vite-plugin-svelte/pull/892)) + + +- breaking(types): some types that have been unintentionally public are now private ([#934](https://github.com/sveltejs/vite-plugin-svelte/pull/934)) + + +- disable script preprocessing in vitePreprocess() by default because Svelte 5 supports lang=ts out of the box ([#892](https://github.com/sveltejs/vite-plugin-svelte/pull/892)) + + +- replaced svelte-hmr with Svelte 5 compiler hmr integration ([#892](https://github.com/sveltejs/vite-plugin-svelte/pull/892)) + + +### Minor Changes + + +- allow infix notation for svelte modules ([#901](https://github.com/sveltejs/vite-plugin-svelte/pull/901)) + + Previously, only suffix notation `.svelte.js` was allowed, now you can also use `.svelte.test.js` or `.svelte.stories.js`. + This helps when writing testcases or other auxillary code where you may want to use runes too. + +- feat(config): dynamically extract list of svelte exports from peer dependency so that new exports work automatically" ([#941](https://github.com/sveltejs/vite-plugin-svelte/pull/941)) + + +- feat(warnings): change default loglevel of warnings originating from files in node_modules to debug. To see them call `DEBUG:vite-plugin-svelte:node-modules-onwarn pnpm build`. ([#989](https://github.com/sveltejs/vite-plugin-svelte/pull/989)) + + +### Patch Changes + + +- fix: make defaultHandler a required argument for onwarn in plugin options ([#895](https://github.com/sveltejs/vite-plugin-svelte/pull/895)) + + +- prebundle with dev: true by default ([#901](https://github.com/sveltejs/vite-plugin-svelte/pull/901)) + + +- fix(dev): compile with hmr: false for prebundled deps as hmr does not work with that ([#950](https://github.com/sveltejs/vite-plugin-svelte/pull/950)) + + +- fix: ensure svelte modules correctly run in DEV mode ([#906](https://github.com/sveltejs/vite-plugin-svelte/pull/906)) + + +- ensure consistent use of compileOptions.hmr also for prebundling ([#956](https://github.com/sveltejs/vite-plugin-svelte/pull/956)) + + +- fix(optimizeDeps): avoid to optimise server only entrypoints of svelte that are never used on the client ([#941](https://github.com/sveltejs/vite-plugin-svelte/pull/941)) + + +- update peer on workspace packages to avoid packages bumping each other ([#916](https://github.com/sveltejs/vite-plugin-svelte/pull/916)) + + +- export PluginOptions interface ([#976](https://github.com/sveltejs/vite-plugin-svelte/pull/976)) + + +- Remove log about experimental status of Svelte 5. Note that breaking changes can still occur while vite-plugin-svelte 4 is in prerelease mode ([#894](https://github.com/sveltejs/vite-plugin-svelte/pull/894)) + + +- fix: ensure vite config is only resolved once during lazy init of vitePreprocess ([#912](https://github.com/sveltejs/vite-plugin-svelte/pull/912)) + + +- fix(vitePreprocess): default to build config so that svelte-check does not trigger dev-only plugins ([#931](https://github.com/sveltejs/vite-plugin-svelte/pull/931)) + + +- fix: only apply infix filter to basename ([#920](https://github.com/sveltejs/vite-plugin-svelte/pull/920)) + + +- fix: disable hmr when vite config server.hmr is false ([#913](https://github.com/sveltejs/vite-plugin-svelte/pull/913)) + + +- fix(dev): make sure custom cssHash is applied consistently even for prebundled components to avoid hash mismatches during hydration ([#950](https://github.com/sveltejs/vite-plugin-svelte/pull/950)) + +- Updated dependencies [[`22baa25`](https://github.com/sveltejs/vite-plugin-svelte/commit/22baa25b5e98ddc92715bfc430dc9d0cfad99bb0), [`49324db`](https://github.com/sveltejs/vite-plugin-svelte/commit/49324dbf747a46ae75b405a29fc7feac2db966dd), [`e9f048c`](https://github.com/sveltejs/vite-plugin-svelte/commit/e9f048c362a0769b3d5afa87da6f8398f46fe1a9), [`213fedd`](https://github.com/sveltejs/vite-plugin-svelte/commit/213fedd68ec2c5fcb41752e05dcded4abfa8d0c0)]: + - @sveltejs/vite-plugin-svelte-inspector@3.0.0 + +## 4.0.0-next.8 +### Minor Changes + + +- feat(warnings): change default loglevel of warnings originating from files in node_modules to debug. To see them call `DEBUG:vite-plugin-svelte:node-modules-onwarn pnpm build`. ([#989](https://github.com/sveltejs/vite-plugin-svelte/pull/989)) + +## 4.0.0-next.7 +### Patch Changes + + +- export PluginOptions interface ([#976](https://github.com/sveltejs/vite-plugin-svelte/pull/976)) + +## 4.0.0-next.6 +### Patch Changes + + +- ensure consistent use of compileOptions.hmr also for prebundling ([#956](https://github.com/sveltejs/vite-plugin-svelte/pull/956)) + +## 4.0.0-next.5 +### Patch Changes + + +- fix(dev): compile with hmr: false for prebundled deps as hmr does not work with that ([#950](https://github.com/sveltejs/vite-plugin-svelte/pull/950)) + + +- fix(dev): make sure custom cssHash is applied consistently even for prebundled components to avoid hash mismatches during hydration ([#950](https://github.com/sveltejs/vite-plugin-svelte/pull/950)) + +## 4.0.0-next.4 +### Major Changes + + +- breaking(types): some types that have been unintentionally public are now private ([#934](https://github.com/sveltejs/vite-plugin-svelte/pull/934)) + + +### Minor Changes + + +- feat(config): dynamically extract list of svelte exports from peer dependency so that new exports work automatically" ([#941](https://github.com/sveltejs/vite-plugin-svelte/pull/941)) + + +### Patch Changes + + +- fix(optimizeDeps): avoid to optimise server only entrypoints of svelte that are never used on the client ([#941](https://github.com/sveltejs/vite-plugin-svelte/pull/941)) + + +- fix(vitePreprocess): default to build config so that svelte-check does not trigger dev-only plugins ([#931](https://github.com/sveltejs/vite-plugin-svelte/pull/931)) + +- Updated dependencies [[`e9f048c362a0769b3d5afa87da6f8398f46fe1a9`](https://github.com/sveltejs/vite-plugin-svelte/commit/e9f048c362a0769b3d5afa87da6f8398f46fe1a9)]: + - @sveltejs/vite-plugin-svelte-inspector@3.0.0-next.3 + +## 4.0.0-next.3 +### Patch Changes + + +- fix: only apply infix filter to basename ([#920](https://github.com/sveltejs/vite-plugin-svelte/pull/920)) + +## 4.0.0-next.2 +### Patch Changes + + +- update peer on workspace packages to avoid packages bumping each other ([#916](https://github.com/sveltejs/vite-plugin-svelte/pull/916)) + + +- fix: ensure vite config is only resolved once during lazy init of vitePreprocess ([#912](https://github.com/sveltejs/vite-plugin-svelte/pull/912)) + + +- fix: disable hmr when vite config server.hmr is false ([#913](https://github.com/sveltejs/vite-plugin-svelte/pull/913)) + +## 4.0.0-next.1 + +### Major Changes + +- only prebundle files with default filenames (.svelte for components, .svelte.(js|ts) for modules) ([#901](https://github.com/sveltejs/vite-plugin-svelte/pull/901)) + +### Minor Changes + +- allow infix notation for svelte modules ([#901](https://github.com/sveltejs/vite-plugin-svelte/pull/901)) + + Previously, only suffix notation `.svelte.js` was allowed, now you can also use `.svelte.test.js` or `.svelte.stories.js`. + This helps when writing testcases or other auxillary code where you may want to use runes too. + +### Patch Changes + +- prebundle with dev: true by default ([#901](https://github.com/sveltejs/vite-plugin-svelte/pull/901)) + +- fix: ensure svelte modules correctly run in DEV mode ([#906](https://github.com/sveltejs/vite-plugin-svelte/pull/906)) + +- Updated dependencies []: + - @sveltejs/vite-plugin-svelte-inspector@3.0.0-next.1 + +## 4.0.0-next.0 + +### Major Changes + +- remove support for Svelte 4 ([#892](https://github.com/sveltejs/vite-plugin-svelte/pull/892)) + +- disable script preprocessing in vitePreprocess() by default because Svelte 5 supports lang=ts out of the box ([#892](https://github.com/sveltejs/vite-plugin-svelte/pull/892)) + +- replaced svelte-hmr with Svelte 5 compiler hmr integration ([#892](https://github.com/sveltejs/vite-plugin-svelte/pull/892)) + +### Patch Changes + +- fix: make defaultHandler a required argument for onwarn in plugin options ([#895](https://github.com/sveltejs/vite-plugin-svelte/pull/895)) + +- Remove log about experimental status of Svelte 5. Note that breaking changes can still occur while vite-plugin-svelte 4 is in prerelease mode ([#894](https://github.com/sveltejs/vite-plugin-svelte/pull/894)) + +- Updated dependencies [[`49324dbf747a46ae75b405a29fc7feac2db966dd`](https://github.com/sveltejs/vite-plugin-svelte/commit/49324dbf747a46ae75b405a29fc7feac2db966dd)]: + - @sveltejs/vite-plugin-svelte-inspector@3.0.0-next.0 + +## 3.1.0 + +### Minor Changes + +- feat(svelte5): enable hmr option in dev ([#836](https://github.com/sveltejs/vite-plugin-svelte/pull/836)) + +### Patch Changes + +- Remove unnecessary `enableSourcemap` option usage and prevent passing it in Svelte 5 ([#862](https://github.com/sveltejs/vite-plugin-svelte/pull/862)) + +- Updated dependencies [[`8ae3dc8cf415355f406f23d6104cb6153d75dfc8`](https://github.com/sveltejs/vite-plugin-svelte/commit/8ae3dc8cf415355f406f23d6104cb6153d75dfc8)]: + - @sveltejs/vite-plugin-svelte-inspector@2.1.0 + +## 3.0.2 + +### Patch Changes + +- fix(compile): correctly determine script lang in files where a comment precedes the script tag ([#844](https://github.com/sveltejs/vite-plugin-svelte/pull/844)) + +## 3.0.1 + +### Patch Changes + +- fix: improve checking of script and style in .svelte code to work with new generic= attribute ([#799](https://github.com/sveltejs/vite-plugin-svelte/pull/799)) + +- Fix optional parameter types ([#797](https://github.com/sveltejs/vite-plugin-svelte/pull/797)) + +- Update log level for HMR updates where the output is functionally equivalent to the previous version to "debug" ([#806](https://github.com/sveltejs/vite-plugin-svelte/pull/806)) + +## 3.0.0 + +### Major Changes + +- breaking: update minimum supported node version to node18 ([#744](https://github.com/sveltejs/vite-plugin-svelte/pull/744)) + +- breaking: update supported vite version to vite 5 ([#743](https://github.com/sveltejs/vite-plugin-svelte/pull/743)) + +- breaking: remove support for svelte 3 ([#746](https://github.com/sveltejs/vite-plugin-svelte/pull/746)) + +- Preprocess style tags by default with vitePreprocess ([#756](https://github.com/sveltejs/vite-plugin-svelte/pull/756)) + +- breaking: remove package.json export ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- breaking(types): emit types with dts-buddy to include type map ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- breaking(debug): remove 'vite:' and add suffixes to debug namespace ([#749](https://github.com/sveltejs/vite-plugin-svelte/pull/749)) + +- breaking(types): rename SvelteOptions to SvelteConfig ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- breaking: prefer svelte exports condition over package.json svelte field ([#747](https://github.com/sveltejs/vite-plugin-svelte/pull/747)) + +### Minor Changes + +- feat(preprocess): add warnings in case preprocess dependencies contain anomalies ([#767](https://github.com/sveltejs/vite-plugin-svelte/pull/767)) + +- Add experimental support for svelte5 ([#787](https://github.com/sveltejs/vite-plugin-svelte/pull/787)) + +### Patch Changes + +- fix(types): use correct type Options for svelte function arg ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- Improve compile error messages ([#757](https://github.com/sveltejs/vite-plugin-svelte/pull/757)) + +- feat(compile): promote experimental.dynamicCompileOptions to stable ([#765](https://github.com/sveltejs/vite-plugin-svelte/pull/765)) + +- update peer dependencies to use final releases ([#794](https://github.com/sveltejs/vite-plugin-svelte/pull/794)) + +- Updated dependencies [[`d5b952f`](https://github.com/sveltejs/vite-plugin-svelte/commit/d5b952f88253e39458a1fbc0a0231b939bba338d), [`bd5d43e`](https://github.com/sveltejs/vite-plugin-svelte/commit/bd5d43e765d35b52b613ddcfd00b8d75491a7d98), [`10ec2a4`](https://github.com/sveltejs/vite-plugin-svelte/commit/10ec2a4429623382cc1a700fe91c129616bca3ef), [`62afd80`](https://github.com/sveltejs/vite-plugin-svelte/commit/62afd80c3a7bd6430be3c552acdb8baa75aac995), [`1be1c08`](https://github.com/sveltejs/vite-plugin-svelte/commit/1be1c085ed75eb8d84cedc5b45077400edd720ef)]: + - @sveltejs/vite-plugin-svelte-inspector@2.0.0 + +## 3.0.0-next.3 + +### Minor Changes + +- Add experimental support for svelte5 ([#787](https://github.com/sveltejs/vite-plugin-svelte/pull/787)) + +## 3.0.0-next.2 + +### Major Changes + +- breaking: remove package.json export ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- breaking(types): emit types with dts-buddy to include type map ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- breaking(types): rename SvelteOptions to SvelteConfig ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +### Patch Changes + +- fix(types): use correct type Options for svelte function arg ([#751](https://github.com/sveltejs/vite-plugin-svelte/pull/751)) + +- Updated dependencies [[`62afd80`](https://github.com/sveltejs/vite-plugin-svelte/commit/62afd80c3a7bd6430be3c552acdb8baa75aac995)]: + - @sveltejs/vite-plugin-svelte-inspector@2.0.0-next.1 + +## 3.0.0-next.1 + +### Major Changes + +- Preprocess style tags by default with vitePreprocess ([#756](https://github.com/sveltejs/vite-plugin-svelte/pull/756)) + +### Minor Changes + +- feat(preprocess): add warnings in case preprocess dependencies contain anomalies ([#767](https://github.com/sveltejs/vite-plugin-svelte/pull/767)) + +### Patch Changes + +- Improve compile error messages ([#757](https://github.com/sveltejs/vite-plugin-svelte/pull/757)) + +- feat(compile): promote experimental.dynamicCompileOptions to stable ([#765](https://github.com/sveltejs/vite-plugin-svelte/pull/765)) + +## 3.0.0-next.0 + +### Major Changes + +- breaking: update minimum supported node version to node18 ([#744](https://github.com/sveltejs/vite-plugin-svelte/pull/744)) + +- breaking: update supported vite version to vite 5 ([#743](https://github.com/sveltejs/vite-plugin-svelte/pull/743)) + +- breaking: remove support for svelte 3 ([#746](https://github.com/sveltejs/vite-plugin-svelte/pull/746)) + +- breaking(debug): remove 'vite:' and add suffixes to debug namespace ([#749](https://github.com/sveltejs/vite-plugin-svelte/pull/749)) + +- breaking: prefer svelte exports condition over package.json svelte field ([#747](https://github.com/sveltejs/vite-plugin-svelte/pull/747)) + +### Patch Changes + +- Updated dependencies [[`d5b952f`](https://github.com/sveltejs/vite-plugin-svelte/commit/d5b952f88253e39458a1fbc0a0231b939bba338d), [`bd5d43e`](https://github.com/sveltejs/vite-plugin-svelte/commit/bd5d43e765d35b52b613ddcfd00b8d75491a7d98), [`10ec2a4`](https://github.com/sveltejs/vite-plugin-svelte/commit/10ec2a4429623382cc1a700fe91c129616bca3ef)]: + - @sveltejs/vite-plugin-svelte-inspector@2.0.0-next.0 + +## 2.4.6 + +### Patch Changes + +- fix(prebundleSvelteLibraries): don't try to append missing sourcemap ([#737](https://github.com/sveltejs/vite-plugin-svelte/pull/737)) + +## 2.4.5 + +### Patch Changes + +- fix(config): ignore @sveltejs/package and svelte2tsx for optimizeDeps.include and ssr.noExternal generated config ([#711](https://github.com/sveltejs/vite-plugin-svelte/pull/711)) + +## 2.4.4 + +### Patch Changes + +- fix links in error handling (console and vite overlay) ([#700](https://github.com/sveltejs/vite-plugin-svelte/pull/700)) + +## 2.4.3 + +### Patch Changes + +- add svelte/internal/disclose-version to vite config optimizeDeps.include by default ([#692](https://github.com/sveltejs/vite-plugin-svelte/pull/692)) + +## 2.4.2 + +### Patch Changes + +- fix: remove pure comments only for Svelte 3 ([#673](https://github.com/sveltejs/vite-plugin-svelte/pull/673)) + +- Bump supported Svelte 4 version to `^4.0.0` ([#675](https://github.com/sveltejs/vite-plugin-svelte/pull/675)) + +- Updated dependencies [[`ffbe8d3`](https://github.com/sveltejs/vite-plugin-svelte/commit/ffbe8d3ebf8b726a31b7614a38ce4b3a0fad7776)]: + - @sveltejs/vite-plugin-svelte-inspector@1.0.3 + +## 2.4.1 + +### Patch Changes + +- Ensure compatibility with Svelte 4 prereleases ([#661](https://github.com/sveltejs/vite-plugin-svelte/pull/661)) + + Note: We are going to remove `-next` from the Svelte peerDependency range in a minor release once Svelte `4.0.0` final has been released. + +- Updated dependencies [[`f5d9bd2`](https://github.com/sveltejs/vite-plugin-svelte/commit/f5d9bd239e23a73417f684c79ba893df42440915)]: + - @sveltejs/vite-plugin-svelte-inspector@1.0.2 + +## 2.4.0 + +### Minor Changes + +- refactor: release vite-plugin-svelte as unbundled javascript with jsdoc types ([#657](https://github.com/sveltejs/vite-plugin-svelte/pull/657)) + +## 2.3.0 + +### Minor Changes + +- Refactor Svelte inspector as a separate package ([#646](https://github.com/sveltejs/vite-plugin-svelte/pull/646)) + +### Patch Changes + +- remove unused invalid property Code.dependencies on compiler ouput type ([#652](https://github.com/sveltejs/vite-plugin-svelte/pull/652)) + +- fix(build): watch preprocessor dependencies during build --watch ([#653](https://github.com/sveltejs/vite-plugin-svelte/pull/653)) + +- Updated dependencies [[`1dd6933`](https://github.com/sveltejs/vite-plugin-svelte/commit/1dd69334240cea76e7db57b5ef1d70ed7f02c8f4)]: + - @sveltejs/vite-plugin-svelte-inspector@1.0.1 + +## 2.2.0 + +### Minor Changes + +- feat(inspector): Promote experimental.inspector to regular option ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +- feat(inspector): allow configuration via environment SVELTE_INSPECTOR_OPTIONS or SVELTE_INSPECTOR_TOGGLE ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +- feat(inspector): enable holdMode by default ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +- Remove internal SvelteKit specific handling ([#638](https://github.com/sveltejs/vite-plugin-svelte/pull/638)) + +### Patch Changes + +- fix(inspector): prepend vite base when calling \_\_openInEditor ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +- fix(inspector): after a file has been opened, automatically disable inspector on leaving browser ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +- fix(inspector): use control-shift as default keycombo on linux to avoid problems in firefox ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +- fix(svelte-inspector): mount outside body to avoid hydration claiming body removing it ([#631](https://github.com/sveltejs/vite-plugin-svelte/pull/631)) + +## 2.1.1 + +### Patch Changes + +- fix(resolve): normalize path resolved from "svelte" field to ensure consistency across operating systems ([#635](https://github.com/sveltejs/vite-plugin-svelte/pull/635)) + +## 2.1.0 + +### Minor Changes + +- log warnings for packages that use the `svelte` field to resolve Svelte files differently than standard Vite resolve ([#510](https://github.com/sveltejs/vite-plugin-svelte/pull/510)) + +### Patch Changes + +- fix(vitePreprocess): add dependencies to style preprocessor output ([#625](https://github.com/sveltejs/vite-plugin-svelte/pull/625)) + +- Skip Vite resolve workaround on Vite 4.1+ or Svelte 4+ ([#622](https://github.com/sveltejs/vite-plugin-svelte/pull/622)) + +- fix(vitePreprocess): use relative paths without lang suffix in sourcemaps to avoid missing source file errors. ([#625](https://github.com/sveltejs/vite-plugin-svelte/pull/625)) + +- Log stats in debug mode and remove `experimental.disableCompileStats` option. Use `DEBUG="vite:vite-plugin-svelte:stats"` when starting the dev server or build to log the compile stats. ([#614](https://github.com/sveltejs/vite-plugin-svelte/pull/614)) + +## 2.0.4 + +### Patch Changes + +- fix(vitePreprocess): remove problematic pure annotations that could lead to wrong build output in some cases ([#609](https://github.com/sveltejs/vite-plugin-svelte/pull/609)) + +## 2.0.3 + +### Patch Changes + +- fix(vitePreprocess): use relative paths in sourcemap sources ([#570](https://github.com/sveltejs/vite-plugin-svelte/pull/570)) + +- show correct error overlay for compiler errors during hot update ([#592](https://github.com/sveltejs/vite-plugin-svelte/pull/592)) + +- respect custom resolve.mainFields config when adding svelte ([#582](https://github.com/sveltejs/vite-plugin-svelte/pull/582)) + +## 2.0.2 + +### Patch Changes + +- improve detection of sveltekit in inspector plugin to be compatible to latest changes ([`47c54c9`](https://github.com/sveltejs/vite-plugin-svelte/commit/47c54c92b886ea9d9bdd1fc7549079b39215ccd1)) + +## 2.0.1 + +### Patch Changes + +- update minimum version of vitefu dependency to avoid peer mismatch ([#543](https://github.com/sveltejs/vite-plugin-svelte/pull/543)) + +## 2.0.0 + +### Major Changes + +- update svelte peerDependency to ^3.54.0 ([#529](https://github.com/sveltejs/vite-plugin-svelte/pull/529)) + +- remove commonjs variant of vite-plugin-svelte ([#522](https://github.com/sveltejs/vite-plugin-svelte/pull/522)) + + Make sure your package.json contains `"type": "module"`and see [FAQ](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#how-can-i-use-vite-plugin-svelte-from-commonjs) for more information + +- update vite peerDependency to vite-4 ([#521](https://github.com/sveltejs/vite-plugin-svelte/pull/521)) + +### Patch Changes + +- Remove `experimental.useVitePreprocess` option in favour of `vitePreprocess` ([#538](https://github.com/sveltejs/vite-plugin-svelte/pull/538)) + +- Remove pre Vite 3.2 support for `vitePreprocess` ([#536](https://github.com/sveltejs/vite-plugin-svelte/pull/536)) + +## 2.0.0-beta.3 + +### Patch Changes + +- Remove `experimental.useVitePreprocess` option in favour of `vitePreprocess` ([#538](https://github.com/sveltejs/vite-plugin-svelte/pull/538)) + +- Remove pre Vite 3.2 support for `vitePreprocess` ([#536](https://github.com/sveltejs/vite-plugin-svelte/pull/536)) + +## 2.0.0-beta.2 + +### Major Changes + +- reintroduce custom svelte/ssr resolve ([#532](https://github.com/sveltejs/vite-plugin-svelte/pull/532)) + +## 2.0.0-beta.1 + +### Major Changes + +- remove custom svelte/ssr resolve that is no longer needed in vite 4 ([#527](https://github.com/sveltejs/vite-plugin-svelte/pull/527)) + +- update svelte peerDependency to ^3.54.0 ([#529](https://github.com/sveltejs/vite-plugin-svelte/pull/529)) + +## 2.0.0-beta.0 + +### Major Changes + +- remove cjs build ([#522](https://github.com/sveltejs/vite-plugin-svelte/pull/522)) + +- update vite peerDependency to vite-4 ([#521](https://github.com/sveltejs/vite-plugin-svelte/pull/521)) + +## 1.4.0 + +### Minor Changes + +- support `&direct` and `&raw` query parameters for svelte requests ([#513](https://github.com/sveltejs/vite-plugin-svelte/pull/513)) + +- Export `vitePreprocess()` Svelte preprocessor ([#509](https://github.com/sveltejs/vite-plugin-svelte/pull/509)) + +### Patch Changes + +- ensure sources paths in sourcemaps are not absolute file paths ([#513](https://github.com/sveltejs/vite-plugin-svelte/pull/513)) + +- remove experimental.generateMissingPreprocessorSourcemaps ([#514](https://github.com/sveltejs/vite-plugin-svelte/pull/514)) + +## 1.3.1 + +### Patch Changes + +- improve robustness of compile stats taking ([#507](https://github.com/sveltejs/vite-plugin-svelte/pull/507)) + +## 1.3.0 + +### Minor Changes + +- enable `prebundleSvelteLibraries` during dev by default to improve page loading for the dev server. ([#494](https://github.com/sveltejs/vite-plugin-svelte/pull/494)) + + see the [FAQ](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) for more information about `prebundleSvelteLibraries` and how to tune it. + +- Enable resolving via "svelte" exports condition ([#502](https://github.com/sveltejs/vite-plugin-svelte/pull/502)) + +- add compile time stats logging ([#503](https://github.com/sveltejs/vite-plugin-svelte/pull/503)) + +## 1.2.0 + +### Minor Changes + +- support string values of compilerOptions.css added in svelte 3.53.0 ([#490](https://github.com/sveltejs/vite-plugin-svelte/pull/490)) + +### Patch Changes + +- simplify init of compilerOptions.hydratable for kit (kit.browser.hydrate is no longer in use) ([#496](https://github.com/sveltejs/vite-plugin-svelte/pull/496)) + +- when prebundleSvelteLibraries is true and a dependency is manually excluded, generate reincludes for it's cjs deps ([#493](https://github.com/sveltejs/vite-plugin-svelte/pull/493)) + +- Refactor Svelte libraries config handling ([#478](https://github.com/sveltejs/vite-plugin-svelte/pull/478)) + +- fix(prebundleSvelteLibraries): avoid resolving via svelte field after a library has been prebundled ([#482](https://github.com/sveltejs/vite-plugin-svelte/pull/482)) + +## 1.1.1 + +### Patch Changes + +- Use `preprocessCSS` API from Vite 3.2 for `useVitePreprocess` option ([#479](https://github.com/sveltejs/vite-plugin-svelte/pull/479)) + +- add types to exports map in package.json ([#488](https://github.com/sveltejs/vite-plugin-svelte/pull/488)) + +## 1.1.0 + +### Minor Changes + +- Bring `prebundleSvelteLibraries` out of experimental, it is now a top-level option ([#476](https://github.com/sveltejs/vite-plugin-svelte/pull/476)) + +### Patch Changes + +- Remove `@rollup/pluginutils` dependency ([#469](https://github.com/sveltejs/vite-plugin-svelte/pull/469)) + +## 1.0.9 + +### Patch Changes + +- Use esnext for useVitePreprocess ([#452](https://github.com/sveltejs/vite-plugin-svelte/pull/452)) + +## 1.0.8 + +### Patch Changes + +- svelte-inspector: select hovered element instead of parent on mousemouse ([#449](https://github.com/sveltejs/vite-plugin-svelte/pull/449)) + +- svelte-inspector: ignore navigation keys while not enabled ([#449](https://github.com/sveltejs/vite-plugin-svelte/pull/449)) + +## 1.0.7 + +### Patch Changes + +- svelte-inspector: prevent info-bubble select ([#445](https://github.com/sveltejs/vite-plugin-svelte/pull/445)) + +## 1.0.6 + +### Patch Changes + +- update svelte-hmr and enable partial hmr accept by default (fixes [#134](https://github.com/sveltejs/vite-plugin-svelte/issues/134)) ([#440](https://github.com/sveltejs/vite-plugin-svelte/pull/440)) + +- svelte-inspector: add keyboard navigation, select element on activation, improve a11y and info bubble position/content ([#438](https://github.com/sveltejs/vite-plugin-svelte/pull/438)) + +## 1.0.5 + +### Patch Changes + +- removed peerDependency for vite 3.1.0-beta as vite 3.1.0 final has been released ([#431](https://github.com/sveltejs/vite-plugin-svelte/pull/431)) + +## 1.0.4 + +### Patch Changes + +- temporarily add vite 3.1 beta to peer dependencies rule to avoid warning on kit projects using it ([#427](https://github.com/sveltejs/vite-plugin-svelte/pull/427)) + + **warning:** this is going to be changed back to `^3.0.0` in a future patch + +## 1.0.3 + +### Patch Changes + +- ignore keyup events without key in inspector ([#417](https://github.com/sveltejs/vite-plugin-svelte/pull/417)) + +* fix svelte-inspector import for vite 3.1 ([#423](https://github.com/sveltejs/vite-plugin-svelte/pull/423)) + +## 1.0.2 + +### Patch Changes + +- update svelte-inspector inject code to be compatible with @sveltejs/kit > 1.0.0-next.405 ([#411](https://github.com/sveltejs/vite-plugin-svelte/pull/411)) + +## 1.0.1 + +### Major Changes + +- update to vite3 ([#359](https://github.com/sveltejs/vite-plugin-svelte/pull/359)) + +* bump minimum required node version to 14.18.0 to align with vite 3 ([#359](https://github.com/sveltejs/vite-plugin-svelte/pull/359)) + +- move plugin options in svelte.config.js into "vitePlugin" ([#389](https://github.com/sveltejs/vite-plugin-svelte/pull/389)) + + update your svelte.config.js and wrap [plugin options](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#plugin-options) with `vitePlugin` + + ```diff + // svelte.config.js + + compilerOptions: {...}, + preprocess: {...}, + extensions: [...], + onwarn: () => {...}, + kit: {}, + + vitePlugin: { + // include, exclude, emitCss, hot, ignorePluginPreprocessors, disableDependencyReinclusion, experimental + + } + ``` + +### Patch Changes + +- Always add dependencies using svelte to ssr.noExternal in vite config ([#359](https://github.com/sveltejs/vite-plugin-svelte/pull/359)) + +## 1.0.0-next.49 + +### Minor Changes + +- New experimental option sendWarningsToBrowser ([#372](https://github.com/sveltejs/vite-plugin-svelte/pull/372)) + +### Patch Changes + +- fix hmr not updating a component when returning to the last working state from an error state ([#371](https://github.com/sveltejs/vite-plugin-svelte/pull/371)) + +## 1.0.0-next.48 + +### Minor Changes + +- Automate setting of compilerOptions.hydratable from kit.browser.hydrate option ([#368](https://github.com/sveltejs/vite-plugin-svelte/pull/368)) + +### Patch Changes + +- Do not try to resolve svelte field in \_\_vite-browser-external, see (#362)" ([#363](https://github.com/sveltejs/vite-plugin-svelte/pull/363)) + +## 1.0.0-next.47 + +### Patch Changes + +- Use last modified time as cache busting parameter ([#356](https://github.com/sveltejs/vite-plugin-svelte/pull/356)) + +* Export loadSvelteConfig ([#356](https://github.com/sveltejs/vite-plugin-svelte/pull/356)) + +## 1.0.0-next.46 + +### Patch Changes + +- Bump svelte-hmr version ([#349](https://github.com/sveltejs/vite-plugin-svelte/pull/349)) + +## 1.0.0-next.45 + +### Patch Changes + +- Handle inspector autocomplete keydown event ([#338](https://github.com/sveltejs/vite-plugin-svelte/pull/338)) + +* Remove user-specified values for essential compilerOptions generate, format, cssHash and filename and log a warning ([#346](https://github.com/sveltejs/vite-plugin-svelte/pull/346)) + +- fix inspector not initializing correctly for sveltekit on windows (see [#342](https://github.com/sveltejs/vite-plugin-svelte/issues/342)) ([#344](https://github.com/sveltejs/vite-plugin-svelte/pull/344)) + +## 1.0.0-next.44 + +### Patch Changes + +- correctly resolve the experimental svelte inspector (see [#332](https://github.com/sveltejs/vite-plugin-svelte/issues/332)) (fixes [#330](https://github.com/sveltejs/vite-plugin-svelte/issues/330)) ([#333](https://github.com/sveltejs/vite-plugin-svelte/pull/333)) + +## 1.0.0-next.43 + +### Minor Changes + +- Add experimental Svelte Inspector to quickly jump to code from your browser. ([#322](https://github.com/sveltejs/vite-plugin-svelte/pull/322)) + +### Patch Changes + +- use deepmerge utility to merge inline config and svelte.config.js ([#322](https://github.com/sveltejs/vite-plugin-svelte/pull/322)) + +* do not warn if kit options are passed as inline config ([#319](https://github.com/sveltejs/vite-plugin-svelte/pull/319)) + +- Support import typescript files with .js extension ([#324](https://github.com/sveltejs/vite-plugin-svelte/pull/324)) + +* do not restart vite devserver on changes of svelte config when `configFile: false` is set ([#319](https://github.com/sveltejs/vite-plugin-svelte/pull/319)) + +## 1.0.0-next.42 + +### Minor Changes + +- skip reading default svelte config file with inline option `configFile: false` ([#317](https://github.com/sveltejs/vite-plugin-svelte/pull/317)) + +## 1.0.0-next.41 + +### Major Changes + +- Update vite peerDependency to ^2.9.0 and handle edge cases for `experimental.prebundleSvelteLibraries` ([#294](https://github.com/sveltejs/vite-plugin-svelte/pull/294)) + +### Patch Changes + +- Improved CSS Source Maps when using vite's `css: { devSourcemap: true }` ([#305](https://github.com/sveltejs/vite-plugin-svelte/pull/305)) + +## 1.0.0-next.40 + +### Patch Changes + +- improve handling of transitive cjs dependencies of svelte libraries during dev ssr ([#289](https://github.com/sveltejs/vite-plugin-svelte/pull/289)) + +## 1.0.0-next.39 + +### Patch Changes + +- prevent errors in resolveViaPackageJsonSvelte breaking vite resolve (fixes [#283](https://github.com/sveltejs/vite-plugin-svelte/issues/283)) ([#286](https://github.com/sveltejs/vite-plugin-svelte/pull/286)) + +## 1.0.0-next.38 + +### Patch Changes + +- don't warn if dependency doesn't export package.json ([#272](https://github.com/sveltejs/vite-plugin-svelte/pull/272)) + +* Optimize nested index-only dependencies ([#282](https://github.com/sveltejs/vite-plugin-svelte/pull/282)) + +- Remove transforming svelte css ([#280](https://github.com/sveltejs/vite-plugin-svelte/pull/280)) + +## 1.0.0-next.37 + +### Patch Changes + +- don't try to resolve node internal modules via package.json svelte field ([#266](https://github.com/sveltejs/vite-plugin-svelte/pull/266)) + +## 1.0.0-next.36 + +### Patch Changes + +- include stack and filename in error reporting for svelte preprocess errors ([#260](https://github.com/sveltejs/vite-plugin-svelte/pull/260)) + +## 1.0.0-next.35 + +### Patch Changes + +- do not use require-relative to resolve svelte field of libraries and cache resolved values (fixes [#244](https://github.com/sveltejs/vite-plugin-svelte/issues/244)) ([#254](https://github.com/sveltejs/vite-plugin-svelte/pull/254)) + +## 1.0.0-next.34 + +### Minor Changes + +- Automatically re-prebundle when Svelte config changed for `experimental.prebundleSvelteLibraries` ([#245](https://github.com/sveltejs/vite-plugin-svelte/pull/245)) + +### Patch Changes + +- use the resolved vite root to support backend integrations ([#247](https://github.com/sveltejs/vite-plugin-svelte/pull/247)) + +* fix `experimental.useVitePreprocess` option for Vite 2.8 ([#240](https://github.com/sveltejs/vite-plugin-svelte/pull/240)) + +## 1.0.0-next.33 + +### Minor Changes + +- auto-restart SvelteKit when Svelte config changed ([#237](https://github.com/sveltejs/vite-plugin-svelte/pull/237)) + +* handle preprocess for prebundleSvelteLibraries ([#229](https://github.com/sveltejs/vite-plugin-svelte/pull/229)) + +### Patch Changes + +- Skip prebundle non-js nested dependencies ([#234](https://github.com/sveltejs/vite-plugin-svelte/pull/234)) + +* handle production builds for non "production" mode ([#229](https://github.com/sveltejs/vite-plugin-svelte/pull/229)) + +## 1.0.0-next.32 + +### Major Changes + +- update vite peerDependency to ^2.7.0 and refactor server restart on change of svelte.config.js ([#223](https://github.com/sveltejs/vite-plugin-svelte/pull/223)) + +### Patch Changes + +- Ignore import protocols like `node:` when resolving the `svelte` field in package.json ([#225](https://github.com/sveltejs/vite-plugin-svelte/pull/225)) + +## 1.0.0-next.31 + +### Minor Changes + +- Improved error reporting for svelte compiler errors ([#220](https://github.com/sveltejs/vite-plugin-svelte/pull/220)) + +## 1.0.0-next.30 + +### Major Changes + +- Bump svelte peer dependency to ^3.44.0 ([#202](https://github.com/sveltejs/vite-plugin-svelte/pull/202)) + +## 1.0.0-next.29 + +### Major Changes + +- drop support for node12 ([#198](https://github.com/sveltejs/vite-plugin-svelte/pull/198)) + +### Minor Changes + +- Add `experimental.prebundleSvelteLibraries` option ([#200](https://github.com/sveltejs/vite-plugin-svelte/pull/200)) + +### Patch Changes + +- Disable CSS sourcemap in SSR ([#201](https://github.com/sveltejs/vite-plugin-svelte/pull/201)) + +## 1.0.0-next.28 + +### Patch Changes + +- Fix emitCss behaviour in a svelte config ([#194](https://github.com/sveltejs/vite-plugin-svelte/pull/194)) + +## 1.0.0-next.27 + +### Minor Changes + +- Run Vite preprocessors first in markup phase ([#189](https://github.com/sveltejs/vite-plugin-svelte/pull/189)) + +### Patch Changes + +- Handle flexible ssr signature for hooks with ssr argument ([#187](https://github.com/sveltejs/vite-plugin-svelte/pull/187)) + +## 1.0.0-next.26 + +### Major Changes + +- minimum required version of vite is 2.6.0 ([#182](https://github.com/sveltejs/vite-plugin-svelte/pull/182)) + +## 1.0.0-next.25 + +### Minor Changes + +- Use transformWithEsbuild for vite script preprocessor ([#173](https://github.com/sveltejs/vite-plugin-svelte/pull/173)) + +## 1.0.0-next.24 + +### Patch Changes + +- Only add all Svelte dependencies to ssr.noExternal in SSR build ([#169](https://github.com/sveltejs/vite-plugin-svelte/pull/169)) + +## 1.0.0-next.23 + +### Patch Changes + +- Svelte libraries without any Svelte components are also added to ssr.noExternal ([#166](https://github.com/sveltejs/vite-plugin-svelte/pull/166)) + +## 1.0.0-next.22 + +### Patch Changes + +- Only optimize nested cjs dependencies ([#163](https://github.com/sveltejs/vite-plugin-svelte/pull/163)) + +## 1.0.0-next.21 + +### Minor Changes + +- Add option disableDependencyReinclusion to offer users a way out of automatic optimization for hybrid packages ([#161](https://github.com/sveltejs/vite-plugin-svelte/pull/161)) + +### Patch Changes + +- Improve automatic dependency pre-bundling by not reincluding dependencies that are already present in optimizeDeps.exclude ([#159](https://github.com/sveltejs/vite-plugin-svelte/pull/159)) + +## 1.0.0-next.20 + +### Major Changes + +- Enable optimization for nested dependencies of excluded svelte dependencies ([#157](https://github.com/sveltejs/vite-plugin-svelte/pull/157)) + + Vite 2.5.3 and above is needed to support this feature. + +### Minor Changes + +- Improve dev warning message for components including only unscoped styles (fixes [#153](https://github.com/sveltejs/vite-plugin-svelte/issues/153)) ([#154](https://github.com/sveltejs/vite-plugin-svelte/pull/154)) + +## 1.0.0-next.19 + +### Patch Changes + +- add automatically excluded svelte dependencies to ssr.noExternal ([#147](https://github.com/sveltejs/vite-plugin-svelte/pull/147)) + +## 1.0.0-next.18 + +### Minor Changes + +- automatically exclude svelte dependencies in vite.optimizeDeps ([#145](https://github.com/sveltejs/vite-plugin-svelte/pull/145)) + +### Patch Changes + +- use createRequire to load svelte.config.cjs in esm projects (fixes [#141](https://github.com/sveltejs/vite-plugin-svelte/issues/141)) ([#142](https://github.com/sveltejs/vite-plugin-svelte/pull/142)) + +## 1.0.0-next.17 + +### Patch Changes + +- don't add svelte/ssr to vite.optimizeDeps.include (fixes [#138](https://github.com/sveltejs/vite-plugin-svelte/issues/138)) ([#139](https://github.com/sveltejs/vite-plugin-svelte/pull/139)) + +## 1.0.0-next.16 + +### Major Changes + +- automatically include svelte in vite config optimizeDeps.include ([#137](https://github.com/sveltejs/vite-plugin-svelte/pull/137)) + + Previously, svelte was automatically excluded. We include it now by default to improve deduplication. + + As a result, svelte is pre-bundled by vite during dev, which it logs when starting the devserver + + ```shell + Pre-bundling dependencies: + svelte/animate + svelte/easing + svelte/internal + svelte/motion + svelte/store + (...and 2 more) + (this will be run only when your dependencies or config have changed) + ``` + + And it's also visible in the browsers network tab, where requests for svelte imports now start with `node_modules/.vite/` during dev. + + Check out the [vite pre-bundling documentation](https://vitejs.dev/guide/dep-pre-bundling.html) for more information. + + To get the old behavior back, add the following to your vite config + + ```js + optimizeDeps: { + exclude: ["svelte"]; + } + ``` + +### Patch Changes + +- prepare for a change in vite 2.5.0 that would lead to errors in preprocessor dependency handling (fixes [#130](https://github.com/sveltejs/vite-plugin-svelte/issues/130)) ([#131](https://github.com/sveltejs/vite-plugin-svelte/pull/131)) + +## 1.0.0-next.15 + +### Major Changes + +- change default value of compilerOptions.hydratable to false ([#122](https://github.com/sveltejs/vite-plugin-svelte/pull/122)) + + This is done to align with svelte compiler defaults and improve output in non-ssr scenarios. + + Add `{compilerOptions: {hydratable: true}}` to vite-plugin-svelte config if you need hydration (eg. for ssr) + +### Minor Changes + +- add config option `experimental.dynamicCompileOptions` for finegrained control over compileOptions ([#122](https://github.com/sveltejs/vite-plugin-svelte/pull/122)) + +### Patch Changes + +- resolve vite.root option correctly (fixes [#113](https://github.com/sveltejs/vite-plugin-svelte/issues/113)) ([#115](https://github.com/sveltejs/vite-plugin-svelte/pull/115)) + +## 1.0.0-next.14 + +### Patch Changes + +- replace querystring with URLSearchParams ([#107](https://github.com/sveltejs/vite-plugin-svelte/pull/107)) + +* import svelte types instead of duplicating them ([#105](https://github.com/sveltejs/vite-plugin-svelte/pull/105)) + +- update svelte-hmr to 0.14.7 to fix issue with svelte 3.40 ([#112](https://github.com/sveltejs/vite-plugin-svelte/pull/112)) + +* turn diff-match-patch into an optional peer dependency to reduce footprint ([#110](https://github.com/sveltejs/vite-plugin-svelte/pull/110)) + +## 1.0.0-next.13 + +### Minor Changes + +- Add `experimental` section to options and move `useVitePreprocess` there ([#99](https://github.com/sveltejs/vite-plugin-svelte/pull/99)) + + Experimental options are not ready for production use and breaking changes to them can occur in any release + + If you already had `useVitePreprocess` enabled, update you config: + + ```diff + - svelte({useVitePreprocess: true}) + + svelte({experimental: {useVitePreprocess: true}}) + ``` + +* Add option to ignore svelte preprocessors of other vite plugins ([#98](https://github.com/sveltejs/vite-plugin-svelte/pull/98)) + + - ignore them all: `ignorePluginPreprocessors: true` + - ignore by name: `ignorePluginPreprocessors: ['',...]` + +- Move plugin preprocessor definition to api namespace ([#98](https://github.com/sveltejs/vite-plugin-svelte/pull/98)) + + Plugins that provide `myplugin.sveltePreprocess`, should move it to `myplugin.api.sveltePreprocess`, as suggested by [rollup](https://rollupjs.org/guide/en/#direct-plugin-communication) + +* Experimental: Generate sourcemaps for preprocessors that lack them ([#101](https://github.com/sveltejs/vite-plugin-svelte/pull/101)) + + enable option `experimental.generateMissingPreprocessorSourcemaps` to use it + +### Patch Changes + +- removed redundant `disableCssHmr` option ([#99](https://github.com/sveltejs/vite-plugin-svelte/pull/99)) + + You can use `emitCss: false` or `emitCss: !!isProduction` instead + +* further improvements to changelog (see [#93](https://github.com/sveltejs/vite-plugin-svelte/issues/93)) ([#94](https://github.com/sveltejs/vite-plugin-svelte/pull/94)) + +- reduce log output with log.once function to filter repetetive messages ([#101](https://github.com/sveltejs/vite-plugin-svelte/pull/101)) + +* remove transitive peer dependency on rollup (fixes [#57](https://github.com/sveltejs/vite-plugin-svelte/issues/57)) ([#103](https://github.com/sveltejs/vite-plugin-svelte/pull/103)) + +## 1.0.0-next.12 + +### Minor Changes + +- Resolve svelte to svelte/ssr when building for ssr (fixes [#74](https://github.com/sveltejs/vite-plugin-svelte/issues/74)) ([#75](https://github.com/sveltejs/vite-plugin-svelte/pull/75)) ([`f6f56fe`](https://github.com/sveltejs/vite-plugin-svelte/commit/f6f56fee7d3567196052a23440cb1818187fa232)) + +- Support svg extension ([#78](https://github.com/sveltejs/vite-plugin-svelte/pull/78)) ([`2eb09cf`](https://github.com/sveltejs/vite-plugin-svelte/commit/2eb09cf180c7ebf0fb4ccfccee663e5264b3814c)) + +- Restart dev server when svelte config file changes ([#72](https://github.com/sveltejs/vite-plugin-svelte/pull/72)) ([`5100376`](https://github.com/sveltejs/vite-plugin-svelte/commit/5100376ef91d5e39ec00222f1043e4fda047678b)) + +- Allow svelte imports to be added to optimizeDeps.include and don't exclude svelte from optimizeDeps then ([#68](https://github.com/sveltejs/vite-plugin-svelte/pull/68)) ([`9583900`](https://github.com/sveltejs/vite-plugin-svelte/commit/9583900a2b3600133cee3a46b6dbb7df137977b6)) + +- Vite config can be updated based on values in svelte config (see [#60](https://github.com/sveltejs/vite-plugin-svelte/issues/60)) ([#64](https://github.com/sveltejs/vite-plugin-svelte/pull/64)) ([`c3f65fd`](https://github.com/sveltejs/vite-plugin-svelte/commit/c3f65fdf414b22810ad60817b3e1e62790ba816f)) + +### Patch Changes + +- customize changelog format ([#90](https://github.com/sveltejs/vite-plugin-svelte/pull/90)) ([`b5a58cd`](https://github.com/sveltejs/vite-plugin-svelte/commit/b5a58cd814bbc71a5e59060d436770f7a0102262)) + +- relax svelte peer dependency to 3.34.0 ([#70](https://github.com/sveltejs/vite-plugin-svelte/pull/70)) ([`377d464`](https://github.com/sveltejs/vite-plugin-svelte/commit/377d464eba30c56f012deba3d306cb5a7195b787)) + +- do not transform imports tagged with ?url or ?raw (fixes #87) ([#88](https://github.com/sveltejs/vite-plugin-svelte/pull/88)) ([`d1d2638`](https://github.com/sveltejs/vite-plugin-svelte/commit/d1d2638b247830852faa89e7b9bc9a430b81ba51)) + +- update svelte-hmr to ^0.14.5 to fix hmr reordering issue introduced by a change in svelte 3.38.3 ([#92](https://github.com/sveltejs/vite-plugin-svelte/pull/92)) ([`cdfd821`](https://github.com/sveltejs/vite-plugin-svelte/commit/cdfd8210770150c6e40f68b6b48cd2e455414299)) + +- fix kit-node tests ([#55](https://github.com/sveltejs/vite-plugin-svelte/pull/55)) ([`09b63d3`](https://github.com/sveltejs/vite-plugin-svelte/commit/09b63d32e8816acc554a66d4d01062be197dfbb7)) + +- output sourcemap in hmr helper preprocessor ([#71](https://github.com/sveltejs/vite-plugin-svelte/pull/71)) ([`97ee68c`](https://github.com/sveltejs/vite-plugin-svelte/commit/97ee68c5106e58b2e7c4eb97e8cf7dd1c52bbfd3)) + +- reduced debug output ([#83](https://github.com/sveltejs/vite-plugin-svelte/pull/83)) ([`eb048ff`](https://github.com/sveltejs/vite-plugin-svelte/commit/eb048ff9419488f75869ffb880a78a2a3aa5a6bb)) + +- Refactored e2e-tests to use package.json scripts + +- Updated dependencies + +## 1.0.0-next.11 + +### Major Changes + +- convert to es module with cjs fallback, use named export instead of default ([#54](https://github.com/sveltejs/vite-plugin-svelte/pull/54)) ([`0f7e256`](https://github.com/sveltejs/vite-plugin-svelte/commit/0f7e256a9ebb0ee9ac6075146d27bf4f11ecdab3)) + + If you are using vite-plugin-svelte with require, you should switch to esm and import the named export "svelte". + An example can be found in the usage section of the [readme](README.md) + + For existing esm configs update your import to use the new named export. + + ```diff + - import svelte from '@sveltejs/vite-plugin-svelte'; + + import { svelte } from '@sveltejs/vite-plugin-svelte'; + ``` + + continuing with cjs/require is discouraged but if you must use it, update your require statement to use the named export + + ```diff + - const svelte = require('@sveltejs/vite-plugin-svelte'); + + const { svelte } = require('@sveltejs/vite-plugin-svelte'); + ``` + +### Minor Changes + +- Log svelte compiler warnings to console. use options.onwarn to customize logging ([#45](https://github.com/sveltejs/vite-plugin-svelte/pull/45)) ([`673cf61`](https://github.com/sveltejs/vite-plugin-svelte/commit/673cf61b3800e7a64be2b73a7273909da95729d2)) + +### Patch Changes + +- Update to esbuild 0.12 and vite 2.3.7 ([#44](https://github.com/sveltejs/vite-plugin-svelte/pull/44)) ([`24ae093`](https://github.com/sveltejs/vite-plugin-svelte/commit/24ae0934301cb50506bf39cdccc07ad3eac546fd)) + +- Update engines.node to "^12.20 || ^14.13.1 || >= 16" ([#44](https://github.com/sveltejs/vite-plugin-svelte/pull/44)) ([`24ae093`](https://github.com/sveltejs/vite-plugin-svelte/commit/24ae0934301cb50506bf39cdccc07ad3eac546fd)) + +- Enable logging for compiler warnings ([#45](https://github.com/sveltejs/vite-plugin-svelte/pull/45)) ([`673cf61`](https://github.com/sveltejs/vite-plugin-svelte/commit/673cf61b3800e7a64be2b73a7273909da95729d2)) + +## 1.0.0-next.10 + +### Minor Changes + +- Allow `emitCss: false` for production builds and customizable compilerOptions.css and hydratable (fixes [#9](https://github.com/sveltejs/vite-plugin-svelte/issues/9)) ([#41](https://github.com/sveltejs/vite-plugin-svelte/pull/41)) ([`cb7f03d`](https://github.com/sveltejs/vite-plugin-svelte/commit/cb7f03d61c19f0b98c6412c11bbaa4af978da9ed)) + +## 1.0.0-next.9 + +### Patch Changes + +- Ensure esm config loading works on windows ([#38](https://github.com/sveltejs/vite-plugin-svelte/pull/38)) ([`5aef91c`](https://github.com/sveltejs/vite-plugin-svelte/commit/5aef91c8752c8de94a1f1fcb28618606b7c44670)) + +## 1.0.0-next.8 + +### Minor Changes + +- Support esm in svelte.config.js and svelte.config.mjs ([#35](https://github.com/sveltejs/vite-plugin-svelte/pull/35)) ([`4018ce6`](https://github.com/sveltejs/vite-plugin-svelte/commit/4018ce621b4df75877e0e18057c332f27158d42b)) + +- Add configFile option ([#35](https://github.com/sveltejs/vite-plugin-svelte/pull/35)) ([`4018ce6`](https://github.com/sveltejs/vite-plugin-svelte/commit/4018ce621b4df75877e0e18057c332f27158d42b)) + +### Patch Changes + +- Watch preprocessor dependencies and trigger hmr on change ([#34](https://github.com/sveltejs/vite-plugin-svelte/pull/34)) ([`e5d4749`](https://github.com/sveltejs/vite-plugin-svelte/commit/e5d4749c0850260a295daab9cb15866fe58ee709)) + +## 1.0.0-next.7 + +### Minor Changes + +- Reduced cache usage, share css cache between SSR and client ([#32](https://github.com/sveltejs/vite-plugin-svelte/pull/32)) ([`113bb7d`](https://github.com/sveltejs/vite-plugin-svelte/commit/113bb7dc330a7517085d12d1d0758a376a12253f)) + +## 1.0.0-next.6 + +### Minor Changes + +- 1be46f1: improved css hmr +- a0f5a65: Allow other vite plugins to define preprocessors + +### Patch Changes + +- 8d9ef96: fix: do not preserve types unless useVitePreprocess option is true +- 6f4a253: disable svelte-hmr overlay by default +- 18647aa: improve virtual css module path (fixes #14) + +## 1.0.0-next.5 + +### Patch Changes + +- 61439ae: initial release diff --git a/js-plugins/svelte/README.md b/js-plugins/svelte/README.md new file mode 100644 index 0000000..48798bc --- /dev/null +++ b/js-plugins/svelte/README.md @@ -0,0 +1,28 @@ +# @sveltejs/vite-plugin-svelte + +The official [Svelte](https://svelte.dev) plugin for [Vite](https://vitejs.dev). + +## Usage + +```js +// vite.config.js +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +export default defineConfig({ + plugins: [ + svelte({ + /* plugin options */ + }) + ] +}); +``` + +## Documentation + +- [Plugin options](../../docs/config.md) +- [FAQ](../../docs/faq.md) + +## License + +[MIT](./LICENSE) diff --git a/js-plugins/svelte/__tests__/compile.spec.js b/js-plugins/svelte/__tests__/compile.spec.js new file mode 100644 index 0000000..ca6d193 --- /dev/null +++ b/js-plugins/svelte/__tests__/compile.spec.js @@ -0,0 +1,81 @@ +import process from 'node:process'; +import { describe, it, expect } from 'vitest'; +import { createCompileSvelte } from '../src/utils/compile.js'; +/** @type {import('../../types/options.d.ts').ResolvedOptions} */ +const options = { + compilerOptions: { + dev: false, + format: 'esm', + css: 'external' + }, + isBuild: false, + isDebug: false, + isProduction: false, + isServe: false, + root: process.cwd() +}; + +describe('createCompileSvelte', () => { + it('returns function', () => { + const compileSvelte = createCompileSvelte(options); + expect(typeof compileSvelte).toBe('function'); + }); + + describe('compileSvelte', async () => { + it('removes dangling pure annotations', async () => { + const code = ` +
{x}
`; + const compileSvelte = createCompileSvelte(options); + const output = await compileSvelte( + { + cssId: 'svelte-xxxxx', + query: {}, + raw: false, + ssr: false, + timestamp: Date.now(), + id: 'id', + filename: '/some/File.svelte', + normalizedFilename: 'some/File.svelte' + }, + code, + {} + ); + expect(output.compiled.js.code).not.toContain('/* @__PURE__ */\n'); + }); + + it('detects script lang', async () => { + const code = ` + + +
{x}
`; + + const compileSvelte = createCompileSvelte(options); + const output = await compileSvelte( + { + cssId: 'svelte-xxxxx', + query: {}, + raw: false, + ssr: false, + timestamp: Date.now(), + id: 'id', + filename: '/some/File.svelte', + normalizedFilename: 'some/File.svelte' + }, + code, + {} + ); + + expect(output.lang).toBe('ts'); + }); + }); +}); diff --git a/js-plugins/svelte/__tests__/fixtures/preprocess/foo.css b/js-plugins/svelte/__tests__/fixtures/preprocess/foo.css new file mode 100644 index 0000000..79d0608 --- /dev/null +++ b/js-plugins/svelte/__tests__/fixtures/preprocess/foo.css @@ -0,0 +1,3 @@ +.foo { + color: green; +} diff --git a/js-plugins/svelte/__tests__/fixtures/preprocess/foo.scss b/js-plugins/svelte/__tests__/fixtures/preprocess/foo.scss new file mode 100644 index 0000000..79d0608 --- /dev/null +++ b/js-plugins/svelte/__tests__/fixtures/preprocess/foo.scss @@ -0,0 +1,3 @@ +.foo { + color: green; +} diff --git a/js-plugins/svelte/__tests__/preprocess.spec.js b/js-plugins/svelte/__tests__/preprocess.spec.js new file mode 100644 index 0000000..919b34e --- /dev/null +++ b/js-plugins/svelte/__tests__/preprocess.spec.js @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import { vitePreprocess } from '../src/preprocess.js'; +import path from 'node:path'; +import { normalizePath } from 'vite'; +import { fileURLToPath } from 'node:url'; + +const fixtureDir = normalizePath( + path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'preprocess') +); + +/** @type {import('vite').InlineConfig} */ +const inlineConfig = { + configFile: false, + root: fixtureDir +}; + +describe('vitePreprocess', () => { + it('returns function', () => { + const preprocessorGroup = vitePreprocess({ script: true, style: inlineConfig }); + expect(typeof preprocessorGroup).toBe('object'); + expect(typeof preprocessorGroup.script).toBe('function'); + expect(typeof preprocessorGroup.style).toBe('function'); + }); + + describe('style', async () => { + it('preprocess with postcss if no lang', async () => { + const preprocessorGroup = vitePreprocess({ style: inlineConfig }); + const style = /**@type {import('svelte/types/compiler/preprocess').Preprocessor} */ ( + preprocessorGroup.style + ); + expect(style).toBeDefined(); + + const pcss = "@import './foo';"; + const processed = await style({ + content: pcss, + attributes: {}, + markup: '', // not read by vitePreprocess + filename: `${fixtureDir}/File.svelte` + }); + + expect(processed).toBeDefined(); + expect(processed.code).not.toContain('@import'); + }); + + it('produces sourcemap with relative filename', async () => { + const preprocessorGroup = vitePreprocess({ + style: { ...inlineConfig, css: { devSourcemap: true } } + }); + const style = /**@type {import('svelte/types/compiler/preprocess').Preprocessor} */ ( + preprocessorGroup.style + ); + expect(style).toBeDefined(); + const scss = ` + @use './foo'; + .foo { + &.bar { + color: red; + } + }`.replace(/\t/g, ''); + + const processed = await style({ + content: scss, + attributes: { + lang: 'scss' + }, + markup: '', // not read by vitePreprocess + filename: `${fixtureDir}/File.svelte` + }); + expect(processed).toBeDefined(); + const { code, map, dependencies } = processed; + expect(code).toBe('.foo {\n color: green;\n}\n\n.foo.bar {\n color: red;\n}'); + expect(map.sources.length).toBe(2); + expect(map.sources[0]).toBe('foo.scss'); + expect(map.sources[1]).toBe('File.svelte'); + expect(dependencies).toBeDefined(); + expect(dependencies[0]).toBe(path.resolve(fixtureDir, 'foo.scss')); + expect(dependencies.length).toBe(1); + }); + }); +}); diff --git a/js-plugins/svelte/__tests__/sourcemaps.spec.js b/js-plugins/svelte/__tests__/sourcemaps.spec.js new file mode 100644 index 0000000..afe319f --- /dev/null +++ b/js-plugins/svelte/__tests__/sourcemaps.spec.js @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import { removeLangSuffix, mapToRelative } from '../src/utils/sourcemaps.js'; +import { lang_sep } from '../src/preprocess.js'; +import { normalizePath } from 'vite'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const fixtureDir = normalizePath( + path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'preprocess') +); +const filename = 'File.svelte'; + +describe('removeLangSuffix', () => { + it('removes suffix', () => { + const suffix = `${lang_sep}.scss`; + const map = { + file: `${fixtureDir}/${filename}${suffix}`, + sources: ['foo.scss', `${fixtureDir}/${filename}${suffix}`], + sourceRoot: fixtureDir + }; + removeLangSuffix(map, suffix); + expect(map.file).toBe(`${fixtureDir}/${filename}`); + expect(map.sourceRoot).toBe(fixtureDir); + expect(map.sources[0]).toBe('foo.scss'); + expect(map.sources[1]).toBe(`${fixtureDir}/${filename}`); + }); +}); + +describe('mapToRelative', () => { + it('converts absolute to relative', () => { + const file = `${fixtureDir}/File.svelte`; + const map = { + file, + sources: [`${fixtureDir}/foo.scss`, file] + }; + mapToRelative(map, file); + expect(map.file).toBe('File.svelte'); + expect(map.sources[0]).toBe('foo.scss'); + expect(map.sources[1]).toBe('File.svelte'); + }); + + it('accounts for sourceRoot', () => { + const file = `${fixtureDir}/File.svelte`; + const sourceRoot = normalizePath(path.resolve(fixtureDir, '..')); + const rootedBase = fixtureDir.replace(sourceRoot, ''); + const map = { + file, + sourceRoot, + sources: [ + `${rootedBase}/foo.scss`, + `${rootedBase}/File.svelte`, + `${pathToFileURL(`${fixtureDir}/bar.scss`)}` + ] + }; + mapToRelative(map, file); + expect(map.file).toBe('File.svelte'); + expect(map.sources[0]).toBe('foo.scss'); + expect(map.sources[1]).toBe('File.svelte'); + expect(map.sources[2]).toBe('bar.scss'); + expect(map.sources.length).toBe(3); + expect(map.sourceRoot).not.toBeDefined(); + }); + + it('accounts for relative sourceRoot', () => { + const file = `${fixtureDir}/File.svelte`; + const map = { + file, + sourceRoot: './some-path/..', + sources: ['foo.scss', 'File.svelte', `${pathToFileURL(`${fixtureDir}/bar.scss`)}`] + }; + mapToRelative(map, file); + expect(map.file).toBe('File.svelte'); + expect(map.sources[0]).toBe('./some-path/../foo.scss'); + expect(map.sources[1]).toBe('./some-path/../File.svelte'); + expect(map.sources[2]).toBe('bar.scss'); + expect(map.sources.length).toBe(3); + expect(map.sourceRoot).not.toBeDefined(); + }); +}); diff --git a/js-plugins/svelte/package.json b/js-plugins/svelte/package.json new file mode 100644 index 0000000..47cd0fb --- /dev/null +++ b/js-plugins/svelte/package.json @@ -0,0 +1,54 @@ +{ + "name": "@farmfe/js-plugin-svelte", + "version": "5.0.3", + "license": "MIT", + "author": "dominikg", + "files": [ + "src", + "types" + ], + "type": "module", + "types": "types/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./types/index.d.ts", + "default": "./src/index.js" + } + } + }, + "scripts": { + "check:publint": "publint --strict", + "check:types": "tsc --noEmit", + "generate:types": "dts-buddy -m \"@sveltejs/vite-plugin-svelte:src/public.d.ts\"" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "keywords": [ + "farm-plugin", + "plugin", + "farmfe", + "svelte" + ], + "dependencies": { + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.5" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + }, + "devDependencies": { + "@farmfe/core": "/Users/adny/rust/farm/packages/core", + "@farmfe/cli": "/Users/adny/rust/farm/packages/cli", + "@types/debug": "^4.1.12", + "esbuild": "^0.24.2", + "sass": "^1.83.4", + "svelte": "^5.19.0", + "vite": "^6.0.9" + } +} diff --git a/js-plugins/svelte/src/handle-hot-update.js b/js-plugins/svelte/src/handle-hot-update.js new file mode 100644 index 0000000..82758e5 --- /dev/null +++ b/js-plugins/svelte/src/handle-hot-update.js @@ -0,0 +1,145 @@ +import { log, logCompilerWarnings } from './utils/log.js'; +import { toRollupError } from './utils/error.js'; + +/** + * Vite-specific HMR handling + * + * @param {Function} compileSvelte + * @param {import('vite').HmrContext} ctx + * @param {import('./types/id.d.ts').SvelteRequest} svelteRequest + * @param {import('./utils/vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache + * @param {import('./types/options.d.ts').ResolvedOptions} options + * @returns {Promise} + */ +export async function handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options) { + if (!cache.has(svelteRequest)) { + // file hasn't been requested yet (e.g. async component) + log.debug( + `handleHotUpdate called before initial transform for ${svelteRequest.id}`, + undefined, + 'hmr' + ); + return; + } + const { read, server, modules } = ctx; + + const cachedJS = cache.getJS(svelteRequest); + const cachedCss = cache.getCSS(svelteRequest); + + const content = await read(); + /** @type {import('./types/compile.d.ts').CompileData} */ + let compileData; + try { + compileData = await compileSvelte(svelteRequest, content, options); + cache.update(compileData); + } catch (e) { + cache.setError(svelteRequest, e); + throw toRollupError(e, options); + } + + const affectedModules = [...modules]; + + const cssIdx = modules.findIndex((m) => m.id === svelteRequest.cssId); + if (cssIdx > -1) { + const cssUpdated = cssChanged(cachedCss, compileData.compiled.css); + if (!cssUpdated) { + log.debug(`skipping unchanged css for ${svelteRequest.cssId}`, undefined, 'hmr'); + affectedModules.splice(cssIdx, 1); + } + } + const jsIdx = modules.findIndex((m) => m.id === svelteRequest.id); + if (jsIdx > -1) { + const jsUpdated = jsChanged(cachedJS, compileData.compiled.js, svelteRequest.filename); + if (!jsUpdated) { + log.debug(`skipping unchanged js for ${svelteRequest.id}`, undefined, 'hmr'); + affectedModules.splice(jsIdx, 1); + // transform won't be called, log warnings here + logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); + } + } + + // TODO is this enough? see also: https://github.com/vitejs/vite/issues/2274 + const ssrModulesToInvalidate = affectedModules.filter((m) => !!m.ssrTransformResult); + if (ssrModulesToInvalidate.length > 0) { + log.debug( + `invalidating modules ${ssrModulesToInvalidate.map((m) => m.id).join(', ')}`, + undefined, + 'hmr' + ); + ssrModulesToInvalidate.forEach((moduleNode) => server.moduleGraph.invalidateModule(moduleNode)); + } + if (affectedModules.length > 0) { + log.debug( + `handleHotUpdate for ${svelteRequest.id} result: ${affectedModules + .map((m) => m.id) + .join(', ')}`, + undefined, + 'hmr' + ); + } + return affectedModules; +} + +/** + * @param {import('./types/compile.d.ts').Code | null} [prev] + * @param {import('./types/compile.d.ts').Code | null} [next] + * @returns {boolean} + */ +function cssChanged(prev, next) { + return !isCodeEqual(prev?.code, next?.code); +} + +/** + * @param {import('./types/compile.d.ts').Code | null} [prev] + * @param {import('./types/compile.d.ts').Code | null} [next] + * @param {string} [filename] + * @returns {boolean} + */ +function jsChanged(prev, next, filename) { + const prevJs = prev?.code; + const nextJs = next?.code; + const isStrictEqual = isCodeEqual(prevJs, nextJs); + if (isStrictEqual) { + return false; + } + const isLooseEqual = isCodeEqual(normalizeJsCode(prevJs), normalizeJsCode(nextJs)); + if (!isStrictEqual && isLooseEqual) { + log.debug( + `ignoring compiler output js change for ${filename} as it is equal to previous output after normalization`, + undefined, + 'hmr' + ); + } + return !isLooseEqual; +} + +/** + * @param {string} [prev] + * @param {string} [next] + * @returns {boolean} + */ +function isCodeEqual(prev, next) { + if (!prev && !next) { + return true; + } + if ((!prev && next) || (prev && !next)) { + return false; + } + return prev === next; +} + +/** + * remove code that only changes metadata and does not require a js update for the component to keep working + * + * 1) add_location() calls. These add location metadata to elements, only used by some dev tools + * 2) ... maybe more (or less) in the future + * + * @param {string} [code] + * @returns {string | undefined} + */ +function normalizeJsCode(code) { + if (!code) { + return code; + } + return code.replace(/\s*\badd_location\s*\([^)]*\)\s*;?/g, ''); +} diff --git a/js-plugins/svelte/src/index.js b/js-plugins/svelte/src/index.js new file mode 100644 index 0000000..02ed482 --- /dev/null +++ b/js-plugins/svelte/src/index.js @@ -0,0 +1,297 @@ +import fs from "node:fs"; +import process from "node:process"; +// import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; +import { handleHotUpdate } from "./handle-hot-update.js"; +import { log, logCompilerWarnings } from "./utils/log.js"; +import { createCompileSvelte } from "./utils/compile.js"; +import { buildIdParser, buildModuleIdParser } from "./utils/id.js"; +import { + validateInlineOptions, + resolveOptions, + patchResolvedViteConfig, + preResolveOptions, + ensureConfigEnvironmentMainFields, + ensureConfigEnvironmentConditions, + buildExtraFarmConfig, +} from "./utils/options.js"; +import { ensureWatchedFile, setupWatchers } from "./utils/watch.js"; +import { toRollupError } from "./utils/error.js"; +import { saveSvelteMetadata } from "./utils/optimizer.js"; +import { VitePluginSvelteCache } from "./utils/vite-plugin-svelte-cache.js"; +import { loadRaw } from "./utils/load-raw.js"; +import * as svelteCompiler from "svelte/compiler"; + +/** + * @param {Partial} [inlineOptions] + * @returns {import('vite').Plugin[]} + */ +export function svelte(inlineOptions) { + if (process.env.DEBUG != null) { + log.setLevel("debug"); + } + validateInlineOptions(inlineOptions); + const cache = new VitePluginSvelteCache(); + // updated in configResolved hook + /** @type {import('./types/id.d.ts').IdParser} */ + let requestParser; + /** @type {import('./types/id.d.ts').ModuleIdParser} */ + let moduleRequestParser; + /** @type {import('./types/options.d.ts').ResolvedOptions} */ + let options; + /** @type {import('vite').ResolvedConfig} */ + let viteConfig; + /** @type {import('./types/compile.d.ts').CompileSvelte} */ + let compileSvelte; + const plugins = [ + { + name: "svelte", + // make sure our resolver runs before vite internal resolver to resolve svelte field correctly + priority: 105, + async config(config, configEnv) { + options = await preResolveOptions(inlineOptions, config, configEnv); + + const extraFarmConfig = await buildExtraFarmConfig(); + + return extraFarmConfig; + }, + + async configResolved(config) { + options = resolveOptions(options, config, cache); + + requestParser = buildIdParser(options); + + compileSvelte = createCompileSvelte(); + viteConfig = config; + }, + configureServer(server) { + options.server = server; + setupWatchers(options, cache, requestParser); + }, + + load: { + filters: { + resolvedPaths: [".*"], + }, + async executor(param) { + const isSsr = options.ssr ?? false; + const svelteRequest = requestParser( + param.resolvedPath, + !!options.ssr + ); + if (svelteRequest) { + const { filename, query, raw } = svelteRequest; + if (raw) { + console.log(filename); + + const code = await loadRaw(svelteRequest, compileSvelte, options); + // prevent vite from injecting sourcemaps in the results. + console.log(code); + return { + content: code, + moduleType: "svelte", + }; + } else { + if (query.svelte && query.type === "style") { + const css = cache.getCSS(svelteRequest); + + if (css) { + return { + content: css, + moduleType: "css", + }; + } + } + } + } + }, + }, + + // async load(id, opts) { + // const ssr = !!opts?.ssr; + // const svelteRequest = requestParser(id, !!ssr); + // if (svelteRequest) { + // const { filename, query, raw } = svelteRequest; + // if (raw) { + // const code = await loadRaw(svelteRequest, compileSvelte, options); + // // prevent vite from injecting sourcemaps in the results. + // return { + // code, + // map: { + // mappings: "", + // }, + // }; + // } else { + // if (query.svelte && query.type === "style") { + // const css = cache.getCSS(svelteRequest); + // if (css) { + // return css; + // } + // } + // // prevent vite asset plugin from loading files as url that should be compiled in transform + // if (viteConfig.assetsInclude(filename)) { + // log.debug( + // `load returns raw content for ${filename}`, + // undefined, + // "load" + // ); + // return fs.readFileSync(filename, "utf-8"); + // } + // } + // } + // }, + + resolve: { + filters: { + sources: [".*"], + importers: [".*"], + }, + executor: async (param, context, hookContext) => { + const isSsr = options.ssr ?? false; + const svelteRequest = requestParser(param.source, !!options.ssr); + if (svelteRequest?.query.svelte) { + if ( + svelteRequest.query.type === "style" && + !svelteRequest.raw && + !svelteRequest.query.inline + ) { + return svelteRequest.cssId; + } + } + }, + }, + + // async resolveId(importee, importer, opts) { + // const ssr = !!opts?.ssr; + // const svelteRequest = requestParser(importee, ssr); + // if (svelteRequest?.query.svelte) { + // if ( + // svelteRequest.query.type === "style" && + // !svelteRequest.raw && + // !svelteRequest.query.inline + // ) { + // // return cssId with root prefix so postcss pipeline of vite finds the directory correctly + // // see https://github.com/sveltejs/vite-plugin-svelte/issues/14 + // log.debug( + // `resolveId resolved virtual css module ${svelteRequest.cssId}`, + // undefined, + // "resolve" + // ); + // return svelteRequest.cssId; + // } + // } + // }, + + transform: { + filters: { + moduleTypes: ["*"], + resolvedPaths: [".*"], + }, + async executor(param, ctx) { + console.log(param); + // const { css: compiledCss, map } = compileSass(param.content); + // return { + // content: compiledCss, + // moduleType: 'css' // transformed sass to css, + // sourceMap: JSON.stringify(map) + // ignorePreviousSourceMap: false, + // } + }, + }, + + // async transform(code, id, opts) { + // const ssr = !!opts?.ssr; + // const svelteRequest = requestParser(id, ssr); + // if ( + // !svelteRequest || + // svelteRequest.query.type === "style" || + // svelteRequest.raw + // ) { + // return; + // } + // let compileData; + // try { + // compileData = await compileSvelte(svelteRequest, code, options); + // } catch (e) { + // cache.setError(svelteRequest, e); + // throw toRollupError(e, options); + // } + // logCompilerWarnings( + // svelteRequest, + // compileData.compiled.warnings, + // options + // ); + // cache.update(compileData); + // if (compileData.dependencies?.length) { + // if (options.server) { + // for (const dep of compileData.dependencies) { + // ensureWatchedFile(options.server.watcher, dep, options.root); + // } + // } else if (options.isBuild && viteConfig.build.watch) { + // for (const dep of compileData.dependencies) { + // this.addWatchFile(dep); + // } + // } + // } + // return { + // ...compileData.compiled.js, + // meta: { + // vite: { + // lang: compileData.lang, + // }, + // }, + // }; + // }, + + // handleHotUpdate(ctx) { + // if (!options.compilerOptions.hmr || !options.emitCss) { + // return; + // } + // const svelteRequest = requestParser(ctx.file, false, ctx.timestamp); + // if (svelteRequest) { + // return handleHotUpdate( + // compileSvelte, + // ctx, + // svelteRequest, + // cache, + // options + // ); + // } + // }, + + finish: { + async executor() { + await options.stats?.finishAll(); + }, + }, + }, + { + name: "vite-plugin-svelte-module", + priority: 99, + // async configResolved() { + // moduleRequestParser = buildModuleIdParser(options); + // }, + // async transform(code, id, opts) { + // const ssr = !!opts?.ssr; + // const moduleRequest = moduleRequestParser(id, ssr); + // if (!moduleRequest) { + // return; + // } + // try { + // const compileResult = svelteCompiler.compileModule(code, { + // dev: !viteConfig.isProduction, + // generate: ssr ? "server" : "client", + // filename: moduleRequest.filename, + // }); + // logCompilerWarnings(moduleRequest, compileResult.warnings, options); + // return compileResult.js; + // } catch (e) { + // throw toRollupError(e, options); + // } + // }, + }, + ]; + return plugins; +} + +export { vitePreprocess } from "./preprocess.js"; +export { loadSvelteConfig } from "./utils/load-svelte-config.js"; diff --git a/js-plugins/svelte/src/index2.js b/js-plugins/svelte/src/index2.js new file mode 100644 index 0000000..9ec43e9 --- /dev/null +++ b/js-plugins/svelte/src/index2.js @@ -0,0 +1,236 @@ +import fs from 'node:fs'; +import process from 'node:process'; +import { svelteInspector } from '@sveltejs/vite-plugin-svelte-inspector'; +import { handleHotUpdate } from './handle-hot-update.js'; +import { log, logCompilerWarnings } from './utils/log.js'; +import { createCompileSvelte } from './utils/compile.js'; +import { buildIdParser, buildModuleIdParser } from './utils/id.js'; +import { + buildExtraViteConfig, + validateInlineOptions, + resolveOptions, + patchResolvedViteConfig, + preResolveOptions, + ensureConfigEnvironmentMainFields, + ensureConfigEnvironmentConditions +} from './utils/options.js'; +import { ensureWatchedFile, setupWatchers } from './utils/watch.js'; +import { toRollupError } from './utils/error.js'; +import { saveSvelteMetadata } from './utils/optimizer.js'; +import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache.js'; +import { loadRaw } from './utils/load-raw.js'; +import * as svelteCompiler from 'svelte/compiler'; + +/** + * @param {Partial} [inlineOptions] + * @returns {import('vite').Plugin[]} + */ +export function svelte(inlineOptions) { + if (process.env.DEBUG != null) { + log.setLevel('debug'); + } + validateInlineOptions(inlineOptions); + const cache = new VitePluginSvelteCache(); + // updated in configResolved hook + /** @type {import('./types/id.d.ts').IdParser} */ + let requestParser; + /** @type {import('./types/id.d.ts').ModuleIdParser} */ + let moduleRequestParser; + /** @type {import('./types/options.d.ts').ResolvedOptions} */ + let options; + /** @type {import('vite').ResolvedConfig} */ + let viteConfig; + /** @type {import('./types/compile.d.ts').CompileSvelte} */ + let compileSvelte; + /** @type {import('./types/plugin-api.d.ts').PluginAPI} */ + const api = {}; + /** @type {import('vite').Plugin[]} */ + const plugins = [ + { + name: 'vite-plugin-svelte', + // make sure our resolver runs before vite internal resolver to resolve svelte field correctly + enforce: 'pre', + api, + async config(config, configEnv) { + // setup logger + if (process.env.DEBUG) { + log.setLevel('debug'); + } else if (config.logLevel) { + log.setLevel(config.logLevel); + } + // @ts-expect-error temporarily lend the options variable until fixed in configResolved + options = await preResolveOptions(inlineOptions, config, configEnv); + // extra vite config + const extraViteConfig = await buildExtraViteConfig(options, config); + log.debug('additional vite config', extraViteConfig, 'config'); + return extraViteConfig; + }, + + configEnvironment(name, config, opts) { + ensureConfigEnvironmentMainFields(name, config, opts); + // @ts-expect-error the function above should make `resolve.mainFields` non-nullable + config.resolve.mainFields.unshift('svelte'); + + ensureConfigEnvironmentConditions(name, config, opts); + // @ts-expect-error the function above should make `resolve.conditions` non-nullable + config.resolve.conditions.push('svelte'); + }, + + async configResolved(config) { + options = resolveOptions(options, config, cache); + patchResolvedViteConfig(config, options); + requestParser = buildIdParser(options); + compileSvelte = createCompileSvelte(); + viteConfig = config; + // TODO deep clone to avoid mutability from outside? + api.options = options; + log.debug('resolved options', options, 'config'); + }, + + async buildStart() { + if (!options.prebundleSvelteLibraries) return; + const isSvelteMetadataChanged = await saveSvelteMetadata(viteConfig.cacheDir, options); + if (isSvelteMetadataChanged) { + // Force Vite to optimize again. Although we mutate the config here, it works because + // Vite's optimizer runs after `buildStart()`. + viteConfig.optimizeDeps.force = true; + } + }, + + configureServer(server) { + options.server = server; + setupWatchers(options, cache, requestParser); + }, + + async load(id, opts) { + const ssr = !!opts?.ssr; + const svelteRequest = requestParser(id, !!ssr); + if (svelteRequest) { + const { filename, query, raw } = svelteRequest; + if (raw) { + const code = await loadRaw(svelteRequest, compileSvelte, options); + // prevent vite from injecting sourcemaps in the results. + return { + code, + map: { + mappings: '' + } + }; + } else { + if (query.svelte && query.type === 'style') { + const css = cache.getCSS(svelteRequest); + if (css) { + return css; + } + } + // prevent vite asset plugin from loading files as url that should be compiled in transform + if (viteConfig.assetsInclude(filename)) { + log.debug(`load returns raw content for ${filename}`, undefined, 'load'); + return fs.readFileSync(filename, 'utf-8'); + } + } + } + }, + + async resolveId(importee, importer, opts) { + const ssr = !!opts?.ssr; + const svelteRequest = requestParser(importee, ssr); + if (svelteRequest?.query.svelte) { + if ( + svelteRequest.query.type === 'style' && + !svelteRequest.raw && + !svelteRequest.query.inline + ) { + // return cssId with root prefix so postcss pipeline of vite finds the directory correctly + // see https://github.com/sveltejs/vite-plugin-svelte/issues/14 + log.debug( + `resolveId resolved virtual css module ${svelteRequest.cssId}`, + undefined, + 'resolve' + ); + return svelteRequest.cssId; + } + } + }, + + async transform(code, id, opts) { + const ssr = !!opts?.ssr; + const svelteRequest = requestParser(id, ssr); + if (!svelteRequest || svelteRequest.query.type === 'style' || svelteRequest.raw) { + return; + } + let compileData; + try { + compileData = await compileSvelte(svelteRequest, code, options); + } catch (e) { + cache.setError(svelteRequest, e); + throw toRollupError(e, options); + } + logCompilerWarnings(svelteRequest, compileData.compiled.warnings, options); + cache.update(compileData); + if (compileData.dependencies?.length) { + if (options.server) { + for (const dep of compileData.dependencies) { + ensureWatchedFile(options.server.watcher, dep, options.root); + } + } else if (options.isBuild && viteConfig.build.watch) { + for (const dep of compileData.dependencies) { + this.addWatchFile(dep); + } + } + } + return { + ...compileData.compiled.js, + meta: { + vite: { + lang: compileData.lang + } + } + }; + }, + + handleHotUpdate(ctx) { + if (!options.compilerOptions.hmr || !options.emitCss) { + return; + } + const svelteRequest = requestParser(ctx.file, false, ctx.timestamp); + if (svelteRequest) { + return handleHotUpdate(compileSvelte, ctx, svelteRequest, cache, options); + } + }, + async buildEnd() { + await options.stats?.finishAll(); + } + }, + { + name: 'vite-plugin-svelte-module', + enforce: 'post', + async configResolved() { + moduleRequestParser = buildModuleIdParser(options); + }, + async transform(code, id, opts) { + const ssr = !!opts?.ssr; + const moduleRequest = moduleRequestParser(id, ssr); + if (!moduleRequest) { + return; + } + try { + const compileResult = svelteCompiler.compileModule(code, { + dev: !viteConfig.isProduction, + generate: ssr ? 'server' : 'client', + filename: moduleRequest.filename + }); + logCompilerWarnings(moduleRequest, compileResult.warnings, options); + return compileResult.js; + } catch (e) { + throw toRollupError(e, options); + } + } + }, + svelteInspector() + ]; + return plugins; +} + +export { vitePreprocess } from './preprocess.js'; +export { loadSvelteConfig } from './utils/load-svelte-config.js'; diff --git a/js-plugins/svelte/src/preprocess.js b/js-plugins/svelte/src/preprocess.js new file mode 100644 index 0000000..d63ea23 --- /dev/null +++ b/js-plugins/svelte/src/preprocess.js @@ -0,0 +1,124 @@ +import process from 'node:process'; +import { isCSSRequest, preprocessCSS, resolveConfig, transformWithEsbuild } from 'vite'; +import { mapToRelative, removeLangSuffix } from './utils/sourcemaps.js'; + +/** + * @typedef {(code: string, filename: string) => Promise<{ code: string; map?: any; deps?: Set }>} CssTransform + */ + +const supportedScriptLangs = ['ts']; + +export const lang_sep = '.vite-preprocess'; + +/** + * @param {import('./public.d.ts').VitePreprocessOptions} [opts] + * @returns {import('svelte/compiler').PreprocessorGroup} + */ +export function vitePreprocess(opts) { + /** @type {import('svelte/compiler').PreprocessorGroup} */ + const preprocessor = { name: 'vite-preprocess' }; + if (opts?.script === true) { + preprocessor.script = viteScript().script; + } + if (opts?.style !== false) { + const styleOpts = typeof opts?.style == 'object' ? opts?.style : undefined; + preprocessor.style = viteStyle(styleOpts).style; + } + return preprocessor; +} + +/** + * @returns {{ script: import('svelte/compiler').Preprocessor }} + */ +function viteScript() { + return { + async script({ attributes, content, filename = '' }) { + const lang = /** @type {string} */ (attributes.lang); + if (!supportedScriptLangs.includes(lang)) return; + const { code, map } = await transformWithEsbuild(content, filename, { + loader: /** @type {import('vite').ESBuildOptions['loader']} */ (lang), + target: 'esnext', + tsconfigRaw: { + compilerOptions: { + // svelte typescript needs this flag to work with type imports + importsNotUsedAsValues: 'preserve', + preserveValueImports: true + } + } + }); + + mapToRelative(map, filename); + + return { + code, + map + }; + } + }; +} + +/** + * @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config + * @returns {{ style: import('svelte/compiler').Preprocessor }} + */ +function viteStyle(config = {}) { + /** @type {Promise | CssTransform} */ + let cssTransform; + /** @type {import('svelte/compiler').Preprocessor} */ + const style = async ({ attributes, content, filename = '' }) => { + const ext = attributes.lang ? `.${attributes.lang}` : '.css'; + if (attributes.lang && !isCSSRequest(ext)) return; + if (!cssTransform) { + cssTransform = createCssTransform(style, config).then((t) => (cssTransform = t)); + } + const transform = await cssTransform; + const suffix = `${lang_sep}${ext}`; + const moduleId = `${filename}${suffix}`; + const { code, map, deps } = await transform(content, moduleId); + removeLangSuffix(map, suffix); + mapToRelative(map, filename); + const dependencies = deps ? Array.from(deps).filter((d) => !d.endsWith(suffix)) : undefined; + return { + code, + map: map ?? undefined, + dependencies + }; + }; + // @ts-expect-error tag so can be found by v-p-s + style.__resolvedConfig = null; + return { style }; +} + +/** + * @param {import('svelte/compiler').Preprocessor} style + * @param {import('vite').ResolvedConfig | import('vite').InlineConfig} config + * @returns {Promise} + */ +async function createCssTransform(style, config) { + /** @type {import('vite').ResolvedConfig} */ + let resolvedConfig; + // @ts-expect-error special prop added if running in v-p-s + if (style.__resolvedConfig) { + // @ts-expect-error not typed + resolvedConfig = style.__resolvedConfig; + } else if (isResolvedConfig(config)) { + resolvedConfig = config; + } else { + // default to "build" if no NODE_ENV is set to avoid running in dev mode for svelte-check etc. + const useBuild = !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; + const command = useBuild ? 'build' : 'serve'; + const defaultMode = useBuild ? 'production' : 'development'; + resolvedConfig = await resolveConfig(config, command, defaultMode, defaultMode, false); + } + return async (code, filename) => { + return preprocessCSS(code, filename, resolvedConfig); + }; +} + +/** + * @param {any} config + * @returns {config is import('vite').ResolvedConfig} + */ +function isResolvedConfig(config) { + return !!config.inlineConfig; +} diff --git a/js-plugins/svelte/src/public.d.ts b/js-plugins/svelte/src/public.d.ts new file mode 100644 index 0000000..6335e1a --- /dev/null +++ b/js-plugins/svelte/src/public.d.ts @@ -0,0 +1,210 @@ +import type { InlineConfig, ResolvedConfig } from 'vite'; +import type { CompileOptions, Warning, PreprocessorGroup } from 'svelte/compiler'; +import type { Options as InspectorOptions } from '@sveltejs/vite-plugin-svelte-inspector'; + +export type Options = Omit & PluginOptionsInline; + +interface PluginOptionsInline extends PluginOptions { + /** + * Path to a svelte config file, either absolute or relative to Vite root + * + * set to `false` to ignore the svelte config file + * + * @see https://vitejs.dev/config/#root + */ + configFile?: string | false; +} + +export interface PluginOptions { + /** + * A `picomatch` pattern, or array of patterns, which specifies the files the plugin should + * operate on. By default, all svelte files are included. + * + * @see https://github.com/micromatch/picomatch + */ + include?: Arrayable; + /** + * A `picomatch` pattern, or array of patterns, which specifies the files to be ignored by the + * plugin. By default, no files are ignored. + * + * @see https://github.com/micromatch/picomatch + */ + exclude?: Arrayable; + /** + * Emit Svelte styles as virtual CSS files for Vite and other plugins to process + * + * @default true + */ + emitCss?: boolean; + /** + * Enable or disable Hot Module Replacement. + * Deprecated, use compilerOptions.hmr instead! + * + * @deprecated + * @default true for development, always false for production + */ + hot?: boolean; + + /** + * Some Vite plugins can contribute additional preprocessors by defining `api.sveltePreprocess`. + * If you don't want to use them, set this to true to ignore them all or use an array of strings + * with plugin names to specify which. + * + * @default false + */ + ignorePluginPreprocessors?: boolean | string[]; + /** + * vite-plugin-svelte automatically handles excluding svelte libraries and reinclusion of their dependencies + * in vite.optimizeDeps. + * + * `disableDependencyReinclusion: true` disables all reinclusions + * `disableDependencyReinclusion: ['foo','bar']` disables reinclusions for dependencies of foo and bar + * + * This should be used for hybrid packages that contain both node and browser dependencies, eg Routify + * + * @default false + */ + disableDependencyReinclusion?: boolean | string[]; + /** + * Enable support for Vite's dependency optimization to prebundle Svelte libraries. + * + * To disable prebundling for a specific library, add it to `optimizeDeps.exclude`. + * + * @default true for dev, false for build + */ + prebundleSvelteLibraries?: boolean; + /** + * toggle/configure Svelte Inspector + * + * @default unset for dev, always false for build + */ + inspector?: InspectorOptions | boolean; + + /** + * A function to update `compilerOptions` before compilation + * + * `data.filename` - The file to be compiled + * `data.code` - The preprocessed Svelte code + * `data.compileOptions` - The current compiler options + * + * To change part of the compiler options, return an object with the changes you need. + * + * @example + * ``` + * ({ filename, compileOptions }) => { + * // Dynamically set runes mode per Svelte file + * if (forceRunesMode(filename) && !compileOptions.runes) { + * return { runes: true }; + * } + * } + * ``` + */ + dynamicCompileOptions?: (data: { + filename: string; + code: string; + compileOptions: Partial; + }) => Promise | void> | Partial | void; + + /** + * These options are considered experimental and breaking changes to them can occur in any release + */ + experimental?: ExperimentalOptions; +} + +export interface SvelteConfig { + /** + * A list of file extensions to be compiled by Svelte + * + * @default ['.svelte'] + */ + extensions?: string[]; + /** + * An array of preprocessors to transform the Svelte source code before compilation + * + * @see https://svelte.dev/docs#svelte_preprocess + */ + preprocess?: Arrayable; + /** + * The options to be passed to the Svelte compiler. A few options are set by default, + * including `dev` and `css`. However, some options are non-configurable, like + * `filename`, `format`, `generate`, and `cssHash` (in dev). + * + * @see https://svelte.dev/docs#svelte_compile + */ + compilerOptions?: Omit; + + /** + * Handles warning emitted from the Svelte compiler + * + * warnings emitted for files in node_modules are logged at the debug level, to see them run + * `DEBUG=vite-plugin-svelte:node-modules-onwarn pnpm build` + * + * @example + * ``` + * (warning, defaultHandler) => { + * // ignore some warnings + * if (!['foo','bar'].includes(warning.code)) { + * defaultHandler(warning); + * } + * } + * ``` + * + */ + onwarn?: (warning: Warning, defaultHandler: (warning: Warning) => void) => void; + /** + * Options for vite-plugin-svelte + */ + vitePlugin?: PluginOptions; +} + +/** + * These options are considered experimental and breaking changes to them can occur in any release + */ +interface ExperimentalOptions { + /** + * send a websocket message with svelte compiler warnings during dev + * + */ + sendWarningsToBrowser?: boolean; + /** + * disable svelte field resolve warnings + * + * @default false + */ + disableSvelteResolveWarnings?: boolean; + + compileModule?: CompileModuleOptions; +} + +interface CompileModuleOptions { + /** + * infix that must be present in filename + * @default ['.svelte.'] + */ + infixes?: string[]; + /** + * module extensions + * @default ['.ts','.js'] + */ + extensions?: string[]; + include?: Arrayable; + exclude?: Arrayable; +} + +type Arrayable = T | T[]; + +export interface VitePreprocessOptions { + /** + * preprocess script block with vite pipeline. + * Since svelte5 this is not needed for typescript anymore + * + * @default false + */ + script?: boolean; + /** + * preprocess style blocks with vite pipeline + */ + style?: boolean | InlineConfig | ResolvedConfig; +} +// eslint-disable-next-line n/no-missing-import +export * from './index.js'; diff --git a/js-plugins/svelte/src/types/compile.d.ts b/js-plugins/svelte/src/types/compile.d.ts new file mode 100644 index 0000000..d6ba48e --- /dev/null +++ b/js-plugins/svelte/src/types/compile.d.ts @@ -0,0 +1,25 @@ +import type { Processed, CompileResult } from 'svelte/compiler'; +import type { SvelteRequest } from './id.d.ts'; +import type { ResolvedOptions } from './options.d.ts'; + +export type CompileSvelte = ( + svelteRequest: SvelteRequest, + code: string, + options: Partial +) => Promise; + +export interface Code { + code: string; + map?: any; + dependencies?: any[]; +} + +export interface CompileData { + filename: string; + normalizedFilename: string; + lang: string; + compiled: CompileResult; + ssr: boolean | undefined; + dependencies: string[]; + preprocessed: Processed; +} diff --git a/js-plugins/svelte/src/types/id.d.ts b/js-plugins/svelte/src/types/id.d.ts new file mode 100644 index 0000000..bbd4dd4 --- /dev/null +++ b/js-plugins/svelte/src/types/id.d.ts @@ -0,0 +1,46 @@ +import type { CompileOptions } from 'svelte/compiler'; + +export type SvelteQueryTypes = 'style' | 'script' | 'preprocessed' | 'all'; + +export interface RequestQuery { + // our own + svelte?: boolean; + type?: SvelteQueryTypes; + sourcemap?: boolean; + compilerOptions?: Pick< + CompileOptions, + 'generate' | 'dev' | 'css' | 'customElement' | 'immutable' + >; + // vite specific + url?: boolean; + raw?: boolean; + direct?: boolean; + inline?: boolean; +} + +export interface SvelteRequest { + id: string; + cssId: string; + filename: string; + normalizedFilename: string; + query: RequestQuery; + timestamp: number; + ssr: boolean; + raw: boolean; +} + +export interface SvelteModuleRequest { + id: string; + filename: string; + normalizedFilename: string; + query: RequestQuery; + timestamp: number; + ssr: boolean; +} + +export type IdParser = (id: string, ssr: boolean, timestamp?: number) => SvelteRequest | undefined; +export type ModuleIdParser = ( + id: string, + ssr: boolean, + timestamp?: number +) => SvelteModuleRequest | undefined; diff --git a/js-plugins/svelte/src/types/log.d.ts b/js-plugins/svelte/src/types/log.d.ts new file mode 100644 index 0000000..fd4f13e --- /dev/null +++ b/js-plugins/svelte/src/types/log.d.ts @@ -0,0 +1,24 @@ +import type { Warning } from 'svelte/compiler'; + +export interface LogFn extends SimpleLogFn { + (message: string, payload?: unknown, namespace?: string): void; + + enabled: boolean; + once: SimpleLogFn; +} + +export interface SimpleLogFn { + (message: string, payload?: unknown, namespace?: string): void; +} + +export type SvelteWarningsMessage = { + id: string; + filename: string; + normalizedFilename: string; + timestamp: number; + warnings: Warning[]; // allWarnings filtered by warnings where onwarn did not call the default handler + allWarnings: Warning[]; // includes warnings filtered by onwarn and our extra vite plugin svelte warnings + rawWarnings: Warning[]; // raw compiler output +}; + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'; diff --git a/js-plugins/svelte/src/types/options.d.ts b/js-plugins/svelte/src/types/options.d.ts new file mode 100644 index 0000000..5b4ca42 --- /dev/null +++ b/js-plugins/svelte/src/types/options.d.ts @@ -0,0 +1,21 @@ +import type { CompileOptions } from 'svelte/compiler'; +import type { ViteDevServer } from 'vite'; +// eslint-disable-next-line n/no-missing-import +import { VitePluginSvelteStats } from '../utils/vite-plugin-svelte-stats.js'; +import type { Options } from '../public.d.ts'; + +export interface PreResolvedOptions extends Options { + // these options are non-nullable after resolve + compilerOptions: CompileOptions; + // extra options + root: string; + isBuild: boolean; + isServe: boolean; + isDebug: boolean; +} + +export interface ResolvedOptions extends PreResolvedOptions { + isProduction: boolean; + server?: ViteDevServer; + stats?: VitePluginSvelteStats; +} diff --git a/js-plugins/svelte/src/types/plugin-api.d.ts b/js-plugins/svelte/src/types/plugin-api.d.ts new file mode 100644 index 0000000..36c42b6 --- /dev/null +++ b/js-plugins/svelte/src/types/plugin-api.d.ts @@ -0,0 +1,11 @@ +import type { ResolvedOptions } from './options.d.ts'; + +export interface PluginAPI { + /** + * must not be modified, should not be used outside of vite-plugin-svelte repo + * @internal + * @experimental + */ + options?: ResolvedOptions; + // TODO expose compile cache here so other utility plugins can use it +} diff --git a/js-plugins/svelte/src/types/vite-plugin-svelte-stats.d.ts b/js-plugins/svelte/src/types/vite-plugin-svelte-stats.d.ts new file mode 100644 index 0000000..1b69ebd --- /dev/null +++ b/js-plugins/svelte/src/types/vite-plugin-svelte-stats.d.ts @@ -0,0 +1,30 @@ +export interface Stat { + file: string; + pkg?: string; + start: number; + end: number; +} + +export interface StatCollection { + name: string; + options: CollectionOptions; + + start: (file: string) => () => void; + stats: Stat[]; + packageStats?: PackageStats[]; + collectionStart: number; + duration?: number; + finish: () => Promise | void; + finished: boolean; +} + +export interface PackageStats { + pkg: string; + files: number; + duration: number; +} + +export interface CollectionOptions { + logInProgress: (collection: StatCollection, now: number) => boolean; + logResult: (collection: StatCollection) => boolean; +} diff --git a/js-plugins/svelte/src/utils/compile.js b/js-plugins/svelte/src/utils/compile.js new file mode 100644 index 0000000..4c0e524 --- /dev/null +++ b/js-plugins/svelte/src/utils/compile.js @@ -0,0 +1,191 @@ +import * as svelte from 'svelte/compiler'; + +import { safeBase64Hash } from './hash.js'; +import { log } from './log.js'; + +import { + checkPreprocessDependencies, + createInjectScopeEverythingRulePreprocessorGroup +} from './preprocess.js'; +import { mapToRelative } from './sourcemaps.js'; +import { enhanceCompileError } from './error.js'; + +// TODO this is a patched version of https://github.com/sveltejs/vite-plugin-svelte/pull/796/files#diff-3bce0b33034aad4b35ca094893671f7e7ddf4d27254ae7b9b0f912027a001b15R10 +// which is closer to the other regexes in at least not falling into commented script +// but ideally would be shared exactly with svelte and other tools that use it +const scriptLangRE = + /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g; + +/** + * @returns {import('../types/compile.d.ts').CompileSvelte} + */ +export function createCompileSvelte() { + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ + let stats; + const devStylePreprocessor = createInjectScopeEverythingRulePreprocessorGroup(); + /** @type {import('../types/compile.d.ts').CompileSvelte} */ + return async function compileSvelte(svelteRequest, code, options) { + const { filename, normalizedFilename, cssId, ssr, raw } = svelteRequest; + const { emitCss = true } = options; + /** @type {string[]} */ + const dependencies = []; + /** @type {import('svelte/compiler').Warning[]} */ + const warnings = []; + + if (options.stats) { + if (options.isBuild) { + if (!stats) { + // build is either completely ssr or csr, create stats collector on first compile + // it is then finished in the buildEnd hook. + stats = options.stats.startCollection(`${ssr ? 'ssr' : 'dom'} compile`, { + logInProgress: () => false + }); + } + } else { + // dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting + if (ssr && !stats) { + stats = options.stats.startCollection('ssr compile'); + } + // stats are being collected but this isn't an ssr request, assume page loaded and stop collecting + if (!ssr && stats) { + stats.finish(); + stats = undefined; + } + // TODO find a way to trace dom compile during dev + // problem: we need to call finish at some point but have no way to tell if page load finished + // also they for hmr updates too + } + } + /** @type {import('svelte/compiler').CompileOptions} */ + const compileOptions = { + ...options.compilerOptions, + filename, + generate: ssr ? 'server' : 'client' + }; + + if (compileOptions.hmr && options.emitCss) { + const hash = `s-${safeBase64Hash(normalizedFilename)}`; + compileOptions.cssHash = () => hash; + } + + let preprocessed; + let preprocessors = options.preprocess; + if (!options.isBuild && options.emitCss && compileOptions.hmr) { + // inject preprocessor that ensures css hmr works better + if (!Array.isArray(preprocessors)) { + preprocessors = preprocessors + ? [preprocessors, devStylePreprocessor] + : [devStylePreprocessor]; + } else { + preprocessors = preprocessors.concat(devStylePreprocessor); + } + } + if (preprocessors) { + try { + preprocessed = await svelte.preprocess(code, preprocessors, { filename }); // full filename here so postcss works + } catch (e) { + e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`; + throw e; + } + + if (preprocessed.dependencies?.length) { + const checked = checkPreprocessDependencies(filename, preprocessed.dependencies); + if (checked.warnings.length) { + warnings.push(...checked.warnings); + } + if (checked.dependencies.length) { + dependencies.push(...checked.dependencies); + } + } + + if (preprocessed.map) compileOptions.sourcemap = preprocessed.map; + } + if (typeof preprocessed?.map === 'object') { + mapToRelative(preprocessed?.map, filename); + } + if (raw && svelteRequest.query.type === 'preprocessed') { + // @ts-expect-error shortcut + return /** @type {import('../types/compile.d.ts').CompileData} */ { + preprocessed: preprocessed ?? { code } + }; + } + const finalCode = preprocessed ? preprocessed.code : code; + const dynamicCompileOptions = await options?.dynamicCompileOptions?.({ + filename, + code: finalCode, + compileOptions + }); + if (dynamicCompileOptions && log.debug.enabled) { + log.debug( + `dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`, + undefined, + 'compile' + ); + } + const finalCompileOptions = dynamicCompileOptions + ? { + ...compileOptions, + ...dynamicCompileOptions + } + : compileOptions; + const endStat = stats?.start(filename); + /** @type {import('svelte/compiler').CompileResult} */ + let compiled; + try { + compiled = svelte.compile(finalCode, { ...finalCompileOptions, filename }); + // patch output with partial accept until svelte does it + // TODO remove later + if ( + options.server?.config.experimental.hmrPartialAccept && + compiled.js.code.includes('import.meta.hot.accept(') + ) { + compiled.js.code = compiled.js.code.replaceAll( + 'import.meta.hot.accept(', + 'import.meta.hot.acceptExports(["default"],' + ); + } + } catch (e) { + enhanceCompileError(e, code, preprocessors); + throw e; + } + + if (endStat) { + endStat(); + } + mapToRelative(compiled.js?.map, filename); + mapToRelative(compiled.css?.map, filename); + if (warnings.length) { + if (!compiled.warnings) { + compiled.warnings = []; + } + compiled.warnings.push(...warnings); + } + if (!raw) { + // wire css import and code for hmr + const hasCss = compiled.css?.code?.trim()?.length ?? 0 > 0; + // compiler might not emit css with mode none or it may be empty + if (emitCss && hasCss) { + // TODO properly update sourcemap? + compiled.js.code += `\nimport ${JSON.stringify(cssId)};\n`; + } + } + + let lang = 'js'; + for (const match of code.matchAll(scriptLangRE)) { + if (match[2]) { + lang = match[2]; + break; + } + } + + return { + filename, + normalizedFilename, + lang, + compiled, + ssr, + dependencies, + preprocessed: preprocessed ?? { code } + }; + }; +} diff --git a/js-plugins/svelte/src/utils/constants.js b/js-plugins/svelte/src/utils/constants.js new file mode 100644 index 0000000..3cc59d4 --- /dev/null +++ b/js-plugins/svelte/src/utils/constants.js @@ -0,0 +1,26 @@ +import { createRequire } from 'node:module'; + +export const SVELTE_IMPORTS = Object.entries( + createRequire(import.meta.url)('svelte/package.json').exports +) + .map(([name, config]) => { + // ignore type only + if (typeof config === 'object' && Object.keys(config).length === 1 && config.types) { + return ''; + } + // ignore names + if (name === './package.json' || name === './compiler') { + return ''; + } + return name.replace(/^\./, 'svelte'); + }) + .filter((s) => s.length > 0); + +export const SVELTE_EXPORT_CONDITIONS = ['svelte']; + +export const FAQ_LINK_MISSING_EXPORTS_CONDITION = + 'https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#missing-exports-condition'; + +export const DEFAULT_SVELTE_EXT = ['.svelte']; +export const DEFAULT_SVELTE_MODULE_INFIX = ['.svelte.']; +export const DEFAULT_SVELTE_MODULE_EXT = ['.js', '.ts']; diff --git a/js-plugins/svelte/src/utils/dependencies.js b/js-plugins/svelte/src/utils/dependencies.js new file mode 100644 index 0000000..30caf59 --- /dev/null +++ b/js-plugins/svelte/src/utils/dependencies.js @@ -0,0 +1,89 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { findDepPkgJsonPath } from 'vitefu'; + +/** + * @typedef {{ + * dir: string; + * pkg: Record; + * }} DependencyData + */ + +/** + * @param {string} dep + * @param {string} parent + * @returns {Promise} + */ +export async function resolveDependencyData(dep, parent) { + const depDataPath = await findDepPkgJsonPath(dep, parent); + if (!depDataPath) return undefined; + try { + return { + dir: path.dirname(depDataPath), + pkg: JSON.parse(await fs.readFile(depDataPath, 'utf-8')) + }; + } catch { + return undefined; + } +} + +const COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [ + '@lukeed/uuid', + '@playwright/test', + '@sveltejs/kit', + '@sveltejs/package', + '@sveltejs/vite-plugin-svelte', + 'autoprefixer', + 'cookie', + 'dotenv', + 'esbuild', + 'eslint', + 'jest', + 'mdsvex', + 'playwright', + 'postcss', + 'prettier', + 'svelte', + 'svelte2tsx', + 'svelte-check', + 'svelte-preprocess', + 'tslib', + 'typescript', + 'vite', + 'vitest', + '__vite-browser-external' // see https://github.com/sveltejs/vite-plugin-svelte/issues/362 +]; +const COMMON_PREFIXES_WITHOUT_SVELTE_FIELD = [ + '@fontsource/', + '@postcss-plugins/', + '@rollup/', + '@sveltejs/adapter-', + '@types/', + '@typescript-eslint/', + 'eslint-', + 'jest-', + 'postcss-plugin-', + 'prettier-plugin-', + 'rollup-plugin-', + 'vite-plugin-' +]; + +/** + * Test for common dependency names that tell us it is not a package including a svelte field, eg. eslint + plugins. + * + * This speeds up the find process as we don't have to try and require the package.json for all of them + * + * @param {string} dependency + * @returns {boolean} true if it is a dependency without a svelte field + */ +export function isCommonDepWithoutSvelteField(dependency) { + return ( + COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD.includes(dependency) || + COMMON_PREFIXES_WITHOUT_SVELTE_FIELD.some( + (prefix) => + prefix.startsWith('@') + ? dependency.startsWith(prefix) + : dependency.substring(dependency.lastIndexOf('/') + 1).startsWith(prefix) // check prefix omitting @scope/ + ) + ); +} diff --git a/js-plugins/svelte/src/utils/error.js b/js-plugins/svelte/src/utils/error.js new file mode 100644 index 0000000..31e3c5a --- /dev/null +++ b/js-plugins/svelte/src/utils/error.js @@ -0,0 +1,162 @@ +import { buildExtendedLogMessage } from './log.js'; + +/** + * convert an error thrown by svelte.compile to a RollupError so that vite displays it in a user friendly way + * @param {import('svelte/compiler').Warning & Error & {frame?: string}} error a svelte compiler error, which is a mix of Warning and an error + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {import('vite').Rollup.RollupError} the converted error + */ +export function toRollupError(error, options) { + const { filename, frame, start, code, name, stack } = error; + /** @type {import('vite').Rollup.RollupError} */ + const rollupError = { + name, // needed otherwise sveltekit coalesce_to_error turns it into a string + id: filename, + message: buildExtendedLogMessage(error), // include filename:line:column so that it's clickable + frame: formatFrameForVite(frame), + code, + stack: options.isBuild || options.isDebug || !frame ? stack : '' + }; + if (start) { + rollupError.loc = { + line: start.line, + column: start.column, + file: filename + }; + } + return rollupError; +} + +/** + * convert an error thrown by svelte.compile to an esbuild PartialMessage + * @param {import('svelte/compiler').Warning & Error & {frame?: string}} error a svelte compiler error, which is a mix of Warning and an error + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {import('esbuild').PartialMessage} the converted error + */ +export function toESBuildError(error, options) { + const { filename, frame, start, stack } = error; + /** @type {import('esbuild').PartialMessage} */ + const partialMessage = { + text: buildExtendedLogMessage(error) + }; + if (start) { + partialMessage.location = { + line: start.line, + column: start.column, + file: filename, + lineText: lineFromFrame(start.line, frame) // needed to get a meaningful error message on cli + }; + } + if (options.isBuild || options.isDebug || !frame) { + partialMessage.detail = stack; + } + return partialMessage; +} + +/** + * extract line with number from codeframe + * + * @param {number} lineNo + * @param {string} [frame] + * @returns {string} + */ +function lineFromFrame(lineNo, frame) { + if (!frame) { + return ''; + } + const lines = frame.split('\n'); + const errorLine = lines.find((line) => line.trimStart().startsWith(`${lineNo}: `)); + return errorLine ? errorLine.substring(errorLine.indexOf(': ') + 3) : ''; +} + +/** + * vite error overlay expects a specific format to show frames + * this reformats svelte frame (colon separated, less whitespace) + * to one that vite displays on overlay ( pipe separated, more whitespace) + * e.g. + * ``` + * 1: foo + * 2: bar; + * ^ + * 3: baz + * ``` + * to + * ``` + * 1 | foo + * 2 | bar; + * ^ + * 3 | baz + * ``` + * @see https://github.com/vitejs/vite/blob/96591bf9989529de839ba89958755eafe4c445ae/packages/vite/src/client/overlay.ts#L116 + * @param {string} [frame] + * @returns {string} + */ +function formatFrameForVite(frame) { + if (!frame) { + return ''; + } + return frame + .split('\n') + .map((line) => (line.match(/^\s+\^/) ? ' ' + line : ' ' + line.replace(':', ' | '))) + .join('\n'); +} + +/** + * + * @param {string} code the svelte error code + * @see https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/errors.js + * @returns {boolean} + */ +function couldBeFixedByCssPreprocessor(code) { + return code === 'expected_token' || code === 'unexpected_eof' || code?.startsWith('css_'); +} + +/** + * @param {import('svelte/compiler').Warning & Error} err a svelte compiler error, which is a mix of Warning and an error + * @param {string} originalCode + * @param {import('../public.d.ts').Options['preprocess']} [preprocessors] + */ +export function enhanceCompileError(err, originalCode, preprocessors) { + preprocessors = arraify(preprocessors ?? []); + + /** @type {string[]} */ + const additionalMessages = []; + + // Handle incorrect CSS preprocessor usage + if (couldBeFixedByCssPreprocessor(err.code)) { + // Reference from Svelte: https://github.com/sveltejs/svelte/blob/9926347ad9dbdd0f3324d5538e25dcb7f5e442f8/packages/svelte/src/compiler/preprocess/index.js#L257 + const styleRe = + /|'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g; + + let m; + while ((m = styleRe.exec(originalCode))) { + // Warn missing lang attribute + if (!m[1]?.includes('lang=')) { + additionalMessages.push('Did you forget to add a lang attribute to your style tag?'); + } + // Warn missing style preprocessor + if ( + preprocessors.every((p) => p.style == null || p.name === 'inject-scope-everything-rule') + ) { + const preprocessorType = m[1]?.match(/lang="(.+?)"/)?.[1] ?? 'style'; + additionalMessages.push( + `Did you forget to add a ${preprocessorType} preprocessor? See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/preprocess.md for more information.` + ); + } + } + } + + if (additionalMessages.length) { + err.message += '\n\n- ' + additionalMessages.join('\n- '); + } + + return err; +} + +/** + * @param {T | T[]} value + * @template T + */ +function arraify(value) { + return Array.isArray(value) ? value : [value]; +} diff --git a/js-plugins/svelte/src/utils/esbuild.js b/js-plugins/svelte/src/utils/esbuild.js new file mode 100644 index 0000000..5a1ca89 --- /dev/null +++ b/js-plugins/svelte/src/utils/esbuild.js @@ -0,0 +1,177 @@ +import { readFileSync } from 'node:fs'; +import * as svelte from 'svelte/compiler'; +import { log } from './log.js'; +import { toESBuildError } from './error.js'; +import { safeBase64Hash } from './hash.js'; +import { normalize } from './id.js'; + +/** + * @typedef {NonNullable} EsbuildOptions + * @typedef {NonNullable[number]} EsbuildPlugin + */ + +export const facadeEsbuildSveltePluginName = 'vite-plugin-svelte:facade'; +export const facadeEsbuildSvelteModulePluginName = 'vite-plugin-svelte-module:facade'; + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {EsbuildPlugin} + */ +export function esbuildSveltePlugin(options) { + return { + name: 'vite-plugin-svelte:optimize-svelte', + setup(build) { + // Skip in scanning phase as Vite already handles scanning Svelte files. + // Otherwise this would heavily slow down the scanning phase. + if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return; + + const filter = /\.svelte(?:\?.*)?$/; + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ + let statsCollection; + build.onStart(() => { + statsCollection = options.stats?.startCollection('prebundle library components', { + logResult: (c) => c.stats.length > 1 + }); + }); + build.onLoad({ filter }, async ({ path: filename }) => { + const code = readFileSync(filename, 'utf8'); + try { + const contents = await compileSvelte(options, { filename, code }, statsCollection); + return { contents }; + } catch (e) { + return { errors: [toESBuildError(e, options)] }; + } + }); + build.onEnd(() => { + statsCollection?.finish(); + }); + } + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @param {{ filename: string, code: string }} input + * @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection] + * @returns {Promise} + */ +async function compileSvelte(options, { filename, code }, statsCollection) { + let css = options.compilerOptions.css; + if (css !== 'injected') { + // TODO ideally we'd be able to externalize prebundled styles too, but for now always put them in the js + css = 'injected'; + } + /** @type {import('svelte/compiler').CompileOptions} */ + const compileOptions = { + dev: true, // default to dev: true because prebundling is only used in dev + ...options.compilerOptions, + css, + filename, + generate: 'client' + }; + + if (compileOptions.hmr && options.emitCss) { + const hash = `s-${safeBase64Hash(normalize(filename, options.root))}`; + compileOptions.cssHash = () => hash; + } + + let preprocessed; + + if (options.preprocess) { + try { + preprocessed = await svelte.preprocess(code, options.preprocess, { filename }); + } catch (e) { + e.message = `Error while preprocessing ${filename}${e.message ? ` - ${e.message}` : ''}`; + throw e; + } + if (preprocessed.map) compileOptions.sourcemap = preprocessed.map; + } + + const finalCode = preprocessed ? preprocessed.code : code; + + const dynamicCompileOptions = await options?.dynamicCompileOptions?.({ + filename, + code: finalCode, + compileOptions + }); + + if (dynamicCompileOptions && log.debug.enabled) { + log.debug( + `dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`, + undefined, + 'compile' + ); + } + + const finalCompileOptions = dynamicCompileOptions + ? { + ...compileOptions, + ...dynamicCompileOptions + } + : compileOptions; + const endStat = statsCollection?.start(filename); + const compiled = svelte.compile(finalCode, finalCompileOptions); + if (endStat) { + endStat(); + } + return compiled.js.map + ? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl() + : compiled.js.code; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {EsbuildPlugin} + */ +export function esbuildSvelteModulePlugin(options) { + return { + name: 'vite-plugin-svelte-module:optimize-svelte', + setup(build) { + // Skip in scanning phase as Vite already handles scanning Svelte files. + // Otherwise this would heavily slow down the scanning phase. + if (build.initialOptions.plugins?.some((v) => v.name === 'vite:dep-scan')) return; + + const filter = /\.svelte\.[jt]s(?:\?.*)?$/; + /** @type {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection | undefined} */ + let statsCollection; + build.onStart(() => { + statsCollection = options.stats?.startCollection('prebundle library modules', { + logResult: (c) => c.stats.length > 1 + }); + }); + build.onLoad({ filter }, async ({ path: filename }) => { + const code = readFileSync(filename, 'utf8'); + try { + const contents = await compileSvelteModule(options, { filename, code }, statsCollection); + return { contents }; + } catch (e) { + return { errors: [toESBuildError(e, options)] }; + } + }); + build.onEnd(() => { + statsCollection?.finish(); + }); + } + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @param {{ filename: string; code: string }} input + * @param {import('../types/vite-plugin-svelte-stats.d.ts').StatCollection} [statsCollection] + * @returns {Promise} + */ +async function compileSvelteModule(options, { filename, code }, statsCollection) { + const endStat = statsCollection?.start(filename); + const compiled = svelte.compileModule(code, { + dev: options.compilerOptions?.dev ?? true, // default to dev: true because prebundling is only used in dev + filename, + generate: 'client' + }); + if (endStat) { + endStat(); + } + return compiled.js.map + ? compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl() + : compiled.js.code; +} diff --git a/js-plugins/svelte/src/utils/hash.js b/js-plugins/svelte/src/utils/hash.js new file mode 100644 index 0000000..6d0dad2 --- /dev/null +++ b/js-plugins/svelte/src/utils/hash.js @@ -0,0 +1,43 @@ +import crypto from 'node:crypto'; + +const hashes = Object.create(null); + +//TODO shorter? +const hash_length = 12; + +/** + * replaces +/= in base64 output so they don't interfere + * + * @param {string} input + * @returns {string} base64 hash safe to use in any context + */ +export function safeBase64Hash(input) { + if (hashes[input]) { + return hashes[input]; + } + //TODO if performance really matters, use a faster one like xx-hash etc. + // should be evenly distributed because short input length and similarities in paths could cause collisions otherwise + // OR DON'T USE A HASH AT ALL, what about a simple counter? + const md5 = crypto.createHash('md5'); + md5.update(input); + const hash = toSafe(md5.digest('base64')).slice(0, hash_length); + hashes[input] = hash; + return hash; +} + +/** @type {Record} */ +const replacements = { + '+': '-', + '/': '_', + '=': '' +}; + +const replaceRE = new RegExp(`[${Object.keys(replacements).join('')}]`, 'g'); + +/** + * @param {string} base64 + * @returns {string} + */ +function toSafe(base64) { + return base64.replace(replaceRE, (x) => replacements[x]); +} diff --git a/js-plugins/svelte/src/utils/id.js b/js-plugins/svelte/src/utils/id.js new file mode 100644 index 0000000..cb420dd --- /dev/null +++ b/js-plugins/svelte/src/utils/id.js @@ -0,0 +1,252 @@ +import { createFilter, normalizePath } from 'vite'; +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { log } from './log.js'; +import { DEFAULT_SVELTE_MODULE_EXT, DEFAULT_SVELTE_MODULE_INFIX } from './constants.js'; + +const VITE_FS_PREFIX = '/@fs/'; +const IS_WINDOWS = process.platform === 'win32'; + +const SUPPORTED_COMPILER_OPTIONS = ['generate', 'dev', 'css', 'customElement', 'immutable']; +const TYPES_WITH_COMPILER_OPTIONS = ['style', 'script', 'all']; + +/** + * @param {string} id + * @returns {{ filename: string, rawQuery: string }} + */ +function splitId(id) { + const parts = id.split('?', 2); + const filename = parts[0]; + const rawQuery = parts[1]; + return { filename, rawQuery }; +} + +/** + * @param {string} id + * @param {string} filename + * @param {string} rawQuery + * @param {string} root + * @param {number} timestamp + * @param {boolean} ssr + * @returns {import('../types/id.d.ts').SvelteRequest | undefined} + */ +function parseToSvelteRequest(id, filename, rawQuery, root, timestamp, ssr) { + const query = parseRequestQuery(rawQuery); + const rawOrDirect = !!(query.raw || query.direct); + if (query.url || (!query.svelte && rawOrDirect)) { + // skip requests with special vite tags + return; + } + const raw = rawOrDirect; + const normalizedFilename = normalize(filename, root); + const cssId = createVirtualImportId(filename, root, 'style'); + + return { + id, + filename, + normalizedFilename, + cssId, + query, + timestamp, + ssr, + raw + }; +} + +/** + * @param {string} filename + * @param {string} root + * @param {import('../types/id.d.ts').SvelteQueryTypes} type + * @returns {string} + */ +function createVirtualImportId(filename, root, type) { + const parts = ['svelte', `type=${type}`]; + if (type === 'style') { + parts.push('lang.css'); + } + if (existsInRoot(filename, root)) { + filename = root + filename; + } else if (filename.startsWith(VITE_FS_PREFIX)) { + filename = IS_WINDOWS + ? filename.slice(VITE_FS_PREFIX.length) // remove /@fs/ from /@fs/C:/... + : filename.slice(VITE_FS_PREFIX.length - 1); // remove /@fs from /@fs/home/user + } + // return same virtual id format as vite-plugin-vue eg ...App.svelte?svelte&type=style&lang.css + return `${filename}?${parts.join('&')}`; +} + +/** + * @param {string} rawQuery + * @returns {import('../types/id.d.ts').RequestQuery} + */ +function parseRequestQuery(rawQuery) { + const query = Object.fromEntries(new URLSearchParams(rawQuery)); + for (const key in query) { + if (query[key] === '') { + // @ts-expect-error not boolean + query[key] = true; + } + } + const compilerOptions = query.compilerOptions; + if (compilerOptions) { + if (!((query.raw || query.direct) && TYPES_WITH_COMPILER_OPTIONS.includes(query.type))) { + throw new Error( + `Invalid compilerOptions in query ${rawQuery}. CompilerOptions are only supported for raw or direct queries with type in "${TYPES_WITH_COMPILER_OPTIONS.join( + ', ' + )}" e.g. '?svelte&raw&type=script&compilerOptions={"generate":"server","dev":false}` + ); + } + try { + const parsed = JSON.parse(compilerOptions); + const invalid = Object.keys(parsed).filter( + (key) => !SUPPORTED_COMPILER_OPTIONS.includes(key) + ); + if (invalid.length) { + throw new Error( + `Invalid compilerOptions in query ${rawQuery}: ${invalid.join( + ', ' + )}. Supported: ${SUPPORTED_COMPILER_OPTIONS.join(', ')}` + ); + } + query.compilerOptions = parsed; + } catch (e) { + log.error('failed to parse request query compilerOptions', e); + throw e; + } + } + + return /** @type {import('../types/id.d.ts').RequestQuery}*/ query; +} + +/** + * posixify and remove root at start + * + * @param {string} filename + * @param {string} normalizedRoot + * @returns {string} + */ +export function normalize(filename, normalizedRoot) { + return stripRoot(normalizePath(filename), normalizedRoot); +} + +/** + * @param {string} filename + * @param {string} root + * @returns {boolean} + */ +function existsInRoot(filename, root) { + if (filename.startsWith(VITE_FS_PREFIX)) { + return false; // vite already tagged it as out of root + } + return fs.existsSync(root + filename); +} + +/** + * @param {string} normalizedFilename + * @param {string} normalizedRoot + * @returns {string} + */ +function stripRoot(normalizedFilename, normalizedRoot) { + return normalizedFilename.startsWith(normalizedRoot + '/') + ? normalizedFilename.slice(normalizedRoot.length) + : normalizedFilename; +} + +/** + * @param {import('../public.d.ts').Options['include'] | undefined} include + * @param {import('../public.d.ts').Options['exclude'] | undefined} exclude + * @param {string[]} extensions + * @returns {(filename: string) => boolean} + */ +function buildFilter(include, exclude, extensions) { + const rollupFilter = createFilter(include, exclude); + return (filename) => rollupFilter(filename) && extensions.some((ext) => filename.endsWith(ext)); +} + +/** + * @param {import('../public.d.ts').Options['include'] | undefined} include + * @param {import('../public.d.ts').Options['exclude'] | undefined} exclude + * @param {string[]} infixes + * @param {string[]} extensions + * @returns {(filename: string) => boolean} + */ +function buildModuleFilter(include, exclude, infixes, extensions) { + const rollupFilter = createFilter(include, exclude); + return (filename) => { + const basename = path.basename(filename); + + return ( + rollupFilter(filename) && + infixes.some((infix) => basename.includes(infix)) && + extensions.some((ext) => basename.endsWith(ext)) + ); + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {import('../types/id.d.ts').IdParser} + */ +export function buildIdParser(options) { + const { include, exclude, extensions, root } = options; + const normalizedRoot = normalizePath(root); + const filter = buildFilter(include, exclude, extensions ?? []); + return (id, ssr, timestamp = Date.now()) => { + const { filename, rawQuery } = splitId(id); + if (filter(filename)) { + return parseToSvelteRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr); + } + }; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {import('../types/id.d.ts').ModuleIdParser} + */ +export function buildModuleIdParser(options) { + const { + include, + exclude, + infixes = DEFAULT_SVELTE_MODULE_INFIX, + extensions = DEFAULT_SVELTE_MODULE_EXT + } = options?.experimental?.compileModule ?? {}; + const root = options.root; + const normalizedRoot = normalizePath(root); + const filter = buildModuleFilter(include, exclude, infixes, extensions); + return (id, ssr, timestamp = Date.now()) => { + const { filename, rawQuery } = splitId(id); + if (filter(filename)) { + return parseToSvelteModuleRequest(id, filename, rawQuery, normalizedRoot, timestamp, ssr); + } + }; +} + +/** + * @param {string} id + * @param {string} filename + * @param {string} rawQuery + * @param {string} root + * @param {number} timestamp + * @param {boolean} ssr + * @returns {import('../types/id.d.ts').SvelteModuleRequest | undefined} + */ +function parseToSvelteModuleRequest(id, filename, rawQuery, root, timestamp, ssr) { + const query = parseRequestQuery(rawQuery); + + if (query.url || query.raw || query.direct) { + // skip requests with special vite tags + return; + } + + const normalizedFilename = normalize(filename, root); + + return { + id, + filename, + normalizedFilename, + query, + timestamp, + ssr + }; +} diff --git a/js-plugins/svelte/src/utils/load-raw.js b/js-plugins/svelte/src/utils/load-raw.js new file mode 100644 index 0000000..f800f1b --- /dev/null +++ b/js-plugins/svelte/src/utils/load-raw.js @@ -0,0 +1,125 @@ +import fs from 'node:fs'; +import { toRollupError } from './error.js'; +import { log } from './log.js'; + +/** + * utility function to compile ?raw and ?direct requests in load hook + * + * @param {import('../types/id.d.ts').SvelteRequest} svelteRequest + * @param {import('../types/compile.d.ts').CompileSvelte} compileSvelte + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {Promise} + */ +export async function loadRaw(svelteRequest, compileSvelte, options) { + const { id, filename, query } = svelteRequest; + + // raw svelte subrequest, compile on the fly and return requested subpart + let compileData; + const source = fs.readFileSync(filename, 'utf-8'); + try { + //avoid compileSvelte doing extra ssr stuff unless requested + svelteRequest.ssr = query.compilerOptions?.generate === 'server'; + compileData = await compileSvelte(svelteRequest, source, { + ...options, + // don't use dynamic vite-plugin-svelte defaults here to ensure stable result between ssr,dev and build + compilerOptions: { + dev: false, + css: 'external', + hmr: false, + ...svelteRequest.query.compilerOptions + }, + emitCss: true + }); + } catch (e) { + throw toRollupError(e, options); + } + let result; + if (query.type === 'style') { + result = compileData.compiled.css ?? { code: '', map: null }; + } else if (query.type === 'script') { + result = compileData.compiled.js; + } else if (query.type === 'preprocessed') { + result = compileData.preprocessed; + } else if (query.type === 'all' && query.raw) { + return allToRawExports(compileData, source); + } else { + throw new Error( + `invalid "type=${query.type}" in ${id}. supported are script, style, preprocessed, all` + ); + } + if (query.direct) { + const supportedDirectTypes = ['script', 'style']; + if (!supportedDirectTypes.includes(query.type)) { + throw new Error( + `invalid "type=${ + query.type + }" combined with direct in ${id}. supported are: ${supportedDirectTypes.join(', ')}` + ); + } + log.debug(`load returns direct result for ${id}`, undefined, 'load'); + let directOutput = result.code; + // @ts-expect-error might not be SourceMap but toUrl check should suffice + if (query.sourcemap && result.map?.toUrl) { + // @ts-expect-error toUrl might not exist + const map = `sourceMappingURL=${result.map.toUrl()}`; + if (query.type === 'style') { + directOutput += `\n\n/*# ${map} */\n`; + } else if (query.type === 'script') { + directOutput += `\n\n//# ${map}\n`; + } + } + return directOutput; + } else if (query.raw) { + log.debug(`load returns raw result for ${id}`, undefined, 'load'); + return toRawExports(result); + } else { + throw new Error(`invalid raw mode in ${id}, supported are raw, direct`); + } +} + +/** + * turn compileData and source into a flat list of raw exports + * + * @param {import('../types/compile.d.ts').CompileData} compileData + * @param {string} source + */ +function allToRawExports(compileData, source) { + // flatten CompileData + /** @type {Partial} */ + const exports = { + ...compileData, + ...compileData.compiled, + source + }; + delete exports.compiled; + delete exports.filename; // absolute path, remove to avoid it in output + return toRawExports(exports); +} + +/** + * turn object into raw exports. + * + * every prop is returned as a const export, and if prop 'code' exists it is additionally added as default export + * + * eg {'foo':'bar','code':'baz'} results in + * + * ```js + * export const code='baz' + * export const foo='bar' + * export default code + * ``` + * @param {object} object + * @returns {string} + */ +function toRawExports(object) { + let exports = + Object.entries(object) + .filter(([_key, value]) => typeof value !== 'function') // preprocess output has a toString function that's enumerable + .sort(([a], [b]) => (a < b ? -1 : a === b ? 0 : 1)) + .map(([key, value]) => `export const ${key}=${JSON.stringify(value)}`) + .join('\n') + '\n'; + if (Object.prototype.hasOwnProperty.call(object, 'code')) { + exports += 'export default code\n'; + } + return exports; +} diff --git a/js-plugins/svelte/src/utils/load-svelte-config.js b/js-plugins/svelte/src/utils/load-svelte-config.js new file mode 100644 index 0000000..1b32ba4 --- /dev/null +++ b/js-plugins/svelte/src/utils/load-svelte-config.js @@ -0,0 +1,122 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import process from 'node:process'; +import fs from 'node:fs'; +import { pathToFileURL } from 'node:url'; +import { log } from './log.js'; + +// used to require cjs config in esm. +// NOTE dynamic import() cjs technically works, but timestamp query cache bust +// have no effect, likely because it has another internal cache? +/** @type {NodeRequire}*/ +let esmRequire; + +export const knownSvelteConfigNames = [ + 'svelte.config.js', + 'svelte.config.cjs', + 'svelte.config.mjs' +]; + +/** + * @param {string} filePath + * @param {number} timestamp + */ +async function dynamicImportDefault(filePath, timestamp) { + return await import(filePath + '?t=' + timestamp).then((m) => m.default); +} + +/** + * @param {import('vite').UserConfig} [viteConfig] + * @param {Partial} [inlineOptions] + * @returns {Promise | undefined>} + */ +export async function loadSvelteConfig(viteConfig, inlineOptions) { + if (inlineOptions?.configFile === false) { + return; + } + const configFile = findConfigToLoad(viteConfig, inlineOptions); + if (configFile) { + let err; + // try to use dynamic import for svelte.config.js first + if (configFile.endsWith('.js') || configFile.endsWith('.mjs')) { + try { + const result = await dynamicImportDefault( + pathToFileURL(configFile).href, + fs.statSync(configFile).mtimeMs + ); + if (result != null) { + return { + ...result, + configFile + }; + } else { + throw new Error(`invalid export in ${configFile}`); + } + } catch (e) { + log.error(`failed to import config ${configFile}`, e); + err = e; + } + } + // cjs or error with dynamic import + if (!configFile.endsWith('.mjs')) { + try { + // identify which require function to use (esm and cjs mode) + const _require = import.meta.url + ? (esmRequire ?? (esmRequire = createRequire(import.meta.url))) + : // eslint-disable-next-line no-undef + require; + + // avoid loading cached version on reload + delete _require.cache[_require.resolve(configFile)]; + const result = _require(configFile); + if (result != null) { + return { + ...result, + configFile + }; + } else { + throw new Error(`invalid export in ${configFile}`); + } + } catch (e) { + log.error(`failed to require config ${configFile}`, e); + if (!err) { + err = e; + } + } + } + // failed to load existing config file + throw err; + } +} + +/** + * @param {import('vite').UserConfig | undefined} viteConfig + * @param {Partial | undefined} inlineOptions + * @returns {string | undefined} + */ +function findConfigToLoad(viteConfig, inlineOptions) { + const root = viteConfig?.root || process.cwd(); + if (inlineOptions?.configFile) { + const abolutePath = path.isAbsolute(inlineOptions.configFile) + ? inlineOptions.configFile + : path.resolve(root, inlineOptions.configFile); + if (!fs.existsSync(abolutePath)) { + throw new Error(`failed to find svelte config file ${abolutePath}.`); + } + return abolutePath; + } else { + const existingKnownConfigFiles = knownSvelteConfigNames + .map((candidate) => path.resolve(root, candidate)) + .filter((file) => fs.existsSync(file)); + if (existingKnownConfigFiles.length === 0) { + log.debug(`no svelte config found at ${root}`, undefined, 'config'); + return; + } else if (existingKnownConfigFiles.length > 1) { + log.warn( + `found more than one svelte config file, using ${existingKnownConfigFiles[0]}. you should only have one!`, + existingKnownConfigFiles + ); + } + return existingKnownConfigFiles[0]; + } +} diff --git a/js-plugins/svelte/src/utils/log.js b/js-plugins/svelte/src/utils/log.js new file mode 100644 index 0000000..0b43cff --- /dev/null +++ b/js-plugins/svelte/src/utils/log.js @@ -0,0 +1,277 @@ +/* eslint-disable no-console */ +import { cyan, red, yellow } from 'kleur/colors'; +import debug from 'debug'; + +/** @type {import('../types/log.d.ts').LogLevel[]} */ +const levels = ['debug', 'info', 'warn', 'error', 'silent']; +const prefix = 'vite-plugin-svelte'; +/** @type {Record} */ +const loggers = { + debug: { + log: debug(`${prefix}`), + enabled: false, + isDebug: true + }, + info: { + color: cyan, + log: console.log, + enabled: true + }, + warn: { + color: yellow, + log: console.warn, + enabled: true + }, + error: { + color: red, + log: console.error, + enabled: true + }, + silent: { + enabled: false + } +}; + +/** @type {import('../types/log.d.ts').LogLevel} */ +let _level = 'info'; +/** + * @param {import('../types/log.d.ts').LogLevel} level + * @returns {void} + */ +function setLevel(level) { + if (level === _level) { + return; + } + const levelIndex = levels.indexOf(level); + if (levelIndex > -1) { + _level = level; + for (let i = 0; i < levels.length; i++) { + loggers[levels[i]].enabled = i >= levelIndex; + } + } else { + _log(loggers.error, `invalid log level: ${level} `); + } +} + +/** + * @param {any} logger + * @param {string} message + * @param {any} [payload] + * @param {string} [namespace] + * @returns + */ +function _log(logger, message, payload, namespace) { + if (!logger.enabled) { + return; + } + if (logger.isDebug) { + let log = logger.log; + if (namespace) { + if (!isDebugNamespaceEnabled(namespace)) { + return; + } + log = logger.log.extend(namespace); + } + if (payload !== undefined) { + log(message, payload); + } else { + log(message); + } + } else { + logger.log( + logger.color( + `${new Date().toLocaleTimeString()} [${prefix}${ + namespace ? `:${namespace}` : '' + }] ${message}` + ) + ); + if (payload) { + logger.log(payload); + } + } +} + +/** + * @param {import('../types/log.d.ts').LogLevel} level + * @returns {import('../types/log.d.ts').LogFn} + */ +function createLogger(level) { + const logger = loggers[level]; + const logFn = /** @type {import('../types/log.d.ts').LogFn} */ (_log.bind(null, logger)); + /** @type {Set} */ + const logged = new Set(); + /** @type {import('../types/log.d.ts').SimpleLogFn} */ + const once = function (message, payload, namespace) { + if (!logger.enabled || logged.has(message)) { + return; + } + logged.add(message); + logFn.apply(null, [message, payload, namespace]); + }; + Object.defineProperty(logFn, 'enabled', { + get() { + return logger.enabled; + } + }); + Object.defineProperty(logFn, 'once', { + get() { + return once; + } + }); + return logFn; +} + +export const log = { + debug: createLogger('debug'), + info: createLogger('info'), + warn: createLogger('warn'), + error: createLogger('error'), + setLevel +}; + +/** + * @param {import('../types/id.d.ts').SvelteRequest | import('../types/id.d.ts').SvelteModuleRequest} svelteRequest + * @param {import('svelte/compiler').Warning[]} warnings + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +export function logCompilerWarnings(svelteRequest, warnings, options) { + const { emitCss, onwarn, isBuild } = options; + const sendViaWS = !isBuild && options.experimental?.sendWarningsToBrowser; + let warn = isBuild ? warnBuild : warnDev; + /** @type {import('svelte/compiler').Warning[]} */ + const handledByDefaultWarn = []; + const notIgnored = warnings?.filter((w) => !ignoreCompilerWarning(w, isBuild, emitCss)); + const extra = buildExtraWarnings(warnings, isBuild); + const allWarnings = [...notIgnored, ...extra]; + if (sendViaWS) { + const _warn = warn; + /** @type {(w: import('svelte/compiler').Warning) => void} */ + warn = (w) => { + handledByDefaultWarn.push(w); + _warn(w); + }; + } + allWarnings.forEach((warning) => { + if (onwarn) { + onwarn(warning, warn); + } else { + warn(warning); + } + }); + if (sendViaWS) { + /** @type {import('../types/log.d.ts').SvelteWarningsMessage} */ + const message = { + id: svelteRequest.id, + filename: svelteRequest.filename, + normalizedFilename: svelteRequest.normalizedFilename, + timestamp: svelteRequest.timestamp, + warnings: handledByDefaultWarn, // allWarnings filtered by warnings where onwarn did not call the default handler + allWarnings, // includes warnings filtered by onwarn and our extra vite plugin svelte warnings + rawWarnings: warnings // raw compiler output + }; + log.debug(`sending svelte:warnings message for ${svelteRequest.normalizedFilename}`); + options.server?.ws?.send('svelte:warnings', message); + } +} + +/** + * @param {import('svelte/compiler').Warning} warning + * @param {boolean} isBuild + * @param {boolean} [emitCss] + * @returns {boolean} + */ +function ignoreCompilerWarning(warning, isBuild, emitCss) { + return ( + (!emitCss && warning.code === 'css_unused_selector') || // same as rollup-plugin-svelte + (!isBuild && isNoScopableElementWarning(warning)) + ); +} + +/** + * + * @param {import('svelte/compiler').Warning} warning + * @returns {boolean} + */ +function isNoScopableElementWarning(warning) { + // see https://github.com/sveltejs/vite-plugin-svelte/issues/153 + return warning.code === 'css_unused_selector' && warning.message.includes('"*"'); +} + +/** + * + * @param {import('svelte/compiler').Warning[]} warnings + * @param {boolean} isBuild + * @returns {import('svelte/compiler').Warning[]} + */ +function buildExtraWarnings(warnings, isBuild) { + const extraWarnings = []; + if (!isBuild) { + const noScopableElementWarnings = warnings.filter((w) => isNoScopableElementWarning(w)); + if (noScopableElementWarnings.length > 0) { + // in case there are multiple, use last one as that is the one caused by our *{} rule + const noScopableElementWarning = + noScopableElementWarnings[noScopableElementWarnings.length - 1]; + extraWarnings.push({ + ...noScopableElementWarning, + code: 'vite-plugin-svelte-css-no-scopable-elements', + message: + "No scopable elements found in template. If you're using global styles in the style tag, you should move it into an external stylesheet file and import it in JS. See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/faq.md#where-should-i-put-my-global-styles." + }); + } + } + return extraWarnings; +} + +/** + * @param {import('svelte/compiler').Warning} w + */ +function warnDev(w) { + if (w.filename?.includes('node_modules')) { + if (isDebugNamespaceEnabled('node-modules-onwarn')) { + log.debug(buildExtendedLogMessage(w), undefined, 'node-modules-onwarn'); + } + } else if (log.info.enabled) { + log.info(buildExtendedLogMessage(w)); + } +} + +/** + * @param {import('svelte/compiler').Warning & {frame?: string}} w + */ +function warnBuild(w) { + if (w.filename?.includes('node_modules')) { + if (isDebugNamespaceEnabled('node-modules-onwarn')) { + log.debug(buildExtendedLogMessage(w), w.frame, 'node-modules-onwarn'); + } + } else if (log.warn.enabled) { + log.warn(buildExtendedLogMessage(w), w.frame); + } +} + +/** + * @param {import('svelte/compiler').Warning} w + */ +export function buildExtendedLogMessage(w) { + const parts = []; + if (w.filename) { + parts.push(w.filename); + } + if (w.start) { + parts.push(':', w.start.line, ':', w.start.column); + } + if (w.message) { + if (parts.length > 0) { + parts.push(' '); + } + parts.push(w.message); + } + return parts.join(''); +} + +/** + * @param {string} namespace + * @returns {boolean} + */ +export function isDebugNamespaceEnabled(namespace) { + return debug.enabled(`${prefix}:${namespace}`); +} diff --git a/js-plugins/svelte/src/utils/optimizer.js b/js-plugins/svelte/src/utils/optimizer.js new file mode 100644 index 0000000..71aa571 --- /dev/null +++ b/js-plugins/svelte/src/utils/optimizer.js @@ -0,0 +1,53 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +// List of options that changes the prebundling result +/** @type {(keyof import('../types/options.d.ts').ResolvedOptions)[]} */ +const PREBUNDLE_SENSITIVE_OPTIONS = [ + 'compilerOptions', + 'configFile', + 'experimental', + 'extensions', + 'ignorePluginPreprocessors', + 'preprocess' +]; + +/** + * @param {string} cacheDir + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {Promise} Whether the Svelte metadata has changed + */ +export async function saveSvelteMetadata(cacheDir, options) { + const svelteMetadata = generateSvelteMetadata(options); + const svelteMetadataPath = path.resolve(cacheDir, '_svelte_metadata.json'); + + const currentSvelteMetadata = JSON.stringify(svelteMetadata, (_, value) => { + // Handle preprocessors + return typeof value === 'function' ? value.toString() : value; + }); + + /** @type {string | undefined} */ + let existingSvelteMetadata; + try { + existingSvelteMetadata = await fs.readFile(svelteMetadataPath, 'utf8'); + } catch { + // ignore + } + + await fs.mkdir(cacheDir, { recursive: true }); + await fs.writeFile(svelteMetadataPath, currentSvelteMetadata); + return currentSvelteMetadata !== existingSvelteMetadata; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @returns {Partial} + */ +function generateSvelteMetadata(options) { + /** @type {Record} */ + const metadata = {}; + for (const key of PREBUNDLE_SENSITIVE_OPTIONS) { + metadata[key] = options[key]; + } + return metadata; +} diff --git a/js-plugins/svelte/src/utils/options.js b/js-plugins/svelte/src/utils/options.js new file mode 100644 index 0000000..3206f50 --- /dev/null +++ b/js-plugins/svelte/src/utils/options.js @@ -0,0 +1,467 @@ +import process from "node:process"; +import { + defaultClientMainFields, + defaultServerMainFields, + defaultClientConditions, + defaultServerConditions, + normalizePath, +} from "vite"; +import { isDebugNamespaceEnabled, log } from "./log.js"; +import { loadSvelteConfig } from "./load-svelte-config.js"; +import { + DEFAULT_SVELTE_EXT, + FAQ_LINK_MISSING_EXPORTS_CONDITION, + SVELTE_EXPORT_CONDITIONS, + SVELTE_IMPORTS, +} from "./constants.js"; + +import path from "node:path"; +import { + esbuildSvelteModulePlugin, + esbuildSveltePlugin, + facadeEsbuildSvelteModulePluginName, + facadeEsbuildSveltePluginName, +} from "./esbuild.js"; +import { addExtraPreprocessors } from "./preprocess.js"; +import deepmerge from "deepmerge"; +import { + crawlFrameworkPkgs, + isDepExcluded, + isDepExternaled, + isDepIncluded, + isDepNoExternaled, +} from "vitefu"; + +import { isCommonDepWithoutSvelteField } from "./dependencies.js"; +import { VitePluginSvelteStats } from "./vite-plugin-svelte-stats.js"; + +const allowedPluginOptions = new Set([ + "include", + "exclude", + "emitCss", + "hot", + "ignorePluginPreprocessors", + "disableDependencyReinclusion", + "prebundleSvelteLibraries", + "inspector", + "dynamicCompileOptions", + "experimental", +]); + +const knownRootOptions = new Set([ + "extensions", + "compilerOptions", + "preprocess", + "onwarn", +]); + +const allowedInlineOptions = new Set([ + "configFile", + ...allowedPluginOptions, + ...knownRootOptions, +]); + +/** + * @param {Partial} [inlineOptions] + */ +export function validateInlineOptions(inlineOptions) { + const invalidKeys = Object.keys(inlineOptions || {}).filter( + (key) => !allowedInlineOptions.has(key) + ); + if (invalidKeys.length) { + log.warn( + `invalid plugin options "${invalidKeys.join(", ")}" in inline config`, + inlineOptions + ); + } +} + +/** + * @param {Partial} [config] + * @returns {Partial | undefined} + */ +function convertPluginOptions(config) { + if (!config) { + return; + } + const invalidRootOptions = Object.keys(config).filter((key) => + allowedPluginOptions.has(key) + ); + if (invalidRootOptions.length > 0) { + throw new Error( + `Invalid options in svelte config. Move the following options into 'vitePlugin:{...}': ${invalidRootOptions.join( + ", " + )}` + ); + } + if (!config.vitePlugin) { + return config; + } + const pluginOptions = config.vitePlugin; + const pluginOptionKeys = Object.keys(pluginOptions); + + const rootOptionsInPluginOptions = pluginOptionKeys.filter((key) => + knownRootOptions.has(key) + ); + if (rootOptionsInPluginOptions.length > 0) { + throw new Error( + `Invalid options in svelte config under vitePlugin:{...}', move them to the config root : ${rootOptionsInPluginOptions.join( + ", " + )}` + ); + } + const duplicateOptions = pluginOptionKeys.filter((key) => + Object.prototype.hasOwnProperty.call(config, key) + ); + if (duplicateOptions.length > 0) { + throw new Error( + `Invalid duplicate options in svelte config under vitePlugin:{...}', they are defined in root too and must only exist once: ${duplicateOptions.join( + ", " + )}` + ); + } + const unknownPluginOptions = pluginOptionKeys.filter( + (key) => !allowedPluginOptions.has(key) + ); + if (unknownPluginOptions.length > 0) { + log.warn( + `ignoring unknown plugin options in svelte config under vitePlugin:{...}: ${unknownPluginOptions.join( + ", " + )}` + ); + unknownPluginOptions.forEach((unkownOption) => { + // @ts-expect-error not typed + delete pluginOptions[unkownOption]; + }); + } + /** @type {import('../public.d.ts').Options} */ + const result = { + ...config, + ...pluginOptions, + }; + // @ts-expect-error it exists + delete result.vitePlugin; + + return result; +} + +/** + * used in config phase, merges the default options, svelte config, and inline options + * @param {Partial | undefined} inlineOptions + * @param {import('vite').UserConfig} viteUserConfig + * @param {import('vite').ConfigEnv} viteEnv + * @returns {Promise} + */ +export async function preResolveOptions( + inlineOptions, + viteUserConfig, + viteEnv +) { + if (!inlineOptions) { + inlineOptions = {}; + } + /** @type {import('vite').UserConfig} */ + const viteConfigWithResolvedRoot = { + ...viteUserConfig, + root: resolveViteRoot(viteUserConfig), + }; + const isBuild = viteEnv.command === "build"; + /** @type {Partial} */ + const defaultOptions = { + extensions: DEFAULT_SVELTE_EXT, + emitCss: true, + prebundleSvelteLibraries: !isBuild, + }; + const svelteConfig = convertPluginOptions( + await loadSvelteConfig(viteConfigWithResolvedRoot, inlineOptions) + ); + /** @type {Partial} */ + const extraOptions = { + root: viteConfigWithResolvedRoot.root, + isBuild, + isServe: viteEnv.command === "serve", + isDebug: process.env.DEBUG != null, + }; + + const merged = + /** @type {import('../types/options.d.ts').PreResolvedOptions} */ ( + mergeConfigs(defaultOptions, svelteConfig, inlineOptions, extraOptions) + ); + // configFile of svelteConfig contains the absolute path it was loaded from, + // prefer it over the possibly relative inline path + if (svelteConfig?.configFile) { + merged.configFile = svelteConfig.configFile; + } + return merged; +} + +/** + * @template T + * @param {(Partial | undefined)[]} configs + * @returns T + */ +function mergeConfigs(...configs) { + /** @type {Partial} */ + let result = {}; + for (const config of configs.filter((x) => x != null)) { + result = deepmerge(result, /** @type {Partial} */ (config), { + // replace arrays + arrayMerge: (target, source) => source ?? target, + }); + } + return /** @type {T} */ result; +} + +/** + * used in configResolved phase, merges a contextual default config, pre-resolved options, and some preprocessors. also validates the final config. + * + * @param {import('../types/options.d.ts').PreResolvedOptions} preResolveOptions + * @param {import('vite').ResolvedConfig} viteConfig + * @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache + * @returns {import('../types/options.d.ts').ResolvedOptions} + */ +export function resolveOptions(preResolveOptions, viteConfig, cache) { + const css = preResolveOptions.emitCss ? "external" : "injected"; + /** @type {Partial} */ + const defaultOptions = { + compilerOptions: { + css, + dev: !viteConfig.isProduction, + hmr: + !viteConfig.isProduction && + !preResolveOptions.isBuild && + viteConfig.server && + viteConfig.server.hmr !== false, + }, + }; + + /** @type {Partial} */ + const extraOptions = { + root: viteConfig.root, + isProduction: viteConfig.isProduction, + }; + const merged = /** @type {import('../types/options.d.ts').ResolvedOptions}*/ ( + mergeConfigs(defaultOptions, preResolveOptions, extraOptions) + ); + + removeIgnoredOptions(merged); + handleDeprecatedOptions(merged); + addExtraPreprocessors(merged, viteConfig); + enforceOptionsForHmr(merged, viteConfig); + enforceOptionsForProduction(merged); + // mergeConfigs would mangle functions on the stats class, so do this afterwards + if (log.debug.enabled && isDebugNamespaceEnabled("stats")) { + merged.stats = new VitePluginSvelteStats(cache); + } + return merged; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @param {import('vite').ResolvedConfig} viteConfig + */ +function enforceOptionsForHmr(options, viteConfig) { + if (options.hot) { + log.warn( + "svelte 5 has hmr integrated in core. Please remove the vitePlugin.hot option and use compilerOptions.hmr instead" + ); + delete options.hot; + options.compilerOptions.hmr = true; + } + if (options.compilerOptions.hmr && viteConfig.server?.hmr === false) { + log.warn( + "vite config server.hmr is false but compilerOptions.hmr is true. Forcing compilerOptions.hmr to false as it would not work." + ); + options.compilerOptions.hmr = false; + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function enforceOptionsForProduction(options) { + if (options.isProduction) { + if (options.compilerOptions.hmr) { + log.warn( + "you are building for production but compilerOptions.hmr is true, forcing it to false" + ); + options.compilerOptions.hmr = false; + } + if (options.compilerOptions.dev) { + log.warn( + "you are building for production but compilerOptions.dev is true, forcing it to false" + ); + options.compilerOptions.dev = false; + } + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function removeIgnoredOptions(options) { + const ignoredCompilerOptions = ["generate", "format", "filename"]; + if (options.compilerOptions.hmr && options.emitCss) { + ignoredCompilerOptions.push("cssHash"); + } + const passedCompilerOptions = Object.keys(options.compilerOptions || {}); + const passedIgnored = passedCompilerOptions.filter((o) => + ignoredCompilerOptions.includes(o) + ); + if (passedIgnored.length) { + log.warn( + `The following Svelte compilerOptions are controlled by vite-plugin-svelte and essential to its functionality. User-specified values are ignored. Please remove them from your configuration: ${passedIgnored.join( + ", " + )}` + ); + passedIgnored.forEach((ignored) => { + // @ts-expect-error string access + delete options.compilerOptions[ignored]; + }); + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function handleDeprecatedOptions(options) { + const experimental = /** @type {Record} */ ( + options.experimental + ); + if (experimental) { + for (const promoted of [ + "prebundleSvelteLibraries", + "inspector", + "dynamicCompileOptions", + ]) { + if (experimental[promoted]) { + //@ts-expect-error untyped assign + options[promoted] = experimental[promoted]; + delete experimental[promoted]; + log.warn( + `Option "experimental.${promoted}" is no longer experimental and has moved to "${promoted}". Please update your Svelte or Vite config.` + ); + } + } + if (experimental.generateMissingPreprocessorSourcemaps) { + log.warn( + "experimental.generateMissingPreprocessorSourcemaps has been removed." + ); + } + } +} + +/** + * vite passes unresolved `root`option to config hook but we need the resolved value, so do it here + * + * @see https://github.com/sveltejs/vite-plugin-svelte/issues/113 + * @see https://github.com/vitejs/vite/blob/43c957de8a99bb326afd732c962f42127b0a4d1e/packages/vite/src/node/config.ts#L293 + * + * @param {import('vite').UserConfig} viteConfig + * @returns {string | undefined} + */ +function resolveViteRoot(viteConfig) { + return normalizePath( + viteConfig.root ? path.resolve(viteConfig.root) : process.cwd() + ); +} + +export async function buildExtraFarmConfig() { + const extraFarmConfig = { + resolve: { + dedupe: [...SVELTE_IMPORTS], + mainFields: ["svelte"], + conditions: ["svelte"], + }, + }; + return { + compilation: { + ...extraFarmConfig, + }, + }; +} + +/** + * @param {import('vite').ResolvedConfig} viteConfig + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +export function patchResolvedViteConfig(viteConfig, options) { + if (options.preprocess) { + for (const preprocessor of arraify(options.preprocess)) { + if (preprocessor.style && "__resolvedConfig" in preprocessor.style) { + preprocessor.style.__resolvedConfig = viteConfig; + } + } + } + + // replace facade esbuild plugin with a real one + const facadeEsbuildSveltePlugin = + viteConfig.optimizeDeps.esbuildOptions?.plugins?.find( + (plugin) => plugin.name === facadeEsbuildSveltePluginName + ); + if (facadeEsbuildSveltePlugin) { + Object.assign(facadeEsbuildSveltePlugin, esbuildSveltePlugin(options)); + } + const facadeEsbuildSvelteModulePlugin = + viteConfig.optimizeDeps.esbuildOptions?.plugins?.find( + (plugin) => plugin.name === facadeEsbuildSvelteModulePluginName + ); + if (facadeEsbuildSvelteModulePlugin) { + Object.assign( + facadeEsbuildSvelteModulePlugin, + esbuildSvelteModulePlugin(options) + ); + } +} + +/** + * Mutates `config` to ensure `resolve.mainFields` is set. If unset, it emulates Vite's default fallback. + * @param {string} name + * @param {import('vite').EnvironmentOptions} config + * @param {{ isSsrTargetWebworker?: boolean }} opts + */ +export function ensureConfigEnvironmentMainFields(name, config, opts) { + config.resolve ??= {}; + if (config.resolve.mainFields == null) { + if ( + config.consumer === "client" || + name === "client" || + opts.isSsrTargetWebworker + ) { + config.resolve.mainFields = [...defaultClientMainFields]; + } else { + config.resolve.mainFields = [...defaultServerMainFields]; + } + } + return true; +} + +/** + * Mutates `config` to ensure `resolve.conditions` is set. If unset, it emulates Vite's default fallback. + * @param {string} name + * @param {import('vite').EnvironmentOptions} config + * @param {{ isSsrTargetWebworker?: boolean }} opts + */ +export function ensureConfigEnvironmentConditions(name, config, opts) { + config.resolve ??= {}; + if (config.resolve.conditions == null) { + if ( + config.consumer === "client" || + name === "client" || + opts.isSsrTargetWebworker + ) { + config.resolve.conditions = [...defaultClientConditions]; + } else { + config.resolve.conditions = [...defaultServerConditions]; + } + } +} + +/** + * @template T + * @param {T | T[]} value + * @returns {T[]} + */ +function arraify(value) { + return Array.isArray(value) ? value : [value]; +} diff --git a/js-plugins/svelte/src/utils/options2.js b/js-plugins/svelte/src/utils/options2.js new file mode 100644 index 0000000..6582e6e --- /dev/null +++ b/js-plugins/svelte/src/utils/options2.js @@ -0,0 +1,653 @@ +import process from 'node:process'; +import { + defaultClientMainFields, + defaultServerMainFields, + defaultClientConditions, + defaultServerConditions, + normalizePath +} from 'vite'; +import { isDebugNamespaceEnabled, log } from './log.js'; +import { loadSvelteConfig } from './load-svelte-config.js'; +import { + DEFAULT_SVELTE_EXT, + FAQ_LINK_MISSING_EXPORTS_CONDITION, + SVELTE_EXPORT_CONDITIONS, + SVELTE_IMPORTS +} from './constants.js'; + +import path from 'node:path'; +import { + esbuildSvelteModulePlugin, + esbuildSveltePlugin, + facadeEsbuildSvelteModulePluginName, + facadeEsbuildSveltePluginName +} from './esbuild.js'; +import { addExtraPreprocessors } from './preprocess.js'; +import deepmerge from 'deepmerge'; +import { + crawlFrameworkPkgs, + isDepExcluded, + isDepExternaled, + isDepIncluded, + isDepNoExternaled +} from 'vitefu'; + +import { isCommonDepWithoutSvelteField } from './dependencies.js'; +import { VitePluginSvelteStats } from './vite-plugin-svelte-stats.js'; + +const allowedPluginOptions = new Set([ + 'include', + 'exclude', + 'emitCss', + 'hot', + 'ignorePluginPreprocessors', + 'disableDependencyReinclusion', + 'prebundleSvelteLibraries', + 'inspector', + 'dynamicCompileOptions', + 'experimental' +]); + +const knownRootOptions = new Set(['extensions', 'compilerOptions', 'preprocess', 'onwarn']); + +const allowedInlineOptions = new Set(['configFile', ...allowedPluginOptions, ...knownRootOptions]); + +/** + * @param {Partial} [inlineOptions] + */ +export function validateInlineOptions(inlineOptions) { + const invalidKeys = Object.keys(inlineOptions || {}).filter( + (key) => !allowedInlineOptions.has(key) + ); + if (invalidKeys.length) { + log.warn(`invalid plugin options "${invalidKeys.join(', ')}" in inline config`, inlineOptions); + } +} + +/** + * @param {Partial} [config] + * @returns {Partial | undefined} + */ +function convertPluginOptions(config) { + if (!config) { + return; + } + const invalidRootOptions = Object.keys(config).filter((key) => allowedPluginOptions.has(key)); + if (invalidRootOptions.length > 0) { + throw new Error( + `Invalid options in svelte config. Move the following options into 'vitePlugin:{...}': ${invalidRootOptions.join( + ', ' + )}` + ); + } + if (!config.vitePlugin) { + return config; + } + const pluginOptions = config.vitePlugin; + const pluginOptionKeys = Object.keys(pluginOptions); + + const rootOptionsInPluginOptions = pluginOptionKeys.filter((key) => knownRootOptions.has(key)); + if (rootOptionsInPluginOptions.length > 0) { + throw new Error( + `Invalid options in svelte config under vitePlugin:{...}', move them to the config root : ${rootOptionsInPluginOptions.join( + ', ' + )}` + ); + } + const duplicateOptions = pluginOptionKeys.filter((key) => + Object.prototype.hasOwnProperty.call(config, key) + ); + if (duplicateOptions.length > 0) { + throw new Error( + `Invalid duplicate options in svelte config under vitePlugin:{...}', they are defined in root too and must only exist once: ${duplicateOptions.join( + ', ' + )}` + ); + } + const unknownPluginOptions = pluginOptionKeys.filter((key) => !allowedPluginOptions.has(key)); + if (unknownPluginOptions.length > 0) { + log.warn( + `ignoring unknown plugin options in svelte config under vitePlugin:{...}: ${unknownPluginOptions.join( + ', ' + )}` + ); + unknownPluginOptions.forEach((unkownOption) => { + // @ts-expect-error not typed + delete pluginOptions[unkownOption]; + }); + } + /** @type {import('../public.d.ts').Options} */ + const result = { + ...config, + ...pluginOptions + }; + // @ts-expect-error it exists + delete result.vitePlugin; + + return result; +} + +/** + * used in config phase, merges the default options, svelte config, and inline options + * @param {Partial | undefined} inlineOptions + * @param {import('vite').UserConfig} viteUserConfig + * @param {import('vite').ConfigEnv} viteEnv + * @returns {Promise} + */ +export async function preResolveOptions(inlineOptions, viteUserConfig, viteEnv) { + if (!inlineOptions) { + inlineOptions = {}; + } + /** @type {import('vite').UserConfig} */ + const viteConfigWithResolvedRoot = { + ...viteUserConfig, + root: resolveViteRoot(viteUserConfig) + }; + const isBuild = viteEnv.command === 'build'; + /** @type {Partial} */ + const defaultOptions = { + extensions: DEFAULT_SVELTE_EXT, + emitCss: true, + prebundleSvelteLibraries: !isBuild + }; + const svelteConfig = convertPluginOptions( + await loadSvelteConfig(viteConfigWithResolvedRoot, inlineOptions) + ); + /** @type {Partial} */ + const extraOptions = { + root: viteConfigWithResolvedRoot.root, + isBuild, + isServe: viteEnv.command === 'serve', + isDebug: process.env.DEBUG != null + }; + + const merged = /** @type {import('../types/options.d.ts').PreResolvedOptions} */ ( + mergeConfigs(defaultOptions, svelteConfig, inlineOptions, extraOptions) + ); + // configFile of svelteConfig contains the absolute path it was loaded from, + // prefer it over the possibly relative inline path + if (svelteConfig?.configFile) { + merged.configFile = svelteConfig.configFile; + } + return merged; +} + +/** + * @template T + * @param {(Partial | undefined)[]} configs + * @returns T + */ +function mergeConfigs(...configs) { + /** @type {Partial} */ + let result = {}; + for (const config of configs.filter((x) => x != null)) { + result = deepmerge(result, /** @type {Partial} */ (config), { + // replace arrays + arrayMerge: (target, source) => source ?? target + }); + } + return /** @type {T} */ result; +} + +/** + * used in configResolved phase, merges a contextual default config, pre-resolved options, and some preprocessors. also validates the final config. + * + * @param {import('../types/options.d.ts').PreResolvedOptions} preResolveOptions + * @param {import('vite').ResolvedConfig} viteConfig + * @param {import('./vite-plugin-svelte-cache.js').VitePluginSvelteCache} cache + * @returns {import('../types/options.d.ts').ResolvedOptions} + */ +export function resolveOptions(preResolveOptions, viteConfig, cache) { + const css = preResolveOptions.emitCss ? 'external' : 'injected'; + /** @type {Partial} */ + const defaultOptions = { + compilerOptions: { + css, + dev: !viteConfig.isProduction, + hmr: + !viteConfig.isProduction && + !preResolveOptions.isBuild && + viteConfig.server && + viteConfig.server.hmr !== false + } + }; + + /** @type {Partial} */ + const extraOptions = { + root: viteConfig.root, + isProduction: viteConfig.isProduction + }; + const merged = /** @type {import('../types/options.d.ts').ResolvedOptions}*/ ( + mergeConfigs(defaultOptions, preResolveOptions, extraOptions) + ); + + removeIgnoredOptions(merged); + handleDeprecatedOptions(merged); + addExtraPreprocessors(merged, viteConfig); + enforceOptionsForHmr(merged, viteConfig); + enforceOptionsForProduction(merged); + // mergeConfigs would mangle functions on the stats class, so do this afterwards + if (log.debug.enabled && isDebugNamespaceEnabled('stats')) { + merged.stats = new VitePluginSvelteStats(cache); + } + return merged; +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + * @param {import('vite').ResolvedConfig} viteConfig + */ +function enforceOptionsForHmr(options, viteConfig) { + if (options.hot) { + log.warn( + 'svelte 5 has hmr integrated in core. Please remove the vitePlugin.hot option and use compilerOptions.hmr instead' + ); + delete options.hot; + options.compilerOptions.hmr = true; + } + if (options.compilerOptions.hmr && viteConfig.server?.hmr === false) { + log.warn( + 'vite config server.hmr is false but compilerOptions.hmr is true. Forcing compilerOptions.hmr to false as it would not work.' + ); + options.compilerOptions.hmr = false; + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function enforceOptionsForProduction(options) { + if (options.isProduction) { + if (options.compilerOptions.hmr) { + log.warn( + 'you are building for production but compilerOptions.hmr is true, forcing it to false' + ); + options.compilerOptions.hmr = false; + } + if (options.compilerOptions.dev) { + log.warn( + 'you are building for production but compilerOptions.dev is true, forcing it to false' + ); + options.compilerOptions.dev = false; + } + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function removeIgnoredOptions(options) { + const ignoredCompilerOptions = ['generate', 'format', 'filename']; + if (options.compilerOptions.hmr && options.emitCss) { + ignoredCompilerOptions.push('cssHash'); + } + const passedCompilerOptions = Object.keys(options.compilerOptions || {}); + const passedIgnored = passedCompilerOptions.filter((o) => ignoredCompilerOptions.includes(o)); + if (passedIgnored.length) { + log.warn( + `The following Svelte compilerOptions are controlled by vite-plugin-svelte and essential to its functionality. User-specified values are ignored. Please remove them from your configuration: ${passedIgnored.join( + ', ' + )}` + ); + passedIgnored.forEach((ignored) => { + // @ts-expect-error string access + delete options.compilerOptions[ignored]; + }); + } +} + +/** + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +function handleDeprecatedOptions(options) { + const experimental = /** @type {Record} */ (options.experimental); + if (experimental) { + for (const promoted of ['prebundleSvelteLibraries', 'inspector', 'dynamicCompileOptions']) { + if (experimental[promoted]) { + //@ts-expect-error untyped assign + options[promoted] = experimental[promoted]; + delete experimental[promoted]; + log.warn( + `Option "experimental.${promoted}" is no longer experimental and has moved to "${promoted}". Please update your Svelte or Vite config.` + ); + } + } + if (experimental.generateMissingPreprocessorSourcemaps) { + log.warn('experimental.generateMissingPreprocessorSourcemaps has been removed.'); + } + } +} + +/** + * vite passes unresolved `root`option to config hook but we need the resolved value, so do it here + * + * @see https://github.com/sveltejs/vite-plugin-svelte/issues/113 + * @see https://github.com/vitejs/vite/blob/43c957de8a99bb326afd732c962f42127b0a4d1e/packages/vite/src/node/config.ts#L293 + * + * @param {import('vite').UserConfig} viteConfig + * @returns {string | undefined} + */ +function resolveViteRoot(viteConfig) { + return normalizePath(viteConfig.root ? path.resolve(viteConfig.root) : process.cwd()); +} + +/** + * @param {import('../types/options.d.ts').PreResolvedOptions} options + * @param {import('vite').UserConfig} config + * @returns {Promise>} + */ +export async function buildExtraViteConfig(options, config) { + /** @type {Partial} */ + const extraViteConfig = { + resolve: { + dedupe: [...SVELTE_IMPORTS] + } + // this option is still awaiting a PR in vite to be supported + // see https://github.com/sveltejs/vite-plugin-svelte/issues/60 + // knownJsSrcExtensions: options.extensions + }; + + const extraSvelteConfig = buildExtraConfigForSvelte(config); + const extraDepsConfig = await buildExtraConfigForDependencies(options, config); + // merge extra svelte and deps config, but make sure dep values are not contradicting svelte + extraViteConfig.optimizeDeps = { + include: [ + ...extraSvelteConfig.optimizeDeps.include, + ...extraDepsConfig.optimizeDeps.include.filter( + (dep) => !isDepExcluded(dep, extraSvelteConfig.optimizeDeps.exclude) + ) + ], + exclude: [ + ...extraSvelteConfig.optimizeDeps.exclude, + ...extraDepsConfig.optimizeDeps.exclude.filter( + (dep) => !isDepIncluded(dep, extraSvelteConfig.optimizeDeps.include) + ) + ] + }; + + extraViteConfig.ssr = { + external: [ + ...extraSvelteConfig.ssr.external, + ...extraDepsConfig.ssr.external.filter( + (dep) => !isDepNoExternaled(dep, extraSvelteConfig.ssr.noExternal) + ) + ], + noExternal: [ + ...extraSvelteConfig.ssr.noExternal, + ...extraDepsConfig.ssr.noExternal.filter( + (dep) => !isDepExternaled(dep, extraSvelteConfig.ssr.external) + ) + ] + }; + + // handle prebundling for svelte files + if (options.prebundleSvelteLibraries) { + extraViteConfig.optimizeDeps = { + ...extraViteConfig.optimizeDeps, + // Experimental Vite API to allow these extensions to be scanned and prebundled + extensions: options.extensions ?? ['.svelte'], + // Add esbuild plugin to prebundle Svelte files. + // Currently a placeholder as more information is needed after Vite config is resolved, + // the real Svelte plugin is added in `patchResolvedViteConfig()` + esbuildOptions: { + plugins: [ + { name: facadeEsbuildSveltePluginName, setup: () => {} }, + { name: facadeEsbuildSvelteModulePluginName, setup: () => {} } + ] + } + }; + } + + // enable hmrPartialAccept if not explicitly disabled + if (config.experimental?.hmrPartialAccept !== false) { + log.debug('enabling "experimental.hmrPartialAccept" in vite config', undefined, 'config'); + extraViteConfig.experimental = { hmrPartialAccept: true }; + } + validateViteConfig(extraViteConfig, config, options); + return extraViteConfig; +} + +/** + * @param {Partial} extraViteConfig + * @param {import('vite').UserConfig} config + * @param {import('../types/options.d.ts').PreResolvedOptions} options + */ +function validateViteConfig(extraViteConfig, config, options) { + const { prebundleSvelteLibraries, isBuild } = options; + if (prebundleSvelteLibraries) { + /** @type {(option: 'dev' | 'build' | boolean)=> boolean} */ + const isEnabled = (option) => option !== true && option !== (isBuild ? 'build' : 'dev'); + /** @type {(name: string, value: 'dev' | 'build' | boolean, recommendation: string)=> void} */ + const logWarning = (name, value, recommendation) => + log.warn.once( + `Incompatible options: \`prebundleSvelteLibraries: true\` and vite \`${name}: ${JSON.stringify( + value + )}\` ${isBuild ? 'during build.' : '.'} ${recommendation}` + ); + const viteOptimizeDepsDisabled = config.optimizeDeps?.disabled ?? 'build'; // fall back to vite default + const isOptimizeDepsEnabled = isEnabled(viteOptimizeDepsDisabled); + if (!isBuild && !isOptimizeDepsEnabled) { + logWarning( + 'optimizeDeps.disabled', + viteOptimizeDepsDisabled, + 'Forcing `optimizeDeps.disabled: "build"`. Disable prebundleSvelteLibraries or update your vite config to enable optimizeDeps during dev.' + ); + if (!extraViteConfig.optimizeDeps) { + extraViteConfig.optimizeDeps = {}; + } + extraViteConfig.optimizeDeps.disabled = 'build'; + } else if (isBuild && isOptimizeDepsEnabled) { + logWarning( + 'optimizeDeps.disabled', + viteOptimizeDepsDisabled, + 'Disable optimizeDeps or prebundleSvelteLibraries for build if you experience errors.' + ); + } + } +} + +/** + * @param {import('../types/options.d.ts').PreResolvedOptions} options + * @param {import('vite').UserConfig} config + * @returns {Promise} + */ +async function buildExtraConfigForDependencies(options, config) { + // extra handling for svelte dependencies in the project + const packagesWithoutSvelteExportsCondition = new Set(); + const depsConfig = await crawlFrameworkPkgs({ + root: options.root, + isBuild: options.isBuild, + viteUserConfig: config, + isFrameworkPkgByJson(pkgJson) { + let hasSvelteCondition = false; + if (typeof pkgJson.exports === 'object') { + // use replacer as a simple way to iterate over nested keys + JSON.stringify(pkgJson.exports, (key, value) => { + if (SVELTE_EXPORT_CONDITIONS.includes(key)) { + hasSvelteCondition = true; + } + return value; + }); + } + const hasSvelteField = !!pkgJson.svelte; + if (hasSvelteField && !hasSvelteCondition) { + packagesWithoutSvelteExportsCondition.add(`${pkgJson.name}@${pkgJson.version}`); + } + return hasSvelteCondition || hasSvelteField; + }, + isSemiFrameworkPkgByJson(pkgJson) { + return !!pkgJson.dependencies?.svelte || !!pkgJson.peerDependencies?.svelte; + }, + isFrameworkPkgByName(pkgName) { + const isNotSveltePackage = isCommonDepWithoutSvelteField(pkgName); + if (isNotSveltePackage) { + return false; + } else { + return undefined; + } + } + }); + if ( + !options.experimental?.disableSvelteResolveWarnings && + packagesWithoutSvelteExportsCondition?.size > 0 + ) { + log.warn( + `WARNING: The following packages have a svelte field in their package.json but no exports condition for svelte.\n\n${[ + ...packagesWithoutSvelteExportsCondition + ].join('\n')}\n\nPlease see ${FAQ_LINK_MISSING_EXPORTS_CONDITION} for details.` + ); + } + log.debug('extra config for dependencies generated by vitefu', depsConfig, 'config'); + + if (options.prebundleSvelteLibraries) { + // prebundling enabled, so we don't need extra dependency excludes + depsConfig.optimizeDeps.exclude = []; + // but keep dependency reinclusions of explicit user excludes + const userExclude = config.optimizeDeps?.exclude; + depsConfig.optimizeDeps.include = !userExclude + ? [] + : depsConfig.optimizeDeps.include.filter((dep) => { + // reincludes look like this: foo > bar > baz + // in case foo or bar are excluded, we have to retain the reinclude even with prebundling + return ( + dep.includes('>') && + dep + .split('>') + .slice(0, -1) + .some((d) => isDepExcluded(d.trim(), userExclude)) + ); + }); + } + if (options.disableDependencyReinclusion === true) { + depsConfig.optimizeDeps.include = depsConfig.optimizeDeps.include.filter( + (dep) => !dep.includes('>') + ); + } else if (Array.isArray(options.disableDependencyReinclusion)) { + const disabledDeps = options.disableDependencyReinclusion; + depsConfig.optimizeDeps.include = depsConfig.optimizeDeps.include.filter((dep) => { + if (!dep.includes('>')) return true; + const trimDep = dep.replace(/\s+/g, ''); + return disabledDeps.some((disabled) => trimDep.includes(`${disabled}>`)); + }); + } + + log.debug('post-processed extra config for dependencies', depsConfig, 'config'); + + return depsConfig; +} + +/** + * @param {import('vite').UserConfig} config + * @returns {import('vite').UserConfig & { optimizeDeps: { include: string[], exclude:string[] }, ssr: { noExternal:(string|RegExp)[], external: string[] } } } + */ +function buildExtraConfigForSvelte(config) { + // include svelte imports for optimization unless explicitly excluded + /** @type {string[]} */ + const include = []; + /** @type {string[]} */ + const exclude = []; + if (!isDepExcluded('svelte', config.optimizeDeps?.exclude ?? [])) { + const svelteImportsToInclude = SVELTE_IMPORTS.filter( + (si) => !(si.endsWith('/server') || si.includes('/server/')) + ); + log.debug( + `adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `, + undefined, + 'config' + ); + include.push(...svelteImportsToInclude); + } else { + log.debug( + '"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.', + undefined, + 'config' + ); + } + /** @type {(string | RegExp)[]} */ + const noExternal = []; + /** @type {string[]} */ + const external = []; + // add svelte to ssr.noExternal unless it is present in ssr.external + // so it is correctly resolving according to the conditions in sveltes exports map + if (!isDepExternaled('svelte', config.ssr?.external ?? [])) { + noExternal.push('svelte', /^svelte\//); + } + // esm-env needs to be bundled by default for the development/production condition + // be properly used by svelte + if (!isDepExternaled('esm-env', config.ssr?.external ?? [])) { + noExternal.push('esm-env'); + } + return { optimizeDeps: { include, exclude }, ssr: { noExternal, external } }; +} + +/** + * @param {import('vite').ResolvedConfig} viteConfig + * @param {import('../types/options.d.ts').ResolvedOptions} options + */ +export function patchResolvedViteConfig(viteConfig, options) { + if (options.preprocess) { + for (const preprocessor of arraify(options.preprocess)) { + if (preprocessor.style && '__resolvedConfig' in preprocessor.style) { + preprocessor.style.__resolvedConfig = viteConfig; + } + } + } + + // replace facade esbuild plugin with a real one + const facadeEsbuildSveltePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find( + (plugin) => plugin.name === facadeEsbuildSveltePluginName + ); + if (facadeEsbuildSveltePlugin) { + Object.assign(facadeEsbuildSveltePlugin, esbuildSveltePlugin(options)); + } + const facadeEsbuildSvelteModulePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find( + (plugin) => plugin.name === facadeEsbuildSvelteModulePluginName + ); + if (facadeEsbuildSvelteModulePlugin) { + Object.assign(facadeEsbuildSvelteModulePlugin, esbuildSvelteModulePlugin(options)); + } +} + +/** + * Mutates `config` to ensure `resolve.mainFields` is set. If unset, it emulates Vite's default fallback. + * @param {string} name + * @param {import('vite').EnvironmentOptions} config + * @param {{ isSsrTargetWebworker?: boolean }} opts + */ +export function ensureConfigEnvironmentMainFields(name, config, opts) { + config.resolve ??= {}; + if (config.resolve.mainFields == null) { + if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) { + config.resolve.mainFields = [...defaultClientMainFields]; + } else { + config.resolve.mainFields = [...defaultServerMainFields]; + } + } + return true; +} + +/** + * Mutates `config` to ensure `resolve.conditions` is set. If unset, it emulates Vite's default fallback. + * @param {string} name + * @param {import('vite').EnvironmentOptions} config + * @param {{ isSsrTargetWebworker?: boolean }} opts + */ +export function ensureConfigEnvironmentConditions(name, config, opts) { + config.resolve ??= {}; + if (config.resolve.conditions == null) { + if (config.consumer === 'client' || name === 'client' || opts.isSsrTargetWebworker) { + config.resolve.conditions = [...defaultClientConditions]; + } else { + config.resolve.conditions = [...defaultServerConditions]; + } + } +} + +/** + * @template T + * @param {T | T[]} value + * @returns {T[]} + */ +function arraify(value) { + return Array.isArray(value) ? value : [value]; +} diff --git a/js-plugins/svelte/src/utils/preprocess.js b/js-plugins/svelte/src/utils/preprocess.js new file mode 100644 index 0000000..602c649 --- /dev/null +++ b/js-plugins/svelte/src/utils/preprocess.js @@ -0,0 +1,173 @@ +import MagicString from 'magic-string'; +import { log } from './log.js'; +import path from 'node:path'; +import { normalizePath } from 'vite'; + +/** + * this appends a *{} rule to component styles to force the svelte compiler to add style classes to all nodes + * That means adding/removing class rules from