UNPKG

unplugin-svelte-components

Version:
760 lines (744 loc) 22.4 kB
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 };