diff --git a/integration/unplugin-examples/esbuild/src/main.civet b/integration/unplugin-examples/esbuild/src/main.civet index e7ec9b5c..b264cd02 100644 --- a/integration/unplugin-examples/esbuild/src/main.civet +++ b/integration/unplugin-examples/esbuild/src/main.civet @@ -1,4 +1,4 @@ -{a} from "./module" +{a} from "./module.civet" console.log a diff --git a/source/parser.hera b/source/parser.hera index a9fa9c07..ab342be0 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -5699,8 +5699,8 @@ ModuleExportName # https://262.ecma-international.org/#prod-ModuleSpecifier ModuleSpecifier - UnprocessedModuleSpecifier ImportAssertion?:a -> - let { token } = $1 + UnprocessedModuleSpecifier:module ImportAssertion?:assertion -> + let { token } = module if (config.rewriteTsImports) { // Workaround to fix: // https://github.com/microsoft/TypeScript/issues/42151 @@ -5714,10 +5714,16 @@ ModuleSpecifier `${config.rewriteCivetImports.replace(/\$/g, '$$')}$1`) } - if (a) - return [{ ...$1, token }, a] + if (token !== module.token) { + module = { ...module, token, input: module.token } + } - return { ...$1, token } + return { + type: "ModuleSpecifier", + module, + assertion, + children: [ module, assertion ], + } UnprocessedModuleSpecifier StringLiteral diff --git a/source/parser/declaration.civet b/source/parser/declaration.civet index 0fb0ff04..79654f87 100644 --- a/source/parser/declaration.civet +++ b/source/parser/declaration.civet @@ -38,6 +38,7 @@ import { import { convertOptionalType + insertTrimmingSpace isExit makeLeftHandSideExpression makeNode @@ -61,7 +62,6 @@ import { import { convertNamedImportsToObject - insertTrimmingSpace processCallMemberExpression } from ./lib.civet @@ -223,7 +223,7 @@ function processDeclarationCondition(condition, rootCondition, parent: ASTNodePa simple := ref is expression let children if simple - ref = insertTrimmingSpace ref, "" + ref = trimFirstSpace ref children = [ref] else children = [ref, initializer] @@ -406,9 +406,9 @@ function processDeclarationConditionStatement(s: IfStatement | IterationStatemen // Convert FromClause into arguments for dynamic import function dynamizeFromClause(from) from = from[1..] // remove 'from' - from = insertTrimmingSpace from, "" - if from.-1?.type is "ImportAssertion" - assert := from.pop() + from = trimFirstSpace from + if assert := from.-1?.assertion + from.-1.children |>= .filter (is not assert) from.push ", {", assert.keyword, ":", assert.object, "}" ["(", ...from, ")"] @@ -490,22 +490,21 @@ function dynamizeImportDeclarationExpression($0) [imp, ws1, named, ws2, from] := $0 object := convertNamedImportsToObject(named) dot := "." - processCallMemberExpression { + processCallMemberExpression type: "CallExpression", children: [ - { type: "Await", children: "await" }, " ", - imp, - insertTrimmingSpace(ws2, ""), - dynamizeFromClause(from), + { type: "Await", children: "await" }, " " + imp + trimFirstSpace(ws2) + dynamizeFromClause(from) { - type: "PropertyGlob", - dot, - object, - children: [ws1, dot, object], - reversed: true, + type: "PropertyGlob" + dot + object + children: [ws1, dot, object] + reversed: true } ] - } function convertWithClause(withClause: WithClause, extendsClause?: [ASTLeaf, WSNode, ExpressionNode]) let extendsToken, extendsTarget, ws: WSNode diff --git a/source/parser/types.civet b/source/parser/types.civet index 0e49b6e7..4b3e772d 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -95,9 +95,11 @@ export type OtherNode = | FieldDefinition | FinallyClause | ForDeclaration + | ImportAssertion | Index | Initializer | Label + | ModuleSpecifier | NonNullAssertion | NormalCatchParameter | ObjectBindingPattern @@ -567,6 +569,20 @@ export type ExportDeclaration ts?: boolean declaration?: ASTNode +export type ModuleSpecifier + type: "ModuleSpecifier" + children: Children + parent?: Parent + module: ASTLeaf | StringLiteral + assertion?: ImportAssertion + +export type ImportAssertion + type: "ImportAssertion" + children: Children + parent?: Parent + keyword: "with" | "assert" + object: ASTNode + export type DeclarationStatement = type: "Declaration" children: Children @@ -1008,6 +1024,9 @@ export type LiteralContentNode = | ASTLeaf | ASTLeafWithType "NumericLiteral" | "StringLiteral" +export type NumericLiteral = ASTLeafWithType "NumericLiteral" +export type StringLiteral = ASTLeafWithType "StringLiteral" + export type RangeExpression type: "RangeExpression" children: Children diff --git a/source/unplugin/README.md b/source/unplugin/README.md index 7572763e..773585f3 100644 --- a/source/unplugin/README.md +++ b/source/unplugin/README.md @@ -199,6 +199,7 @@ interface PluginOptions { - Specifying `true` aborts the build (with an error code) on TypeScript errors. - Alternatively, you can specify a string with any combination of `error`, `warning`, `suggestion`, or `message` to specify which diagnostics abort the build. For example, `"none"` ignores all diagnostics, `"error+warning"` aborts on errors and warnings, and `"all"` aborts on all diagnostics. - `implicitExtension`: Whether to allow importing `filename.civet` via `import "filename"`. Default: `true`. + - *Note*: Incompatible with `typecheck: true` (TypeScript needs an explicit `.civet` extension) - `outputExtension`: JavaScript or TypeScript extension to append to `.civet` for internal purposes. Default: `".jsx"`, or `".tsx"` if `ts` is `"preserve"`. - `ts`: Mode for transpiling TypeScript features into JavaScript. Default: `"civet"`. Options: - `"civet"`: Use Civet's JS mode. (Not all TS features supported.) diff --git a/source/unplugin/unplugin.civet b/source/unplugin/unplugin.civet index c91ae40c..0ab156a2 100644 --- a/source/unplugin/unplugin.civet +++ b/source/unplugin/unplugin.civet @@ -1,29 +1,28 @@ -import { type TransformResult, createUnplugin } from 'unplugin'; -import civet, { SourceMap, type CompileOptions, type ParseOptions } from '@danielx/civet'; -import { findInDir, loadConfig } from '@danielx/civet/config'; +import { type TransformResult, createUnplugin } from 'unplugin' +import civet, { lib, SourceMap, type CompileOptions, type ParseOptions } from '@danielx/civet' +import { findInDir, loadConfig } from '@danielx/civet/config' import { remapRange, flattenDiagnosticMessageText, // @ts-ignore // using ts-ignore because the version of @danielx/civet typescript is checking against // is the one published to npm, not the one in the repo -} from '@danielx/civet/ts-diagnostic'; -import * as fs from 'fs'; -import path from 'path'; -import type { FormatDiagnosticsHost, Diagnostic, System } from 'typescript'; -import * as tsvfs from '@typescript/vfs'; -import type { UserConfig } from 'vite'; -import type { BuildOptions } from 'esbuild'; -import os from 'os'; -import { DEFAULT_EXTENSIONS } from './constants.mjs'; +} from '@danielx/civet/ts-diagnostic' +import * as fs from 'fs' +import path from 'path' +import type { FormatDiagnosticsHost, Diagnostic, System } from 'typescript' +import * as tsvfs from '@typescript/vfs' +import type { UserConfig } from 'vite' +import type { BuildOptions } from 'esbuild' +import os from 'os' +import { DEFAULT_EXTENSIONS } from './constants.mjs' // Copied from typescript to avoid importing the whole package -enum DiagnosticCategory { - Warning = 0, - Error = 1, - Suggestion = 2, +enum DiagnosticCategory + Warning = 0 + Error = 1 + Suggestion = 2 Message = 3 -} export type PluginOptions implicitExtension?: boolean @@ -62,56 +61,54 @@ function cleanCivetId(id: string): {id: string, postfix: string} .replace /\.[jt]sx$/, '' {id, postfix} -function tryStatSync(file: string): fs.Stats | undefined { - try { +function tryStatSync(file: string): fs.Stats? + try // The "throwIfNoEntry" is a performance optimization for cases where the file does not exist return fs.statSync(file, { throwIfNoEntry: false }); - } catch { - return undefined; - } -} - -export function slash(p: string): string { - return p.replace(windowsSlashRE, '/'); -} -function normalizePath(id: string): string { - return path.posix.normalize(isWindows ? slash(id) : id); -} +export function slash(p: string): string + p.replace windowsSlashRE, '/' -function tryFsResolve(file: string): string | undefined { - const fileStat = tryStatSync(file); - if (fileStat?.isFile()) return normalizePath(file); +function normalizePath(id: string): string + path.posix.normalize isWindows ? slash(id) : id - return undefined; -} +function tryFsResolve(file: string): string? + fileStat := tryStatSync file + if fileStat?.isFile() + normalizePath file -function resolveAbsolutePath(rootDir: string, id: string, implicitExtension: boolean) { - const file = path.join(rootDir, id); +function resolveAbsolutePath(rootDir: string, id: string, implicitExtension: boolean) + file := path.join rootDir, id // Check for existence of resolved file and unresolved id, // without and with implicit .civet extension, and return first existing - return tryFsResolve(file) || - (implicitExtension && implicitCivet(file)) || - tryFsResolve(id) || - (implicitExtension && implicitCivet(id)); -} - -function implicitCivet(file: string): string | undefined { - if (tryFsResolve(file)) return - const civet = file + '.civet' - if (tryFsResolve(civet)) return civet - return -} + (or) + tryFsResolve(file) + implicitExtension and implicitCivet file + tryFsResolve id + implicitExtension and implicitCivet id + +function implicitCivet(file: string): string? + return if tryFsResolve file + civet := file + '.civet' + return civet if tryFsResolve civet export const rawPlugin: Parameters>[0] = (options: PluginOptions = {}, meta) => if (options.dts) options.emitDeclaration = options.dts - if (options.js) options.ts = 'civet' compileOptions: CompileOptions .= {} + ts .= options.ts + if (options.js) ts = 'civet' + unless ts? + console.log 'WARNING: You are using the default mode for `options.ts` which is `"civet"`. This mode does not support all TS features. If this is intentional, you should explicitly set `options.ts` to `"civet"`, or choose a different mode.' + ts = "civet" + unless ts is in ["civet", "esbuild", "tsc", "preserve"] + console.log `WARNING: Invalid option ts: ${JSON.stringify ts}; switching to "civet"` + ts = "civet" + transformTS := options.emitDeclaration or options.typecheck outExt := - options.outputExtension ?? (options.ts is 'preserve' ? '.tsx' : '.jsx') + options.outputExtension ?? (ts is "preserve" ? ".tsx" : ".jsx") implicitExtension := options.implicitExtension ?? true let aliasResolver: (id: string) => string @@ -123,10 +120,8 @@ export const rawPlugin: Parameters>[0] = let configErrors: Diagnostic[]? let configFileNames: string[] - tsPromise := - transformTS || options.ts === 'tsc' - ? import('typescript').then .default - : null; + tsPromise := if transformTS or ts is "tsc" + import('typescript').then .default getFormatHost := (sys: System): FormatDiagnosticsHost => return { getCurrentDirectory: => sys.getCurrentDirectory() @@ -141,220 +136,198 @@ export const rawPlugin: Parameters>[0] = plugin: ReturnType & { __virtualModulePrefix?: string } := { name: 'unplugin-civet' enforce: 'pre' - async buildStart(): Promise { + + async buildStart(): Promise const civetConfigPath = 'config' in options ? options.config : await findInDir(process.cwd()) - if (civetConfigPath) { + if civetConfigPath compileOptions = await loadConfig(civetConfigPath) - } // Merge parseOptions, with plugin options taking priority compileOptions.parseOptions = { - ...compileOptions.parseOptions, - ...options.parseOptions, + ...compileOptions.parseOptions + ...options.parseOptions } - if (transformTS || options.ts === 'tsc') { - const ts = await tsPromise!; + if transformTS or ts is "tsc" + ts := await tsPromise! - const tsConfigPath = ts.findConfigFile(process.cwd(), ts.sys.fileExists); + tsConfigPath := ts.findConfigFile process.cwd(), ts.sys.fileExists - if (!tsConfigPath) { - throw new Error("Could not find 'tsconfig.json'"); - } + unless tsConfigPath + throw new Error "Could not find 'tsconfig.json'" - const { config, error } = ts.readConfigFile( - tsConfigPath, + { config, error } := ts.readConfigFile + tsConfigPath ts.sys.readFile - ); - if (error) { - console.error(ts.formatDiagnostic(error, getFormatHost(ts.sys))); - throw error; - } + if error + console.error ts.formatDiagnostic error, getFormatHost ts.sys + throw error // Mogrify tsconfig.json "files" field to use .civet.tsx - function mogrify(key: string) { - if (key in config && Array.isArray(config[key])) { - config[key] = config[key].map((item: unknown) => { - if (typeof item !== 'string') return item + function mogrify(key: string) + if key in config and Array.isArray config[key] + config[key] = config[key].map (item: unknown) => + return item unless item { - extensions = [ ...(extensions ?? []), ".civet" ]; - return systemReadDirectory(path, extensions, excludes, includes, depth) + system := {...ts.sys} + {readDirectory: systemReadDirectory} := system + system.readDirectory = (path: string, extensions?: readonly string[], excludes?: readonly string[], includes?: readonly string[], depth?: number): string[] => + extensions = [ ...(extensions ?? []), ".civet" ] + systemReadDirectory(path, extensions, excludes, includes, depth) .map &.endsWith(".civet") ? & + ".tsx" : & - } - const configContents = ts.parseJsonConfigFileContent( - config, - system, + configContents := ts.parseJsonConfigFileContent + config + system process.cwd() - ); - configErrors = configContents.errors; - configFileNames = configContents.fileNames; + configErrors = configContents.errors + configFileNames = configContents.fileNames compilerOptions = { - ...configContents.options, - target: ts.ScriptTarget.ESNext, - composite: false, - }; + ...configContents.options + target: ts.ScriptTarget.ESNext + composite: false + } // We use .tsx extensions when type checking, so need to enable // JSX mode even if the user doesn't request/use it. - compilerOptions.jsx ??= ts.JsxEmit.Preserve; + compilerOptions.jsx ??= ts.JsxEmit.Preserve compilerOptionsWithSourceMap = { - ...compilerOptions, - sourceMap: true, - }; - fsMap = new Map(); - } - } - async buildEnd(useConfigFileNames = false): Promise { - if (transformTS) { - const ts = await tsPromise!; + ...compilerOptions + sourceMap: true + } + fsMap = new Map() + + async buildEnd(useConfigFileNames = false): Promise + if transformTS + const ts = await tsPromise! // Create a virtual file system with all source files processed so far, // but which further resolves any Civet dependencies that are needed // just for typechecking (e.g. `import type` which get removed in JS). - const system = tsvfs.createFSBackedSystem(fsMap, process.cwd(), ts) - const { - fileExists: systemFileExists, - readFile: systemReadFile, - readDirectory: systemReadDirectory, - } = system - - system.fileExists = (filename: string): boolean => { + system := tsvfs.createFSBackedSystem fsMap, process.cwd(), ts + { + fileExists: systemFileExists + readFile: systemReadFile + readDirectory: systemReadDirectory + } := system + + system.fileExists = (filename: string): boolean => if (!filename.endsWith('.civet.tsx')) return systemFileExists(filename) if (fsMap.has(filename)) return true - return systemFileExists(filename.slice(0, -4)) - }; + return systemFileExists filename[...-4] system.readDirectory = (path: string): string[] => systemReadDirectory(path) .map &.endsWith('.civet') ? & + '.tsx' : & - system.readFile = (filename: string, encoding: string = 'utf-8'): string | undefined => { + tsCompileOptions := { + ...compileOptions + rewriteCivetImports: false + rewriteTsImports: true + } + system.readFile = (filename: string, encoding = 'utf-8'): string? => // Mogrify package.json imports field to use .civet.tsx - if (path.basename(filename) === 'package.json') { - const json = systemReadFile(filename, encoding) - if (!json) return json - const parsed: Record = JSON.parse(json) - let modified = false - function recurse(node: unknown): void { - if (node && typeof node === 'object') { - for (const key in node) { - const value = (node as Record)[key] - if (typeof value === 'string') { - if (value.endsWith('.civet')) { + if path.basename(filename) is "package.json" + json := systemReadFile filename, encoding + return json unless json + parsed: Record := JSON.parse(json) + modified .= false + function recurse(node: unknown): void + if node? )[key] + if value )[key] = value + '.tsx' modified = true - } - } else if (value) { - recurse(value) - } - } - } - } - recurse(parsed.imports) + else if value + recurse value + recurse parsed.imports return modified ? JSON.stringify(parsed) : json - } // Generate .civet.tsx files on the fly if (!filename.endsWith('.civet.tsx')) return systemReadFile(filename, encoding) if (fsMap.has(filename)) return fsMap.get(filename) - const civetFilename = filename.slice(0, -4) - const rawCivetSource = fs.readFileSync(civetFilename, { + civetFilename := filename[...-4] + rawCivetSource := fs.readFileSync civetFilename, encoding: encoding as BufferEncoding - }) - const compiledTS = civet.compile(rawCivetSource, { - ...compileOptions, - filename, - js: false, - sync: true, // TS readFile API seems to need to be synchronous - }); - fsMap.set(filename, compiledTS) + { code: compiledTS, sourceMap } := civet.compile rawCivetSource, { + ...tsCompileOptions + filename + js: false + sourceMap: true + sync: true // TS readFile API seems to need to be synchronous + } + fsMap.set filename, compiledTS + sourceMaps.set filename, sourceMap return compiledTS - }; - const host = tsvfs.createVirtualCompilerHost( - system, - compilerOptions, + host := tsvfs.createVirtualCompilerHost + system + compilerOptions ts - ); - const program = ts.createProgram({ - rootNames: useConfigFileNames ? configFileNames : [...fsMap.keys()], - options: compilerOptions, - host: host.compilerHost, - }); + program := ts.createProgram + rootNames: useConfigFileNames ? configFileNames : [...fsMap.keys()] + options: compilerOptions + host: host.compilerHost - const diagnostics: Diagnostic[] = ts + diagnostics: Diagnostic[] := ts .getPreEmitDiagnostics(program) - .map((diagnostic) => - const file = diagnostic.file; - if (!file) return diagnostic; + .map (diagnostic) => + file := diagnostic.file + if (!file) return diagnostic - const sourceMap = sourceMaps.get(file.fileName); - if (!sourceMap) return diagnostic; + sourceMap := sourceMaps.get file.fileName + if (!sourceMap) return diagnostic - const sourcemapLines = sourceMap.data.lines; - const range = remapRange( + sourcemapLines := sourceMap.data.lines + range := remapRange( { start: diagnostic.start || 0, end: (diagnostic.start || 0) + (diagnostic.length || 1), }, sourcemapLines - ); + ) - return { + { ...diagnostic, messageText: flattenDiagnosticMessageText(diagnostic.messageText), length: diagnostic.length, start: range.start, - }; - ); - - if (configErrors?.length) { - diagnostics.unshift(...configErrors); - } + } - if (diagnostics.length > 0) { - console.error( - ts.formatDiagnosticsWithColorAndContext( - diagnostics, - getFormatHost(ts.sys) - ) - ); - if (options.typecheck) { - let failures: DiagnosticCategory[] = []; - if (typeof options.typecheck === 'string') { + if configErrors?# + diagnostics.unshift ...configErrors + + if diagnostics# > 0 + console.error + ts.formatDiagnosticsWithColorAndContext + diagnostics + getFormatHost ts.sys + if options.typecheck + failures: DiagnosticCategory[] .= [] + if options.typecheck true } as any as DiagnosticCategory[] - } - } else { + else // Default behavior: fail on errors failures.push(DiagnosticCategory.Error) - } - const count = diagnostics.filter((d) => failures.includes(d.category)).length - if (count) { - const reason = - (count === diagnostics.length ? count : `${count} out of ${diagnostics.length}`) - throw new Error(`Aborting build because of ${reason} TypeScript diagnostic${diagnostics.length > 1 ? 's' : ''} above`) - } - } - } + count := diagnostics.filter((d) => failures.includes(d.category)).length + if count + reason := + (count is diagnostics# ? count : `${count} out of ${diagnostics#}`) + throw new Error `Aborting build because of ${reason} TypeScript diagnostic${diagnostics.length > 1 ? 's' : ''} above` if options.emitDeclaration if meta.framework is 'esbuild' and not esbuildOptions.outdir @@ -404,8 +377,6 @@ export const rawPlugin: Parameters>[0] = undefined // @ts-ignore @internal interface true // forceDtsEmit - } - } resolveId(id, importer, options) id = aliasResolver id if aliasResolver? @@ -445,133 +416,130 @@ export const rawPlugin: Parameters>[0] = loadInclude(id) isCivetTranspiled.test id - async load(id) { - const match = isCivetTranspiled.exec(id); - if (!match) return null; - const basename = id.slice(0, match.index + match[1].length); - - const filename = path.resolve(rootDir, basename); - - let mtime; - if (cache) { - mtime = (await fs.promises.stat(filename)).mtimeMs; - const cached = cache?.get(filename); - if (cached && cached.mtime === mtime) { - return cached.result; - } + async load(id) + match := isCivetTranspiled.exec id + return null unless match + basename := id[...match.index + match[1]#] + + filename := path.resolve rootDir, basename + + let mtime + if cache + mtime = fs.promises.stat(filename) |> await |> .mtimeMs + const cached = cache?.get filename + if cached and cached.mtime is mtime + return cached.result + + rawCivetSource := await fs.promises.readFile filename, 'utf-8' + this.addWatchFile filename + + let compiled: string + let sourceMap: SourceMap | string | undefined + civetOptions := { + ...compileOptions + filename: id + errors: [] + } as const + function checkErrors + if civetOptions.errors# + throw new civet.ParseErrors civetOptions.errors + + ast := await civet.compile rawCivetSource, { + ...civetOptions + ast: true } + civetSourceMap := SourceMap rawCivetSource - const rawCivetSource = await fs.promises.readFile(filename, 'utf-8'); - this.addWatchFile(filename); - - let compiled: { - code: string; - sourceMap: SourceMap | string | undefined; - }; - const civetOptions = { - ...compileOptions, - filename: id, - sourceMap: true, - } as const; - - if (options.ts === 'civet' && !transformTS) { - compiled = await civet.compile(rawCivetSource, { - ...civetOptions, - js: true, - }); - } else { - const compiledTS = await civet.compile(rawCivetSource, { - ...civetOptions, - js: false, - }); - - const resolved = filename + outExt; - sourceMaps.set( - resolved, - compiledTS.sourceMap as SourceMap - ); - - if (transformTS) { - // Force .tsx extension for type checking purposes. - // Otherwise, TypeScript complains about types in .jsx files. - const tsx = filename + '.tsx'; - fsMap.set(tsx, compiledTS.code); - // Vite and Rollup normalize filenames to use `/` instead of `\`. - // We give the TypeScript VFS both versions just in case. - const slashed = slash(tsx); - if (tsx !== slashed) fsMap.set(slashed, compiledTS.code); + if ts is "civet" + compiled = await civet.generate ast, { + ...civetOptions + js: true + sourceMap: civetSourceMap } - - switch (options.ts) { - case 'esbuild': { - const esbuildTransform = (await import('esbuild')).transform; - const result = await esbuildTransform(compiledTS.code, { - jsx: 'preserve', - loader: 'tsx', - sourcefile: id, - sourcemap: 'external', - }); - - compiled = { code: result.code, sourceMap: result.map }; - break; - } - case 'tsc': { - const tsTranspile = (await tsPromise!).transpileModule; - const result = tsTranspile(compiledTS.code, - { compilerOptions: compilerOptionsWithSourceMap }); - - compiled = { - code: result.outputText, - sourceMap: result.sourceMapText, - }; - break; - } - case 'preserve': { - compiled = compiledTS; - break; - } - case 'civet': - default: { - compiled = await civet.compile(rawCivetSource, { - ...civetOptions, - js: true, - }); - - if (options.ts == undefined) { - console.log( - 'WARNING: You are using the default mode for `options.ts` which is `"civet"`. This mode does not support all TS features. If this is intentional, you should explicitly set `options.ts` to `"civet"`, or choose a different mode.' - ); - } - - break; - } + sourceMap = civetSourceMap + checkErrors() + else + compiledTS := await civet.generate ast, { + ...civetOptions + js: false + sourceMap: civetSourceMap } - } - - const jsonSourceMap = compiled.sourceMap && ( - typeof compiled.sourceMap === 'string' - ? JSON.parse(compiled.sourceMap) - : compiled.sourceMap.json( - path.basename(id.replace(/\.[jt]sx$/, '')), - path.basename(id) - ) - ); + checkErrors() + + switch ts + when "esbuild" + esbuildTransform := import("esbuild") |> await |> .transform + result := await esbuildTransform compiledTS, + jsx: "preserve" + loader: "tsx" + sourcefile: id + sourcemap: "external" + + compiled = result.code + sourceMap = result.map + when "tsc" + tsTranspile := tsPromise! |> await |> .transpileModule + result := tsTranspile compiledTS, + compilerOptions: compilerOptionsWithSourceMap + + compiled = result.outputText + sourceMap = result.sourceMapText + when "preserve" + compiled = compiledTS + + if transformTS + // When working with TypeScript, disable rewriteCivetImports and + // force rewriteTsImports by rewriting imports again. + // See `ModuleSpecifier` in parser.hera + for each _spec of lib.gatherRecursive ast, ( + ($) => ($ as {type: string}).type is "ModuleSpecifier" + ) + spec := _spec as { module?: { token: string, input?: string } } + if spec.module?.input + spec.module.token = spec.module.input + .replace /\.([mc])?ts(['"])$/, ".$1js$2" + + compiledTS := await civet.generate ast, { + ...civetOptions + js: false + sourceMap: civetSourceMap + } + checkErrors() + + // Force .tsx extension for type checking purposes. + // Otherwise, TypeScript complains about types in .jsx files. + tsx := filename + '.tsx' + fsMap.set tsx, compiledTS + sourceMaps.set tsx, civetSourceMap + // Vite and Rollup normalize filenames to use `/` instead of `\`. + // We give the TypeScript VFS both versions just in case. + slashed := slash tsx + unless tsx is slashed + fsMap.set slashed, compiledTS + sourceMaps.set slashed, civetSourceMap + + jsonSourceMap := sourceMap and + if sourceMap >[0] = return [ ...modules, module ] modules } + webpack(compiler) compiler.options.resolve.extensions.unshift ".civet" if implicitExtension aliasResolver = (id) => diff --git a/types/types.d.ts b/types/types.d.ts index 907b3e7f..dc698cab 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -49,6 +49,9 @@ declare module "@danielx/civet" { trace?: string parseOptions?: ParseOptions } + export type GenerateOptions = Omit & { + sourceMap?: undefined | SourceMap + } export type SyncCompileOptions = CompileOptions & { parseOptions?: { comptime?: false } } @@ -61,6 +64,7 @@ declare module "@danielx/civet" { lines: SourceMapping[][] } } + export function SourceMap(source: string): SourceMap // TODO: Import ParseError class from Hera export type ParseError = { @@ -93,7 +97,12 @@ declare module "@danielx/civet" { /** Warning: No caching */ export function parseProgram(source: string, options?: T): T extends { comptime: true } ? Promise : CivetAST - export function generate(ast: CivetAST, options?: CompileOptions): string + export function generate(ast: CivetAST, options?: GenerateOptions): string + + export const lib: { + gatherRecursive(ast: CivetAST, predicate: (node: CivetAST) => boolean): CivetAST[] + gatherRecursiveAll(ast: CivetAST, predicate: (node: CivetAST) => boolean): CivetAST[] + } const Civet: { version: string @@ -101,6 +110,9 @@ declare module "@danielx/civet" { isCompileError: typeof isCompileError parse: typeof parse generate: typeof generate + SourceMap: typeof SourceMap + ParseError: typeof ParseError + ParseErrors: typeof ParseErrors sourcemap: { locationTable(input: string): number[] lookupLineColumn(table: number[], pos: number): [number, number]