Skip to content

Commit 066be0c

Browse files
authored
feat: auto-exclude svelte dependencies in vite.optimizeDeps (#145)
* wip: deep search for svelte dependencies * wip: refactor to use recursion, return flat list and prepare for adding optimizeDeps.include * wip: add tests, fix path for createRequire and improve exclusions * wip: improve naming and documentation * cover all dependencies of a default create-svelte project in common dependency test * fix: remove console.log and enable eslint no-console * add changeset * fix: limit eslint no-console to vite-plugin-svelte/src * fix: remove warning log that might confuse user, add extra try/catch to ensure find doesn't stop early * fix: prevent duplicate entries in optimizeDeps.include/exclude
1 parent c2b9e74 commit 066be0c

File tree

11 files changed

+286
-25
lines changed

11 files changed

+286
-25
lines changed

.changeset/four-chairs-rhyme.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': minor
3+
---
4+
5+
automatically exclude svelte dependencies in vite.optimizeDeps

.eslintrc.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ module.exports = {
2222
ecmaVersion: 2020
2323
},
2424
rules: {
25-
'no-debugger': ['error'],
25+
'no-console': 'off',
26+
'no-debugger': 'error',
2627
'node/no-missing-import': [
2728
'error',
2829
{
@@ -58,6 +59,12 @@ module.exports = {
5859
'no-process-exit': 'off'
5960
},
6061
overrides: [
62+
{
63+
files: ['packages/vite-plugin-svelte/src/**'],
64+
rules: {
65+
'no-console': 'error'
66+
}
67+
},
6168
{
6269
files: ['packages/e2e-tests/**', 'packages/playground/**'],
6370
rules: {

packages/e2e-tests/hmr-test-dependency/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"version": "1.0.0",
33
"private": true,
44
"name": "e2e-tests-hmr-test-dependency",
5-
"main": "index.js"
5+
"main": "index.js",
6+
"svelte": "index.js"
67
}

packages/e2e-tests/test-dependency-svelte-field/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
],
1010
"exports": {
1111
"./package.json": "./package.json"
12+
},
13+
"dependencies": {
14+
"e2e-tests-hmr-test-dependency": "workspace:*"
1215
}
1316
}

packages/playground/optimizedeps-include/vite.config.js

-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ const SVELTE_IMPORTS = [
1212
export default defineConfig(({ command, mode }) => {
1313
const isProduction = mode === 'production';
1414
return {
15-
optimizeDeps: {
16-
include: [...SVELTE_IMPORTS]
17-
},
1815
plugins: [svelte()],
1916
build: {
2017
minify: isProduction

packages/vite-plugin-svelte/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"./package.json": "./package.json"
2323
},
2424
"scripts": {
25-
"dev": "pnpm run build:ci -- --watch src",
25+
"dev": "pnpm run build:ci -- --sourcemap --watch src",
2626
"build:ci": "rimraf dist && tsup-node src/index.ts --format esm,cjs --no-splitting",
2727
"build": "pnpm run build:ci -- --dts --sourcemap"
2828
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { findRootSvelteDependencies } from '../dependencies';
2+
import * as path from 'path';
3+
4+
describe('dependencies', () => {
5+
describe('findRootSvelteDependencies', () => {
6+
it('should find svelte dependencies in packages/e2e-test/hmr', async () => {
7+
const deps = findRootSvelteDependencies(path.resolve('packages/e2e-tests/hmr'));
8+
expect(deps).toHaveLength(1);
9+
expect(deps[0].name).toBe('e2e-tests-hmr-test-dependency');
10+
expect(deps[0].path).toEqual([]);
11+
});
12+
it('should find nested svelte dependencies in packages/e2e-test/package-json-svelte-field', async () => {
13+
const deps = findRootSvelteDependencies(
14+
path.resolve('packages/e2e-tests/package-json-svelte-field')
15+
);
16+
expect(deps).toHaveLength(2);
17+
expect(deps[0].name).toBe('e2e-tests-test-dependency-svelte-field');
18+
expect(deps[1].name).toBe('e2e-tests-hmr-test-dependency');
19+
expect(deps[1].path).toEqual(['e2e-tests-test-dependency-svelte-field']);
20+
});
21+
});
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import { log } from './log';
2+
import path from 'path';
3+
import fs from 'fs';
4+
import { createRequire } from 'module';
5+
6+
export function findRootSvelteDependencies(root: string, cwdFallback = true): SvelteDependency[] {
7+
log.debug(`findSvelteDependencies: searching svelte dependencies in ${root}`);
8+
const pkgFile = path.join(root, 'package.json');
9+
if (!fs.existsSync(pkgFile)) {
10+
if (cwdFallback) {
11+
const cwd = process.cwd();
12+
if (root !== cwd) {
13+
log.debug(`no package.json found in vite root ${root}`);
14+
return findRootSvelteDependencies(cwd, false);
15+
}
16+
}
17+
log.warn(`no package.json found, findRootSvelteDependencies failed`);
18+
return [];
19+
}
20+
21+
const pkg = parsePkg(root);
22+
if (!pkg) {
23+
return [];
24+
}
25+
26+
const deps = [
27+
...Object.keys(pkg.dependencies || {}),
28+
...Object.keys(pkg.devDependencies || {})
29+
].filter((dep) => !is_common_without_svelte_field(dep));
30+
31+
return getSvelteDependencies(deps, root);
32+
}
33+
34+
function getSvelteDependencies(
35+
deps: string[],
36+
pkgDir: string,
37+
path: string[] = []
38+
): SvelteDependency[] {
39+
const result = [];
40+
const localRequire = createRequire(`${pkgDir}/package.json`);
41+
const resolvedDeps = deps
42+
.map((dep) => resolveSvelteDependency(dep, localRequire))
43+
.filter(Boolean);
44+
// @ts-ignore
45+
for (const { pkg, dir } of resolvedDeps) {
46+
result.push({ name: pkg.name, pkg, dir, path });
47+
if (pkg.dependencies) {
48+
let dependencyNames = Object.keys(pkg.dependencies);
49+
const circular = dependencyNames.filter((name) => path.includes(name));
50+
if (circular.length > 0) {
51+
log.warn.enabled &&
52+
log.warn(
53+
`skipping circular svelte dependencies in automated vite optimizeDeps handling`,
54+
circular.map((x) => path.concat(x).join('>'))
55+
);
56+
dependencyNames = dependencyNames.filter((name) => !path.includes(name));
57+
}
58+
if (path.length === 3) {
59+
log.debug.once(`encountered deep svelte dependency tree: ${path.join('>')}`);
60+
}
61+
result.push(...getSvelteDependencies(dependencyNames, dir, path.concat(pkg.name)));
62+
}
63+
}
64+
return result;
65+
}
66+
67+
function resolveSvelteDependency(
68+
dep: string,
69+
localRequire: NodeRequire
70+
): { dir: string; pkg: Pkg } | void {
71+
try {
72+
const pkgJson = `${dep}/package.json`;
73+
const pkg = localRequire(pkgJson);
74+
if (!isSvelte(pkg)) {
75+
return;
76+
}
77+
const dir = path.dirname(localRequire.resolve(pkgJson));
78+
return { dir, pkg };
79+
} catch (e) {
80+
log.debug.once(`dependency ${dep} does not export package.json`, e);
81+
// walk up from default export until we find package.json with name=dep
82+
try {
83+
let dir = path.dirname(localRequire.resolve(dep));
84+
while (dir) {
85+
const pkg = parsePkg(dir, true);
86+
if (pkg && pkg.name === dep) {
87+
if (!isSvelte(pkg)) {
88+
return;
89+
}
90+
log.warn.once(
91+
`package.json of ${dep} has a "svelte" field but does not include itself in exports field. Please ask package maintainer to update`
92+
);
93+
return { dir, pkg };
94+
}
95+
const parent = path.dirname(dir);
96+
if (parent === dir) {
97+
break;
98+
}
99+
dir = parent;
100+
}
101+
} catch (e) {
102+
log.debug.once(`error while trying to find package.json of ${dep}`, e);
103+
}
104+
}
105+
log.debug.once(`failed to resolve ${dep}`);
106+
}
107+
108+
function parsePkg(dir: string, silent = false): Pkg | void {
109+
const pkgFile = path.join(dir, 'package.json');
110+
try {
111+
return JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
112+
} catch (e) {
113+
!silent && log.warn.enabled && log.warn(`failed to parse ${pkgFile}`, e);
114+
}
115+
}
116+
117+
function isSvelte(pkg: Pkg) {
118+
return !!pkg.svelte;
119+
}
120+
121+
const COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [
122+
'@lukeed/uuid',
123+
'@sveltejs/vite-plugin-svelte',
124+
'@sveltejs/kit',
125+
'autoprefixer',
126+
'cookie',
127+
'dotenv',
128+
'esbuild',
129+
'eslint',
130+
'jest',
131+
'mdsvex',
132+
'postcss',
133+
'prettier',
134+
'svelte',
135+
'svelte-check',
136+
'svelte-hmr',
137+
'svelte-preprocess',
138+
'tslib',
139+
'typescript',
140+
'vite'
141+
];
142+
const COMMON_PREFIXES_WITHOUT_SVELTE_FIELD = [
143+
'@fontsource/',
144+
'@postcss-plugins/',
145+
'@rollup/',
146+
'@sveltejs/adapter-',
147+
'@types/',
148+
'@typescript-eslint/',
149+
'eslint-',
150+
'jest-',
151+
'postcss-plugin-',
152+
'prettier-plugin-',
153+
'rollup-plugin-',
154+
'vite-plugin-'
155+
];
156+
157+
/**
158+
* Test for common dependency names that tell us it is not a package including a svelte field, eg. eslint + plugins.
159+
*
160+
* This speeds up the find process as we don't have to try and require the package.json for all of them
161+
*
162+
* @param dependency {string}
163+
* @returns {boolean} true if it is a dependency without a svelte field
164+
*/
165+
function is_common_without_svelte_field(dependency: string): boolean {
166+
return (
167+
COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD.includes(dependency) ||
168+
COMMON_PREFIXES_WITHOUT_SVELTE_FIELD.some(
169+
(prefix) =>
170+
prefix.startsWith('@')
171+
? dependency.startsWith(prefix)
172+
: dependency.substring(dependency.lastIndexOf('/') + 1).startsWith(prefix) // check prefix omitting @scope/
173+
)
174+
);
175+
}
176+
177+
export interface SvelteDependency {
178+
name: string;
179+
dir: string;
180+
pkg: Pkg;
181+
path: string[];
182+
}
183+
184+
export interface Pkg {
185+
name: string;
186+
svelte?: string;
187+
dependencies?: DependencyList;
188+
devDependencies?: DependencyList;
189+
[key: string]: any;
190+
}
191+
192+
export interface DependencyList {
193+
[key: string]: string;
194+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* eslint-disable no-unused-vars */
1+
/* eslint-disable no-unused-vars,no-console */
22
import { cyan, yellow, red } from 'kleur/colors';
33
import debug from 'debug';
44
import { ResolvedOptions, Warning } from './options';

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

+46-17
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
// eslint-disable-next-line node/no-missing-import
1414
} from 'svelte/types/compiler/preprocess';
1515
import path from 'path';
16+
import { findRootSvelteDependencies } from './dependencies';
17+
import { DepOptimizationOptions } from 'vite/src/node/optimizer/index';
1618

1719
const knownOptions = new Set([
1820
'configFile',
@@ -179,24 +181,8 @@ export function buildExtraViteConfig(
179181
options: ResolvedOptions,
180182
config: UserConfig
181183
): Partial<UserConfig> {
182-
// include svelte imports for optimization unless explicitly excluded
183-
const include: string[] = [];
184-
const exclude: string[] = ['svelte-hmr'];
185-
const isSvelteExcluded = config.optimizeDeps?.exclude?.includes('svelte');
186-
if (!isSvelteExcluded) {
187-
const svelteImportsToInclude = SVELTE_IMPORTS.filter((x) => x !== 'svelte/ssr'); // not used on clientside
188-
log.debug(
189-
`adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `
190-
);
191-
include.push(...svelteImportsToInclude);
192-
} else {
193-
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
194-
}
195184
const extraViteConfig: Partial<UserConfig> = {
196-
optimizeDeps: {
197-
include,
198-
exclude
199-
},
185+
optimizeDeps: buildOptimizeDepsForSvelte(options.root, config.optimizeDeps),
200186
resolve: {
201187
mainFields: [...SVELTE_RESOLVE_MAIN_FIELDS],
202188
dedupe: [...SVELTE_IMPORTS, ...SVELTE_HMR_IMPORTS]
@@ -233,6 +219,49 @@ export function buildExtraViteConfig(
233219
return extraViteConfig;
234220
}
235221

222+
function buildOptimizeDepsForSvelte(
223+
root: string,
224+
optimizeDeps?: DepOptimizationOptions
225+
): DepOptimizationOptions {
226+
// include svelte imports for optimization unless explicitly excluded
227+
const include: string[] = [];
228+
const exclude: string[] = ['svelte-hmr'];
229+
const isSvelteExcluded = optimizeDeps?.exclude?.includes('svelte');
230+
if (!isSvelteExcluded) {
231+
const svelteImportsToInclude = SVELTE_IMPORTS.filter((x) => x !== 'svelte/ssr'); // not used on clientside
232+
log.debug(
233+
`adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `
234+
);
235+
include.push(...svelteImportsToInclude.filter((x) => !optimizeDeps?.include?.includes(x)));
236+
} else {
237+
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
238+
}
239+
240+
// extra handling for svelte dependencies in the project
241+
const svelteDeps = findRootSvelteDependencies(root);
242+
const svelteDepsToExclude = Array.from(new Set(svelteDeps.map((dep) => dep.name))).filter(
243+
(dep) => !optimizeDeps?.include?.includes(dep)
244+
);
245+
log.debug(`automatically excluding found svelte dependencies: ${svelteDepsToExclude.join(', ')}`);
246+
exclude.push(...svelteDepsToExclude.filter((x) => !optimizeDeps?.exclude?.includes(x)));
247+
248+
/* // TODO enable once https://github.com/vitejs/vite/pull/4634 lands
249+
const transitiveDepsToInclude = svelteDeps
250+
.filter((dep) => svelteDepsToExclude.includes(dep.name))
251+
.flatMap((dep) =>
252+
Object.keys(dep.pkg.dependencies || {})
253+
.filter((depOfDep) => !svelteDepsToExclude.includes(depOfDep))
254+
.map((depOfDep) => dep.path.concat(depOfDep).join('>'))
255+
);
256+
log.debug(
257+
`reincluding transitive dependencies of excluded svelte dependencies`,
258+
transitiveDepsToInclude
259+
);
260+
include.push(...transitiveDepsToInclude);
261+
*/
262+
return { include, exclude };
263+
}
264+
236265
export interface Options {
237266
// eslint-disable no-unused-vars
238267
/** path to svelte config file, either absolute or relative to vite root*/

pnpm-lock.yaml

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)