Skip to content
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

feat: auto-exclude svelte dependencies in vite.optimizeDeps #145

Merged
merged 10 commits into from
Aug 20, 2021
3 changes: 0 additions & 3 deletions packages/playground/optimizedeps-include/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ const SVELTE_IMPORTS = [
export default defineConfig(({ command, mode }) => {
const isProduction = mode === 'production';
return {
optimizeDeps: {
include: [...SVELTE_IMPORTS]
},
plugins: [svelte()],
build: {
minify: isProduction
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"./package.json": "./package.json"
},
"scripts": {
"dev": "pnpm run build:ci -- --watch src",
"dev": "pnpm run build:ci -- --sourcemap --watch src",
"build:ci": "rimraf dist && tsup-node src/index.ts --format esm,cjs --no-splitting",
"build": "pnpm run build:ci -- --dts --sourcemap"
},
Expand Down
156 changes: 156 additions & 0 deletions packages/vite-plugin-svelte/src/utils/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { log } from './log';
import path from 'path';
import fs from 'fs';
import { createRequire } from 'module';

export function findSvelteDependencies(root: string, cwdFallback = true): SvelteDependency[] {
dominikg marked this conversation as resolved.
Show resolved Hide resolved
log.debug(`findSvelteDependencies: searching svelte dependencies in ${root}`);
const pkgFile = path.join(root, 'package.json');
if (!fs.existsSync(pkgFile)) {
if (cwdFallback) {
const cwd = process.cwd();
if (root !== cwd) {
log.debug(`no package.json found in vite root ${root}`);
return findSvelteDependencies(cwd, false);
}
}
log.debug(`no package.json found, search failed`);
dominikg marked this conversation as resolved.
Show resolved Hide resolved
return [];
}

const pkg = parsePkg(root);
if (!pkg) {
return [];
}

const deps = [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.devDependencies || {})
].filter((dep) => !excludeFromScan(dep));

return getSvelteDependencies(deps, root);
}

function getSvelteDependencies(
deps: string[],
pkgDir: string,
path: string[] = []
): SvelteDependency[] {
const result = [];
const require = createRequire(pkgDir);
const resolvedDeps = deps.map((dep) => resolveSvelteDependency(dep, require)).filter(Boolean);
// @ts-ignore
for (const { pkg, dir } of resolvedDeps) {
result.push({ name: pkg.name, pkg, dir, path });
if (pkg.dependencies) {
let dependencyNames = Object.keys(pkg.dependencies);
const circular = dependencyNames.filter((name) => path.includes(name));
if (circular.length > 0) {
log.warn.enabled &&
log.warn(
`skipping circular svelte dependencies in automated vite optimizeDeps handling`,
circular.map((x) => path.concat(x).join('>'))
);
dependencyNames = dependencyNames.filter((name) => !path.includes(name));
}
if (path.length === 3) {
log.warn.once(`encountered deep svelte dependency tree ${path.join('>')}`);
}
result.push(...getSvelteDependencies(dependencyNames, dir, path.concat(pkg.name)));
}
}
return result;
}

function resolveSvelteDependency(
dep: string,
require: NodeRequire
): { dir: string; pkg: Pkg } | void {
try {
const pkgJson = `${dep}/package.json`;
const pkg = require(pkgJson);
if (!isSvelte(pkg)) {
return;
}
const dir = path.dirname(require.resolve(pkgJson));
return { dir, pkg };
} catch (e) {
log.debug.once(`dependency ${dep} does not export package.json`, e);
dominikg marked this conversation as resolved.
Show resolved Hide resolved
// walk up from default export until we find package.json with name=dep
let dir = path.dirname(require.resolve(dep));
while (dir) {
const pkg = parsePkg(dir, true);
if (pkg && pkg.name === dep) {
if (!isSvelte(pkg)) {
return;
}
log.warn(`package ${dep} has a "svelte" field but does not export it's package.json`);
return { dir, pkg };
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
}
log.debug.once(`failed to resolve ${dep}`);
}

function parsePkg(dir: string, silent = false): Pkg | void {
const pkgFile = path.join(dir, 'package.json');
try {
return JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
} catch (e) {
!silent && log.warn.enabled && log.warn(`failed to parse ${pkgFile}`, e);
}
}

function isSvelte(pkg: Pkg) {
return !!pkg.svelte;
}

const EXCLUDE = [
'@sveltejs/vite-plugin-svelte',
'@sveltejs/kit',
'svelte',
'svelte-hmr',
'svelte-preprocess',
'eslint',
'prettier',
'vite',
'postcss'
];
const EXCLUDE_PREFIX = [
'@types/',
'@rollup/',
'@sveltejs/adapter-',
'eslint-plugin-',
'prettier-plugin-',
'postcss-plugin-',
'@postcss-plugins/',
'@rollup/'
];

function excludeFromScan(dep: string): boolean {
dominikg marked this conversation as resolved.
Show resolved Hide resolved
return EXCLUDE.some((ex) => dep === ex) || EXCLUDE_PREFIX.some((ex) => dep.startsWith(ex));
}

export interface SvelteDependency {
name: string;
dir: string;
pkg: Pkg;
path: string[];
}

export interface Pkg {
name: string;
svelte?: string;
dependencies?: DependencyList;
devDependencies?: DependencyList;
[key: string]: any;
}

export interface DependencyList {
[key: string]: string;
}
64 changes: 47 additions & 17 deletions packages/vite-plugin-svelte/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
// eslint-disable-next-line node/no-missing-import
} from 'svelte/types/compiler/preprocess';
import path from 'path';
import { findSvelteDependencies } from './dependencies';
import { DepOptimizationOptions } from 'vite/src/node/optimizer/index';

const knownOptions = new Set([
'configFile',
Expand Down Expand Up @@ -179,24 +181,8 @@ export function buildExtraViteConfig(
options: ResolvedOptions,
config: UserConfig
): Partial<UserConfig> {
// include svelte imports for optimization unless explicitly excluded
const include: string[] = [];
const exclude: string[] = ['svelte-hmr'];
const isSvelteExcluded = config.optimizeDeps?.exclude?.includes('svelte');
if (!isSvelteExcluded) {
const svelteImportsToInclude = SVELTE_IMPORTS.filter((x) => x !== 'svelte/ssr'); // not used on clientside
log.debug(
`adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `
);
include.push(...svelteImportsToInclude);
} else {
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
}
const extraViteConfig: Partial<UserConfig> = {
optimizeDeps: {
include,
exclude
},
optimizeDeps: buildOptimizeDepsForSvelte(options.root, config.optimizeDeps),
resolve: {
mainFields: [...SVELTE_RESOLVE_MAIN_FIELDS],
dedupe: [...SVELTE_IMPORTS, ...SVELTE_HMR_IMPORTS]
Expand Down Expand Up @@ -233,6 +219,50 @@ export function buildExtraViteConfig(
return extraViteConfig;
}

function buildOptimizeDepsForSvelte(
root: string,
optimizeDeps?: DepOptimizationOptions
): DepOptimizationOptions {
// include svelte imports for optimization unless explicitly excluded
const include: string[] = [];
const exclude: string[] = ['svelte-hmr'];
const isSvelteExcluded = optimizeDeps?.exclude?.includes('svelte');
if (!isSvelteExcluded) {
const svelteImportsToInclude = SVELTE_IMPORTS.filter((x) => x !== 'svelte/ssr'); // not used on clientside
log.debug(
`adding bare svelte packages to optimizeDeps.include: ${svelteImportsToInclude.join(', ')} `
);
include.push(...svelteImportsToInclude);
} else {
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
}

// extra handling for svelte dependencies in the project
const svelteDeps = findSvelteDependencies(root);
console.log('svelteDeps', svelteDeps);
const svelteDepsToExclude = Array.from(new Set(svelteDeps.map((dep) => dep.name))).filter(
(dep) => !optimizeDeps?.include?.includes(dep)
);
log.debug(`automatically excluding found svelte dependencies: ${svelteDepsToExclude.join(', ')}`);
exclude.push(...svelteDepsToExclude);

/* // TODO enable once https://github.com/vitejs/vite/pull/4634 lands
const transitiveDepsToInclude = svelteDeps
.filter((dep) => svelteDepsToExclude.includes(dep.name))
.flatMap((dep) =>
Object.keys(dep.pkg.dependencies || {})
.filter((depOfDep) => !svelteDepsToExclude.includes(depOfDep))
.map((depOfDep) => dep.path.concat(depOfDep).join('>'))
);
log.debug(
`reincluding transitive dependencies of excluded svelte dependencies`,
transitiveDepsToInclude
);
include.push(...transitiveDepsToInclude);
*/
return { include, exclude };
}

export interface Options {
// eslint-disable no-unused-vars
/** path to svelte config file, either absolute or relative to vite root*/
Expand Down