Skip to content

Commit

Permalink
Replace TS extensions in generated declaration files (#556)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed May 19, 2023
1 parent 6860429 commit 908c43e
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-eagles-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preconstruct/cli": minor
---

Always emit relative paths used in generated TS declaration files with resolved extensions of their runtime equivalents. This currently requires one of the 2 experimental flags: `importsConditions` or `onlyEmitUsedTypeScriptDeclarations`
285 changes: 281 additions & 4 deletions packages/cli/src/build/__tests__/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ test("onlyEmitUsedTypeScriptDeclarations", async () => {
await build(dir);
expect(await getFiles(dir, ["dist/**/*.d.ts"])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
import { A } from "./other";
import { A } from "./other.js";
export declare function thing(): A;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/other.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Expand Down Expand Up @@ -164,7 +164,7 @@ test("onlyEmitUsedTypeScriptDeclarations with export from", async () => {
await build(dir);
expect(await getFiles(dir, ["dist/**/*.d.ts"])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export type { A } from "./other";
export type { A } from "./other.js";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/other.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export type A = {
Expand Down Expand Up @@ -193,7 +193,7 @@ test("onlyEmitUsedTypeScriptDeclarations with inline import type", async () => {
await build(dir);
expect(await getFiles(dir, ["dist/**/*.d.ts"])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export declare function a(): import("./other").A;
export declare function a(): import("./other.js").A;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/other.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export type A = {
Expand Down Expand Up @@ -226,7 +226,7 @@ test("onlyEmitUsedTypeScriptDeclarations with import x = require('')", async ()
expect(await getFiles(dir, ["dist/**/*.d.ts"])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
declare namespace something {
export import x = require("./other");
export import x = require("./other.js");
}
export declare function a(): something.x.A;
export {};
Expand All @@ -242,3 +242,280 @@ test("onlyEmitUsedTypeScriptDeclarations with import x = require('')", async ()
`);
});

test("replaces ts extensions in module specifiers within generated declarations with importsConditions", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "@explicit-ts-extensions/repo",
preconstruct: {
packages: ["packages/pkg-a"],
exports: {},
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
importsConditions: true,
},
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
exports: {
".": {
types: "./dist/pkg-a.cjs.js",
module: "./dist/pkg-a.esm.js",
default: "./dist/pkg-a.cjs.js",
},
"./package.json": "./package.json",
},
}),
"packages/pkg-a/src/index.ts": ts`
export { fromTsExt } from "./foo.ts";
`,
"packages/pkg-a/src/foo.ts": ts`
export const fromTsExt = 1;
`,
node_modules: typescriptFixture.node_modules,
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "node",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await build(dir);

expect(await getFiles(dir, ["packages/*/dist/**/*.d.*"]))
.toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/foo.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export declare const fromTsExt = 1;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export { fromTsExt } from "./foo.js";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=pkg-a.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.ts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}
`);
});

test("replaces ts extensions in module specifiers within generated declarations with onlyEmitUsedTypeScriptDeclarations", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "@explicit-ts-extensions/repo",
preconstruct: {
packages: ["packages/pkg-a"],
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
onlyEmitUsedTypeScriptDeclarations: true,
},
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
}),
"packages/pkg-a/src/index.ts": ts`
export { fromTsExt } from "./foo.ts";
`,
"packages/pkg-a/src/foo.ts": ts`
export const fromTsExt = 1;
`,
node_modules: typescriptFixture.node_modules,
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "node",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await build(dir);

expect(await getFiles(dir, ["packages/*/dist/**/*.d.*"]))
.toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/foo.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export declare const fromTsExt = 1;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export { fromTsExt } from "./foo.js";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=pkg-a.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.ts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}
`);
});

test('doesn\'t replace ts "extensions" in module specifiers that are only parts of the actual filenames and not their extensions', async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "@explicit-ts-extensions/repo",
preconstruct: {
packages: ["packages/pkg-a"],
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
onlyEmitUsedTypeScriptDeclarations: true,
},
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
}),
"packages/pkg-a/src/index.ts": ts`
export { fromPseudoTsExt } from "./foo.ts";
`,
"packages/pkg-a/src/foo.ts.ts": ts`
export const fromPseudoTsExt = 1;
`,
node_modules: typescriptFixture.node_modules,
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "node",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await build(dir);

expect(await getFiles(dir, ["packages/*/dist/**/*.d.*"]))
.toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/foo.ts.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export declare const fromPseudoTsExt = 1;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export { fromPseudoTsExt } from "./foo.ts.js";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=pkg-a.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.ts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}
`);
});

test("replaces declaration extensions with their runtime counterparts", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "@explicit-dts-extension/repo",
preconstruct: {
packages: ["packages/pkg-a"],
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
onlyEmitUsedTypeScriptDeclarations: true,
},
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
}),
"packages/pkg-a/src/index.ts": ts`
export type { DtsExt } from "./types.d.ts";
`,
"packages/pkg-a/src/types.d.ts": ts`
export type DtsExt = 1;
`,
node_modules: typescriptFixture.node_modules,
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "node",
strict: true,
declaration: true,
},
}),
"babel.config.json": JSON.stringify({
presets: [require.resolve("@babel/preset-typescript")],
}),
});
await build(dir);

expect(await getFiles(dir, ["packages/*/dist/**/*.d.*"]))
.toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export type { DtsExt } from "./types.js";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/types.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export type DtsExt = 1;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=pkg-a.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.ts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}
`);
});

test("replaces package.json#imports in declaration files without importConditions flags", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "@imports-replacing/repo",
preconstruct: {
packages: ["packages/pkg-a"],
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
onlyEmitUsedTypeScriptDeclarations: true,
},
},
}),
"packages/pkg-a/package.json": JSON.stringify({
name: "pkg-a",
main: "dist/pkg-a.cjs.js",
module: "dist/pkg-a.esm.js",
imports: {
"#hidden": "./src/hidden_stuff.ts",
},
}),
"packages/pkg-a/src/index.ts": ts`
export { gem } from "#hidden";
`,
"packages/pkg-a/src/hidden_stuff.ts": ts`
export const gem = "🎁";
`,
node_modules: typescriptFixture.node_modules,
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "node16",
strict: true,
declaration: true,
},
}),
});
await build(dir);

expect(await getFiles(dir, ["packages/*/dist/**/*.d.*"]))
.toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/hidden_stuff.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export declare const gem = "\\uD83C\\uDF81";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export { gem } from "./hidden_stuff.js";
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=pkg-a.cjs.d.ts.map
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ packages/pkg-a/dist/pkg-a.cjs.d.ts.map ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
{"version":3,"file":"pkg-a.cjs.d.ts","sourceRoot":"","sources":["./declarations/src/index.d.ts"],"names":[],"mappings":"AAAA"}
`);
});
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ export const getDeclarationsForFile = async (
if (!visitModuleSpecifier) {
return node;
}

const cachedVisitModuleSpecifier = memoize(visitModuleSpecifier);

const replacedNodes = new Map<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import {
import { Program, ResolvedModuleFull } from "typescript";

function replaceExt(filename: string) {
return filename.replace(/\.([cm]?ts|tsx)$/, (match) => {
if (match === ".cts") return ".cjs";
if (match === ".mts") return ".mjs";
return filename.replace(/(\.d)?\.([cm]?ts|tsx)$/, (match, p1, p2) => {
if (p2 === ".cts") return ".cjs";
if (p2 === ".mts") return ".mjs";
return ".js";
});
}

export async function getUsedDeclarationsWithPackageJsonImportsReplaced(
export async function getDeclarationsWithImportedModuleSpecifiersReplacing(
typescript: TS,
program: Program,
normalizedPkgDir: string,
Expand Down Expand Up @@ -44,10 +44,9 @@ export async function getUsedDeclarationsWithPackageJsonImportsReplaced(
) {
return imported;
}

depQueue.add(resolvedModule.resolvedFileName);
if (imported[0] !== "#") {
return imported;
}

let forImport = replaceExt(
normalizePath(
path.relative(path.dirname(filename), resolvedModule.resolvedFileName)
Expand All @@ -68,6 +67,7 @@ export async function getUsedDeclarationsWithPackageJsonImportsReplaced(
diagnosticsHost,
handleImport
);

emitted.push(output);
}
return emitted;
Expand Down

0 comments on commit 908c43e

Please sign in to comment.