Skip to content

feat: add vite.addPlugin #633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jul 26, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
61ea4dd
wip
jycouet Jul 15, 2025
991420a
testing things out
jycouet Jul 15, 2025
cc5eb4a
testing things out 2
jycouet Jul 15, 2025
6b30cec
append & prepend
jycouet Jul 15, 2025
40ddef6
looks cool already
jycouet Jul 15, 2025
defc00c
split
jycouet Jul 15, 2025
122ac46
some cleanup
jycouet Jul 15, 2025
2c78390
adding some tests
jycouet Jul 15, 2025
7ba3876
api could look like this
jycouet Jul 15, 2025
42b2052
Merge branch 'main' of github.com:sveltejs/cli into add-plugin-to-vite
jycouet Jul 18, 2025
746b849
refactor location
jycouet Jul 18, 2025
fe70e14
refacto to test style of repo
jycouet Jul 18, 2025
f344a3d
managing also function return (my case actually!)
jycouet Jul 18, 2025
6adc1ee
playing with api style
jycouet Jul 18, 2025
394daa2
lol! that's even simpler now!
jycouet Jul 18, 2025
4726fc2
add convenience imports.addNamed
jycouet Jul 18, 2025
69041ed
update paraglide to new style
jycouet Jul 18, 2025
13c7c1e
update tailwind to new style
jycouet Jul 18, 2025
533e562
.
jycouet Jul 18, 2025
9e9a175
Update .changeset/tough-carrots-eat.md
jycouet Jul 20, 2025
24160b0
chore: allow passing an array of import names to imports.addNamed
jycouet Jul 20, 2025
0326995
update usage
jycouet Jul 20, 2025
ef52ff9
Merge branch 'chore/imports-array' of github.com:jycouet/cli into add…
jycouet Jul 20, 2025
f53fbf0
Merge branch 'add-plugin-to-vite' of github.com:jycouet/cli into add-…
jycouet Jul 20, 2025
f3e47a1
update mode test
jycouet Jul 20, 2025
b299283
bailing out early
jycouet Jul 20, 2025
cfc1102
Merge branch 'main' of github.com:sveltejs/cli into add-plugin-to-vite
jycouet Jul 25, 2025
718f65f
already merged with the other PR. (an better naming)
jycouet Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 9 additions & 21 deletions packages/addons/devtools-json/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineAddon } from '@sveltejs/cli-core';
import { array, functions, imports, object, exports } from '@sveltejs/cli-core/js';
import { parseScript } from '@sveltejs/cli-core/parsers';
import { imports } from '@sveltejs/cli-core/js';
import { addInArrayOfObject, addPluginToViteConfig } from '../../core/tooling/helpers.ts';

export default defineAddon({
id: 'devtools-json',
Expand All @@ -15,26 +15,14 @@ export default defineAddon({

// add the vite plugin
sv.file(`vite.config.${ext}`, (content) => {
const { ast, generateCode } = parseScript(content);

const vitePluginName = 'devtoolsJson';
imports.addDefault(ast, { from: 'vite-plugin-devtools-json', as: vitePluginName });

const { value: rootObject } = exports.createDefault(ast, {
fallback: functions.createCall({ name: 'defineConfig', args: [] })
});

const param1 = functions.getArgument(rootObject, {
index: 0,
fallback: object.create({})
return addPluginToViteConfig(content, (ast, configObject) => {
const vitePluginName = 'devtoolsJson';
imports.addDefault(ast, { from: 'vite-plugin-devtools-json', as: vitePluginName });
addInArrayOfObject(configObject, {
array: 'plugins',
code: `${vitePluginName}()`
});
});

const pluginsArray = object.property(param1, { name: 'plugins', fallback: array.create() });
const pluginFunctionCall = functions.createCall({ name: vitePluginName, args: [] });

array.append(pluginsArray, pluginFunctionCall);

return generateCode();
});
}
});
240 changes: 240 additions & 0 deletions packages/core/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { describe, expect, it } from 'vitest';
import {
addPluginToViteConfig,
exportDefaultConfig,
addInArrayOfObject
} from '../tooling/helpers.ts';
import { parseScript } from '../tooling/parsers.ts';
import { imports } from '../dist/js.js';

const config_default = `
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [sveltekit()]
});
`;

const config_w_variable = `
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

const config = defineConfig({
plugins: [sveltekit()]
});

export default config;
`;

const config_wo_defineConfig = `
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite';

const config: UserConfig = {
plugins: [sveltekit()]
};

export default config;
`;

describe('helpers', () => {
describe('getConfigObject', () => {
it('gets object from defineConfig wrapper', () => {
const { ast } = parseScript(config_default);
const configObject = exportDefaultConfig(ast, { ignoreWrapper: 'defineConfig' });

expect(configObject.type).toBe('ObjectExpression');
expect(configObject.properties).toHaveLength(1);
});

it('gets object without wrapper', () => {
const { ast } = parseScript(config_wo_defineConfig);
const configObject = exportDefaultConfig(ast);

expect(configObject.type).toBe('ObjectExpression');
expect(configObject.properties).toHaveLength(1);
});

it('creates fallback config when no export default exists', () => {
const { ast } = parseScript(`import { someHelper } from './helper';`);
const configObject = exportDefaultConfig(ast, {
fallback: 'defineConfig({ build: { target: "es2015" } })',
ignoreWrapper: 'defineConfig'
});

expect(configObject.type).toBe('ObjectExpression');
expect(configObject.properties).toHaveLength(1);
});
});

describe('addToObjectArray', () => {
it('adds to existing array property', () => {
const { ast, generateCode } = parseScript(config_default);
const configObject = exportDefaultConfig(ast, { ignoreWrapper: 'defineConfig' });

imports.addDefault(ast, { from: 'my-best-plugin', as: 'newPlugin' });
addInArrayOfObject(configObject, {
code: 'newPlugin({ hello: "world" })',
array: 'plugins',
mode: 'append'
});

expect(generateCode()).toMatchInlineSnapshot(`
"import newPlugin from 'my-best-plugin';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
sveltekit(),
newPlugin({ hello: 'world' })
]
});"
`);
});

it('creates new array property', () => {
const { ast, generateCode } = parseScript(config_default);
const configObject = exportDefaultConfig(ast, { ignoreWrapper: 'defineConfig' });

imports.addDefault(ast, { from: 'eslint', as: 'eslint' });
addInArrayOfObject(configObject, {
code: 'eslint()',
array: 'tools',
mode: 'append'
});

expect(generateCode()).toMatchInlineSnapshot(`
"import eslint from 'eslint';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [sveltekit()],
tools: [eslint()]
});"
`);
});

it('prepends to array', () => {
const { ast, generateCode } = parseScript(config_default);
const configObject = exportDefaultConfig(ast, { ignoreWrapper: 'defineConfig' });

imports.addDefault(ast, { from: 'firstPlugin', as: 'firstPlugin' });
addInArrayOfObject(configObject, {
code: 'firstPlugin()',
array: 'plugins',
mode: 'prepend'
});

expect(generateCode()).toMatchInlineSnapshot(`
"import firstPlugin from 'firstPlugin';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [firstPlugin(), sveltekit()]
});"
`);
});

it('multiple items with different modes', () => {
const { ast, generateCode } = parseScript(config_default);
const configObject = exportDefaultConfig(ast, { ignoreWrapper: 'defineConfig' });

// Add to end first
imports.addDefault(ast, { from: 'lastPlugin', as: 'lastPlugin' });
addInArrayOfObject(configObject, {
code: 'lastPlugin()',
array: 'plugins',
mode: 'append'
});

// Then add to beginning
imports.addDefault(ast, { from: 'firstPlugin', as: 'firstPlugin' });
addInArrayOfObject(configObject, {
code: 'firstPlugin()',
array: 'plugins',
mode: 'prepend'
});

expect(generateCode()).toMatchInlineSnapshot(`
"import firstPlugin from 'firstPlugin';
import lastPlugin from 'lastPlugin';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
firstPlugin(),
sveltekit(),
lastPlugin()
]
});"
`);
});
});

describe('addPluginToViteConfig', () => {
const addTestPlugin = (content: string) =>
addPluginToViteConfig(content, (ast, configObject) => {
const vitePluginName = 'bestPlugin';
imports.addDefault(ast, { from: 'my-best-plugin', as: vitePluginName });

addInArrayOfObject(configObject, {
array: 'plugins',
code: `${vitePluginName}()`
});
});

it('empty config', () => {
expect(addTestPlugin(``)).toMatchInlineSnapshot(`
"import bestPlugin from 'my-best-plugin';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [bestPlugin()] });"
`);
});

it('config_default', () => {
expect(addTestPlugin(config_default)).toMatchInlineSnapshot(`
"import bestPlugin from 'my-best-plugin';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [sveltekit(), bestPlugin()]
});"
`);
});

it('config_w_variable', () => {
expect(addTestPlugin(config_w_variable)).toMatchInlineSnapshot(`
"import bestPlugin from 'my-best-plugin';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

const config = defineConfig({
plugins: [sveltekit(), bestPlugin()]
});

export default config;"
`);
});

it('config_wo_defineConfig', () => {
expect(addTestPlugin(config_wo_defineConfig)).toMatchInlineSnapshot(`
"import bestPlugin from 'my-best-plugin';
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig, defineConfig } from 'vite';

const config: UserConfig = {
plugins: [sveltekit(), bestPlugin()]
};

export default config;"
`);
});
});
});
94 changes: 94 additions & 0 deletions packages/core/tooling/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { array, functions, imports, object, exports, type AstTypes, common } from './js/index.ts';
import { parseScript } from './parsers.ts';

export function exportDefaultConfig(
ast: AstTypes.Program,
options: {
fallback?: AstTypes.Expression | string;
ignoreWrapper?: string;
} = {}
): AstTypes.ObjectExpression {
const { fallback, ignoreWrapper } = options;

// Get or create the default export
let fallbackExpression: AstTypes.Expression;
if (fallback) {
fallbackExpression = typeof fallback === 'string' ? common.parseExpression(fallback) : fallback;
} else {
fallbackExpression = object.create({});
}

const { value: rootObject } = exports.createDefault(ast, { fallback: fallbackExpression });

// Handle wrapper functions (e.g., defineConfig({})) if ignoreWrapper is specified
let configObject: AstTypes.ObjectExpression;
if (ignoreWrapper && 'arguments' in rootObject && Array.isArray(rootObject.arguments)) {
// Check if this is the wrapper we want to ignore
if (
rootObject.type === 'CallExpression' &&
rootObject.callee.type === 'Identifier' &&
rootObject.callee.name === ignoreWrapper
) {
// For wrapper function calls like defineConfig({})
configObject = functions.getArgument(rootObject as any, {
index: 0,
fallback: object.create({})
});
} else {
// For other function calls, treat as the config object
configObject = rootObject as unknown as AstTypes.ObjectExpression;
}
} else {
// For plain object literals
configObject = rootObject as unknown as AstTypes.ObjectExpression;
}

return configObject;
}

export function addInArrayOfObject(
ast: AstTypes.ObjectExpression,
options: {
array: string;
code: string;
/** default: `append` */
mode?: 'append' | 'prepend';
}
): void {
const { code, array: arrayProperty, mode = 'append' } = options;

// Get or create the array property
const targetArray = object.property(ast, {
name: arrayProperty,
fallback: array.create()
});

// Parse the expression
const expression = common.parseExpression(code);

// Add to array based on mode
if (mode === 'prepend') {
array.prepend(targetArray, expression);
} else {
array.append(targetArray, expression);
}
}

export const addPluginToViteConfig = (
content: string,
fn: (ast: AstTypes.Program, configObject: AstTypes.ObjectExpression) => void
): string => {
const { ast, generateCode } = parseScript(content);

// Step 1: Get the config object, or fallback.
imports.addNamed(ast, { from: 'vite', imports: { defineConfig: 'defineConfig' } });
const configObject = exportDefaultConfig(ast, {
fallback: 'defineConfig()',
ignoreWrapper: 'defineConfig'
});

// Step 2: Add the plugin to the plugins array
fn(ast, configObject);

return generateCode();
};
Loading