UNPKG

@sveltejs/package

Version:

The fastest way to build Svelte packages

211 lines (186 loc) 5.94 kB
import * as fs from 'node:fs'; import * as path from 'node:path'; import { createRequire } from 'node:module'; import semver from 'semver'; import { posixify, mkdirp, rimraf, walk } from './filesystem.js'; import { resolve_aliases, write } from './utils.js'; import { emitDts } from 'svelte2tsx'; import { load_pkg_json } from './config.js'; /** * Generates d.ts files by invoking TypeScript's "emit d.ts files from input files". * The files are written to a temporary location and those which should be kept * are sanitized ($lib alias resolved) and copied over to the destination folder. * * @param {string} input * @param {string} output * @param {string} final_output * @param {string} cwd * @param {Record<string, string>} alias * @param {import('./types.js').File[]} files * @param {string | undefined} tsconfig */ export async function emit_dts(input, output, final_output, cwd, alias, files, tsconfig) { const tmp = `${output}/__package_types_tmp__`; rimraf(tmp); mkdirp(tmp); const require = createRequire(import.meta.url); const pkg = load_pkg_json(cwd); const svelte_dep = pkg.peerDependencies?.svelte || pkg.dependencies?.svelte || '3.0'; let no_svelte_3; try { no_svelte_3 = !semver.intersects(svelte_dep, '^3.0.0'); } catch { // Not all version specs are valid semver, e.g. "latest" or "next" or catalog references no_svelte_3 = true; } await emitDts({ libRoot: input, svelteShimsPath: no_svelte_3 ? require.resolve('svelte2tsx/svelte-shims-v4.d.ts') : require.resolve('svelte2tsx/svelte-shims.d.ts'), declarationDir: path.relative(cwd, tmp), tsconfig }); const handwritten = new Set(); // skip files that conflict with hand-written .d.ts for (const file of files) { if (file.name.endsWith('.d.ts')) { handwritten.add(file.name); } } // resolve $lib alias (TODO others), copy into package dir for (const file of walk(tmp)) { const normalized = posixify(file); if (handwritten.has(normalized)) { console.warn(`Using $lib/${normalized} instead of generated .d.ts file`); } let source = fs.readFileSync(path.join(tmp, normalized), 'utf8'); if (file.endsWith('.d.ts.map')) { // Because we put the .d.ts files in a temporary directory, the relative path needs to be adjusted const parsed = JSON.parse(source); if (parsed.sources) { parsed.sources = /** @type {string[]} */ (parsed.sources).map((source) => posixify( path.join( path.relative( path.dirname(path.join(final_output, normalized)), path.dirname(path.join(input, normalized)) ), path.basename(source) ) ) ); source = JSON.stringify(parsed); } } else { source = resolve_aliases(input, normalized, source, alias); } write(path.join(output, normalized), source); } rimraf(tmp); } /** * TS -> JS * * @param {string | undefined} tsconfig * @param {string} filename * @param {string} source * @param {Map<string, import('typescript').CompilerOptions>} cache */ export async function transpile_ts(tsconfig, filename, source, cache) { const ts = await try_load_ts(); const options = await load_tsconfig(tsconfig, filename, cache, ts); // transpileModule treats NodeNext as CommonJS because it doesn't read the package.json. Therefore we need to override it. // Also see https://github.com/microsoft/TypeScript/issues/53022 (the filename workaround doesn't work). return ts.transpileModule(source, { compilerOptions: { ...options, module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.NodeNext }, fileName: filename }).outputText; } async function try_load_ts() { try { return (await import('typescript')).default; } catch { throw new Error( 'You need to install TypeScript if you want to transpile TypeScript files and/or generate type definitions' ); } } /** * @param {string | undefined} tsconfig * @param {string} filename * @param {Map<string, import('typescript').CompilerOptions>} cache * @param {import('typescript')} [ts] */ export async function load_tsconfig(tsconfig, filename, cache, ts) { if (!ts) { ts = await try_load_ts(); } let config_filename; /** @type {string[]} */ const traversed_dirs = []; if (tsconfig) { if (fs.existsSync(tsconfig)) { config_filename = tsconfig; const cached = cache.get(config_filename); if (cached) { return cached; } else { // This isn't really a dir, but it simplifies the caching logic traversed_dirs.push(config_filename); } } else { throw new Error('Failed to locate provided tsconfig or jsconfig'); } } else { // ts.findConfigFile is broken (it will favour a distant tsconfig // over a near jsconfig, and then only when you call it twice) // so we implement it ourselves let dir = filename; while (dir !== (dir = path.dirname(dir))) { const cached = cache.get(dir); if (cached) { for (const traversed of traversed_dirs) { cache.set(traversed, cached); } return cached; } traversed_dirs.push(dir); const tsconfig = path.join(dir, 'tsconfig.json'); const jsconfig = path.join(dir, 'jsconfig.json'); if (fs.existsSync(tsconfig)) { config_filename = tsconfig; break; } if (fs.existsSync(jsconfig)) { config_filename = jsconfig; break; } } } if (!config_filename) { throw new Error('Failed to locate tsconfig or jsconfig'); } const { error, config } = ts.readConfigFile(config_filename, ts.sys.readFile); if (error) { throw new Error('Malformed tsconfig\n' + JSON.stringify(error, null, 2)); } // Do this so TS will not search for initial files which might take a while config.include = []; config.files = []; const { options } = ts.parseJsonConfigFileContent( config, ts.sys, path.dirname(config_filename), { sourceMap: false }, config_filename ); for (const dir of traversed_dirs) { cache.set(dir, options); } return options; }