build: extend legacy type resolution to Octagonal Wheels

This commit is contained in:
vorotamoroz
2026-07-30 11:27:12 +00:00
parent 74ddb5bbbd
commit 5aad2623a1
11 changed files with 461 additions and 87 deletions
+128 -21
View File
@@ -65,6 +65,16 @@ async function directoryDigest(root: string): Promise<string> {
return [...digest].map((value) => value.toString(16).padStart(2, "0")).join("");
}
async function pathExists(path: string): Promise<boolean> {
try {
await Deno.lstat(path);
return true;
} catch (error) {
if (error instanceof Deno.errors.NotFound) return false;
throw error;
}
}
async function writeSyntheticPackage(
packageRoot: string,
exports: Record<string, unknown>,
@@ -112,35 +122,65 @@ function validSyntheticExports(): Record<string, unknown> {
};
}
Deno.test("generator covers the installed Commonlib export map deterministically", async () => {
Deno.test("generator covers the installed package export maps deterministically", async () => {
const repositoryRoot = await Deno.realPath(new URL("../../", import.meta.url));
const temporaryDirectory = await Deno.makeTempDir({ prefix: "livesync-type-generator-" });
const outputRoot = `${temporaryDirectory}/output`;
try {
const packageJson = JSON.parse(
await Deno.readTextFile(`${repositoryRoot}/node_modules/@vrtmrz/livesync-commonlib/package.json`)
) as { exports: Record<string, unknown> };
const exportDefinitions = Object.values(packageJson.exports);
const expectedTypedExports = exportDefinitions.filter(
(definition) =>
typeof definition === "object" &&
definition !== null &&
"types" in definition &&
typeof definition.types === "string"
).length;
const installedPackages = await Promise.all(
["@vrtmrz/livesync-commonlib", "octagonal-wheels"].map(async (packageName) => {
const packageJson = JSON.parse(
await Deno.readTextFile(`${repositoryRoot}/node_modules/${packageName}/package.json`)
) as { exports: Record<string, unknown> };
const typedExportCount = Object.values(packageJson.exports).filter(
(definition) =>
typeof definition === "object" &&
definition !== null &&
"types" in definition &&
typeof definition.types === "string"
).length;
return { packageJson, packageName, typedExportCount };
})
);
const first = await runGenerator(repositoryRoot, ["--output", outputRoot]);
assert(first.success, `first generation failed:\n${first.stderr}`);
const firstSummary = JSON.parse(first.stdout) as {
declarationCount: number;
exportCount: number;
metadataExportCount: number;
typedExportCount: number;
packages: {
declarationCount: number;
exportCount: number;
metadataExportCount: number;
packageName: string;
typedExportCount: number;
}[];
};
assert(firstSummary.exportCount === Object.keys(packageJson.exports).length, "not every export was covered");
assert(firstSummary.typedExportCount === expectedTypedExports, "typed export coverage is incomplete");
assert(firstSummary.metadataExportCount === 1, "the package metadata export was not accounted for");
assert(firstSummary.declarationCount === 244, "the installed Commonlib declaration count changed unexpectedly");
assert(firstSummary.packages.length === installedPackages.length, "not every package was generated");
const expectedDeclarationCounts = new Map([
["@vrtmrz/livesync-commonlib", 244],
["octagonal-wheels", 109],
]);
for (const installedPackage of installedPackages) {
const summary = firstSummary.packages.find(
(candidate) => candidate.packageName === installedPackage.packageName
);
assert(summary !== undefined, `${installedPackage.packageName} is missing from the generation summary`);
assert(
summary.exportCount === Object.keys(installedPackage.packageJson.exports).length,
`${installedPackage.packageName} export coverage is incomplete`
);
assert(
summary.typedExportCount === installedPackage.typedExportCount,
`${installedPackage.packageName} typed export coverage is incomplete`
);
assert(
summary.metadataExportCount === 1,
`${installedPackage.packageName} metadata export was not accounted for`
);
assert(
summary.declarationCount === expectedDeclarationCounts.get(installedPackage.packageName),
`${installedPackage.packageName} declaration count changed unexpectedly`
);
}
const firstDigest = await directoryDigest(outputRoot);
const second = await runGenerator(repositoryRoot, ["--output", outputRoot]);
@@ -151,7 +191,11 @@ Deno.test("generator covers the installed Commonlib export map deterministically
const publicWrappers = generatedFiles.filter(
(filePath) => filePath.endsWith(".d.ts") && !filePath.includes("/__package__/")
);
assert(publicWrappers.length === expectedTypedExports, "generated wrapper count does not match typed exports");
const expectedWrapperCount = installedPackages.reduce(
(total, installedPackage) => total + installedPackage.typedExportCount,
0
);
assert(publicWrappers.length === expectedWrapperCount, "generated wrapper count does not match typed exports");
for (const filePath of generatedFiles.filter((candidate) => candidate.endsWith(".d.ts"))) {
const source = await Deno.readTextFile(`${outputRoot}/${filePath}`);
assert(
@@ -164,6 +208,23 @@ Deno.test("generator covers the installed Commonlib export map deterministically
'export * from "../../__package__/dist/common/types.js";\n',
"representative wrapper does not re-export the copied declaration"
);
const octagonalWheelsWrappers = new Map([
["octagonal-wheels/common/types.d.ts", 'export * from "../__package__/dist/common/types.js";\n'],
[
"octagonal-wheels/databases/SimpleStoreBase.d.ts",
'export * from "../__package__/dist/databases/SimpleStoreBase.js";\n',
],
[
"octagonal-wheels/dataobject/reactive.d.ts",
'export * from "../__package__/dist/dataobject/reactive.js";\n',
],
]);
for (const [relativePath, expectedSource] of octagonalWheelsWrappers) {
assert(
(await Deno.readTextFile(`${outputRoot}/${relativePath}`)) === expectedSource,
`Octagonal Wheels wrapper does not re-export its copied declaration: ${relativePath}`
);
}
const ignored = await new Deno.Command("git", {
args: ["check-ignore", "--quiet", "dist/type-resolution-compat"],
@@ -190,6 +251,30 @@ Deno.test("generator rejects invalid package boundaries and preserves the previo
assert(initial.success, `valid synthetic generation failed:\n${initial.stderr}`);
const initialDigest = await directoryDigest(outputRoot);
const nestedOutput = `${packageRoot}/generated`;
const nestedOutputResult = await runGenerator(repositoryRoot, [...commonArguments, "--output", nestedOutput]);
assert(!nestedOutputResult.success, "an output inside the input package was accepted");
assert(nestedOutputResult.stderr.includes("unsafe output"), "unsafe nested output failure was not explained");
assert(!(await pathExists(nestedOutput)), "unsafe nested output was created");
if (Deno.build.os !== "windows") {
const linkedPackageRoot = `${temporaryDirectory}/linked-package`;
await Deno.symlink(packageRoot, linkedPackageRoot);
const linkedOutput = `${linkedPackageRoot}/generated`;
const linkedOutputResult = await runGenerator(repositoryRoot, [
...commonArguments,
"--output",
linkedOutput,
]);
assert(!linkedOutputResult.success, "a symlinked output inside the input package was accepted");
assert(
linkedOutputResult.stderr.includes("unsafe output"),
"unsafe symlinked-output failure was not explained"
);
assert(!(await pathExists(linkedOutput)), "unsafe symlinked output was created");
}
assert((await directoryDigest(outputRoot)) === initialDigest, "unsafe output validation replaced prior output");
await writeSyntheticPackage(
packageRoot,
{
@@ -260,6 +345,28 @@ Deno.test("generator rejects invalid package boundaries and preserves the previo
assert(!suppression.success, "a declaration suppression directive was copied");
assert(suppression.stderr.includes("suppression directive"), "suppression failure was not explained");
const unsupportedDefaultExports = [
"export default interface UnsupportedDefault {}\n",
'export * as default from "./common/types.js";\n',
'declare const value: string;\nexport { value as "default" };\n',
];
for (const defaultExportSource of unsupportedDefaultExports) {
await writeSyntheticPackage(packageRoot, validSyntheticExports(), {
"dist/common/types.d.ts": "export type FilePath = string;\n",
"dist/index.d.ts": defaultExportSource,
});
const defaultExport = await runGenerator(repositoryRoot, commonArguments);
assert(!defaultExport.success, "a default export unsupported by the wrapper was accepted");
assert(
defaultExport.stderr.includes("unsupported default export"),
"default-export failure was not explained"
);
assert(
(await directoryDigest(outputRoot)) === initialDigest,
"default-export failure replaced prior output"
);
}
await writeSyntheticPackage(
packageRoot,
validSyntheticExports(),
@@ -3,6 +3,12 @@ const COMMONLIB_PATHS = {
"@vrtmrz/livesync-commonlib/*": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/*"],
};
const COMPATIBILITY_PATHS = {
...COMMONLIB_PATHS,
"octagonal-wheels": ["./dist/type-resolution-compat/octagonal-wheels/index"],
"octagonal-wheels/*": ["./dist/type-resolution-compat/octagonal-wheels/*"],
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
@@ -25,7 +31,7 @@ async function writeProject(
fixturePath: string,
options: {
moduleResolution: "Bundler" | "Node10";
paths?: typeof COMMONLIB_PATHS;
paths?: Record<string, string[]>;
skipLibCheck: boolean;
}
): Promise<void> {
@@ -34,6 +40,7 @@ async function writeProject(
JSON.stringify(
{
compilerOptions: {
allowImportingTsExtensions: true,
baseUrl: repositoryRoot,
lib: ["ES2022", "DOM"],
module: "ESNext",
@@ -110,10 +117,14 @@ Deno.test("compatibility mirror preserves representative bundler type facts", as
'import type { FilePath, ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";',
'import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";',
'import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";',
'import type { TaggedType } from "octagonal-wheels/common/types";',
'import type { ReactiveValue } from "octagonal-wheels/dataobject/reactive";',
'import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";',
"",
"type IsAny<T> = 0 extends 1 & T ? true : false;",
"type ExpectFalse<T extends false> = T;",
"type ExpectTrue<T extends true> = T;",
'type TaggedPath = TaggedType<string, "path">;',
"type FilePathIsTyped = ExpectFalse<IsAny<FilePath>>;",
"type FilePathIsString = ExpectTrue<FilePath extends string ? true : false>;",
"type SettingsAreTyped = ExpectFalse<IsAny<ObsidianLiveSyncSettings>>;",
@@ -122,6 +133,13 @@ Deno.test("compatibility mirror preserves representative bundler type facts", as
'type ServiceHubRetainsSetting = ExpectTrue<"setting" extends keyof InjectableServiceHub ? true : false>;',
"type P2PResultIsTyped = ExpectFalse<IsAny<UseP2PReplicatorResult>>;",
'type P2PResultRetainsReplicator = ExpectTrue<"replicator" extends keyof UseP2PReplicatorResult ? true : false>;',
"type TaggedPathIsTyped = ExpectFalse<IsAny<TaggedPath>>;",
"type TaggedPathIsString = ExpectTrue<TaggedPath extends string ? true : false>;",
"type StringIsNotTaggedPath = ExpectFalse<string extends TaggedPath ? true : false>;",
"type SimpleStoreIsTyped = ExpectFalse<IsAny<SimpleStore<string>>>;",
'type SimpleStoreRetainsGet = ExpectTrue<"get" extends keyof SimpleStore<string> ? true : false>;',
"type ReactiveValueIsTyped = ExpectFalse<IsAny<ReactiveValue<string>>>;",
'type ReactiveValueRetainsValue = ExpectTrue<"value" extends keyof ReactiveValue<string> ? true : false>;',
"",
"export type Proof = [",
" FilePathIsTyped,",
@@ -132,13 +150,20 @@ Deno.test("compatibility mirror preserves representative bundler type facts", as
" ServiceHubRetainsSetting,",
" P2PResultIsTyped,",
" P2PResultRetainsReplicator,",
" TaggedPathIsTyped,",
" TaggedPathIsString,",
" StringIsNotTaggedPath,",
" SimpleStoreIsTyped,",
" SimpleStoreRetainsGet,",
" ReactiveValueIsTyped,",
" ReactiveValueRetainsValue,",
"];",
"",
].join("\n")
);
const projects = [
{ name: "published package", paths: undefined },
{ name: "compatibility mirror", paths: COMMONLIB_PATHS },
{ name: "compatibility mirror", paths: COMPATIBILITY_PATHS },
] as const;
for (const [index, project] of projects.entries()) {
const projectPath = `${temporaryDirectory}/tsconfig-${index}.json`;
@@ -155,7 +180,7 @@ Deno.test("compatibility mirror preserves representative bundler type facts", as
}
});
Deno.test("legacy Commonlib mirror exposes its unresolved Octagonal Wheels boundary", async () => {
Deno.test("legacy compatibility mirrors preserve Commonlib and Octagonal Wheels type facts", async () => {
const repositoryRoot = await Deno.realPath(new URL("../../", import.meta.url));
const temporaryDirectory = await makeTemporaryDirectory(repositoryRoot);
const fixturePath = `${temporaryDirectory}/consumer.ts`;
@@ -165,25 +190,86 @@ Deno.test("legacy Commonlib mirror exposes its unresolved Octagonal Wheels bound
fixturePath,
[
'import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";',
'import type { TaggedType } from "octagonal-wheels/common/types";',
'import type { ReactiveValue } from "octagonal-wheels/dataobject/reactive";',
'import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";',
"",
"type IsAny<T> = 0 extends 1 & T ? true : false;",
"type ExpectFalse<T extends false> = T;",
"type ExpectTrue<T extends true> = T;",
"export type FilePathIsString = ExpectTrue<FilePath extends string ? true : false>;",
'type TaggedPath = TaggedType<string, "path">;',
"type FilePathIsTyped = ExpectFalse<IsAny<FilePath>>;",
"type FilePathIsString = ExpectTrue<FilePath extends string ? true : false>;",
"type TaggedPathIsTyped = ExpectFalse<IsAny<TaggedPath>>;",
"type TaggedPathIsString = ExpectTrue<TaggedPath extends string ? true : false>;",
"type StringIsNotTaggedPath = ExpectFalse<string extends TaggedPath ? true : false>;",
"type SimpleStoreIsTyped = ExpectFalse<IsAny<SimpleStore<string>>>;",
'type SimpleStoreRetainsGet = ExpectTrue<"get" extends keyof SimpleStore<string> ? true : false>;',
"type ReactiveValueIsTyped = ExpectFalse<IsAny<ReactiveValue<string>>>;",
'type ReactiveValueRetainsValue = ExpectTrue<"value" extends keyof ReactiveValue<string> ? true : false>;',
"",
"export type Proof = [",
" FilePathIsTyped,",
" FilePathIsString,",
" TaggedPathIsTyped,",
" TaggedPathIsString,",
" StringIsNotTaggedPath,",
" SimpleStoreIsTyped,",
" SimpleStoreRetainsGet,",
" ReactiveValueIsTyped,",
" ReactiveValueRetainsValue,",
"];",
"",
].join("\n")
);
await writeProject(projectPath, repositoryRoot, fixturePath, {
moduleResolution: "Node10",
paths: COMMONLIB_PATHS,
skipLibCheck: false,
paths: COMPATIBILITY_PATHS,
skipLibCheck: true,
});
const result = await runTypeScript(projectPath, repositoryRoot);
const output = commandOutput(result);
assert(!result.success, "legacy resolution unexpectedly preserved every transitive package type");
assert(
output.includes("Cannot find module 'octagonal-wheels/common/types'"),
`the expected transitive Octagonal Wheels boundary was not reported:\n${output}`
assert(result.success, `legacy compatibility resolution lost package type facts:\n${commandOutput(result)}`);
} finally {
await Deno.remove(temporaryDirectory, { recursive: true });
}
});
Deno.test("legacy compatibility mirror exposes every Octagonal Wheels typed export entry point", async () => {
const repositoryRoot = await Deno.realPath(new URL("../../", import.meta.url));
const temporaryDirectory = await makeTemporaryDirectory(repositoryRoot);
const fixturePath = `${temporaryDirectory}/consumer.ts`;
const projectPath = `${temporaryDirectory}/tsconfig.json`;
try {
const packageJson = JSON.parse(
await Deno.readTextFile(`${repositoryRoot}/node_modules/octagonal-wheels/package.json`)
) as { exports: Record<string, unknown> };
const typedExportSubpaths = Object.entries(packageJson.exports)
.filter(
([, definition]) =>
typeof definition === "object" &&
definition !== null &&
"types" in definition &&
typeof definition.types === "string"
)
.map(([subpath]) => subpath);
const imports = typedExportSubpaths.map((subpath, index) => {
const specifier = subpath === "." ? "octagonal-wheels" : `octagonal-wheels/${subpath.slice(2)}`;
return `import type * as PackageExport${index} from ${JSON.stringify(specifier)};`;
});
const proofTypes = typedExportSubpaths.map((_, index) => `typeof PackageExport${index}`);
await Deno.writeTextFile(
fixturePath,
[...imports, "", `export type Proof = [${proofTypes.join(", ")}];`, ""].join("\n")
);
await writeProject(projectPath, repositoryRoot, fixturePath, {
moduleResolution: "Node10",
paths: COMPATIBILITY_PATHS,
skipLibCheck: true,
});
const result = await runTypeScript(projectPath, repositoryRoot);
assert(result.success, `legacy resolution failed for an Octagonal Wheels export:\n${commandOutput(result)}`);
} finally {
await Deno.remove(temporaryDirectory, { recursive: true });
}
@@ -4,7 +4,9 @@
"paths": {
"@/*": ["./src/*"],
"@vrtmrz/livesync-commonlib": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/index"],
"@vrtmrz/livesync-commonlib/*": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/*"]
"@vrtmrz/livesync-commonlib/*": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/*"],
"octagonal-wheels": ["./dist/type-resolution-compat/octagonal-wheels/index"],
"octagonal-wheels/*": ["./dist/type-resolution-compat/octagonal-wheels/*"]
}
}
}
@@ -29,7 +29,7 @@ async function scan(repositoryRoot: string, config: string): Promise<EslintMessa
);
}
Deno.test("Commonlib mirror reduces synthetic legacy-resolution warnings", async () => {
Deno.test("compatibility mirrors resolve Commonlib and Octagonal Wheels warning types", async () => {
const repositoryRoot = await Deno.realPath(new URL("../../", import.meta.url));
const legacy = await scan(repositoryRoot, "test/type-resolution-compat/eslint.legacy.config.mjs");
const compatible = await scan(repositoryRoot, "test/type-resolution-compat/eslint.compat.config.mjs");
@@ -37,20 +37,22 @@ Deno.test("Commonlib mirror reduces synthetic legacy-resolution warnings", async
"FilePath",
"InjectableServiceHub",
"ObsidianLiveSyncSettings",
"ReactiveValue",
"SimpleStore",
"UseP2PReplicatorResult",
];
assert(legacy.length > 0, "the legacy configuration did not reproduce the warning family");
for (const typeName of representativeTypes) {
assert(
legacy.some((warning) => warning.message.includes(`'${typeName}' is an 'error' type`)),
`the legacy configuration did not reproduce the ${typeName} warning`
);
assert(
!compatible.some((warning) => warning.message.includes(`'${typeName}' is an 'error' type`)),
`the compatibility mirror did not resolve ${typeName}`
);
const isErrorTypeWarning = (warning: EslintMessage) =>
warning.message.includes(typeName) && warning.message.includes("is an 'error' type");
assert(legacy.some(isErrorTypeWarning), `the legacy configuration did not reproduce the ${typeName} warning`);
assert(!compatible.some(isErrorTypeWarning), `the compatibility mirror did not resolve ${typeName}`);
}
assert(
!compatible.some((warning) => warning.message.includes("is an 'error' type")),
"the compatibility mirrors left an unresolved error-type warning"
);
assert(
compatible.length < legacy.length,
`the compatibility mirror did not reduce warnings: ${legacy.length} before, ${compatible.length} after`