Skip to content

Commit

Permalink
feat: auto-exclude svelte dependencies in vite.optimizeDeps (#145)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dominikg authored Aug 20, 2021
1 parent c2b9e74 commit 066be0c
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-chairs-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': minor
---

automatically exclude svelte dependencies in vite.optimizeDeps
9 changes: 8 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ module.exports = {
ecmaVersion: 2020
},
rules: {
'no-debugger': ['error'],
'no-console': 'off',
'no-debugger': 'error',
'node/no-missing-import': [
'error',
{
Expand Down Expand Up @@ -58,6 +59,12 @@ module.exports = {
'no-process-exit': 'off'
},
overrides: [
{
files: ['packages/vite-plugin-svelte/src/**'],
rules: {
'no-console': 'error'
}
},
{
files: ['packages/e2e-tests/**', 'packages/playground/**'],
rules: {
Expand Down
3 changes: 2 additions & 1 deletion packages/e2e-tests/hmr-test-dependency/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"version": "1.0.0",
"private": true,
"name": "e2e-tests-hmr-test-dependency",
"main": "index.js"
"main": "index.js",
"svelte": "index.js"
}
3 changes: 3 additions & 0 deletions packages/e2e-tests/test-dependency-svelte-field/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
],
"exports": {
"./package.json": "./package.json"
},
"dependencies": {
"e2e-tests-hmr-test-dependency": "workspace:*"
}
}
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { findRootSvelteDependencies } from '../dependencies';
import * as path from 'path';

describe('dependencies', () => {
describe('findRootSvelteDependencies', () => {
it('should find svelte dependencies in packages/e2e-test/hmr', async () => {
const deps = findRootSvelteDependencies(path.resolve('packages/e2e-tests/hmr'));
expect(deps).toHaveLength(1);
expect(deps[0].name).toBe('e2e-tests-hmr-test-dependency');
expect(deps[0].path).toEqual([]);
});
it('should find nested svelte dependencies in packages/e2e-test/package-json-svelte-field', async () => {
const deps = findRootSvelteDependencies(
path.resolve('packages/e2e-tests/package-json-svelte-field')
);
expect(deps).toHaveLength(2);
expect(deps[0].name).toBe('e2e-tests-test-dependency-svelte-field');
expect(deps[1].name).toBe('e2e-tests-hmr-test-dependency');
expect(deps[1].path).toEqual(['e2e-tests-test-dependency-svelte-field']);
});
});
});
194 changes: 194 additions & 0 deletions packages/vite-plugin-svelte/src/utils/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { log } from './log';
import path from 'path';
import fs from 'fs';
import { createRequire } from 'module';

export function findRootSvelteDependencies(root: string, cwdFallback = true): SvelteDependency[] {
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 findRootSvelteDependencies(cwd, false);
}
}
log.warn(`no package.json found, findRootSvelteDependencies failed`);
return [];
}

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

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

return getSvelteDependencies(deps, root);
}

function getSvelteDependencies(
deps: string[],
pkgDir: string,
path: string[] = []
): SvelteDependency[] {
const result = [];
const localRequire = createRequire(`${pkgDir}/package.json`);
const resolvedDeps = deps
.map((dep) => resolveSvelteDependency(dep, localRequire))
.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.debug.once(`encountered deep svelte dependency tree: ${path.join('>')}`);
}
result.push(...getSvelteDependencies(dependencyNames, dir, path.concat(pkg.name)));
}
}
return result;
}

function resolveSvelteDependency(
dep: string,
localRequire: NodeRequire
): { dir: string; pkg: Pkg } | void {
try {
const pkgJson = `${dep}/package.json`;
const pkg = localRequire(pkgJson);
if (!isSvelte(pkg)) {
return;
}
const dir = path.dirname(localRequire.resolve(pkgJson));
return { dir, pkg };
} catch (e) {
log.debug.once(`dependency ${dep} does not export package.json`, e);
// walk up from default export until we find package.json with name=dep
try {
let dir = path.dirname(localRequire.resolve(dep));
while (dir) {
const pkg = parsePkg(dir, true);
if (pkg && pkg.name === dep) {
if (!isSvelte(pkg)) {
return;
}
log.warn.once(
`package.json of ${dep} has a "svelte" field but does not include itself in exports field. Please ask package maintainer to update`
);
return { dir, pkg };
}
const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
} catch (e) {
log.debug.once(`error while trying to find package.json of ${dep}`, e);
}
}
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 COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD = [
'@lukeed/uuid',
'@sveltejs/vite-plugin-svelte',
'@sveltejs/kit',
'autoprefixer',
'cookie',
'dotenv',
'esbuild',
'eslint',
'jest',
'mdsvex',
'postcss',
'prettier',
'svelte',
'svelte-check',
'svelte-hmr',
'svelte-preprocess',
'tslib',
'typescript',
'vite'
];
const COMMON_PREFIXES_WITHOUT_SVELTE_FIELD = [
'@fontsource/',
'@postcss-plugins/',
'@rollup/',
'@sveltejs/adapter-',
'@types/',
'@typescript-eslint/',
'eslint-',
'jest-',
'postcss-plugin-',
'prettier-plugin-',
'rollup-plugin-',
'vite-plugin-'
];

/**
* Test for common dependency names that tell us it is not a package including a svelte field, eg. eslint + plugins.
*
* This speeds up the find process as we don't have to try and require the package.json for all of them
*
* @param dependency {string}
* @returns {boolean} true if it is a dependency without a svelte field
*/
function is_common_without_svelte_field(dependency: string): boolean {
return (
COMMON_DEPENDENCIES_WITHOUT_SVELTE_FIELD.includes(dependency) ||
COMMON_PREFIXES_WITHOUT_SVELTE_FIELD.some(
(prefix) =>
prefix.startsWith('@')
? dependency.startsWith(prefix)
: dependency.substring(dependency.lastIndexOf('/') + 1).startsWith(prefix) // check prefix omitting @scope/
)
);
}

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;
}
2 changes: 1 addition & 1 deletion packages/vite-plugin-svelte/src/utils/log.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable no-unused-vars */
/* eslint-disable no-unused-vars,no-console */
import { cyan, yellow, red } from 'kleur/colors';
import debug from 'debug';
import { ResolvedOptions, Warning } from './options';
Expand Down
63 changes: 46 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 { findRootSvelteDependencies } 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,49 @@ 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.filter((x) => !optimizeDeps?.include?.includes(x)));
} else {
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
}

// extra handling for svelte dependencies in the project
const svelteDeps = findRootSvelteDependencies(root);
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.filter((x) => !optimizeDeps?.exclude?.includes(x)));

/* // 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
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 066be0c

Please sign in to comment.