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
26 changes: 25 additions & 1 deletion .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:

steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: setup deno
uses: denoland/setup-deno@v2
Expand All @@ -33,3 +33,27 @@ jobs:

- name: test
run: deno task test

diagnostics:
runs-on: ubuntu-latest

steps:
- name: checkout
uses: actions/checkout@v7

- name: setup node
uses: actions/setup-node@v6
with:
node-version: 24.x

- name: install node diagnostics
run: npm install
working-directory: diagnostics/node-ts5

- name: check node diagnostics
run: npm run check
working-directory: diagnostics/node-ts5

- name: run node diagnostics
run: npm run diagnostics:all
working-directory: diagnostics/node-ts5
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
/build/
/.agent-shell/
/diagnostics/node-ts5/node_modules/
/diagnostics/node-ts5/package-lock.json
/diagnostics/node-ts5/.diagnostic.tsconfig.json
/diagnostics/node-ts5/fixtures/generated/
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "diagnostics/node-ts5/node_modules/typescript/lib",
"js/ts.tsdk.path": "diagnostics/node-ts5/node_modules/typescript/lib"
}
84 changes: 84 additions & 0 deletions diagnostics/node-ts5/diagnose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { readdir, unlink, writeFile } from "node:fs/promises";
import { relative } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";

const dir = fileURLToPath(new URL(".", import.meta.url));
const tsc = fileURLToPath(
new URL("./node_modules/typescript/bin/tsc", import.meta.url),
);
const config = fileURLToPath(
new URL("./.diagnostic.tsconfig.json", import.meta.url),
);
const examples = fileURLToPath(new URL("../../examples/", import.meta.url));
const generated = fileURLToPath(
new URL("./fixtures/generated/", import.meta.url),
);

let failed = false;
const files = [
...(await filesIn(examples)),
...(await filesIn(generated, true)),
];

try {
for (const file of files) {
const path = relative(dir, file);
await writeFile(
config,
JSON.stringify({
extends: "./tsconfig.json",
files: [path],
}),
);

const result = spawnSync(process.execPath, [
tsc,
"--extendedDiagnostics",
"--project",
config,
"--pretty",
"false",
], { cwd: dir, encoding: "utf8" });

const output = `${result.stdout}${result.stderr}`;
const instantiations = match(output, /^Instantiations:\s+(.+)$/m);
const checkTime = match(output, /^Check time:\s+(.+)$/m);
const error = output.match(/^.*error TS\d+:.*$/m)?.[0];
const status = result.status === 0 ? "PASS" : "FAIL";

console.log(
`${path.padEnd(34)} ${status} ` +
`instantiations=${instantiations ?? "n/a"} ` +
`check=${checkTime ?? "n/a"}`,
);
if (error) console.log(` ${error}`);
failed ||= result.status !== 0;
}
} finally {
await unlink(config).catch(() => {});
}

if (failed) process.exitCode = 1;

function match(value: string, pattern: RegExp) {
return value.match(pattern)?.[1]?.trim();
}

async function filesIn(dir: string, optional = false): Promise<string[]> {
try {
return (await readdir(dir, { withFileTypes: true }))
.filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
.map((entry) => `${dir}${entry.name}`)
.sort();
} catch (error) {
if (
optional &&
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "ENOENT"
) return [];
throw error;
}
}
48 changes: 48 additions & 0 deletions diagnostics/node-ts5/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";

const output = fileURLToPath(new URL("./fixtures/generated/", import.meta.url));
const sizes = [5, 20, 30, 50, 75, 100, 101];
const modes = ["static", "checkpoint"] as const;

await mkdir(output, { recursive: true });

for (const entry of await readdir(output)) {
if (entry.endsWith(".ts")) await unlink(`${output}${entry}`);
}

for (const size of sizes) {
for (const mode of modes) {
const prefix = mode === "static" ? "" : `${mode}-`;
const file = `${output}${prefix}options-${
String(size).padStart(3, "0")
}.ts`;
await writeFile(file, source(size, mode));
console.log(`generated ${file}`);
}
}

function source(size: number, mode: (typeof modes)[number]) {
const split = Math.floor(size / 2);
const lines = Array.from({ length: size }, (_, index) => {
const field = String(index + 1).padStart(3, "0");
return ` option(name("p${field}"), description("p${field}"), cli(["--p${field}"]), schema(z.number())),`;
});
const fields = mode === "checkpoint"
? [...lines.slice(0, split), " checkpoint(),", ...lines.slice(split)].join(
"\n",
)
: lines.join("\n");
const imports = mode === "checkpoint" ? "checkpoint, " : "";
const prefix = mode === "static" ? "" : `${mode}-`;

return `import { ${imports}cli, command, description, name, option } from "../../../../mod.ts";
import { schema } from "../../../../lib/param.ts";
import { z } from "zod";

export const app = command(
name("${prefix}options-${size}"),
${fields}
);
`;
}
17 changes: 17 additions & 0 deletions diagnostics/node-ts5/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "configliere-node-ts5-diagnostics",
"private": true,
"type": "module",
"scripts": {
"check": "tsc --noEmit --project tsconfig.json",
"generate": "node generate.ts",
"diagnostics": "node diagnose.ts",
"diagnostics:all": "npm run generate && npm run diagnostics"
},
"devDependencies": {
"@standard-schema/spec": "1.1.0",
"@types/node": "^26.5.0",
"typescript": "5.5.4",
"zod": "4.4.3"
}
}
14 changes: 14 additions & 0 deletions diagnostics/node-ts5/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["node"]
},
"files": [
"../../examples/options.ts",
"../../examples/checkpoint-options.ts",
"../../examples/routes.ts",
"../../examples/routes-checkpoint.ts",
"diagnose.ts",
"generate.ts"
]
}
6 changes: 6 additions & 0 deletions docs/insights.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ The detailed phase-binding design is recorded in [Binding](./binding.md).
- Definitions are immutable function-composition pipelines.
- Any valid `A → B` transformation may participate in a pipeline.
- `extend()` packages context-free composition; `extend()` is identity.
- Static built-in route elements carry a hidden operation type and fold in
bounded chunks; their number is not limited by an overload tower.
- Macros composed with `extend()` inherit that operation type without an
explicit registration step. Concrete unbranded `A → B` functions compose
directly; bespoke generic atoms use an explicit type-lambda marker because
TypeScript cannot otherwise apply their input-dependent output type.
- Static definitions contain enough metadata to render help and schemas without
parsing input.
- Standard Schema validates values; it does not define source syntax or token
Expand Down
86 changes: 86 additions & 0 deletions examples/checkpoint-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { checkpoint, cli, command, description, name, option } from "../mod.ts";
import { schema } from "../lib/param.ts";
import type { ModelOf } from "../lib/types.ts";
import { z } from "zod";

export const app = command(
name("auth0"),
option(
name("port"),
description("server port"),
cli(["--port", "-p"]),
schema(z.number()),
),
option(
name("domain"),
description("server domain"),
cli(["--domain"]),
schema(z.string()),
),
option(
name("audience"),
description("Auth0 audience"),
cli(["--audience"]),
schema(z.string()),
),
option(
name("clientID"),
description("Auth0 client ID"),
cli(["--client-id"]),
schema(z.string()),
),
option(
name("protocol"),
description("server protocol"),
cli(["--protocol"]),
schema(z.enum(["http", "https"])),
),
checkpoint(),
option(
name("clientSecret"),
description("client secret"),
cli(["--client-secret"]),
schema(z.string()),
),
option(
name("scope"),
description("OAuth scope"),
cli(["--scope"]),
schema(z.string()),
),
option(
name("rulesDirectory"),
description("rules directory"),
cli(["--rules-directory"]),
schema(z.string()),
),
option(
name("connection"),
description("Auth0 connection"),
cli(["--connection"]),
schema(z.string()),
),
option(
name("config"),
description("JSON config path"),
cli(["--config", "-c"]),
schema(z.string()),
),
);

export type Configuration = ModelOf<typeof app>;

type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2)
? (<T>() => T extends B ? 1 : 2) extends (<T>() => T extends A ? 1 : 2) ? true
: false
: false;
type Assert<T extends true> = T;
type ConfigIsPresent = Assert<
Equal<"config" extends keyof Configuration ? true : false, true>
>;
type ConnectionIsPresent = Assert<
Equal<"connection" extends keyof Configuration ? true : false, true>
>;
type PortIsNumber = Assert<Equal<Configuration["port"], number>>;
type ConfigIsString = Assert<Equal<Configuration["config"], string>>;
Loading
Loading