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 = /