knip
Version:
Find and fix unused dependencies, exports and files in your TypeScript and JavaScript projects
205 lines (204 loc) • 8.96 kB
JavaScript
import { isDirectory } from "../../util/fs.js";
import { _syncGlob } from "../../util/glob.js";
import { toAlias, toConfig, toDeferResolveProductionEntry, toDependency, toEntry, toIgnore, toProductionEntry, } from "../../util/input.js";
import { loadTSConfig } from "../../util/load-tsconfig.js";
import { join } from "../../util/path.js";
import { hasDependency } from "../../util/plugin.js";
import { buildAutoImportMap, collectIdentifiers, collectLocalImportPaths, collectTemplateInfo, createSourceFile, getVueSfc, toKebabCase, } from "./helpers.js";
const title = 'Nuxt';
const enablers = ['nuxt', 'nuxt-nightly'];
const isEnabled = ({ dependencies }) => hasDependency(dependencies, enablers);
const config = ['nuxt.config.{js,mjs,ts}'];
const entry = ['app.config.ts', '**/*.d.vue.ts'];
const app = ['app.{vue,jsx,tsx}', 'error.{vue,jsx,tsx}', 'router.options.ts'];
const layout = (dir = 'layouts') => join(dir, '**/*.{vue,jsx,tsx}');
const middleware = (dir = 'middleware') => join(dir, '**/*.ts');
const pages = (dir = 'pages') => join(dir, '**/*.{vue,jsx,tsx}');
const plugins = (dir = 'plugins') => join(dir, '**/*.ts');
const modules = 'modules/**/*.{ts,vue}';
const server = ['api/**/*.ts', 'middleware/**/*.ts', 'plugins/**/*.ts', 'routes/**/*.ts', 'tasks/**/*.ts'];
const production = [
...app,
layout(),
middleware(),
pages(),
plugins(),
modules,
...server.map(id => join('server', id)),
];
const setup = async () => {
if (globalThis && !('defineNuxtConfig' in globalThis)) {
Object.defineProperty(globalThis, 'defineNuxtConfig', {
value: (id) => id,
writable: true,
configurable: true,
});
}
};
const resolveAlias = (specifier, srcDir, rootDir) => {
if (specifier.startsWith('~~/') || specifier.startsWith('@@/'))
return join(rootDir, specifier.slice(3));
if (specifier.startsWith('~/') || specifier.startsWith('@/'))
return join(srcDir, specifier.slice(2));
return specifier;
};
const addAppEntries = (inputs, srcDir, serverDir, config, dir) => {
for (const id of entry)
inputs.push(toEntry(join(srcDir, id)));
for (const id of app)
inputs.push(toProductionEntry(join(srcDir, id)));
inputs.push(toProductionEntry(join(srcDir, layout(config.dir?.layouts))));
inputs.push(toProductionEntry(join(srcDir, middleware(config.dir?.middleware))));
inputs.push(toProductionEntry(join(srcDir, pages(config.dir?.pages))));
inputs.push(toProductionEntry(join(srcDir, plugins(config.dir?.plugins))));
inputs.push(toProductionEntry(join(srcDir, 'components/global/**/*.{vue,jsx,tsx}')));
for (const id of server)
inputs.push(toProductionEntry(join(dir, serverDir, id)));
inputs.push(toProductionEntry(join(dir, modules)));
if (config.css)
for (const id of config.css)
inputs.push(toDeferResolveProductionEntry(resolveAlias(id, srcDir, dir)));
};
const findLayerConfigs = (cwd) => _syncGlob({ cwd, patterns: [`layers/*/${config.at(0)}`] });
const registerCompilers = async ({ cwd, hasDependency, registerCompiler }) => {
if (hasDependency('nuxt') || hasDependency('nuxt-nightly')) {
const vueSfc = getVueSfc(cwd);
const importMap = new Map();
const componentMap = new Map();
const definitionFiles = [
'.nuxt/imports.d.ts',
'.nuxt/components.d.ts',
'.nuxt/types/nitro-routes.d.ts',
'.nuxt/types/nitro-imports.d.ts',
];
for (const file of definitionFiles) {
const sourceFile = createSourceFile(join(cwd, file));
const maps = buildAutoImportMap(sourceFile);
for (const [id, specifier] of maps.importMap)
importMap.set(id, specifier);
for (const [id, components] of maps.componentMap) {
const store = componentMap.get(id);
if (store)
store.push(...components);
else
componentMap.set(id, [...components]);
}
}
const getSyntheticImports = (identifiers, templateTags) => {
const syntheticImports = [];
for (const [name, specifier] of importMap) {
if (identifiers.has(name))
syntheticImports.push(`import { ${name} } from '${specifier}';`);
}
if (templateTags) {
for (const [name, specifiers] of componentMap) {
const kebab = toKebabCase(name);
if (templateTags.has(name) ||
templateTags.has(kebab) ||
templateTags.has(`Lazy${name}`) ||
templateTags.has(`lazy-${kebab}`)) {
syntheticImports.push(`import { default as ${name} } from '${specifiers[0]}';`);
for (let i = 1; i < specifiers.length; i++)
syntheticImports.push(`import '${specifiers[i]}';`);
}
}
}
return syntheticImports;
};
const compiler = (source, path) => {
const { descriptor } = vueSfc.parse(source, path);
const scripts = [];
if (descriptor.script?.content)
scripts.push(descriptor.script.content);
if (descriptor.scriptSetup?.content)
scripts.push(descriptor.scriptSetup.content);
const identifiers = collectIdentifiers(scripts.join('\n'), path);
let templateTags;
if (descriptor.template?.ast) {
const info = collectTemplateInfo(descriptor.template.ast);
templateTags = info.tags;
for (const id of info.identifiers)
identifiers.add(id);
}
const synthetic = getSyntheticImports(identifiers, templateTags);
scripts.push(...synthetic);
return scripts.join(';\n');
};
const tsCompiler = (source, path) => {
if (path.endsWith('.d.ts') || path.endsWith('.config.ts'))
return source;
const identifiers = collectIdentifiers(source, path);
const syntheticImports = getSyntheticImports(identifiers);
if (syntheticImports.length === 0)
return source;
return `${source}\n${syntheticImports.join('\n')}`;
};
registerCompiler({ extension: '.vue', compiler });
registerCompiler({ extension: '.ts', compiler: tsCompiler });
}
};
const resolveConfig = async (localConfig, options) => {
const { configFileDir: cwd } = options;
const hasAppDir = isDirectory(cwd, 'app');
const srcDir = localConfig.srcDir ?? (hasAppDir ? join(cwd, 'app') : cwd);
const serverDir = localConfig.serverDir ?? 'server';
const inputs = [];
for (const id of localConfig.modules ?? []) {
if (Array.isArray(id) && typeof id[0] === 'string')
inputs.push(toDependency(id[0]));
if (typeof id === 'string')
inputs.push(toDependency(id));
}
addAppEntries(inputs, srcDir, serverDir, localConfig, cwd);
const aliases = localConfig.alias;
if (aliases) {
for (const key in aliases) {
const prefix = resolveAlias(aliases[key], srcDir, cwd);
inputs.push(toAlias(key, prefix));
if (prefix.endsWith('/') || isDirectory(prefix)) {
inputs.push(toAlias(join(key, '*'), join(prefix, '*'), { dir: cwd }));
}
}
}
for (const ext of localConfig.extends ?? []) {
const resolved = resolveAlias(ext, srcDir, cwd);
for (const cfg of _syncGlob({ cwd: resolved, patterns: config })) {
inputs.push(toConfig('nuxt', join(resolved, cfg)));
}
}
for (const layerConfig of findLayerConfigs(cwd)) {
inputs.push(toConfig('nuxt', layerConfig));
}
if (cwd !== options.cwd)
return inputs;
for (const file of _syncGlob({ cwd, patterns: ['.nuxt/module/*.d.ts'] })) {
const sourceFile = createSourceFile(join(cwd, file));
for (const path of collectLocalImportPaths(sourceFile))
inputs.push(toProductionEntry(path));
}
const dir = join(cwd, '.nuxt');
const tsConfig = await loadTSConfig(join(dir, 'tsconfig.json'));
const paths = tsConfig.compilerOptions?.paths;
if (paths) {
for (const key in paths) {
if (key === '#imports' || key === '#components')
continue;
inputs.push(toAlias(key, paths[key], { dir }));
}
}
inputs.push(toIgnore('#imports', 'unresolved'));
inputs.push(toIgnore('#components', 'unresolved'));
return inputs;
};
const plugin = {
title,
enablers,
isEnabled,
config,
entry,
production,
setup,
resolveConfig,
registerCompilers,
};
export default plugin;