diff --git a/.changeset/gorgeous-ducks-reflect.md b/.changeset/gorgeous-ducks-reflect.md new file mode 100644 index 00000000..1bfa32e9 --- /dev/null +++ b/.changeset/gorgeous-ducks-reflect.md @@ -0,0 +1,6 @@ +--- +"@marko/language-tools": minor +"@marko/type-check": minor +--- + +Automatically add `.js` extensions where necessary in files output by `@marko/type-check` to work better with native es modules. diff --git a/packages/language-tools/src/processors/index.ts b/packages/language-tools/src/processors/index.ts index 6f0e6b5e..80ddd2a9 100644 --- a/packages/language-tools/src/processors/index.ts +++ b/packages/language-tools/src/processors/index.ts @@ -41,7 +41,7 @@ export interface PrintContext { export const extensions = [marko.extension] as ProcessorExtension[]; -export function create(options: Parameters[0]) { +export function create(options: CreateProcessorOptions) { return { [marko.extension]: marko.create(options), } as Record; diff --git a/packages/language-tools/src/processors/marko.ts b/packages/language-tools/src/processors/marko.ts index 8028a12e..6897abed 100644 --- a/packages/language-tools/src/processors/marko.ts +++ b/packages/language-tools/src/processors/marko.ts @@ -1,4 +1,5 @@ import path from "path"; +import type { Config, types as t } from "@marko/compiler"; import type ts from "typescript/lib/tsserverlibrary"; import { ScriptLang, extractScript } from "../extractors/script"; import { parse } from "../parser"; @@ -6,6 +7,10 @@ import { parse } from "../parser"; import * as Project from "../util/project"; import type { ProcessorConfig } from "."; +const isRemapExtensionReg = /\.ts$/; +const skipRemapExtensionsReg = + /\.(?:[cm]?jsx?|json|marko|css|less|sass|scss|styl|stylus|pcss|postcss|sss|a?png|jpe?g|jfif|pipeg|pjp|gif|svg|ico|web[pm]|avif|mp4|ogg|mp3|wav|flac|aac|opus|woff2?|eot|[ot]tf|webmanifest|pdf|txt)$/; + export default { extension: ".marko", create({ ts, host, configFile }) { @@ -21,7 +26,10 @@ export default { runtimeTypes.internalTypesFile, runtimeTypes.markoTypesFile, ]; - const compileConfig: import("@marko/compiler").Config = { + const getJSFileIfTSExists = (source: string, importer: string) => + host.fileExists(path.join(importer, "..", `${source}.ts`)) && + `${source}.js`; + const compileConfig: Config = { output: "source", stripTypes: true, sourceMaps: true, @@ -29,6 +37,38 @@ export default { babelrc: false, configFile: false, browserslistConfigFile: false, + plugins: [ + { + visitor: { + // Find all relative imports in Marko template + // if they would map to a `.ts` file, then we convert it to a `.js` file for the output. + "ImportDeclaration|ExportNamedDeclaration"( + decl: t.NodePath< + t.ImportDeclaration | t.ExportNamedDeclaration + >, + ) { + const { node } = decl; + const value = node.source?.value; + const importKind = + "importKind" in node ? node.importKind : undefined; + if ( + value?.[0] === "." && + (!importKind || importKind === "value") && + !skipRemapExtensionsReg.test(value) + ) { + const filename = decl.hub.file.opts.filename as string; + const remap = isRemapExtensionReg.test(value) + ? `${value.slice(0, -2)}js` + : getJSFileIfTSExists(value, filename) || + getJSFileIfTSExists(`${value}/index`, filename); + if (remap) { + node.source!.value = remap; + } + } + }, + }, + }, + ], caller: { name: "@marko/type-check", supportsStaticESM: true, diff --git a/packages/type-check/src/run.ts b/packages/type-check/src/run.ts index 8e8d691d..89bf5a6f 100644 --- a/packages/type-check/src/run.ts +++ b/packages/type-check/src/run.ts @@ -38,6 +38,10 @@ const getCanonicalFileName = ts.sys.useCaseSensitiveFileNames : (fileName: string) => fileName.toLowerCase(); const fsPathReg = /^(?:[./\\]|[A-Z]:)/i; const modulePartsReg = /^((?:@(?:[^/]+)\/)?(?:[^/]+))(.*)$/; +const isRemapExtensionReg = /\.ts$/; +const skipRemapExtensionsReg = + /\.(?:[cm]?jsx?|json|marko|css|less|sass|scss|styl|stylus|pcss|postcss|sss|a?png|jpe?g|jfif|pipeg|pjp|gif|svg|ico|web[pm]|avif|mp4|ogg|mp3|wav|flac|aac|opus|woff2?|eot|[ot]tf|webmanifest|pdf|txt)$/; + const extractCache = new WeakMap< ts.SourceFile, ReturnType @@ -95,6 +99,61 @@ export default function run(opts: Options) { getCanonicalFileName, options, ); + const getJSFileIfTSExists = (source: string, importer: string) => + compilerHost.fileExists(path.join(importer, "..", `${source}.ts`)) && + `${source}.js`; + + // Find all relative imports in typescript output + // if they would map to a `.ts` file, then we convert it to a `.js` file for the output. + const customTransformers: ts.CustomTransformers = { + after: [ + (ctx) => (sourceFile) => { + return ts.visitNode(sourceFile, visit) as ts.SourceFile; + + function visit(node: ts.Node): ts.Node { + if (ts.isSourceFile(node)) { + return ts.visitEachChild(node, visit, ctx); + } + + if ( + (ts.isImportDeclaration(node) || + ts.isExportDeclaration(node)) && + node.moduleSpecifier && + ts.isStringLiteral(node.moduleSpecifier) + ) { + const value = node.moduleSpecifier.text; + if (value[0] === "." && !skipRemapExtensionsReg.test(value)) { + const { fileName } = sourceFile; + const remap = isRemapExtensionReg.test(value) + ? `${value.slice(0, -2)}js` + : getJSFileIfTSExists(value, fileName) || + getJSFileIfTSExists(`${value}/index`, fileName); + if (remap) { + return ts.isImportDeclaration(node) + ? ctx.factory.updateImportDeclaration( + node, + node.modifiers, + node.importClause, + ctx.factory.createStringLiteral(remap), + node.attributes, + ) + : ctx.factory.updateExportDeclaration( + node, + node.modifiers, + node.isTypeOnly, + node.exportClause, + ctx.factory.createStringLiteral(remap), + node.attributes, + ); + } + } + } + + return node; + } + }, + ], + }; const { readDirectory = ts.sys.readDirectory } = compilerHost; compilerHost.readDirectory = ( @@ -138,7 +197,7 @@ export default function run(opts: Options) { resolvedFileName = path.resolve(containingFile, "..", moduleName); } else { // For other paths we treat it as a node_module and try resolving - // that modules `marko.json`. If the `marko.json` exists then we'll + // that modules `package.json`. If the `package.json` exists then we'll // try resolving the `.marko` file relative to that. const [, nodeModuleName, relativeModulePath] = modulePartsReg.exec(moduleName)!; @@ -285,7 +344,6 @@ export default function run(opts: Options) { _writeFile, cancellationToken, emitOnlyDtsFiles, - customTransformers, ) => { let writeFile = _writeFile; if (_writeFile) {