Skip to content

Commit

Permalink
Export vitePreprocess API (#509)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Dec 2, 2022
1 parent a0f4cc3 commit 0971449
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 96 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-socks-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': minor
---

Export `vitePreprocess()` Svelte preprocessor
34 changes: 34 additions & 0 deletions docs/preprocess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Preprocess

`vite-plugin-svelte` also exports Vite preprocessors to preprocess Svelte components using Vite's built-in transformers.

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.

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.

## vitePreprocess

- **Type:** `{ script?: boolean, style?: boolean | InlineConfig | ResolvedConfig }`
- **Default:** `{ script: true, style: true }`

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.

- TypeScript: `<script lang="ts">`
- PostCSS: `<style lang="postcss">`
- SCSS: `<style lang="scss">`
- Less: `<style lang="less">`
- Stylus: `<style lang="stylus">`
- SugarSS: `<style lang="sss">`

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.

**Example:**

```js
// svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
preprocess: [vitePreprocess()]
};
```
2 changes: 1 addition & 1 deletion packages/e2e-tests/preprocess-with-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"svelte": "^3.53.1",
"vite": "^3.2.4"
},
"type": "commonjs"
"type": "module"
}
5 changes: 5 additions & 0 deletions packages/e2e-tests/preprocess-with-vite/svelte.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

export default {
preprocess: [vitePreprocess()]
};
14 changes: 4 additions & 10 deletions packages/e2e-tests/preprocess-with-vite/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
const { svelte } = require('@sveltejs/vite-plugin-svelte');
const { defineConfig } = require('vite');
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';

module.exports = defineConfig(({ command, mode }) => {
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
plugins: [
svelte({
experimental: {
useVitePreprocess: true
}
})
],
plugins: [svelte()],
build: {
minify: isProduction
}
Expand Down
6 changes: 2 additions & 4 deletions packages/vite-plugin-svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
"files": [
"dist",
"src",
"README.md",
"LICENSE",
"package.json"
"*.d.ts"
],
"type": "module",
"main": "dist/index.cjs",
Expand All @@ -25,7 +23,7 @@
},
"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": {
Expand Down
1 change: 1 addition & 0 deletions packages/vite-plugin-svelte/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
return plugins.filter(Boolean);
}

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

export {
Expand Down
114 changes: 114 additions & 0 deletions packages/vite-plugin-svelte/src/preprocess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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 | vite.InlineConfig | vite.ResolvedConfig;
}) {
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;
}

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
};
}
};
}

function viteStyle(config: vite.InlineConfig | vite.ResolvedConfig = {}): {
style: Preprocessor;
} {
let transform: CssTransform;
const style: Preprocessor = async ({ attributes, content, filename = '' }) => {
const lang = attributes.lang as string;
if (!supportedStyleLangs.includes(lang)) return;
if (!transform) {
let resolvedConfig: vite.ResolvedConfig;
// @ts-expect-error special prop added if running in v-p-s
if (style.__resolvedConfig) {
// @ts-expect-error
resolvedConfig = style.__resolvedConfig;
} else if (isResolvedConfig(config)) {
resolvedConfig = config;
} else {
resolvedConfig = await vite.resolveConfig(
config,
process.env.NODE_ENV === 'production' ? 'build' : 'serve'
);
}
transform = getCssTransformFn(resolvedConfig);
}
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
};
};
// @ts-expect-error tag so can be found by v-p-s
style.__resolvedConfig = null;
return { style };
}

// 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;
}
13 changes: 13 additions & 0 deletions packages/vite-plugin-svelte/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,15 @@ function buildExtraConfigForSvelte(config: UserConfig) {
}

export function patchResolvedViteConfig(viteConfig: ResolvedConfig, options: ResolvedOptions) {
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
);
Expand All @@ -540,6 +549,10 @@ export function patchResolvedViteConfig(viteConfig: ResolvedConfig, options: Res
}
}

function arraify<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}

export type Options = Omit<SvelteOptions, 'vitePlugin'> & PluginOptionsInline;

interface PluginOptionsInline extends PluginOptions {
Expand Down
89 changes: 8 additions & 81 deletions packages/vite-plugin-svelte/src/utils/preprocess.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,18 @@
import * as vite from 'vite';
import type { ESBuildOptions, ResolvedConfig, Plugin } from 'vite';
import type { ResolvedConfig, Plugin } from 'vite';
import MagicString from 'magic-string';
import { preprocess } from 'svelte/compiler';
import { Preprocessor, PreprocessorGroup, Processed, ResolvedOptions } from './options';
import { PreprocessorGroup, Processed, ResolvedOptions } from './options';
import { log } from './log';
import { buildSourceMap } from './sourcemap';
import path from 'path';

const supportedStyleLangs = ['css', 'less', 'sass', 'scss', 'styl', 'stylus', 'postcss'];

const supportedScriptLangs = ['ts'];

function createViteScriptPreprocessor(): Preprocessor {
return async ({ 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
};
};
}

function createViteStylePreprocessor(config: ResolvedConfig): Preprocessor {
const transform = getCssTransformFn(config);
return async ({ attributes, content, filename = '' }) => {
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);
}
}
import { vitePreprocess } from '../preprocess';

function createVitePreprocessorGroup(config: ResolvedConfig): PreprocessorGroup {
return {
markup({ content, filename }) {
return preprocess(
content,
{
script: createViteScriptPreprocessor(),
style: createViteStylePreprocessor(config)
},
{ filename }
);
return preprocess(content, vitePreprocess({ style: config }), { filename });
}
} as PreprocessorGroup;
};
}

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

if (options.experimental?.useVitePreprocess) {
log.debug('adding vite preprocessor');
log.warn(
'`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.'
);
prependPreprocessors.push(createVitePreprocessorGroup(config));
}

Expand Down

0 comments on commit 0971449

Please sign in to comment.