UNPKG

vite-plugin-lib

Version:

Vite plugin for build configuration, automatic aliases, and type declarations.

346 lines (341 loc) 10.2 kB
import { existsSync, readdirSync, rmSync, readFileSync } from 'node:fs'; import { unlink, readdir, readFile, writeFile } from 'node:fs/promises'; import { builtinModules } from 'node:module'; import path from 'node:path'; import process from 'node:process'; import c from 'picocolors'; import ts from 'typescript'; import dts__default from 'vite-plugin-dts'; import * as dts from 'vite-plugin-dts'; export { dts }; import { normalizePath } from 'vite'; function log(text) { console.log(`${c.cyan("[vite:lib]")} ${text}`); } function logWarn(text) { console.warn(`${c.yellow("[vite:lib]")} ${text}`); } function logError(text) { console.error(`${c.red("[vite:lib]")} ${text}`); } async function generateMTSDeclarations(typesDir, deleteSourceFiles, verbose) { const files = await collectFiles(typesDir); for (const file of files) { await createMTSImports(file, verbose); if (deleteSourceFiles) { unlink(file); } } log(`Generated ${files.length} MTS declarations.`); } async function collectFiles(dir) { const entries = await readdir(dir, { recursive: false, // does not provide full path to nested files withFileTypes: true }); const files = entries.filter((entry) => entry.isFile()); const nestedFiles = await Promise.all( entries.filter((entry) => entry.isDirectory()).map((entry) => collectFiles(normalizePath(path.join(dir, entry.name)))) ); return files.map((file) => normalizePath(path.join(dir, file.name))).concat(...nestedFiles); } async function createMTSImports(file, verbose) { const content = await readFile(file, "utf-8"); const lines = content.split("\n"); const modified = lines.map((line) => transformLine(file, line, verbose)); const targetFile = file.replace(".d.ts", ".d.mts"); await writeFile(targetFile, modified.join("\n")); } function transformLine(file, line, verbose) { return ( // eslint-disable-next-line style/quotes transformStaticImport(file, line, "'", verbose) ?? transformStaticImport(file, line, '"', verbose) ?? // eslint-disable-next-line style/quotes transformExport(file, line, "'", verbose) ?? transformExport(file, line, '"', verbose) ?? line ); } function transformStaticImport(file, line, quote, verbose) { const importPathMarker = `from ${quote}`; const isStaticImport = line.includes("import ") && line.includes(`${importPathMarker}.`); if (!isStaticImport) { return void 0; } const importStartIndex = line.lastIndexOf(importPathMarker); const importPath = line.substring( importStartIndex + importPathMarker.length, line.length - 2 ); const resolvedImport = path.resolve(path.dirname(file), importPath); if (existsSync(resolvedImport)) { if (verbose) { log(`got index import ${resolvedImport}`); } return `${line.substring(0, line.length - 2)}/index.mjs${quote};`; } return `${line.substring(0, line.length - 2)}.mjs${quote};`; } function transformExport(file, line, quote, verbose) { const exportPathMarker = ` from ${quote}`; const isStaticExport = line.includes("export ") && line.includes(`${exportPathMarker}.`); if (!isStaticExport) { return void 0; } const exportStartIndex = line.lastIndexOf(exportPathMarker); const exportPath = line.substring( exportStartIndex + exportPathMarker.length, line.length - 2 ); const resolvedExport = path.resolve(path.dirname(file), exportPath); if (existsSync(resolvedExport)) { if (verbose) { log(`got index export ${resolvedExport}`); } return `${line.substring(0, line.length - 2)}/index.mjs${quote};`; } return `${line.substring(0, line.length - 2)}.mjs${quote};`; } const typesDir = "dist/types"; const coverage = { enabled: !!process.env.COVERAGE, all: true, include: ["src/**/*.*"], provider: "v8" }; const defaults = { entry: "src/index.ts", formats: ["es"], manifest: "package.json", cleanup: true }; function mergeWithDefaults(options) { return { ...defaults, ...options }; } function tsconfigPaths(options = {}) { const { verbose } = mergeWithDefaults(options); return { name: "vite-plugin-lib:alias", enforce: "pre", config: async (config) => { const tsconfigPath = path.resolve(config.root ?? ".", "tsconfig.json"); const { baseUrl, paths } = await readConfig(tsconfigPath); if (!baseUrl || !paths) { log("No paths found in tsconfig.json."); return config; } const pathToAlias = pathToAliasFactory(tsconfigPath, baseUrl, verbose); const aliasOptions = Object.entries(paths).map(pathToAlias).filter((alias) => alias !== void 0); if (aliasOptions.length > 0) { logInjectedAliases(aliasOptions, config, verbose); } const existingAlias = transformExistingAlias(config.resolve?.alias); return { ...config, resolve: { ...config.resolve, alias: [...existingAlias, ...aliasOptions] } }; } }; } function buildConfig({ entry, formats, manifest, name, externalPackages }) { if (!externalPackages) { log("Externalized all packages."); } return { name: "vite-plugin-lib:build", enforce: "pre", config: async (config) => { return { ...config, build: { ...config.build, lib: { ...config.build?.lib, entry: path.resolve(config.root ?? ".", entry), formats, name, fileName: (format) => formatToFileName(entry, format) }, rollupOptions: { external: externalPackages ?? [ /node_modules/, ...builtinModules, /node:/, ...getDependencies(manifest) ] } } }; } }; } function getDependencies(manifest) { try { const content = readFileSync(manifest, { encoding: "utf-8" }); const { dependencies = {}, peerDependencies = {} } = JSON.parse(content); return Object.keys({ ...dependencies, ...peerDependencies }); } catch (error) { const message = getErrorMessage(error); logError(`Could not read ${c.green(manifest)}: ${message}`); throw error; } } function logInjectedAliases(aliasOptions, config, verbose) { log(`Injected ${c.green(aliasOptions.length)} aliases.`); if (!verbose) { return; } const base = `${path.resolve(config.root ?? ".")}/`; aliasOptions.map( ({ find, replacement }) => `${c.gray(">")} ${c.green(find.toString())} ${c.gray( c.bold("->") )} ${c.green(replacement.replace(base, ""))}` ).forEach(log); } function pathToAliasFactory(tsconfigPath, baseUrl, verbose) { return ([alias, replacements]) => { if (replacements.length === 0) { if (verbose) { logWarn(`No replacements for alias ${c.green(alias)}.`); } return void 0; } if (verbose && replacements.length > 1) { logWarn(`Found more than one replacement for alias ${c.green(alias)}.`); logWarn("Using the first existing replacement."); } const find = alias.replace("/*", ""); const replacement = getFirstExistingReplacement( tsconfigPath, baseUrl, replacements, find ); if (!replacement) { if (verbose) { logWarn(`No replacement found for alias ${c.green(alias)}.`); } return void 0; } return { find, replacement }; }; } function getFirstExistingReplacement(tsconfigPath, baseUrl, replacements, find, verbose) { for (const replacement of replacements) { const resolvedReplacement = path.resolve( tsconfigPath, baseUrl, replacement.replace("/*", "") ?? find ); if (existsSync(resolvedReplacement)) { return resolvedReplacement; } } return void 0; } function formatToFileName(entry, format) { const entryFileName = entry.substring( entry.lastIndexOf("/") + 1, entry.lastIndexOf(".") ); if (format === "es") { return `${entryFileName}.mjs`; } if (format === "cjs") { return `${entryFileName}.cjs`; } return `${entryFileName}.${format}.js`; } function library(options = {}) { const mergedOptions = mergeWithDefaults(options); const plugins = [ tsconfigPaths(), buildConfig(mergedOptions), dts__default({ cleanVueFileName: true, copyDtsFiles: true, include: `${path.resolve(mergedOptions.entry, "..")}/**`, outDir: typesDir, staticImport: true, afterBuild: async () => { if (includesESFormat(mergedOptions.formats)) { await generateMTSDeclarations( typesDir, mergedOptions.formats?.length === 1, options.verbose ); } } }) ]; if (mergedOptions.cleanup) { plugins.push(cleanup()); } return plugins; } function transformExistingAlias(alias) { if (!alias) { return []; } if (Array.isArray(alias)) { return alias; } return Object.entries(alias).map(([find, replacement]) => ({ find, replacement })); } async function readConfig(configPath) { try { const configFileText = await readFile(configPath, { encoding: "utf-8" }); const { config } = ts.parseConfigFileTextToJson(configPath, configFileText); const { options } = ts.parseJsonConfigFileContent( config, ts.sys, path.dirname(configPath) ); return options; } catch (error) { const message = getErrorMessage(error); logError(`Could not read tsconfig.json: ${message}`); throw error; } } function includesESFormat(formats) { return formats?.includes("es") ?? true; } function getErrorMessage(error) { const isObject = typeof error === "object" && error !== null && "message" in error; return isObject ? error.message : String(error); } function cleanup() { return { name: "vite-plugin-lib:cleanup", enforce: "post", closeBundle: () => { let deletedCount = 0; readdirSync(".").forEach((file) => { if (!file.startsWith("vite.config.ts.timestamp-")) { return; } rmSync(`./${file}`); deletedCount++; }); log(`Removed ${deletedCount} temporary files.`); } }; } export { cleanup, coverage, library, tsconfigPaths };