unplugin-svelte-components
Version:
Components auto importing for svelte
760 lines (744 loc) • 22.4 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
// src/core/unplugin.ts
import { createFilter as createFilter2 } from "@rollup/pluginutils";
import chokidar from "chokidar";
import path from "path";
import { createUnplugin } from "unplugin";
import { pathToFileURL } from "url";
// src/core/context.ts
import { createFilter } from "@rollup/pluginutils";
import { relative as relative2 } from "path";
// src/core/declaration.ts
import { promises as fs } from "fs";
import { dirname, isAbsolute, relative } from "path";
// src/core/utils.ts
import { minimatch } from "minimatch";
import { parse } from "path";
// src/core/constants.ts
var DISABLE_COMMENT = "<!-- unplugin-svelte-components disabled -->";
// src/core/utils.ts
function pascalCase(str) {
return capitalize(camelCase(str));
}
function camelCase(str) {
return str.replace(/-(\w)/g, (_, c) => c ? c.toUpperCase() : "");
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function parseId(id) {
const index = id.indexOf("?");
if (index < 0) {
return { path: id, query: {} };
} else {
const query = Object.fromEntries(
new URLSearchParams(id.slice(index))
);
return {
path: id.slice(0, index),
query
};
}
}
function isEmpty(value) {
if (!value || value === null || value === void 0 || Array.isArray(value) && Object.keys(value).length <= 0)
return true;
else
return false;
}
function matchGlobs(filepath, globs) {
for (const glob of globs) {
if (minimatch(slash(filepath), glob))
return true;
}
return false;
}
function getTransformedPath(path2, ctx) {
if (ctx.options.importPathTransform) {
const result = ctx.options.importPathTransform(path2);
if (result != null)
path2 = result;
}
return path2;
}
function stringifyImport(info) {
if (typeof info === "string")
return `import '${info}'`;
if (info.name)
return `import { ${info.name} as ${info.as} } from ${JSON.stringify(
info.from
)}`;
else if (!info.defaultImport)
return `import { ${info.as} } from ${JSON.stringify(info.from)}`;
else
return `import ${info.as} from ${JSON.stringify(info.from)}`;
}
function stringifyComponentImport({ as: name, from: path2, name: importName, defaultImport }, ctx) {
path2 = getTransformedPath(path2, ctx);
const imports = [
stringifyImport({
as: name,
from: path2,
name: importName,
defaultImport
})
];
return imports.join(";");
}
function getNameFromFilePath(filePath, options) {
const {
resolvedDirs,
directoryAsNamespace,
globalNamespaces,
collapseSamePrefixes
} = options;
const parsedFilePath = parse(slash(filePath));
let strippedPath = "";
for (const dir of resolvedDirs) {
if (parsedFilePath.dir.startsWith(dir)) {
strippedPath = parsedFilePath.dir.slice(dir.length);
break;
}
}
let folders = strippedPath.slice(1).split("/").filter(Boolean);
let filename = parsedFilePath.name.replace(/[^a-zA-Z0-9]/g, "");
if (filename === "index" && !directoryAsNamespace) {
filename = `${folders.slice(-1)[0]}`;
return filename;
}
if (directoryAsNamespace) {
if (globalNamespaces.some((name) => folders.includes(name)))
folders = folders.filter((f) => !globalNamespaces.includes(f));
folders = folders.map((f) => f.replace(/[^a-zA-Z0-9]/g, ""));
if (filename.toLowerCase() === "index")
filename = "";
if (!isEmpty(folders)) {
let namespaced = [...folders, filename];
if (collapseSamePrefixes) {
const collapsed = [];
for (const fileOrFolderName of namespaced) {
const collapsedFilename = collapsed.join("");
if (collapsedFilename && fileOrFolderName.toLowerCase().startsWith(collapsedFilename.toLowerCase())) {
const collapseSamePrefix = fileOrFolderName.slice(
collapsedFilename.length
);
collapsed.push(collapseSamePrefix);
continue;
}
collapsed.push(fileOrFolderName);
}
namespaced = collapsed;
}
filename = namespaced.filter(Boolean).join("-");
}
return filename;
}
return filename;
}
function resolveAlias(filepath, alias) {
const result = filepath;
if (Array.isArray(alias)) {
for (const { find, replacement } of alias)
result.replace(find, replacement);
}
return result;
}
function shouldTransform(code) {
if (code.includes(DISABLE_COMMENT))
return false;
return true;
}
function resolveExternalImports(imports) {
return imports.flatMap(
(i) => i.names.map((n) => {
let name = n;
let alias = n;
if (n.includes("as")) {
const [, _name, _alias] = n.match(/^(.*) as (.*)$/) || [];
name = _name;
alias = _alias;
}
return { from: i.from, name, as: alias };
})
);
}
function slash(str) {
return str.replace(/\\/g, "/");
}
function toArray(array) {
array = array || [];
if (Array.isArray(array))
return array;
return [array];
}
var noop = () => {
};
function throttle_filter(s, trailing = true, leading = true, reject_on_cancel = false) {
let last_exec = 0;
let timer;
let is_leading = true;
let last_rejector = noop;
let last_value;
const clear = () => {
if (timer) {
clearTimeout(timer);
timer = void 0;
last_rejector();
last_rejector = noop;
}
};
const filter = (_invoke) => {
const duration = s * 1e3;
const elapsed = Date.now() - last_exec;
const invoke = () => {
return last_value = _invoke();
};
clear();
if (duration <= 0) {
last_exec = Date.now();
return invoke();
}
if (elapsed > duration && (leading || !is_leading)) {
last_exec = Date.now();
invoke();
} else if (trailing) {
return new Promise((resolve2, reject) => {
last_rejector = reject_on_cancel ? reject : resolve2;
timer = setTimeout(() => {
last_exec = Date.now();
is_leading = true;
resolve2(invoke());
clear();
}, duration - elapsed);
});
}
if (!leading && !timer)
timer = setTimeout(() => is_leading = true, duration);
is_leading = false;
return last_value;
};
return filter;
}
function create_filter_wrapper(filter, fn) {
function wrapper(...args) {
filter(() => fn.apply(this, args), { fn, this_arg: this, args });
}
return wrapper;
}
function throttle(fn, s = 0.2, trailing = false, leading = true) {
return create_filter_wrapper(throttle_filter(s, trailing, leading), fn);
}
function notNullish(v) {
return v != null;
}
// src/core/declaration.ts
async function generateDeclaration(ctx, filepath, removeUnused = false) {
const items = [
...Object.values(__spreadValues(__spreadValues({}, ctx.componentNameMap), ctx.componentCustomMap)),
...resolveExternalImports(ctx.options.external)
];
const imports = Object.fromEntries(
items.map(({ from: path2, as: name, name: importName }) => {
if (!name)
return void 0;
path2 = getTransformedPath(path2, ctx);
const related = isAbsolute(path2) ? `./${relative(dirname(filepath), path2)}` : path2;
let entry = `typeof import("${slash(related)}")`;
if (importName)
entry += `["${importName}"]`;
else
entry += `["default"]`;
return [name, entry];
}).filter(notNullish)
);
if (!Object.keys(imports).length)
return;
const lines = Object.entries(__spreadValues({}, imports)).sort((a, b) => a[0].localeCompare(b[0])).filter(
([name]) => removeUnused ? items.find((i) => i.as === name) : true
).map(([name, v]) => {
if (!/^\w+$/.test(name))
name = `"${name}"`;
return `const ${name}: ${v}`;
});
const code = `// generated by unplugin-svelte-components
// We suggest you to commit this file into source control
declare global {
${lines.join("\n ")}
}
export {}
`;
fs.writeFile(filepath, code, "utf-8");
}
// src/core/eslintrc.ts
import { promises as fs2 } from "fs";
function generateESLintConfigs(ctx) {
var _a;
const items = [
...Object.values(__spreadValues(__spreadValues({}, ctx.componentNameMap), ctx.componentCustomMap)),
...resolveExternalImports(ctx.options.external)
];
const imports = Object.fromEntries(
items.map(({ as: name }) => {
if (!name)
return void 0;
return [name, ctx.options.eslintrc.globalsPropValue];
}).filter(notNullish)
);
const data = JSON.stringify({ globals: imports }, null, 4);
fs2.writeFile(
(_a = ctx.options.eslintrc.filepath) != null ? _a : "./.eslintrc-components.json",
data,
"utf-8"
);
}
// src/core/glob.ts
import fg from "fast-glob";
function searchComponents(ctx) {
var _a;
const root = ctx.root;
const files = fg.sync(ctx.options.globs, {
ignore: ["node_modules"],
onlyFiles: true,
cwd: root,
absolute: true
});
if (!files.length && !((_a = ctx.options.resolvers) == null ? void 0 : _a.length))
console.warn("[unplugin-svelte-components] no components found");
ctx.addComponents(files);
}
// src/core/options.ts
import { join, resolve } from "path";
import { isPackageExists } from "local-pkg";
var defaultOptions = {
dirs: "src/components",
extensions: "svelte",
deep: true,
dts: isPackageExists("typescript"),
directoryAsNamespace: false,
globalNamespaces: [],
importPathTransform: (v) => v,
allowOverrides: false,
collapseSamePrefixes: false,
preprocess: null,
eslintrc: {
enabled: true,
filepath: "./.eslintrc-components.json",
globalsPropValue: true
}
};
function normalizeResolvers(resolvers) {
return toArray(resolvers).flat().map(
(r) => typeof r === "function" ? { resolve: r, type: "component" } : r
);
}
function resolveOptions(options, root) {
const resolved = Object.assign(
{},
defaultOptions,
options
);
resolved.resolvers = normalizeResolvers(resolved.resolvers);
resolved.extensions = toArray(resolved.extensions);
if (resolved.globs) {
resolved.globs = toArray(resolved.globs).map(
(glob) => slash(resolve(root, glob))
);
resolved.resolvedDirs = [];
} else {
const extsGlob = resolved.extensions.length === 1 ? resolved.extensions : `{${resolved.extensions.join(",")}}`;
resolved.dirs = toArray(resolved.dirs);
resolved.resolvedDirs = resolved.dirs.map(
(i) => slash(resolve(root, i))
);
resolved.globs = resolved.resolvedDirs.map(
(i) => resolved.deep ? slash(join(i, `**/*.${extsGlob}`)) : slash(join(i, `*.${extsGlob}`))
);
if (!resolved.extensions.length)
throw new Error(
"[unplugin-svelte-components] `extensions` option is required to search for components"
);
}
resolved.dts = !resolved.dts ? false : resolve(
root,
typeof resolved.dts === "string" ? resolved.dts : "components.d.ts"
);
resolved.external = resolved.external || [];
resolved.root = root;
return resolved;
}
// src/core/transformer.ts
import { walk } from "estree-walker";
import MagicString from "magic-string";
import { parse as parse2, preprocess } from "svelte/compiler";
function transformer(ctx) {
return async (code, id, path2) => {
ctx.searchGlob();
const sfcPath = ctx.normalizePath(path2);
const s = new MagicString(code);
await transformComponent(code, s, ctx, sfcPath, id);
const result = { code: s.toString() };
if (ctx.sourcemap)
result.map = s.generateMap({ source: id, includeContent: true });
return result;
};
}
function walkAST(ast) {
var _a, _b, _c, _d;
const components = /* @__PURE__ */ new Set();
const importDeclarations = [];
const body = (_b = (_a = ast.instance) == null ? void 0 : _a.content) == null ? void 0 : _b.body;
if (body) {
for (const _body of body) {
if (_body.type === "ImportDeclaration") {
const importName = (_d = (_c = _body == null ? void 0 : _body.specifiers[0]) == null ? void 0 : _c.local) == null ? void 0 : _d.name;
importDeclarations.push(importName);
}
}
}
if (ast.html && ast.html.children) {
walk(ast.html.children, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
enter(node) {
if (node.type == "InlineComponent" && !/^svelte:/.test(node.name)) {
if (!importDeclarations.includes(node.name)) {
components.add(node.name);
}
}
}
});
}
return components;
}
var resolveSvelte = (ast) => {
const results = [];
const components = walkAST(ast);
for (const match of components) {
results.push(`${match}`);
}
return results;
};
async function transformComponent(code, s, ctx, sfcPath, filename) {
if (ctx.preprocess) {
const processed = await preprocess(code, ctx.preprocess, { filename });
code = processed.code;
}
const ast = parse2(code);
const results = resolveSvelte(ast);
let imports = [];
for (const name of results) {
ctx.updateUsageMap(sfcPath, [name]);
const component = await ctx.findComponent(name, [sfcPath]);
if (component) {
imports.push(
stringifyComponentImport(__spreadProps(__spreadValues({}, component), { as: name }), ctx)
);
}
}
if (ast.instance) {
const start = ast.instance.start;
const index = code.indexOf(">", start) + 1;
const oc = s.toString();
imports = imports.filter((item) => {
const exists = oc.includes(item) || oc.includes(item.replace(/'/g, '"'));
if (exists)
return;
return item;
});
s.appendRight(index, `
${imports.join("\n")}
`);
} else {
const script = `<script>${imports.join("\n")}</script>`;
s.appendLeft(ast.html.start, script);
}
}
// src/core/context.ts
var Context = class {
constructor(rawOptions) {
this.rawOptions = rawOptions;
this.transformer = void 0;
this._componentPaths = /* @__PURE__ */ new Set();
this._componentNameMap = {};
this._componentUsageMap = {};
this._componentCustomMap = {};
this.root = process.cwd();
this.sourcemap = true;
this.preprocess = [];
this._searched = false;
this.options = resolveOptions(rawOptions, this.root);
this.generateDeclaration = throttle(
this.generateDeclaration.bind(this),
0.5,
false
);
this.generateESLintConfigs = throttle(
this.generateESLintConfigs.bind(this),
0.5,
false
);
this.transformer = transformer(this);
if (rawOptions.preprocess)
this.preprocess = rawOptions.preprocess;
}
setRoot(root) {
if (this.root === root)
return;
this.root = root;
this.options = resolveOptions(this.rawOptions, this.root);
}
transform(code, id) {
const { path: path2, query } = parseId(id);
return this.transformer(code, id, path2, query);
}
setupViteServer(server) {
if (this._server === server)
return;
this._server = server;
this.setupWatcher(server.watcher);
}
setupWatcher(watcher) {
const { globs } = this.options;
watcher.on("unlink", (path2) => {
if (!matchGlobs(path2, globs))
return;
path2 = slash(path2);
this.removeComponents(path2);
this.onUpdate(path2);
});
watcher.on("add", (path2) => {
if (!matchGlobs(path2, globs))
return;
path2 = slash(path2);
this.addComponents(path2);
this.onUpdate(path2);
});
}
/**
* Record the usage of components
* @param path
* @param paths paths of used components
*/
updateUsageMap(path2, paths) {
if (!this._componentUsageMap[path2])
this._componentUsageMap[path2] = /* @__PURE__ */ new Set();
paths.forEach((p) => {
this._componentUsageMap[path2].add(p);
});
}
addComponents(paths) {
const size = this._componentPaths.size;
toArray(paths).forEach((p) => this._componentPaths.add(p));
if (this._componentPaths.size !== size) {
this.updateComponentNameMap();
return true;
}
return false;
}
addCustomComponents(info) {
if (info.as)
this._componentCustomMap[info.as] = info;
}
removeComponents(paths) {
const size = this._componentPaths.size;
toArray(paths).forEach((p) => this._componentPaths.delete(p));
if (this._componentPaths.size !== size) {
this.updateComponentNameMap();
return true;
}
return false;
}
onUpdate(path2) {
this.generateDeclaration();
this.generateESLintConfigs();
if (!this._server)
return;
const payload = {
type: "update",
updates: []
};
const timestamp = +/* @__PURE__ */ new Date();
const name = pascalCase(getNameFromFilePath(path2, this.options));
Object.entries(this._componentUsageMap).forEach(([key, values]) => {
if (values.has(name)) {
const r = `/${slash(relative2(this.root, key))}`;
payload.updates.push({
acceptedPath: r,
path: r,
timestamp,
type: "js-update"
});
}
});
if (payload.updates.length)
this._server.ws.send(payload);
}
updateComponentNameMap() {
const filter = createFilter(
this.options.include || [/\.svelte$/],
this.options.exclude || [
/[\\/]node_modules[\\/]/,
/[\\/]\.git[\\/]/,
/[\\/]\.svelte-kit[\\/]/,
/\.stories\.svelte$/,
/\.story\.svelte$/,
/\+layout\.svelte$/,
/\+error\.svelte$/,
/\+page\.svelte$/
]
);
this._componentNameMap = {};
Array.from(this._componentPaths).forEach((path2) => {
if (!filter(path2))
return;
const name = pascalCase(getNameFromFilePath(path2, this.options));
if (this._componentNameMap[name] && !this.options.allowOverrides) {
console.warn(
`[unplugin-svelte-components] component "${name}"(${path2}) has naming conflicts with other components, ignored.`
);
return;
}
this._componentNameMap[name] = {
as: name,
from: path2,
defaultImport: true
};
});
}
async findComponent(name, excludePaths = []) {
const info = this._componentNameMap[name];
if (info && !excludePaths.includes(info.from) && !excludePaths.includes(info.from.slice(1)))
return info;
for (const module of this.options.external) {
for (const externalName of module.names) {
const parsed = /^([^\s]+)\s+as\s+([^\s]+)$/.exec(externalName);
if (parsed !== null && parsed.length === 3 && parsed[2] === name) {
return {
as: parsed[2],
from: module.from,
name: parsed[1],
defaultImport: false
};
} else if (externalName === name) {
return {
as: externalName,
from: module.from,
defaultImport: module.defaultImport
};
}
}
}
return void 0;
}
normalizePath(path2) {
var _a, _b, _c;
return resolveAlias(
path2,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
((_b = (_a = this.viteConfig) == null ? void 0 : _a.resolve) == null ? void 0 : _b.alias) || ((_c = this.viteConfig) == null ? void 0 : _c.alias) || []
);
}
/**
* This search for components in with the given options.
* Will be called multiple times to ensure file loaded,
* should normally run only once.
*
* @param ctx
* @param force
*/
searchGlob() {
if (this._searched)
return;
searchComponents(this);
this._searched = true;
}
generateDeclaration() {
if (!this.options.dts)
return;
generateDeclaration(this, this.options.dts, !this._server);
}
generateESLintConfigs() {
if (!this.options.eslintrc || !this.options.eslintrc.enabled)
return;
generateESLintConfigs(this);
}
get componentNameMap() {
return this._componentNameMap;
}
get componentCustomMap() {
return this._componentCustomMap;
}
};
// src/core/unplugin.ts
var unplugin_default = createUnplugin((options = {}) => {
const filter = createFilter2(
options.include || [/\.svelte$/],
options.exclude || [
/[\\/]node_modules[\\/]/,
/[\\/]\.git[\\/]/,
/[\\/]\.svelte-kit[\\/]/
]
);
const ctx = new Context(options);
return {
name: "unplugin-svelte-components",
enforce: "pre",
transformInclude(id) {
return filter(id);
},
async transform(code, id) {
if (!code)
return null;
if (!shouldTransform(code))
return null;
const result = await ctx.transform(code, id);
ctx.generateDeclaration();
ctx.generateESLintConfigs();
return result;
},
vite: {
async configResolved(config) {
var _a;
const configFile = path.join(config.root, "./svelte.config.js");
const pkg = process.platform === "win32" ? await import(pathToFileURL(configFile).toString()) : await import(configFile);
const preprocess2 = pkg.default.preprocess || [];
ctx.preprocess = preprocess2;
ctx.setRoot(config.root);
ctx.sourcemap = true;
if (options.dts) {
ctx.searchGlob();
ctx.generateDeclaration();
}
if ((_a = options.eslintrc) == null ? void 0 : _a.enabled) {
ctx.generateESLintConfigs();
}
if (config.build.watch && config.command === "build")
ctx.setupWatcher(chokidar.watch(ctx.options.globs));
},
configureServer(server) {
ctx.setupViteServer(server);
}
}
};
});
export {
unplugin_default
};