Skip to content

Commit 0971449

Browse files
bluwybenmccanndominikg
authored
Export vitePreprocess API (#509)
* feat: export vite preprocessors * feat: export vitePreprocess * chore: fix types * chore: simplify test * chore: improve type export * chore: add changeset * refactor: preprocess api * docs: add docs * chore: fix typo * chore: update docs * chore: make preprocess vite test esm * chore: default resolve config command by env * feat: use vite resolvedConfig when in vite * chore: update docs Co-authored-by: Ben McCann <[email protected]> * chore: log warning for useVitePreprocess option * docs: update comparison * chore: remove subpath export * chore: fix build * fix: typo Co-authored-by: Dominik G. <[email protected]> * chore: update docs Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Ben McCann <[email protected]> Co-authored-by: Dominik G. <[email protected]>
1 parent a0f4cc3 commit 0971449

File tree

10 files changed

+187
-96
lines changed

10 files changed

+187
-96
lines changed

.changeset/wet-socks-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': minor
3+
---
4+
5+
Export `vitePreprocess()` Svelte preprocessor

docs/preprocess.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Preprocess
2+
3+
`vite-plugin-svelte` also exports Vite preprocessors to preprocess Svelte components using Vite's built-in transformers.
4+
5+
Compared to [`svelte-preprocess`](https://github.com/sveltejs/svelte-preprocess), Vite preprocessors share the same CSS configuration from the Vite config so you don't have to configure them twice. [`esbuild`](http://esbuild.github.io) is also used to transform TypeScript by default.
6+
7+
However, `svelte-preprocess` does provide extra functionalities not available with Vite preprocessors, such as [template tag](https://github.com/sveltejs/svelte-preprocess#template-tag), [external files](https://github.com/sveltejs/svelte-preprocess#external-files), and [global styles](https://github.com/sveltejs/svelte-preprocess#global-style) ([though it's recommended to use import instead](./faq.md#where-should-i-put-my-global-styles)). If those features are required, you can still use `svelte-preprocess`, but make sure to turn off it's script and style preprocessing options.
8+
9+
## vitePreprocess
10+
11+
- **Type:** `{ script?: boolean, style?: boolean | InlineConfig | ResolvedConfig }`
12+
- **Default:** `{ script: true, style: true }`
13+
14+
A Svelte preprocessor that supports transforming TypeScript, PostCSS, SCSS, Less, Stylus, and SugarSS. These are transformed when the script or style tags have the respective `lang` attribute.
15+
16+
- TypeScript: `<script lang="ts">`
17+
- PostCSS: `<style lang="postcss">`
18+
- SCSS: `<style lang="scss">`
19+
- Less: `<style lang="less">`
20+
- Stylus: `<style lang="stylus">`
21+
- SugarSS: `<style lang="sss">`
22+
23+
If required, you can turn script or style transforming off by setting the `script` or `style` option to `false`. The `style` option also accepts Vite's `InlineConfig` and `ResolvedConfig` types for advanced usage.
24+
25+
**Example:**
26+
27+
```js
28+
// svelte.config.js
29+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
30+
31+
export default {
32+
preprocess: [vitePreprocess()]
33+
};
34+
```

packages/e2e-tests/preprocess-with-vite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
"svelte": "^3.53.1",
1515
"vite": "^3.2.4"
1616
},
17-
"type": "commonjs"
17+
"type": "module"
1818
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
2+
3+
export default {
4+
preprocess: [vitePreprocess()]
5+
};

packages/e2e-tests/preprocess-with-vite/vite.config.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
const { svelte } = require('@sveltejs/vite-plugin-svelte');
2-
const { defineConfig } = require('vite');
1+
import { svelte } from '@sveltejs/vite-plugin-svelte';
2+
import { defineConfig } from 'vite';
33

4-
module.exports = defineConfig(({ command, mode }) => {
4+
export default defineConfig(({ command, mode }) => {
55
const isProduction = mode === 'production';
66
return {
7-
plugins: [
8-
svelte({
9-
experimental: {
10-
useVitePreprocess: true
11-
}
12-
})
13-
],
7+
plugins: [svelte()],
148
build: {
159
minify: isProduction
1610
}

packages/vite-plugin-svelte/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
"files": [
77
"dist",
88
"src",
9-
"README.md",
10-
"LICENSE",
11-
"package.json"
9+
"*.d.ts"
1210
],
1311
"type": "module",
1412
"main": "dist/index.cjs",
@@ -25,7 +23,7 @@
2523
},
2624
"scripts": {
2725
"dev": "pnpm build:ci --sourcemap --watch src",
28-
"build:ci": "rimraf dist && tsup-node src/index.ts --format esm,cjs --no-splitting --shims",
26+
"build:ci": "rimraf dist && tsup-node src/index.ts src/preprocess.ts --format esm,cjs --no-splitting --shims",
2927
"build": "pnpm build:ci --dts --sourcemap"
3028
},
3129
"engines": {

packages/vite-plugin-svelte/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
238238
return plugins.filter(Boolean);
239239
}
240240

241+
export { vitePreprocess } from './preprocess';
241242
export { loadSvelteConfig } from './utils/load-svelte-config';
242243

243244
export {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import path from 'path';
2+
import * as vite from 'vite';
3+
import type { ESBuildOptions, ResolvedConfig } from 'vite';
4+
// eslint-disable-next-line node/no-missing-import
5+
import type { Preprocessor, PreprocessorGroup } from 'svelte/types/compiler/preprocess';
6+
7+
const supportedStyleLangs = ['css', 'less', 'sass', 'scss', 'styl', 'stylus', 'postcss', 'sss'];
8+
const supportedScriptLangs = ['ts'];
9+
10+
export function vitePreprocess(opts?: {
11+
script?: boolean;
12+
style?: boolean | vite.InlineConfig | vite.ResolvedConfig;
13+
}) {
14+
const preprocessor: PreprocessorGroup = {};
15+
if (opts?.script !== false) {
16+
preprocessor.script = viteScript().script;
17+
}
18+
if (opts?.style !== false) {
19+
const styleOpts = typeof opts?.style == 'object' ? opts?.style : undefined;
20+
preprocessor.style = viteStyle(styleOpts).style;
21+
}
22+
return preprocessor;
23+
}
24+
25+
function viteScript(): { script: Preprocessor } {
26+
return {
27+
async script({ attributes, content, filename = '' }) {
28+
const lang = attributes.lang as string;
29+
if (!supportedScriptLangs.includes(lang)) return;
30+
const transformResult = await vite.transformWithEsbuild(content, filename, {
31+
loader: lang as ESBuildOptions['loader'],
32+
target: 'esnext',
33+
tsconfigRaw: {
34+
compilerOptions: {
35+
// svelte typescript needs this flag to work with type imports
36+
importsNotUsedAsValues: 'preserve',
37+
preserveValueImports: true
38+
}
39+
}
40+
});
41+
return {
42+
code: transformResult.code,
43+
map: transformResult.map
44+
};
45+
}
46+
};
47+
}
48+
49+
function viteStyle(config: vite.InlineConfig | vite.ResolvedConfig = {}): {
50+
style: Preprocessor;
51+
} {
52+
let transform: CssTransform;
53+
const style: Preprocessor = async ({ attributes, content, filename = '' }) => {
54+
const lang = attributes.lang as string;
55+
if (!supportedStyleLangs.includes(lang)) return;
56+
if (!transform) {
57+
let resolvedConfig: vite.ResolvedConfig;
58+
// @ts-expect-error special prop added if running in v-p-s
59+
if (style.__resolvedConfig) {
60+
// @ts-expect-error
61+
resolvedConfig = style.__resolvedConfig;
62+
} else if (isResolvedConfig(config)) {
63+
resolvedConfig = config;
64+
} else {
65+
resolvedConfig = await vite.resolveConfig(
66+
config,
67+
process.env.NODE_ENV === 'production' ? 'build' : 'serve'
68+
);
69+
}
70+
transform = getCssTransformFn(resolvedConfig);
71+
}
72+
const moduleId = `${filename}.${lang}`;
73+
const result = await transform(content, moduleId);
74+
// patch sourcemap source to point back to original filename
75+
if (result.map?.sources?.[0] === moduleId) {
76+
result.map.sources[0] = path.basename(filename);
77+
}
78+
return {
79+
code: result.code,
80+
map: result.map ?? undefined
81+
};
82+
};
83+
// @ts-expect-error tag so can be found by v-p-s
84+
style.__resolvedConfig = null;
85+
return { style };
86+
}
87+
88+
// eslint-disable-next-line no-unused-vars
89+
type CssTransform = (code: string, filename: string) => Promise<{ code: string; map?: any }>;
90+
91+
function getCssTransformFn(config: ResolvedConfig): CssTransform {
92+
// API is only available in Vite 3.2 and above
93+
// TODO: Remove Vite plugin hack when bump peer dep to Vite 3.2
94+
if (vite.preprocessCSS) {
95+
return async (code, filename) => {
96+
return vite.preprocessCSS(code, filename, config);
97+
};
98+
} else {
99+
const pluginName = 'vite:css';
100+
const plugin = config.plugins.find((p) => p.name === pluginName);
101+
if (!plugin) {
102+
throw new Error(`failed to find plugin ${pluginName}`);
103+
}
104+
if (!plugin.transform) {
105+
throw new Error(`plugin ${pluginName} has no transform`);
106+
}
107+
// @ts-expect-error
108+
return plugin.transform.bind(null);
109+
}
110+
}
111+
112+
function isResolvedConfig(config: any): config is vite.ResolvedConfig {
113+
return !!config.inlineConfig;
114+
}

packages/vite-plugin-svelte/src/utils/options.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,15 @@ function buildExtraConfigForSvelte(config: UserConfig) {
532532
}
533533

534534
export function patchResolvedViteConfig(viteConfig: ResolvedConfig, options: ResolvedOptions) {
535+
if (options.preprocess) {
536+
for (const preprocessor of arraify(options.preprocess)) {
537+
if (preprocessor.style && '__resolvedConfig' in preprocessor.style) {
538+
preprocessor.style.__resolvedConfig = viteConfig;
539+
}
540+
}
541+
}
542+
543+
// replace facade esbuild plugin with a real one
535544
const facadeEsbuildSveltePlugin = viteConfig.optimizeDeps.esbuildOptions?.plugins?.find(
536545
(plugin) => plugin.name === facadeEsbuildSveltePluginName
537546
);
@@ -540,6 +549,10 @@ export function patchResolvedViteConfig(viteConfig: ResolvedConfig, options: Res
540549
}
541550
}
542551

552+
function arraify<T>(value: T | T[]): T[] {
553+
return Array.isArray(value) ? value : [value];
554+
}
555+
543556
export type Options = Omit<SvelteOptions, 'vitePlugin'> & PluginOptionsInline;
544557

545558
interface PluginOptionsInline extends PluginOptions {

packages/vite-plugin-svelte/src/utils/preprocess.ts

Lines changed: 8 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,18 @@
1-
import * as vite from 'vite';
2-
import type { ESBuildOptions, ResolvedConfig, Plugin } from 'vite';
1+
import type { ResolvedConfig, Plugin } from 'vite';
32
import MagicString from 'magic-string';
43
import { preprocess } from 'svelte/compiler';
5-
import { Preprocessor, PreprocessorGroup, Processed, ResolvedOptions } from './options';
4+
import { PreprocessorGroup, Processed, ResolvedOptions } from './options';
65
import { log } from './log';
76
import { buildSourceMap } from './sourcemap';
87
import path from 'path';
9-
10-
const supportedStyleLangs = ['css', 'less', 'sass', 'scss', 'styl', 'stylus', 'postcss'];
11-
12-
const supportedScriptLangs = ['ts'];
13-
14-
function createViteScriptPreprocessor(): Preprocessor {
15-
return async ({ attributes, content, filename = '' }) => {
16-
const lang = attributes.lang as string;
17-
if (!supportedScriptLangs.includes(lang)) return;
18-
const transformResult = await vite.transformWithEsbuild(content, filename, {
19-
loader: lang as ESBuildOptions['loader'],
20-
target: 'esnext',
21-
tsconfigRaw: {
22-
compilerOptions: {
23-
// svelte typescript needs this flag to work with type imports
24-
importsNotUsedAsValues: 'preserve',
25-
preserveValueImports: true
26-
}
27-
}
28-
});
29-
return {
30-
code: transformResult.code,
31-
map: transformResult.map
32-
};
33-
};
34-
}
35-
36-
function createViteStylePreprocessor(config: ResolvedConfig): Preprocessor {
37-
const transform = getCssTransformFn(config);
38-
return async ({ attributes, content, filename = '' }) => {
39-
const lang = attributes.lang as string;
40-
if (!supportedStyleLangs.includes(lang)) return;
41-
const moduleId = `${filename}.${lang}`;
42-
const result = await transform(content, moduleId);
43-
// patch sourcemap source to point back to original filename
44-
if (result.map?.sources?.[0] === moduleId) {
45-
result.map.sources[0] = path.basename(filename);
46-
}
47-
return {
48-
code: result.code,
49-
map: result.map ?? undefined
50-
};
51-
};
52-
}
53-
54-
// eslint-disable-next-line no-unused-vars
55-
type CssTransform = (code: string, filename: string) => Promise<{ code: string; map?: any }>;
56-
57-
function getCssTransformFn(config: ResolvedConfig): CssTransform {
58-
// API is only available in Vite 3.2 and above
59-
// TODO: Remove Vite plugin hack when bump peer dep to Vite 3.2
60-
if (vite.preprocessCSS) {
61-
return async (code, filename) => {
62-
return vite.preprocessCSS(code, filename, config);
63-
};
64-
} else {
65-
const pluginName = 'vite:css';
66-
const plugin = config.plugins.find((p) => p.name === pluginName);
67-
if (!plugin) {
68-
throw new Error(`failed to find plugin ${pluginName}`);
69-
}
70-
if (!plugin.transform) {
71-
throw new Error(`plugin ${pluginName} has no transform`);
72-
}
73-
// @ts-expect-error
74-
return plugin.transform.bind(null);
75-
}
76-
}
8+
import { vitePreprocess } from '../preprocess';
779

7810
function createVitePreprocessorGroup(config: ResolvedConfig): PreprocessorGroup {
7911
return {
8012
markup({ content, filename }) {
81-
return preprocess(
82-
content,
83-
{
84-
script: createViteScriptPreprocessor(),
85-
style: createViteStylePreprocessor(config)
86-
},
87-
{ filename }
88-
);
13+
return preprocess(content, vitePreprocess({ style: config }), { filename });
8914
}
90-
} as PreprocessorGroup;
15+
};
9116
}
9217

9318
/**
@@ -117,7 +42,9 @@ function buildExtraPreprocessors(options: ResolvedOptions, config: ResolvedConfi
11742
const appendPreprocessors: PreprocessorGroup[] = [];
11843

11944
if (options.experimental?.useVitePreprocess) {
120-
log.debug('adding vite preprocessor');
45+
log.warn(
46+
'`experimental.useVitePreprocess` is deprecated. Use the `vitePreprocess()` preprocessor instead. See https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/preprocess.md for more information.'
47+
);
12148
prependPreprocessors.push(createVitePreprocessorGroup(config));
12249
}
12350

0 commit comments

Comments
 (0)