UNPKG

@storm-stack/core

Version:

A build toolkit and runtime used by Storm Software in TypeScript applications

570 lines (552 loc) 20 kB
import { getString, getSourceFile, getMagicString } from './chunk-LK23XV73.js'; import { extendLog } from './chunk-K7ITYUIA.js'; import { init_esm_shims, __name, __reExport } from './chunk-QH7NXH7H.js'; import { LogLevelLabel } from '@storm-software/config-tools/types'; import { existsSync } from '@stryke/fs/exists'; import { createDirectorySync, createDirectory } from '@stryke/fs/helpers'; import { resolvePackage } from '@stryke/fs/resolve'; import { isParentPath } from '@stryke/path/is-parent-path'; import { joinPaths } from '@stryke/path/join-paths'; import { transformAsync } from '@babel/core'; import { isFunction } from '@stryke/type-checks/is-function'; import { isSetString } from '@stryke/type-checks/is-set-string'; import defu, { defu as defu$1 } from 'defu'; import chalk from 'chalk'; import '@stryke/type-checks/is-object'; import { isString } from '@stryke/type-checks/is-string'; import { parse } from '@babel/parser'; import ts from 'typescript'; import * as type_compiler_star from '@deepkit/type-compiler'; import { isWritable } from '@stryke/fs/chmod-x'; import { readFile } from '@stryke/fs/read-file'; import { removeFile } from '@stryke/fs/remove-file'; import { writeFile } from '@stryke/fs/write-file'; import { hash } from '@stryke/hash'; import { findFileName } from '@stryke/path/file-path-fns'; import Diff from 'diff-match-patch'; // src/base/compiler.ts init_esm_shims(); // src/lib/babel/transform.ts init_esm_shims(); // src/lib/babel/options.ts init_esm_shims(); // src/lib/babel/helpers.ts init_esm_shims(); // src/lib/babel/ast.ts init_esm_shims(); function parseAst(code, opts = {}) { return parse(code, { plugins: [ "typescript" ], sourceType: "module", allowImportExportEverywhere: true, allowAwaitOutsideFunction: true, ...opts }); } __name(parseAst, "parseAst"); // src/lib/babel/helpers.ts function listExports(codeOrAst) { const ast = isString(codeOrAst) ? parseAst(codeOrAst) : codeOrAst; return ast.program.body.flatMap((i) => { if (i.type === "ExportDefaultDeclaration") { return [ "default" ]; } if (i.type === "ExportNamedDeclaration" && i.declaration && "declarations" in i.declaration) { return i.declaration.declarations.map((d) => "name" in d.id ? d.id.name : ""); } return []; }).filter(Boolean); } __name(listExports, "listExports"); function getPluginName(plugin) { return isSetString(plugin) ? plugin : Array.isArray(plugin) && plugin.length > 0 ? getPluginName(plugin[0]) : plugin._name || plugin.name ? plugin._name || plugin.name : void 0; } __name(getPluginName, "getPluginName"); function isDuplicatePlugin(plugins, plugin) { return !!(getPluginName(plugin) && plugins.some((existing) => Array.isArray(existing) && getPluginName(existing[0]) === getPluginName(plugin))); } __name(isDuplicatePlugin, "isDuplicatePlugin"); // src/lib/babel/options.ts function resolveBabelPlugins(log, context, sourceFile, options = {}) { return !options.plugins ? [] : options.plugins.reduce((ret, plugin) => { if (!isDuplicatePlugin(ret, plugin)) { if (Array.isArray(plugin) && plugin.length > 0 && plugin[0]) { if (sourceFile && plugin.length > 2 && plugin[2] && isFunction(plugin[2].filter) && // eslint-disable-next-line ts/no-unsafe-call !plugin[2].filter(sourceFile)) { log(LogLevelLabel.TRACE, `Skipping filtered Babel plugin ${chalk.bold.cyanBright(getPluginName(plugin) || "unnamed")} for ${sourceFile.id}`); return ret; } ret.push([ isFunction(plugin[0]) ? plugin[0](context) : plugin[0], { ...plugin.length > 1 && plugin[1] ? plugin[1] : {}, options }, plugin.length > 2 ? plugin[2] : void 0 ]); } else { ret.push([ isFunction(plugin) ? plugin(context) : plugin, { options }, void 0 ]); } } else { log(LogLevelLabel.INFO, `Skipping duplicate Babel plugin ${getPluginName(plugin)}${sourceFile?.id ? ` for ${sourceFile.id}` : ""}`); } return ret; }, []); } __name(resolveBabelPlugins, "resolveBabelPlugins"); function resolveBabelPresets(log, context, sourceFile, options = {}) { return !options.presets ? [] : options.presets.reduce((ret, preset) => { if (!isDuplicatePlugin(ret, preset)) { if (Array.isArray(preset) && preset.length > 0 && preset[0]) { if (sourceFile && preset.length > 2 && preset[2] && isFunction(preset[2].filter) && // eslint-disable-next-line ts/no-unsafe-call !preset[2].filter(sourceFile)) { log(LogLevelLabel.INFO, `Skipping filtered Babel preset ${getPluginName(preset)} for ${sourceFile.id}`); return ret; } ret.push([ isFunction(preset[0]) ? preset[0](context) : preset[0], { ...preset.length > 1 && preset[1] ? preset[1] : {}, options }, preset.length > 2 ? preset[2] : void 0 ]); } else { ret.push([ isFunction(preset) ? preset(context) : preset, { options }, void 0 ]); } } else { log(LogLevelLabel.INFO, `Skipping duplicate Babel preset ${getPluginName(preset)}${sourceFile?.id ? ` for ${sourceFile.id}` : ""}`); } return ret; }, []); } __name(resolveBabelPresets, "resolveBabelPresets"); function resolveBabelInputOptions(context, options = {}, plugins = [], presets = []) { return defu({ plugins: [ "@babel/plugin-syntax-typescript", ...plugins.map((plugin) => { return [ plugin[0], defu(plugin.length > 1 && plugin[1] ? plugin[1] : {}, { options }), plugin[0]?.name ]; }) ], presets: presets.map((preset) => { return [ preset[0], defu(preset.length > 1 && preset[1] ? preset[1] : {}, { options }), preset[0]?.name ]; }) }, options ? { ...options, plugins: [], presets: [] } : {}, { highlightCode: true, code: true, ast: false, cloneInputAst: false, comments: true, sourceType: "module", configFile: false, babelrc: false, envName: context.options.mode, caller: { name: "storm-stack" } }); } __name(resolveBabelInputOptions, "resolveBabelInputOptions"); // src/lib/babel/transform.ts async function transform(log, context, source, options = {}) { try { const corePath = process.env.STORM_STACK_LOCAL ? joinPaths(context.options.workspaceRoot, "packages/core") : await resolvePackage("@storm-stack/core"); if (!corePath) { throw new Error("Could not resolve @storm-stack/core package location."); } let sourceFile = source; if (process.env.STORM_STACK_LOCAL && isParentPath(sourceFile.id, corePath) || options.skipAllTransforms || getString(sourceFile.code).includes("/* @storm-ignore */") || getString(sourceFile.code).includes("/* @storm-disable */")) { return sourceFile; } const opts = defu$1(context.options.babel ?? {}, options.babel ?? {}); const plugins = resolveBabelPlugins(log, context, sourceFile, opts); const presets = resolveBabelPresets(log, context, sourceFile, opts); if (!plugins && !presets || Array.isArray(plugins) && plugins.length === 0 && Array.isArray(presets) && presets.length === 0) { log(LogLevelLabel.WARN, `No Babel plugins or presets configured for ${sourceFile.id}. Skipping Babel transformation.`); return sourceFile; } for (const plugin of plugins.filter((plugin2) => isFunction(plugin2[2]?.onPreTransform))) { sourceFile = await Promise.resolve(plugin[2].onPreTransform(context, sourceFile)); } for (const preset of presets.filter((preset2) => isFunction(preset2[2]?.onPreTransform))) { sourceFile = await Promise.resolve(preset[2].onPreTransform(context, sourceFile)); } log(LogLevelLabel.TRACE, `Transforming ${sourceFile.id} with Babel`); const result = await transformAsync(getString(sourceFile.code), defu$1({ filename: sourceFile.id }, resolveBabelInputOptions(context, opts, plugins, presets))); if (!result?.code) { throw new Error(`BabelPluginStormStack failed to compile ${sourceFile.id}`); } log(LogLevelLabel.TRACE, `Completed Babel transformations of ${sourceFile.id}`); sourceFile.code = getMagicString(result.code); for (const plugin of plugins.filter((plugin2) => isFunction(plugin2[2]?.onPostTransform))) { sourceFile = await Promise.resolve(plugin[2].onPostTransform(context, sourceFile)); } for (const preset of presets.filter((preset2) => isFunction(preset2[2]?.onPostTransform))) { sourceFile = await Promise.resolve(preset[2].onPostTransform(context, sourceFile)); } return sourceFile; } catch (error) { context.log(LogLevelLabel.ERROR, `Error during Babel transformation: ${error?.message ? isSetString(error.message) ? error.message.length > 5e3 ? `${error.message.slice(0, 5e3)}... ${error.message.slice(-100)}` : error.message : error.message : "Unknown error"} ${error?.stack ? ` Stack trace: ${error.stack} ` : ""}`); throw new Error(`Babel transformation failed for ${source.id}`); } } __name(transform, "transform"); // src/lib/typescript/transpile.ts init_esm_shims(); // src/lib/deepkit/transformer.ts init_esm_shims(); // src/deepkit/type-compiler.ts var type_compiler_exports = {}; init_esm_shims(); __reExport(type_compiler_exports, type_compiler_star); var cache = new type_compiler_exports.Cache(); function createTransformer(context, options = {}) { return /* @__PURE__ */ __name(function transformer(ctx) { cache.tick(); return new type_compiler_exports.ReflectionTransformer(ctx, cache).withReflection({ reflection: options.reflectionMode || "default", reflectionLevel: options.reflectionLevel || context.tsconfig.tsconfigJson.compilerOptions?.reflectionLevel || context.tsconfig.tsconfigJson.reflectionLevel || "minimal" }); }, "transformer"); } __name(createTransformer, "createTransformer"); function createDeclarationTransformer(context, options = {}) { return /* @__PURE__ */ __name(function declarationTransformer(ctx) { return new type_compiler_exports.DeclarationTransformer(ctx, cache).withReflection({ reflection: options.reflectionMode || "default", reflectionLevel: options.reflectionLevel || context.tsconfig.tsconfigJson.compilerOptions?.reflectionLevel || context.tsconfig.tsconfigJson.reflectionLevel || "minimal" }); }, "declarationTransformer"); } __name(createDeclarationTransformer, "createDeclarationTransformer"); // src/lib/typescript/transpile.ts function transpile(context, id, code, options = {}) { const transformer = createTransformer(context, options); const declarationTransformer = createDeclarationTransformer(context, options); return ts.transpileModule(code, { compilerOptions: { ...context.tsconfig.options }, fileName: id, transformers: { before: [ transformer ], after: [ declarationTransformer ] } }); } __name(transpile, "transpile"); // src/lib/utilities/cache.ts init_esm_shims(); function getCacheHashKey(id, code) { return hash({ id, code: getString(code) }, { maxLength: 32 }); } __name(getCacheHashKey, "getCacheHashKey"); function getCacheFileName(id, hashKey) { return `${findFileName(id, { withExtension: false })}_${hashKey}.cache`; } __name(getCacheFileName, "getCacheFileName"); async function getCache(sourceFile, cacheDir) { const hashKey = getCacheHashKey(sourceFile.id, sourceFile.code); const cacheFilePath = joinPaths(cacheDir, getCacheFileName(sourceFile.id, hashKey)); if (!existsSync(cacheFilePath)) { return void 0; } const cache2 = await readFile(cacheFilePath); if (!cache2.includes(hashComment(hashKey))) { return void 0; } return cache2; } __name(getCache, "getCache"); async function setCache(sourceFile, cacheDir, transpiled) { const hashKey = getCacheHashKey(sourceFile.id, sourceFile.code); const cacheFilePath = joinPaths(cacheDir, getCacheFileName(sourceFile.id, hashKey)); if (existsSync(cacheFilePath)) { await removeFile(cacheFilePath); } if (transpiled) { if (!existsSync(cacheDir)) { await createDirectory(cacheDir); } if (!await isWritable(cacheDir)) { throw new Error(`Cache directory is not writable: ${cacheDir}`); } await writeFile(cacheFilePath, `${transpiled} ${hashComment(hashKey)}`.trim()); } } __name(setCache, "setCache"); function hashComment(hashKey) { return `/* storm-stack_${hashKey} */`; } __name(hashComment, "hashComment"); // src/lib/utilities/source-map.ts init_esm_shims(); var dmp = new Diff(); function generateSourceMap(id, source, transpiled) { if (!transpiled) { return; } const diff = dmp.diff_main(source.toString(), transpiled); dmp.diff_cleanupSemantic(diff); let offset = 0; for (let index = 0; index < diff.length; index++) { if (diff[index]) { const [type, text] = diff[index]; const textLength = text.length; switch (type) { case 0: { offset += textLength; break; } case 1: { source.prependLeft(offset, text); break; } case -1: { const next = diff.at(index + 1); if (next && next[0] === 1) { const replaceText = next[1]; const firstNonWhitespaceIndexOfText = text.search(/\S/); const offsetStart = offset + Math.max(firstNonWhitespaceIndexOfText, 0); source.update(offsetStart, offset + textLength, replaceText); index += 1; } else { source.remove(offset, offset + textLength); } offset += textLength; break; } } } } if (!source.hasChanged()) { return; } return { code: source.toString(), map: source.generateMap({ source: id, file: `${id}.map`, includeContent: true }) }; } __name(generateSourceMap, "generateSourceMap"); // src/base/compiler.ts var Compiler = class { static { __name(this, "Compiler"); } #cache = /* @__PURE__ */ new WeakMap(); #options; #corePath; /** * The logger function to use */ log; /** * The cache directory */ cacheDir; /** * Create a new compiler instance. * * @param context - The compiler context. * @param options - The compiler options. */ constructor(context, options = {}) { this.log = extendLog(context.log, "compiler"); this.#options = options; this.cacheDir = joinPaths(context.cachePath, "compiler"); if (!existsSync(this.cacheDir)) { createDirectorySync(this.cacheDir); } } /** * Transform the module. * * @param context - The compiler context. * @param fileName - The name of the file to compile. * @param code - The source code to compile. * @param options - The transpile options. * @returns The transpiled module. */ async transform(context, fileName, code, options = {}) { if (await this.shouldSkip(context, fileName, code)) { this.log(LogLevelLabel.TRACE, `Skipping transform for ${fileName}`); return getString(code); } this.log(LogLevelLabel.TRACE, `Transforming ${fileName}`); let source = getSourceFile(fileName, code); if (options.onPreTransform) { this.log(LogLevelLabel.TRACE, `Running onPreTransform hook for ${source.id}`); source = await Promise.resolve(options.onPreTransform(context, source)); } if (this.#options.onPreTransform) { this.log(LogLevelLabel.TRACE, `Running onPreTransform hook for ${source.id}`); source = await Promise.resolve(this.#options.onPreTransform(context, source)); } if (!options.skipAllTransforms) { if (context.unimport && !options.skipTransformUnimport && !context.vfs.isRuntimeFile(fileName)) { source = await context.unimport.injectImports(source); } this.log(LogLevelLabel.TRACE, `Running transforms for ${source.id} with options: ${JSON.stringify(options)}`); source = await transform(this.log, context, source, options); this.log(LogLevelLabel.TRACE, `Transformed: ${source.id}`); } if (this.#options.onPostTransform) { this.log(LogLevelLabel.TRACE, `Running onPostTransform hook for ${source.id}`); source = await Promise.resolve(this.#options.onPostTransform(context, source)); } if (options.onPostTransform) { this.log(LogLevelLabel.TRACE, `Running onPostTransform hook for ${source.id}`); source = await Promise.resolve(options.onPostTransform(context, source)); } return getString(source.code); } /** * Transpile the module. * * @param context - The compiler context. * @param id - The name of the file to compile. * @param code - The source code to compile. * @returns The transpiled module. */ async transpile(context, id, code, options = {}) { this.log(LogLevelLabel.TRACE, `Transpiling ${id} module with TypeScript compiler`); const transpiled = transpile(context, id, getString(code), options); if (transpiled === null) { this.log(LogLevelLabel.ERROR, `Transform is null: ${id}`); throw new Error(`Transform is null: ${id}`); } else { this.log(LogLevelLabel.TRACE, `Transformed: ${id}`); } return transpiled.outputText; } /** * Compile the source code. * * @param context - The compiler context. * @param id - The name of the file to compile. * @param code - The source code to compile. * @returns The compiled source code and source map. */ async compile(context, id, code, options = {}) { this.log(LogLevelLabel.TRACE, `Compiling ${id}`); const source = getSourceFile(id, code); let compiled; if (!options.skipCache) { compiled = await this.getCache(context, source); if (compiled) { this.log(LogLevelLabel.TRACE, `Cache hit: ${source.id}`); } else { this.log(LogLevelLabel.TRACE, `Cache miss: ${source.id}`); } } if (!compiled) { const transformed = await this.transform(context, source.id, source.code, options); compiled = await this.transpile(context, id, transformed, options); await this.setCache(context, source, compiled); } return compiled; } /** * Get the result of the compiler. * * @param sourceFile - The source file. * @param transpiled - The transpiled source code. * @returns The result of the compiler. */ getResult(sourceFile, transpiled) { return generateSourceMap(sourceFile.id, sourceFile.code, transpiled); } async getCache(context, sourceFile) { let cache2 = this.#cache.get(sourceFile); if (cache2) { return cache2; } if (context.options.skipCache) { return; } cache2 = await getCache(sourceFile, this.cacheDir); if (cache2) { this.#cache.set(sourceFile, cache2); } return cache2; } async setCache(context, sourceFile, transpiled) { if (transpiled) { this.#cache.set(sourceFile, transpiled); } else { this.#cache.delete(sourceFile); } if (context.options.skipCache) { return; } return setCache(sourceFile, this.cacheDir, transpiled); } async shouldSkip(context, id, code) { if (!this.#corePath) { this.#corePath = process.env.STORM_STACK_LOCAL ? joinPaths(context.options.workspaceRoot, "packages/core") : await resolvePackage("@storm-stack/core"); if (!this.#corePath) { throw new Error("Could not resolve @storm-stack/core package location."); } } if (process.env.STORM_STACK_LOCAL && isParentPath(id, this.#corePath) || getString(code).includes("/* @storm-ignore */") || getString(code).includes("/* @storm-disable */")) { return true; } return false; } }; export { Compiler, listExports, parseAst };