UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

269 lines (226 loc) • 7.01 kB
/** @import { ValidatedConfig } from 'types' */ import process from 'node:process'; import fs from 'node:fs'; import path from 'node:path'; import { write_if_changed } from '../utils.js'; import { ESSENTIAL_OPTIONS, normalize_config, RECOMMENDED_OPTIONS, remove_trailing_slashstar } from './utils.js'; import { extends_id, validate_resolved_config } from './validate.js'; import * as e from '../../../messages/build-errors.js'; import * as w from '../../../messages/build-warnings.js'; import { posixify } from '../../../utils/os.js'; import { bullet_list } from '../../../utils/format.js'; /** @type {typeof import('typescript')} */ let ts; try { ts = await import('typescript'); } catch { // The user has not installed TypeScript. } /** * Generates the tsconfig that the user's tsconfig inherits from. * @param {ValidatedConfig} kit * @param {string} root */ export function write_tsconfig(kit, root) { const paths = get_paths(kit, root); const types = ['$app/types']; write_parent_tsconfig( root, '$app/tsconfig', { compilerOptions: { paths, rootDirs: [root, `${kit.outDir}/types`], types, // This is required for svelte-package to work as expected // Can be overwritten lib: ['ESNext', 'DOM', 'DOM.Iterable'], ...ESSENTIAL_OPTIONS, ...RECOMMENDED_OPTIONS } }, kit.typescript.config ); validate_config(root, { paths, types, exclusions: [kit.files.serviceWorker], example: { extends: '$app/tsconfig', include: ['src', 'test', '*'], exclude: ['src/service-worker'] } }); write_parent_tsconfig(root, '$app/tsconfig/service-worker', { compilerOptions: { paths, types, lib: ['ESNext', 'WebWorker'], ...ESSENTIAL_OPTIONS, ...RECOMMENDED_OPTIONS } }); validate_config(kit.files.serviceWorker, { paths, types, example: { extends: '$app/tsconfig/service-worker' } }); } /** * Write a generated `tsconfig.json` inside `node_modules`, for the * user config to extend * @param {string} root The project root * @param {string} id The id of the generated config * @param {any} config The contents of the generated tsconfig, with paths relative to `root` * @param {ValidatedConfig['typescript']['config']} [transform] TODO get rid of this */ function write_parent_tsconfig(root, id, config, transform) { // simplified tsconfig resolvers (e.g. Playwright's) only find `${id}.json`, not `${id}/tsconfig.json` const out_file = path.join(root, `node_modules/${id}.json`); let normalized = normalize_config(out_file, config); normalized = transform?.(normalized) ?? normalized; write_if_changed(out_file, JSON.stringify(normalized, null, '\t')); } /** * @param {string} dir The directory to resolve a user config from * @param {Object} options * @param {Record<string, string[]>} options.paths The expected paths * @param {string[]} options.types The expected types * @param {string[]} [options.exclusions] Files that should be excluded * @param {any} options.example What to print if the user config does _not_ extend the generated config */ function validate_config(dir, options) { if (!ts) { // The user has not installed TypeScript. Skip validation of config. return; } const user_config = load_user_tsconfig(dir); if (!user_config || !modified_since_last_check(user_config.file)) return; // now that we've written the parent config, we can resolve the // user config and validate that nothing important was overwritten if (!extends_id(user_config.options, options.example.extends)) { w.tsconfig_extends_missing({ file: path.relative(process.cwd(), user_config.file), example: JSON.stringify(options.example, null, ' ') }); return; } const resolved = ts.parseJsonConfigFileContent(user_config.options, ts.sys, dir); const warnings = validate_resolved_config( dir, resolved, options.paths, options.types, options.exclusions ); if (warnings.length > 0) { w.tsconfig_invalid({ file: path.relative(process.cwd(), user_config.file), issues: bullet_list(warnings) }); } } /** @type {Map<string, number>} */ const mtimes = new Map(); /** * Returns true if the file was modified since we last got here, * otherwise we can skip warnings * @param {string} file */ function modified_since_last_check(file) { const a = mtimes.get(file) ?? -1; const b = fs.statSync(file).mtimeMs; mtimes.set(file, b); return b > a; } /** * @param {string} cwd * @param {string} file */ function maybe_file(cwd, file) { const resolved = path.resolve(cwd, file); if (fs.existsSync(resolved)) { return resolved; } } /** @param {string} cwd */ function load_user_tsconfig(cwd) { const file = maybe_file(cwd, 'tsconfig.json') || maybe_file(cwd, 'jsconfig.json'); if (!file) return; const options = load_tsconfig(file); return { file, kind: path.basename(file), options }; } /** * @param {string} file */ function load_tsconfig(file) { const options = ts.readConfigFile(posixify(file), ts.sys.readFile); if (options.error) { /** @type {string | undefined} */ let location; if (options.error.file && options.error.start !== undefined) { const line_start = options.error.file.text.lastIndexOf('\n', options.error.start); const line = line_start === -1 ? 0 : options.error.file.text.slice(0, options.error.start).split('\n').length; const column = options.error.start - line_start; location = `${path.relative(process.cwd(), file)}:${line}:${column}`; } e.tsconfig_parse_failed( typeof options.error.messageText === 'string' ? { details: options.error.messageText } : undefined, { stackless: true, location } ); } return options.config; } /** Matches anything (except the empty string/string with newline), and separates any trailing `/*` */ const alias_key = /^(.+?)(\/\*)?$/; /** Matches anything (except the empty string/string with newline), and separates any trailing `/*` or file extension */ const alias_value = /^(.+?)((\/\*)|(\.\w+))?$/; /** * Generates tsconfig path aliases from kit's aliases and the package.json `imports` field. * Related to vite alias creation. * * @param {import('types').ValidatedConfig} config * @param {string} root * @returns {Record<string, string[]>} */ function get_paths(config, root) { const alias = { ...config.alias }; /** @type {Record<string, string[]>} */ const paths = {}; for (const [key, value] of Object.entries(alias)) { const key_match = alias_key.exec(key); if (!key_match) e.config_alias_key_invalid({ key }); const value_match = alias_value.exec(value); if (!value_match) e.config_alias_value_invalid({ value }); const resolved = path.resolve(root, remove_trailing_slashstar(value)); const slashstar = key_match[2]; if (slashstar) { paths[key] = [resolved + '/*']; } else { paths[key] = [resolved]; const fileending = value_match[4]; if (!fileending && !(key + '/*' in alias)) { paths[key + '/*'] = [resolved + '/*']; } } } return paths; }