diff --git a/packages/e2e-tests/emit-preprocessed/__tests__/emit-preprocessed.spec.ts b/packages/e2e-tests/emit-preprocessed/__tests__/emit-preprocessed.spec.ts
new file mode 100644
index 000000000..eba113aa3
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/__tests__/emit-preprocessed.spec.ts
@@ -0,0 +1,11 @@
+import { getText, isBuild } from '~utils';
+
+test('should render Counter', async () => {
+ expect(await getText('button')).toBe('clicks: 0');
+});
+
+if (!isBuild) {
+ test('should emit preprocessed', () => {
+ // TODO read assets and ensure they are what we want
+ });
+}
diff --git a/packages/e2e-tests/emit-preprocessed/index.html b/packages/e2e-tests/emit-preprocessed/index.html
new file mode 100644
index 000000000..c321fcb17
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Svelte app
+
+
+
+
+
+
+
+
diff --git a/packages/e2e-tests/emit-preprocessed/package.json b/packages/e2e-tests/emit-preprocessed/package.json
new file mode 100644
index 000000000..b38280d1f
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "e2e-tests-emit-preprocessed",
+ "private": true,
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "workspace:*",
+ "sass": "^1.55.0",
+ "svelte": "^3.52.0",
+ "vite": "^3.2.3"
+ },
+ "type": "module"
+}
diff --git a/packages/e2e-tests/emit-preprocessed/public/favicon.png b/packages/e2e-tests/emit-preprocessed/public/favicon.png
new file mode 100644
index 000000000..7e6f5eb5a
Binary files /dev/null and b/packages/e2e-tests/emit-preprocessed/public/favicon.png differ
diff --git a/packages/e2e-tests/emit-preprocessed/src/App.svelte b/packages/e2e-tests/emit-preprocessed/src/App.svelte
new file mode 100644
index 000000000..5142f27be
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/src/App.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/e2e-tests/emit-preprocessed/src/lib/Counter.svelte b/packages/e2e-tests/emit-preprocessed/src/lib/Counter.svelte
new file mode 100644
index 000000000..a48a22449
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/src/lib/Counter.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/packages/e2e-tests/emit-preprocessed/src/lib/RedButton.svelte b/packages/e2e-tests/emit-preprocessed/src/lib/RedButton.svelte
new file mode 100644
index 000000000..dadec1a8a
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/src/lib/RedButton.svelte
@@ -0,0 +1,12 @@
+
+
+
diff --git a/packages/e2e-tests/emit-preprocessed/src/lib/index.js b/packages/e2e-tests/emit-preprocessed/src/lib/index.js
new file mode 100644
index 000000000..3392dfc7b
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/src/lib/index.js
@@ -0,0 +1,2 @@
+export { default as RedButton } from './RedButton.svelte';
+export { default as Counter } from './Counter.svelte';
diff --git a/packages/e2e-tests/emit-preprocessed/src/main.js b/packages/e2e-tests/emit-preprocessed/src/main.js
new file mode 100644
index 000000000..2c27a2579
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/src/main.js
@@ -0,0 +1,7 @@
+import App from './App.svelte';
+
+const app = new App({
+ target: document.body
+});
+
+export default app;
diff --git a/packages/e2e-tests/emit-preprocessed/svelte.config.js b/packages/e2e-tests/emit-preprocessed/svelte.config.js
new file mode 100644
index 000000000..44ed640a8
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/svelte.config.js
@@ -0,0 +1,16 @@
+// eslint-disable-next-line node/no-missing-import
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte/preprocess';
+export default {
+ preprocess: [vitePreprocess()],
+ vitePlugin: {
+ experimental: {
+ useVitePreprocess: true,
+ emitPreprocessed(fileName, processed) {
+ return {
+ fileName,
+ source: processed.code
+ };
+ }
+ }
+ }
+};
diff --git a/packages/e2e-tests/emit-preprocessed/vite.config.js b/packages/e2e-tests/emit-preprocessed/vite.config.js
new file mode 100644
index 000000000..29494396a
--- /dev/null
+++ b/packages/e2e-tests/emit-preprocessed/vite.config.js
@@ -0,0 +1,25 @@
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { defineConfig } from 'vite';
+import path from 'path';
+export default defineConfig(({ command, mode }) => {
+ return {
+ plugins: [svelte()],
+ build: {
+ // make build faster by skipping transforms and minification
+ target: 'esnext',
+ minify: false,
+ lib: {
+ formats: ['es'],
+ entry: [path.resolve('src/lib/index.js')]
+ }
+ },
+ server: {
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100
+ }
+ }
+ };
+});
diff --git a/packages/e2e-tests/preprocess-with-vite/svelte.config.js b/packages/e2e-tests/preprocess-with-vite/svelte.config.js
new file mode 100644
index 000000000..689f9194f
--- /dev/null
+++ b/packages/e2e-tests/preprocess-with-vite/svelte.config.js
@@ -0,0 +1,6 @@
+// eslint-disable-next-line node/no-missing-require
+const { viteScript, viteStyle } = require('@sveltejs/vite-plugin-svelte/preprocess');
+
+module.exports = {
+ preprocess: [viteScript(), viteStyle()]
+};
diff --git a/packages/e2e-tests/preprocess-with-vite/vite.config.js b/packages/e2e-tests/preprocess-with-vite/vite.config.js
index 5248779a5..3f658aba4 100644
--- a/packages/e2e-tests/preprocess-with-vite/vite.config.js
+++ b/packages/e2e-tests/preprocess-with-vite/vite.config.js
@@ -4,13 +4,7 @@ const { defineConfig } = require('vite');
module.exports = defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
- plugins: [
- svelte({
- experimental: {
- useVitePreprocess: true
- }
- })
- ],
+ plugins: [svelte()],
build: {
minify: isProduction
}
diff --git a/packages/vite-plugin-svelte/package.json b/packages/vite-plugin-svelte/package.json
index 1104ae916..e00a4c378 100644
--- a/packages/vite-plugin-svelte/package.json
+++ b/packages/vite-plugin-svelte/package.json
@@ -16,15 +16,21 @@
"types": "dist/index.d.ts",
"exports": {
".": {
+ "types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
+ "./preprocess": {
+ "types": "./dist/preprocess.d.ts",
+ "import": "./dist/preprocess.js",
+ "require": "./dist/preprocess.cjs"
+ },
"./package.json": "./package.json",
"./src/ui/*": "./src/ui/*"
},
"scripts": {
"dev": "pnpm build:ci --sourcemap --watch src",
- "build:ci": "rimraf dist && tsup-node src/index.ts --format esm,cjs --no-splitting --shims",
+ "build:ci": "rimraf dist && tsup-node src/index.ts src/preprocess.ts --format esm,cjs --no-splitting --shims",
"build": "pnpm build:ci --dts --sourcemap"
},
"engines": {
diff --git a/packages/vite-plugin-svelte/preprocess.d.ts b/packages/vite-plugin-svelte/preprocess.d.ts
new file mode 100644
index 000000000..a632a830e
--- /dev/null
+++ b/packages/vite-plugin-svelte/preprocess.d.ts
@@ -0,0 +1 @@
+export { vitePreprocess, viteScript, viteStyle } from './dist/preprocess';
diff --git a/packages/vite-plugin-svelte/src/index.ts b/packages/vite-plugin-svelte/src/index.ts
index 22dcc4803..e52eb5c52 100644
--- a/packages/vite-plugin-svelte/src/index.ts
+++ b/packages/vite-plugin-svelte/src/index.ts
@@ -215,6 +215,22 @@ export function svelte(inlineOptions?: Partial): Plugin[] {
throw toRollupError(e, options);
}
}
+ },
+
+ generateBundle() {
+ const emitPreprocessed = options.experimental?.emitPreprocessed;
+ if (emitPreprocessed) {
+ const preprocessed = cache.getPreprocessed();
+ preprocessed.forEach((processed, filename) => {
+ const emit = emitPreprocessed(filename, processed);
+ if (emit) {
+ if (emit.fileName.startsWith('/')) {
+ emit.fileName = emit.fileName.slice(1);
+ }
+ this.emitFile({ ...emit, type: 'asset' });
+ }
+ });
+ }
}
}
];
diff --git a/packages/vite-plugin-svelte/src/preprocess.ts b/packages/vite-plugin-svelte/src/preprocess.ts
new file mode 100644
index 000000000..c1bfa94ef
--- /dev/null
+++ b/packages/vite-plugin-svelte/src/preprocess.ts
@@ -0,0 +1,103 @@
+import path from 'path';
+import * as vite from 'vite';
+import type { ESBuildOptions, ResolvedConfig } from 'vite';
+// eslint-disable-next-line node/no-missing-import
+import type { Preprocessor, PreprocessorGroup } from 'svelte/types/compiler/preprocess';
+
+const supportedStyleLangs = ['css', 'less', 'sass', 'scss', 'styl', 'stylus', 'postcss', 'sss'];
+const supportedScriptLangs = ['ts'];
+
+export function vitePreprocess(opts?: {
+ script?: boolean;
+ style?: boolean | Parameters[0];
+}) {
+ const preprocessor: PreprocessorGroup = {};
+ if (opts?.script !== false) {
+ preprocessor.script = viteScript().script;
+ }
+ if (opts?.style !== false) {
+ const styleOpts = typeof opts?.style == 'object' ? opts?.style : undefined;
+ preprocessor.style = viteStyle(styleOpts).style;
+ }
+ return preprocessor;
+}
+
+export function viteScript(): { script: Preprocessor } {
+ return {
+ async script({ attributes, content, filename = '' }) {
+ const lang = attributes.lang as string;
+ if (!supportedScriptLangs.includes(lang)) return;
+ const transformResult = await vite.transformWithEsbuild(content, filename, {
+ loader: lang as ESBuildOptions['loader'],
+ target: 'esnext',
+ tsconfigRaw: {
+ compilerOptions: {
+ // svelte typescript needs this flag to work with type imports
+ importsNotUsedAsValues: 'preserve',
+ preserveValueImports: true
+ }
+ }
+ });
+ return {
+ code: transformResult.code,
+ map: transformResult.map
+ };
+ }
+ };
+}
+
+export function viteStyle(config: vite.InlineConfig | vite.ResolvedConfig = {}): {
+ style: Preprocessor;
+} {
+ let transform: CssTransform;
+ return {
+ async style({ attributes, content, filename = '' }) {
+ if (!transform) {
+ const resolvedConfig = isResolvedConfig(config)
+ ? config
+ : await vite.resolveConfig(config, 'build');
+ transform = getCssTransformFn(resolvedConfig);
+ }
+ const lang = attributes.lang as string;
+ if (!supportedStyleLangs.includes(lang)) return;
+ const moduleId = `${filename}.${lang}`;
+ const result = await transform(content, moduleId);
+ // patch sourcemap source to point back to original filename
+ if (result.map?.sources?.[0] === moduleId) {
+ result.map.sources[0] = path.basename(filename);
+ }
+ return {
+ code: result.code,
+ map: result.map ?? undefined
+ };
+ }
+ };
+}
+
+// eslint-disable-next-line no-unused-vars
+type CssTransform = (code: string, filename: string) => Promise<{ code: string; map?: any }>;
+
+function getCssTransformFn(config: ResolvedConfig): CssTransform {
+ // API is only available in Vite 3.2 and above
+ // TODO: Remove Vite plugin hack when bump peer dep to Vite 3.2
+ if (vite.preprocessCSS) {
+ return async (code, filename) => {
+ return vite.preprocessCSS(code, filename, config);
+ };
+ } else {
+ const pluginName = 'vite:css';
+ const plugin = config.plugins.find((p) => p.name === pluginName);
+ if (!plugin) {
+ throw new Error(`failed to find plugin ${pluginName}`);
+ }
+ if (!plugin.transform) {
+ throw new Error(`plugin ${pluginName} has no transform`);
+ }
+ // @ts-expect-error
+ return plugin.transform.bind(null);
+ }
+}
+
+function isResolvedConfig(config: any): config is vite.ResolvedConfig {
+ return !!config.inlineConfig;
+}
diff --git a/packages/vite-plugin-svelte/src/utils/compile.ts b/packages/vite-plugin-svelte/src/utils/compile.ts
index d5f31f11d..ed10d912d 100644
--- a/packages/vite-plugin-svelte/src/utils/compile.ts
+++ b/packages/vite-plugin-svelte/src/utils/compile.ts
@@ -5,6 +5,8 @@ import { createMakeHot } from 'svelte-hmr';
import { SvelteRequest } from './id';
import { safeBase64Hash } from './hash';
import { log } from './log';
+// eslint-disable-next-line node/no-missing-import
+import type { Processed } from 'svelte/types/compiler/preprocess';
const scriptLangRE = /