Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-readers-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preconstruct/cli": patch
---

Add `checkTypeDependencies` experimental flag
227 changes: 227 additions & 0 deletions packages/cli/src/build/__tests__/declarations.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import path from "path";
import build from "..";
import {
testdir,
typescriptFixture,
getDist,
getFiles,
ts,
repoNodeModules,
} from "../../../test-utils";

test("circular dependency typescript", async () => {
Expand Down Expand Up @@ -967,3 +969,228 @@ test("importing json where json import is emitted in declaration files", async (

`);
});

test("type dep missing", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "a",
main: "dist/a.cjs.js",
module: "dist/a.esm.js",
exports: {
".": {
module: "./dist/a.esm.js",
default: "./dist/a.cjs.js",
},
"./package.json": "./package.json",
},
preconstruct: {
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
checkTypeDependencies: true,
},
},
}),

"src/index.ts": ts`
export type { a } from "something";
`,
"node_modules/typescript": {
kind: "symlink",
path: path.join(repoNodeModules, "typescript"),
},
"node_modules/something/index.js": "export const a = 'blah';",
"node_modules/something/index.d.ts": "export const a = 'blah';",
"node_modules/something/package.json": JSON.stringify({
name: "something",
main: "index.js",
}),
".babelrc": typescriptFixture[".babelrc"],
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "bundler",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await expect(build(dir)).rejects.toMatchInlineSnapshot(
`[Error: 🎁 a dependency "something" used by types for src/index.ts is not declared in dependencies or peerDependencies]`
);
});

test("@types/ dep missing", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "a",
main: "dist/a.cjs.js",
module: "dist/a.esm.js",
exports: {
".": {
module: "./dist/a.esm.js",
default: "./dist/a.cjs.js",
},
"./package.json": "./package.json",
},
preconstruct: {
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
checkTypeDependencies: true,
},
},
}),

"src/index.ts": ts`
export type { a } from "something";
`,
".babelrc": typescriptFixture[".babelrc"],
"node_modules/typescript": {
kind: "symlink",
path: path.join(repoNodeModules, "typescript"),
},
"node_modules/something/index.js": "export const a = 'blah';",
"node_modules/something/package.json": JSON.stringify({
name: "something",
main: "index.js",
}),
"node_modules/@types/something/index.d.ts": "export const a = 'blah';",
"node_modules/@types/something/package.json": JSON.stringify({
name: "something",
main: "index.js",
}),
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "bundler",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await expect(build(dir)).rejects.toMatchInlineSnapshot(
`[Error: 🎁 a dependency "@types/something" used by types for src/index.ts is not declared in dependencies or peerDependencies]`
);
});

test("type dep not missing", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "a",
main: "dist/a.cjs.js",
module: "dist/a.esm.js",
exports: {
".": {
module: "./dist/a.esm.js",
default: "./dist/a.cjs.js",
},
"./package.json": "./package.json",
},
preconstruct: {
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
checkTypeDependencies: true,
},
},
dependencies: {
something: "1.0.0",
},
}),

"src/index.ts": ts`
export type { a } from "something";
`,
"node_modules/typescript": {
kind: "symlink",
path: path.join(repoNodeModules, "typescript"),
},
"node_modules/something/index.js": "export const a = 'blah';",
"node_modules/something/index.d.ts": "export const a = 'blah';",
"node_modules/something/package.json": JSON.stringify({
name: "something",
main: "index.js",
}),
".babelrc": typescriptFixture[".babelrc"],
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "bundler",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await build(dir);
expect(await getFiles(dir, ["dist/**/*.d.*"])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYS5janMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4vZGVjbGFyYXRpb25zL3NyYy9pbmRleC5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIn0=

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export type { a } from "something";

`);
});

test("type dep not used", async () => {
let dir = await testdir({
"package.json": JSON.stringify({
name: "a",
main: "dist/a.cjs.js",
module: "dist/a.esm.js",
exports: {
".": {
module: "./dist/a.esm.js",
default: "./dist/a.cjs.js",
},
"./package.json": "./package.json",
},
preconstruct: {
___experimentalFlags_WILL_CHANGE_IN_PATCH: {
checkTypeDependencies: true,
},
},
dependencies: {
something: "1.0.0",
},
}),

"src/index.ts": ts`
import { a } from "something";
export const b = a;
`,
".babelrc": typescriptFixture[".babelrc"],
"node_modules/typescript": {
kind: "symlink",
path: path.join(repoNodeModules, "typescript"),
},
"node_modules/something/index.js": "export const a = 'blah';",
"node_modules/something/package.json": JSON.stringify({
name: "something",
main: "index.js",
}),
"node_modules/@types/something/index.d.ts": "export const a = 'blah';",
"node_modules/@types/something/package.json": JSON.stringify({
name: "something",
main: "index.js",
}),
"tsconfig.json": JSON.stringify({
compilerOptions: {
module: "ESNext",
moduleResolution: "bundler",
allowImportingTsExtensions: true,
strict: true,
declaration: true,
},
}),
});
await build(dir);
expect(await getFiles(dir, ["dist/**/*.d.*"])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/a.cjs.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export * from "./declarations/src/index";
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYS5janMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4vZGVjbGFyYXRpb25zL3NyYy9pbmRleC5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIn0=

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/declarations/src/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export declare const b = "blah";

`);
});
2 changes: 2 additions & 0 deletions packages/cli/src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class Project extends Item<{
importsConditions?: JSONValue;
distInRoot?: JSONValue;
typeModule?: JSONValue;
checkTypeDependencies?: JSONValue;
};
};
}> {
Expand All @@ -56,6 +57,7 @@ export class Project extends Item<{
importsConditions: !!config.importsConditions,
distInRoot: !!config.distInRoot,
typeModule: !!config.typeModule,
checkTypeDependencies: !!config.checkTypeDependencies,
};
}
get configPackages(): Array<string> {
Expand Down
42 changes: 18 additions & 24 deletions packages/cli/src/rollup-plugins/typescript-declarations/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function getDeclarationsForFile(
projectDir: string,
diagnosticsHost: import("typescript").FormatDiagnosticsHost,
/** This will only be called once per unique module specifier in a file */
visitModuleSpecifier?: (moduleSpecifier: string) => string
visitModuleSpecifier: (moduleSpecifier: string) => string
): EmittedDeclarationOutput {
const cachedVisitModuleSpecifier = memoize(
visitModuleSpecifier ?? ((x) => x)
Expand All @@ -156,26 +156,24 @@ export function getDeclarationsForFile(
);
}
if (dtsFileRegex.test(filename)) {
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)
);
}
const magicString = new MagicString(sourceFile.text);
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();
}
}
typescript.forEachChild(node, visitor);
};
typescript.forEachChild(sourceFile, visitor);
const content = magicString.toString();

return {
types: {
name: filename.replace(
Expand Down Expand Up @@ -232,10 +230,6 @@ export function getDeclarationsForFile(
(context) => (
node
): import("typescript").Bundle | import("typescript").SourceFile => {
if (!visitModuleSpecifier) {
return node;
}

const replacedNodes = new Map<
import("typescript").StringLiteral,
import("typescript").StringLiteral
Expand Down
Loading