Skip to content

Commit

Permalink
Normalise imports in manually written declaration files (#559)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown committed May 19, 2023
1 parent 0a4853c commit a58f021
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-elephants-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preconstruct/cli": patch
---

Extend import path normalisation in generated declaration files with the `importsConditions` and `onlyEmitUsedTypeScriptDeclarations` experimental flags to manually written declaration files as well
64 changes: 64 additions & 0 deletions packages/cli/src/build/__tests__/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,67 @@ test("replaces package.json#imports in declaration files without importCondition
`);
});

test("normalises imports in manually authored .d.ts files", 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.js": ts`
export { gem } from "#hidden";
`,
"packages/pkg-a/src/index.d.ts": ts`
export { gem } from "#hidden";
export type A = typeof import(/** comment */ "#hidden").gem;
export type B = typeof import(/* non-jsdoc comment */ "./hidden_stuff").gem;
export type C = typeof import("./hidden_stuff.ts").gem;
`,
"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",
allowImportingTsExtensions: true,
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";
export type A = typeof import(/** comment */ "./hidden_stuff.js").gem;
export type B = typeof import(/* non-jsdoc comment */ "./hidden_stuff.js").gem;
export type C = typeof import("./hidden_stuff.js").gem;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ 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"}
`);
});
50 changes: 36 additions & 14 deletions packages/cli/src/rollup-plugins/typescript-declarations/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as fs from "fs-extra";
import path from "path";
import normalizePath from "normalize-path";
import { getModuleSpecifier } from "./get-module-specifier";
import MagicString from "magic-string";

export type DeclarationFile = {
name: string;
Expand Down Expand Up @@ -131,7 +132,7 @@ export async function getProgram(dirname: string, pkgName: string, ts: TS) {
: memoizedGetProgram(ts)(configFileName);
}

export const getDeclarationsForFile = async (
export function getDeclarationsForFile(
filename: string,
typescript: TS,
program: import("typescript").Program,
Expand All @@ -140,29 +141,51 @@ export const getDeclarationsForFile = async (
diagnosticsHost: import("typescript").FormatDiagnosticsHost,
/** This will only be called once per unique module specifier in a file */
visitModuleSpecifier?: (moduleSpecifier: string) => string
): Promise<EmittedDeclarationOutput> => {
): EmittedDeclarationOutput {
const cachedVisitModuleSpecifier = memoize(
visitModuleSpecifier ?? ((x) => x)
);
const sourceFile = program.getSourceFile(
typescript.sys.useCaseSensitiveFileNames ? filename : filename.toLowerCase()
);
if (!sourceFile) {
throw new Error(
`Could not find source file at ${filename} in TypeScript declaration generation, this is likely a bug in Preconstruct`
);
}
if (filename.endsWith(".d.ts")) {
let content = sourceFile.text;
if (visitModuleSpecifier) {
const magicString = new MagicString(content);
const visitor = (node: import("typescript").Node): void => {
const moduleSpecifier = getModuleSpecifier(node, typescript);
if (moduleSpecifier) {
const replaced = cachedVisitModuleSpecifier(moduleSpecifier.text);
if (replaced !== moduleSpecifier.text) {
magicString.update(
moduleSpecifier.getStart(sourceFile, false),
moduleSpecifier.getEnd(),
JSON.stringify(replaced)
);
}
}
typescript.forEachChild(node, visitor);
};
typescript.forEachChild(sourceFile, visitor);
content = magicString.toString();
}
return {
types: {
name: filename.replace(
normalizedPkgDir,
normalizePath(path.join(normalizedPkgDir, "dist", "declarations"))
),
content: await fs.readFile(filename, "utf8"),
content,
},
filename,
};
}

const sourceFile = program.getSourceFile(
typescript.sys.useCaseSensitiveFileNames ? filename : filename.toLowerCase()
);
if (!sourceFile) {
throw new Error(
`Could not find source file at ${filename} in TypeScript declaration generation, this is likely a bug in Preconstruct`
);
}

const emitted: Partial<EmittedDeclarationOutput> = {};
const otherEmitted: { name: string; text: string }[] = [];
const { diagnostics } = program.emit(
Expand Down Expand Up @@ -198,7 +221,6 @@ export const getDeclarationsForFile = async (
if (!visitModuleSpecifier) {
return node;
}
const cachedVisitModuleSpecifier = memoize(visitModuleSpecifier);

const replacedNodes = new Map<
import("typescript").StringLiteral,
Expand Down Expand Up @@ -247,7 +269,7 @@ export const getDeclarationsForFile = async (
);
}
return { types: emitted.types, map: emitted.map, filename };
};
}

export function overwriteDeclarationMapSourceRoot(
content: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function replaceExt(filename: string) {
});
}

export async function getDeclarationsWithImportedModuleSpecifiersReplacing(
export function getDeclarationsWithImportedModuleSpecifiersReplacing(
typescript: TS,
program: Program,
normalizedPkgDir: string,
Expand All @@ -26,7 +26,7 @@ export async function getDeclarationsWithImportedModuleSpecifiersReplacing(
containingFile: string
) => ResolvedModuleFull | undefined,
resolvedEntrypointSources: string[]
): Promise<EmittedDeclarationOutput[]> {
): EmittedDeclarationOutput[] {
const depQueue = new Set(resolvedEntrypointSources);
const diagnosticsHost = getDiagnosticsHost(typescript, projectDir);
const normalizedPkgDirNodeModules = normalizePath(
Expand Down Expand Up @@ -57,8 +57,7 @@ export async function getDeclarationsWithImportedModuleSpecifiersReplacing(
}
return forImport;
};
// this is mostly sync except for one bit so running this concurrently wouldn't really help
const output = await getDeclarationsForFile(
const output = getDeclarationsForFile(
filename,
typescript,
program,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "./common";
import { Program, ResolvedModuleFull } from "typescript";

export async function getDeclarations(
export function getDeclarations(
typescript: TS,
program: Program,
normalizedPkgDir: string,
Expand All @@ -18,7 +18,7 @@ export async function getDeclarations(
containingFile: string
) => ResolvedModuleFull | undefined,
resolvedEntrypointSources: string[]
): Promise<EmittedDeclarationOutput[]> {
): EmittedDeclarationOutput[] {
let allDeps = new Set(resolvedEntrypointSources);

for (let dep of allDeps) {
Expand Down Expand Up @@ -49,16 +49,14 @@ export async function getDeclarations(

const diagnosticsHost = getDiagnosticsHost(typescript, projectDir);

return Promise.all(
[...allDeps].map((filename) => {
return getDeclarationsForFile(
filename,
typescript,
program,
normalizedPkgDir,
projectDir,
diagnosticsHost
);
})
);
return [...allDeps].map((filename) => {
return getDeclarationsForFile(
filename,
typescript,
program,
normalizedPkgDir,
projectDir,
diagnosticsHost
);
});
}
40 changes: 20 additions & 20 deletions packages/cli/src/rollup-plugins/typescript-declarations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,26 +100,26 @@ export default function typescriptDeclarations(pkg: Package): Plugin {
})
);

const declarations = await (pkg.project.experimentalFlags
.importsConditions ||
pkg.project.experimentalFlags.onlyEmitUsedTypeScriptDeclarations
? getDeclarationsWithImportedModuleSpecifiersReplacing(
typescript,
program,
normalizedDirname,
pkg.project.directory,
resolveModule,
[...entrypointSourceToTypeScriptSource.values()]
)
: getDeclarations(
typescript,
program,
normalizedDirname,
pkg.project.directory,
pkg.name,
resolveModule,
[...entrypointSourceToTypeScriptSource.values()]
));
const declarations =
pkg.project.experimentalFlags.importsConditions ||
pkg.project.experimentalFlags.onlyEmitUsedTypeScriptDeclarations
? getDeclarationsWithImportedModuleSpecifiersReplacing(
typescript,
program,
normalizedDirname,
pkg.project.directory,
resolveModule,
[...entrypointSourceToTypeScriptSource.values()]
)
: getDeclarations(
typescript,
program,
normalizedDirname,
pkg.project.directory,
pkg.name,
resolveModule,
[...entrypointSourceToTypeScriptSource.values()]
);

let srcFilenameToDtsFilenameMap = new Map<string, string>();

Expand Down

0 comments on commit a58f021

Please sign in to comment.