mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-27 05:47:07 +00:00
build: add legacy type-resolution experiment
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { createSyntheticCommunityReviewConfig } from "./eslint.synthetic-config.mjs";
|
||||
|
||||
export default createSyntheticCommunityReviewConfig("./test/type-resolution-compat/tsconfig.compat.json");
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createSyntheticCommunityReviewConfig } from "./eslint.synthetic-config.mjs";
|
||||
|
||||
export default createSyntheticCommunityReviewConfig("./test/type-resolution-compat/tsconfig.legacy.json");
|
||||
@@ -0,0 +1,33 @@
|
||||
import typescriptEslint from "typescript-eslint";
|
||||
|
||||
const repositoryRoot = new URL("../../", import.meta.url).pathname;
|
||||
|
||||
export function createSyntheticCommunityReviewConfig(project) {
|
||||
return [
|
||||
{
|
||||
ignores: [
|
||||
"**/*.unit.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/test/**",
|
||||
"src/apps/_test/**",
|
||||
"src/apps/cli/testdeno/**",
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: typescriptEslint.parser,
|
||||
parserOptions: {
|
||||
project,
|
||||
tsconfigRootDir: repositoryRoot,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint.plugin,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function decode(bytes: Uint8Array): string {
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
async function runGenerator(
|
||||
repositoryRoot: string,
|
||||
arguments_: string[]
|
||||
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
||||
const result = await new Deno.Command("node", {
|
||||
args: ["scripts/generate-type-resolution-compat.mjs", ...arguments_],
|
||||
cwd: repositoryRoot,
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
}).output();
|
||||
return {
|
||||
success: result.success,
|
||||
stdout: decode(result.stdout),
|
||||
stderr: decode(result.stderr),
|
||||
};
|
||||
}
|
||||
|
||||
async function listFiles(root: string, relativeDirectory = ""): Promise<string[]> {
|
||||
const directory = relativeDirectory === "" ? root : `${root}/${relativeDirectory}`;
|
||||
const entries = [...(await Array.fromAsync(Deno.readDir(directory)))].sort((left, right) =>
|
||||
left.name < right.name ? -1 : left.name > right.name ? 1 : 0
|
||||
);
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const relativePath = relativeDirectory === "" ? entry.name : `${relativeDirectory}/${entry.name}`;
|
||||
if (entry.isDirectory) {
|
||||
files.push(...(await listFiles(root, relativePath)));
|
||||
} else if (entry.isFile) {
|
||||
files.push(relativePath);
|
||||
} else {
|
||||
throw new Error(`unexpected generated entry: ${relativePath}`);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function directoryDigest(root: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const files = await listFiles(root);
|
||||
const chunks: Uint8Array[] = [];
|
||||
let totalLength = 0;
|
||||
for (const relativePath of files) {
|
||||
const name = encoder.encode(`${relativePath}\0`);
|
||||
const contents = await Deno.readFile(`${root}/${relativePath}`);
|
||||
chunks.push(name, contents);
|
||||
totalLength += name.length + contents.length;
|
||||
}
|
||||
const combined = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
combined.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", combined));
|
||||
return [...digest].map((value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function writeSyntheticPackage(
|
||||
packageRoot: string,
|
||||
exports: Record<string, unknown>,
|
||||
declarations: Record<string, string>,
|
||||
overrides: Record<string, unknown> = {}
|
||||
): Promise<void> {
|
||||
await Deno.mkdir(packageRoot, { recursive: true });
|
||||
await Deno.writeTextFile(
|
||||
`${packageRoot}/package.json`,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@vrtmrz/livesync-commonlib",
|
||||
version: "0.1.0",
|
||||
type: "module",
|
||||
exports,
|
||||
...overrides,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
for (const [relativePath, source] of Object.entries(declarations)) {
|
||||
const components = relativePath.split("/");
|
||||
components.pop();
|
||||
if (components.length > 0) {
|
||||
await Deno.mkdir(`${packageRoot}/${components.join("/")}`, { recursive: true });
|
||||
}
|
||||
await Deno.writeTextFile(`${packageRoot}/${relativePath}`, source);
|
||||
}
|
||||
}
|
||||
|
||||
function validSyntheticExports(): Record<string, unknown> {
|
||||
return {
|
||||
".": {
|
||||
types: "./dist/index.d.ts",
|
||||
import: "./dist/index.js",
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./compat/common/types": {
|
||||
types: "./dist/common/types.d.ts",
|
||||
import: "./dist/common/types.js",
|
||||
default: "./dist/common/types.js",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("generator covers the installed Commonlib export map 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 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;
|
||||
};
|
||||
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");
|
||||
|
||||
const firstDigest = await directoryDigest(outputRoot);
|
||||
const second = await runGenerator(repositoryRoot, ["--output", outputRoot]);
|
||||
assert(second.success, `second generation failed:\n${second.stderr}`);
|
||||
assert((await directoryDigest(outputRoot)) === firstDigest, "two generations produced different content");
|
||||
|
||||
const generatedFiles = await listFiles(outputRoot);
|
||||
const publicWrappers = generatedFiles.filter(
|
||||
(filePath) => filePath.endsWith(".d.ts") && !filePath.includes("/__package__/")
|
||||
);
|
||||
assert(publicWrappers.length === expectedTypedExports, "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(
|
||||
!/@ts-(?:expect-error|ignore|nocheck)|eslint-(?:disable|enable)/u.test(source),
|
||||
`generated declaration contains a suppression directive: ${filePath}`
|
||||
);
|
||||
}
|
||||
assert(
|
||||
(await Deno.readTextFile(`${outputRoot}/@vrtmrz/livesync-commonlib/compat/common/types.d.ts`)) ===
|
||||
'export * from "../../__package__/dist/common/types.js";\n',
|
||||
"representative wrapper does not re-export the copied declaration"
|
||||
);
|
||||
|
||||
const ignored = await new Deno.Command("git", {
|
||||
args: ["check-ignore", "--quiet", "dist/type-resolution-compat"],
|
||||
cwd: repositoryRoot,
|
||||
}).output();
|
||||
assert(ignored.success, "the generated repository output is not ignored by Git");
|
||||
} finally {
|
||||
await Deno.remove(temporaryDirectory, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("generator rejects invalid package boundaries and preserves the previous output", async () => {
|
||||
const repositoryRoot = await Deno.realPath(new URL("../../", import.meta.url));
|
||||
const temporaryDirectory = await Deno.makeTempDir({ prefix: "livesync-type-generator-invalid-" });
|
||||
const packageRoot = `${temporaryDirectory}/package`;
|
||||
const outputRoot = `${temporaryDirectory}/output`;
|
||||
const commonArguments = ["--package-root", packageRoot, "--output", outputRoot, "--expected-version", "0.1.0"];
|
||||
try {
|
||||
await writeSyntheticPackage(packageRoot, validSyntheticExports(), {
|
||||
"dist/common/types.d.ts": "export type FilePath = string;\n",
|
||||
"dist/index.d.ts": 'export type { FilePath } from "./common/types.js";\n',
|
||||
});
|
||||
const initial = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(initial.success, `valid synthetic generation failed:\n${initial.stderr}`);
|
||||
const initialDigest = await directoryDigest(outputRoot);
|
||||
|
||||
await writeSyntheticPackage(
|
||||
packageRoot,
|
||||
{
|
||||
".": {
|
||||
types: "./dist/missing.d.ts",
|
||||
import: "./dist/index.js",
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
{}
|
||||
);
|
||||
const missingTarget = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(!missingTarget.success, "a missing types target was accepted");
|
||||
assert(missingTarget.stderr.includes("does not exist"), "missing-target failure was not explained");
|
||||
assert((await directoryDigest(outputRoot)) === initialDigest, "failed generation replaced the previous output");
|
||||
|
||||
await writeSyntheticPackage(
|
||||
packageRoot,
|
||||
{
|
||||
".": {
|
||||
types: "./dist/index.d.ts",
|
||||
import: "./dist/index.js",
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./compat/../escape": {
|
||||
types: "./dist/index.d.ts",
|
||||
import: "./dist/index.js",
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
{ "dist/index.d.ts": "export interface Safe {}\n" }
|
||||
);
|
||||
const traversal = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(!traversal.success, "an export path traversal was accepted");
|
||||
assert(traversal.stderr.includes("path traversal"), "path-traversal failure was not explained");
|
||||
|
||||
await writeSyntheticPackage(
|
||||
packageRoot,
|
||||
{
|
||||
".": {
|
||||
types: "./dist/index.d.ts",
|
||||
import: "./dist/index.js",
|
||||
default: "./dist/index.js",
|
||||
},
|
||||
"./index": {
|
||||
types: "./dist/other.d.ts",
|
||||
import: "./dist/other.js",
|
||||
default: "./dist/other.js",
|
||||
},
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
{
|
||||
"dist/index.d.ts": "export interface First {}\n",
|
||||
"dist/other.d.ts": "export interface Second {}\n",
|
||||
}
|
||||
);
|
||||
const duplicate = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(!duplicate.success, "duplicate wrapper destinations were accepted");
|
||||
assert(duplicate.stderr.includes("same wrapper"), "duplicate-destination failure was not explained");
|
||||
|
||||
await writeSyntheticPackage(packageRoot, validSyntheticExports(), {
|
||||
"dist/common/types.d.ts": "// @ts-ignore\nexport type FilePath = string;\n",
|
||||
"dist/index.d.ts": 'export type { FilePath } from "./common/types.js";\n',
|
||||
});
|
||||
const suppression = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(!suppression.success, "a declaration suppression directive was copied");
|
||||
assert(suppression.stderr.includes("suppression directive"), "suppression failure was not explained");
|
||||
|
||||
await writeSyntheticPackage(
|
||||
packageRoot,
|
||||
validSyntheticExports(),
|
||||
{
|
||||
"dist/common/types.d.ts": "export type FilePath = string;\n",
|
||||
"dist/index.d.ts": 'export type { FilePath } from "./common/types.js";\n',
|
||||
},
|
||||
{ name: "@vrtmrz/not-livesync-commonlib" }
|
||||
);
|
||||
const wrongName = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(!wrongName.success, "an unexpected package name was accepted");
|
||||
assert(wrongName.stderr.includes("Expected package name"), "package-name failure was not explained");
|
||||
|
||||
await writeSyntheticPackage(
|
||||
packageRoot,
|
||||
validSyntheticExports(),
|
||||
{
|
||||
"dist/common/types.d.ts": "export type FilePath = string;\n",
|
||||
"dist/index.d.ts": 'export type { FilePath } from "./common/types.js";\n',
|
||||
},
|
||||
{ version: "0.1.1" }
|
||||
);
|
||||
const wrongVersion = await runGenerator(repositoryRoot, commonArguments);
|
||||
assert(!wrongVersion.success, "an unexpected package version was accepted");
|
||||
assert(
|
||||
wrongVersion.stderr.includes("Expected @vrtmrz/livesync-commonlib@0.1.0"),
|
||||
"version failure was not explained"
|
||||
);
|
||||
} finally {
|
||||
await Deno.remove(temporaryDirectory, { recursive: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
const COMMONLIB_PATHS = {
|
||||
"@vrtmrz/livesync-commonlib": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/index"],
|
||||
"@vrtmrz/livesync-commonlib/*": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/*"],
|
||||
};
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTypeScript(projectPath: string, repositoryRoot: string): Promise<Deno.CommandOutput> {
|
||||
const executable = `${repositoryRoot}/node_modules/.bin/tsc${Deno.build.os === "windows" ? ".cmd" : ""}`;
|
||||
return await new Deno.Command(executable, {
|
||||
args: ["--pretty", "false", "--project", projectPath],
|
||||
cwd: repositoryRoot,
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
}).output();
|
||||
}
|
||||
|
||||
async function writeProject(
|
||||
projectPath: string,
|
||||
repositoryRoot: string,
|
||||
fixturePath: string,
|
||||
options: {
|
||||
moduleResolution: "Bundler" | "Node10";
|
||||
paths?: typeof COMMONLIB_PATHS;
|
||||
skipLibCheck: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
await Deno.writeTextFile(
|
||||
projectPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
compilerOptions: {
|
||||
baseUrl: repositoryRoot,
|
||||
lib: ["ES2022", "DOM"],
|
||||
module: "ESNext",
|
||||
moduleResolution: options.moduleResolution,
|
||||
noEmit: true,
|
||||
...(options.paths === undefined ? {} : { paths: options.paths }),
|
||||
skipLibCheck: options.skipLibCheck,
|
||||
strict: true,
|
||||
target: "ES2022",
|
||||
types: [],
|
||||
},
|
||||
files: [fixturePath],
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function makeTemporaryDirectory(repositoryRoot: string): Promise<string> {
|
||||
return await Deno.makeTempDir({
|
||||
dir: repositoryRoot,
|
||||
prefix: ".livesync-type-resolution-",
|
||||
});
|
||||
}
|
||||
|
||||
function commandOutput(result: Deno.CommandOutput): string {
|
||||
return new TextDecoder().decode(result.stdout) + new TextDecoder().decode(result.stderr);
|
||||
}
|
||||
|
||||
Deno.test("legacy resolver reaches representative Commonlib entry points", 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 {
|
||||
await Deno.writeTextFile(
|
||||
fixturePath,
|
||||
[
|
||||
'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";',
|
||||
"",
|
||||
"export type Proof = [",
|
||||
" FilePath,",
|
||||
" ObsidianLiveSyncSettings,",
|
||||
" InjectableServiceHub,",
|
||||
" UseP2PReplicatorResult,",
|
||||
"];",
|
||||
"",
|
||||
].join("\n")
|
||||
);
|
||||
await writeProject(projectPath, repositoryRoot, fixturePath, {
|
||||
moduleResolution: "Node10",
|
||||
paths: COMMONLIB_PATHS,
|
||||
skipLibCheck: true,
|
||||
});
|
||||
|
||||
const result = await runTypeScript(projectPath, repositoryRoot);
|
||||
assert(result.success, `legacy Commonlib entry-point resolution failed:\n${commandOutput(result)}`);
|
||||
} finally {
|
||||
await Deno.remove(temporaryDirectory, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("compatibility mirror preserves representative bundler type facts", async () => {
|
||||
const repositoryRoot = await Deno.realPath(new URL("../../", import.meta.url));
|
||||
const temporaryDirectory = await makeTemporaryDirectory(repositoryRoot);
|
||||
const fixturePath = `${temporaryDirectory}/consumer.ts`;
|
||||
try {
|
||||
await Deno.writeTextFile(
|
||||
fixturePath,
|
||||
[
|
||||
'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";',
|
||||
"",
|
||||
"type IsAny<T> = 0 extends 1 & T ? true : false;",
|
||||
"type ExpectFalse<T extends false> = T;",
|
||||
"type ExpectTrue<T extends true> = T;",
|
||||
"type FilePathIsTyped = ExpectFalse<IsAny<FilePath>>;",
|
||||
"type FilePathIsString = ExpectTrue<FilePath extends string ? true : false>;",
|
||||
"type SettingsAreTyped = ExpectFalse<IsAny<ObsidianLiveSyncSettings>>;",
|
||||
'type SettingsRetainRemoteType = ExpectTrue<"remoteType" extends keyof ObsidianLiveSyncSettings ? true : false>;',
|
||||
"type ServiceHubIsTyped = ExpectFalse<IsAny<InjectableServiceHub>>;",
|
||||
'type ServiceHubRetainsSetting = ExpectTrue<"setting" extends keyof InjectableServiceHub ? true : false>;',
|
||||
"type P2PResultIsTyped = ExpectFalse<IsAny<UseP2PReplicatorResult>>;",
|
||||
'type P2PResultRetainsReplicator = ExpectTrue<"replicator" extends keyof UseP2PReplicatorResult ? true : false>;',
|
||||
"",
|
||||
"export type Proof = [",
|
||||
" FilePathIsTyped,",
|
||||
" FilePathIsString,",
|
||||
" SettingsAreTyped,",
|
||||
" SettingsRetainRemoteType,",
|
||||
" ServiceHubIsTyped,",
|
||||
" ServiceHubRetainsSetting,",
|
||||
" P2PResultIsTyped,",
|
||||
" P2PResultRetainsReplicator,",
|
||||
"];",
|
||||
"",
|
||||
].join("\n")
|
||||
);
|
||||
const projects = [
|
||||
{ name: "published package", paths: undefined },
|
||||
{ name: "compatibility mirror", paths: COMMONLIB_PATHS },
|
||||
] as const;
|
||||
for (const [index, project] of projects.entries()) {
|
||||
const projectPath = `${temporaryDirectory}/tsconfig-${index}.json`;
|
||||
await writeProject(projectPath, repositoryRoot, fixturePath, {
|
||||
moduleResolution: "Bundler",
|
||||
paths: project.paths,
|
||||
skipLibCheck: true,
|
||||
});
|
||||
const result = await runTypeScript(projectPath, repositoryRoot);
|
||||
assert(result.success, `${project.name} bundler resolution failed:\n${commandOutput(result)}`);
|
||||
}
|
||||
} finally {
|
||||
await Deno.remove(temporaryDirectory, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("legacy Commonlib mirror exposes its unresolved Octagonal Wheels boundary", 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 {
|
||||
await Deno.writeTextFile(
|
||||
fixturePath,
|
||||
[
|
||||
'import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";',
|
||||
"",
|
||||
"type ExpectTrue<T extends true> = T;",
|
||||
"export type FilePathIsString = ExpectTrue<FilePath extends string ? true : false>;",
|
||||
"",
|
||||
].join("\n")
|
||||
);
|
||||
await writeProject(projectPath, repositoryRoot, fixturePath, {
|
||||
moduleResolution: "Node10",
|
||||
paths: COMMONLIB_PATHS,
|
||||
skipLibCheck: false,
|
||||
});
|
||||
|
||||
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}`
|
||||
);
|
||||
} finally {
|
||||
await Deno.remove(temporaryDirectory, { recursive: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.legacy.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@vrtmrz/livesync-commonlib": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/index"],
|
||||
"@vrtmrz/livesync-commonlib/*": ["./dist/type-resolution-compat/@vrtmrz/livesync-commonlib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "../..",
|
||||
"moduleResolution": "Node10",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["../../src/**/*.ts"],
|
||||
"exclude": [
|
||||
"../../src/**/*.unit.spec.ts",
|
||||
"../../src/**/*.test.ts",
|
||||
"../../src/**/_test/**",
|
||||
"../../src/apps/cli/testdeno/**"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
type EslintMessage = {
|
||||
ruleId: string | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type EslintReport = {
|
||||
filePath: string;
|
||||
messages: EslintMessage[];
|
||||
};
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function scan(repositoryRoot: string, config: string): Promise<EslintMessage[]> {
|
||||
const result = await new Deno.Command(`${repositoryRoot}/node_modules/.bin/eslint`, {
|
||||
args: ["--config", config, "--concurrency", "off", "--format", "json", "src"],
|
||||
cwd: repositoryRoot,
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
}).output();
|
||||
const stderr = new TextDecoder().decode(result.stderr);
|
||||
assert(result.success, `synthetic Community Review scan failed:\n${stderr}`);
|
||||
const reports = JSON.parse(new TextDecoder().decode(result.stdout)) as EslintReport[];
|
||||
return reports.flatMap((report) =>
|
||||
report.messages.filter((message) => message.ruleId === "@typescript-eslint/no-redundant-type-constituents")
|
||||
);
|
||||
}
|
||||
|
||||
Deno.test("Commonlib mirror reduces synthetic legacy-resolution warnings", 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");
|
||||
const representativeTypes = [
|
||||
"FilePath",
|
||||
"InjectableServiceHub",
|
||||
"ObsidianLiveSyncSettings",
|
||||
"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}`
|
||||
);
|
||||
}
|
||||
assert(
|
||||
compatible.length < legacy.length,
|
||||
`the compatibility mirror did not reduce warnings: ${legacy.length} before, ${compatible.length} after`
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user