@sveltejs/package
Version:
The fastest way to build Svelte packages
238 lines (195 loc) • 6.03 kB
JavaScript
import * as fs from 'node:fs';
import * as path from 'node:path';
import colors from 'kleur';
import chokidar from 'chokidar';
import { preprocess } from 'svelte/compiler';
import { copy, mkdirp, rimraf } from './filesystem.js';
import { analyze, resolve_aliases, scan, strip_lang_tags, write } from './utils.js';
import { emit_dts, transpile_ts } from './typescript.js';
import { create_validator } from './validate.js';
/**
* @param {import('./types.js').Options} options
*/
export async function build(options) {
const { analyse_code, validate } = create_validator(options);
await do_build(options, analyse_code);
validate();
}
/**
* @param {import('./types.js').Options} options
* @param {(name: string, code: string) => void} analyse_code
*/
async function do_build(options, analyse_code) {
const { input, output, temp, extensions, alias, tsconfig } = normalize_options(options);
if (!fs.existsSync(input)) {
throw new Error(`${path.relative('.', input)} does not exist`);
}
rimraf(temp);
mkdirp(temp);
const files = scan(input, extensions);
if (options.types) {
await emit_dts(input, temp, output, options.cwd, alias, files, tsconfig);
}
for (const file of files) {
await process_file(input, temp, file, options.config.preprocess, alias, tsconfig, analyse_code);
}
if (!options.preserve_output) {
rimraf(output);
}
mkdirp(output);
copy(temp, output);
console.log(
colors
.bold()
.green(`${path.relative(options.cwd, input)} -> ${path.relative(options.cwd, output)}`)
);
}
/**
* @param {import('./types.js').Options} options
*/
export async function watch(options) {
const { analyse_code, validate } = create_validator(options);
await do_build(options, analyse_code);
validate();
const { input, output, extensions, alias, tsconfig } = normalize_options(options);
const message = `\nWatching ${path.relative(options.cwd, input)} for changes...\n`;
console.log(message);
/** @type {Array<{ file: import('./types.js').File, type: string }>} */
const pending = [];
/** @type {Array<(value?: any) => void>} */
const fulfillers = [];
/** @type {NodeJS.Timeout} */
let timeout;
const watcher = chokidar.watch(input, { ignoreInitial: true });
/** @type {Promise<void>} */
const ready = new Promise((resolve) => watcher.on('ready', resolve));
watcher.on('all', (type, filepath) => {
const file = analyze(path.relative(input, filepath), extensions);
pending.push({ file, type });
clearTimeout(timeout);
timeout = setTimeout(async () => {
const files = scan(input, extensions);
const events = pending.slice();
pending.length = 0;
let errored = false;
for (const { file, type } of events) {
if (type === 'unlink') {
for (const candidate of [
file.name,
`${file.base}.d.ts`,
`${file.base}.d.mts`,
`${file.base}.d.cts`
]) {
const resolved = path.join(output, candidate);
if (fs.existsSync(resolved)) {
fs.unlinkSync(resolved);
const parent = path.dirname(resolved);
if (parent !== output && fs.readdirSync(parent).length === 0) {
fs.rmdirSync(parent);
}
}
}
console.log(`Removed ${file.dest}`);
}
if (type === 'add' || type === 'change') {
console.log(`Processing ${file.name}`);
try {
await process_file(
input,
output,
file,
options.config.preprocess,
alias,
tsconfig,
analyse_code
);
} catch (e) {
errored = true;
console.error(e);
}
}
}
if (!errored && options.types) {
try {
await emit_dts(input, output, output, options.cwd, alias, files, tsconfig);
console.log('Updated .d.ts files');
} catch (e) {
errored = true;
console.error(e);
}
}
if (!errored) {
validate();
}
console.log(message);
fulfillers.forEach((fn) => fn());
}, 100);
});
return {
watcher,
ready,
settled: () =>
new Promise((fulfil, reject) => {
fulfillers.push(fulfil);
setTimeout(() => reject(new Error('Timed out')), 1000);
})
};
}
/**
* @param {import('./types.js').Options} options
*/
function normalize_options(options) {
const input = path.resolve(options.cwd, options.input);
const output = path.resolve(options.cwd, options.output);
const preserve_output = options.preserve_output;
const temp = path.resolve(
options.cwd,
options.config.kit?.outDir ?? '.svelte-kit',
'__package__'
);
const extensions = options.config.extensions ?? ['.svelte'];
const tsconfig = options.tsconfig ? path.resolve(options.cwd, options.tsconfig) : undefined;
const alias = {
$lib: path.resolve(options.cwd, options.config.kit?.files?.lib ?? 'src/lib'),
...(options.config.kit?.alias ?? {})
};
return {
input,
output,
preserve_output,
temp,
extensions,
alias,
tsconfig
};
}
/**
* @param {string} input
* @param {string} output
* @param {import('./types.js').File} file
* @param {import('svelte/types/compiler/preprocess').PreprocessorGroup | undefined} preprocessor
* @param {Record<string, string>} aliases
* @param {string | undefined} tsconfig
* @param {(name: string, code: string) => void} analyse_code
*/
async function process_file(input, output, file, preprocessor, aliases, tsconfig, analyse_code) {
const filename = path.join(input, file.name);
const dest = path.join(output, file.dest);
if (file.is_svelte || file.name.endsWith('.ts') || file.name.endsWith('.js')) {
let contents = fs.readFileSync(filename, 'utf-8');
if (file.is_svelte) {
if (preprocessor) {
const preprocessed = (await preprocess(contents, preprocessor, { filename })).code;
contents = strip_lang_tags(preprocessed);
}
}
if (file.name.endsWith('.ts') && !file.name.endsWith('.d.ts')) {
contents = await transpile_ts(tsconfig, filename, contents);
}
contents = resolve_aliases(input, file.name, contents, aliases);
analyse_code(file.name, contents);
write(dest, contents);
} else {
copy(filename, dest);
}
}