diff --git a/.changeset/native-typescript-builds.md b/.changeset/native-typescript-builds.md new file mode 100644 index 00000000..052051e3 --- /dev/null +++ b/.changeset/native-typescript-builds.md @@ -0,0 +1,10 @@ +--- +"@cleverbrush/server": patch +"@cleverbrush/knex-schema": patch +"@cleverbrush/scheduler-postgres": patch +--- + +Build and validate the framework with the pinned TypeScript 7.1 native nightly. +Declare endpoint builder variance so large contracts stay within the native +compiler's instantiation limits, and preserve internal query and scheduler +schema types without changing their runtime behavior. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22a9bb44..f487234a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: run: npm run build:schema-site && npm run build:docs-site - name: Generate every package API reference - run: npx typedoc --out "$RUNNER_TEMP/framework-api-docs" + run: npm run docs:generate -- --out "$RUNNER_TEMP/framework-api-docs" - uses: actions/upload-artifact@v4 if: always() diff --git a/README.md b/README.md index 9dd534f9..d3ccad30 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,14 @@ ES modules. Use Node.js 24 or newer and npm 11. Install the Node version in `.nvmrc`. +Builds and typechecks use the pinned native compiler, +`typescript@7.1.0-dev.20261007.1`. The `tsc` command also powers declaration +generation, Vitest type tests, website builds, and consumer fixtures. +TypeDoc alone uses TypeScript 6.0.3 in the private `scripts/api-docs` workspace +until [TypeDoc supports TypeScript 7](https://github.com/TypeStrong/typedoc/issues/3098). +Use `npm run docs:generate -- --out ` to generate API references +through that isolated toolchain, or `npm run docs` for the website output. + ```bash npm ci npm run lint @@ -108,6 +116,19 @@ npm run build:schema-site npm run build:docs-site ``` +To compare two disposable checkouts after running `npm ci` in each: + +```bash +node scripts/benchmark-build.mjs --baseline /tmp/framework-baseline \ + --candidate /tmp/framework-candidate --runs 5 --output /tmp/build-results.json +``` + +The benchmark removes generated package outputs, bypasses Turbo cache reads, +and alternates the checkouts after a warm-up. It records clean full builds, +compiler-only declaration generation, and warm incremental schema checks. +Run it without other builds or tests competing for CPU. Results and command +logs are written beside the requested JSON file. + The demo app can be started with: ```bash diff --git a/demos/e2e/package.json b/demos/e2e/package.json index 9c98144b..c7b5c6d4 100644 --- a/demos/e2e/package.json +++ b/demos/e2e/package.json @@ -21,7 +21,7 @@ "@types/node": "^22.0.0", "@types/pg": "^8.11.10", "@types/ws": "^8.5.13", - "typescript": "^6.0.2", + "typescript": "7.1.0-dev.20261007.1", "vitest": "^4.1.11" } } diff --git a/demos/todo-backend/Dockerfile b/demos/todo-backend/Dockerfile index fccd77ce..e748d32c 100644 --- a/demos/todo-backend/Dockerfile +++ b/demos/todo-backend/Dockerfile @@ -38,6 +38,7 @@ COPY demos/todo-frontend/package.json ./demos/todo-frontend/ COPY websites/docs/package.json ./websites/docs/ COPY websites/schema/package.json ./websites/schema/ COPY websites/shared/package.json ./websites/shared/ +COPY scripts/api-docs/package.json ./scripts/api-docs/ # Install all workspace dependencies RUN npm ci --ignore-scripts diff --git a/demos/todo-backend/package.json b/demos/todo-backend/package.json index de9b4977..cb1cb3b9 100644 --- a/demos/todo-backend/package.json +++ b/demos/todo-backend/package.json @@ -62,7 +62,7 @@ "@types/node": "^22.0.0", "tsup": "^8.5.1", "tsx": "^4.19.0", - "typescript": "^6.0.2" + "typescript": "7.1.0-dev.20261007.1" }, "type": "module", "types": "./dist/index.d.ts", diff --git a/demos/todo-frontend/Dockerfile b/demos/todo-frontend/Dockerfile index 0e67dc2f..2aef7a1e 100644 --- a/demos/todo-frontend/Dockerfile +++ b/demos/todo-frontend/Dockerfile @@ -38,6 +38,7 @@ COPY demos/todo-frontend/package.json ./demos/todo-frontend/ COPY websites/docs/package.json ./websites/docs/ COPY websites/schema/package.json ./websites/schema/ COPY websites/shared/package.json ./websites/shared/ +COPY scripts/api-docs/package.json ./scripts/api-docs/ # Install workspace dependencies RUN npm ci --ignore-scripts diff --git a/demos/todo-frontend/package.json b/demos/todo-frontend/package.json index 28e74a9e..2df70596 100644 --- a/demos/todo-frontend/package.json +++ b/demos/todo-frontend/package.json @@ -27,7 +27,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", - "typescript": "^6.0.2", + "typescript": "7.1.0-dev.20261007.1", "vite": "^6.4.3" } } diff --git a/libs/knex-schema/src/SchemaQueryBuilder.ts b/libs/knex-schema/src/SchemaQueryBuilder.ts index 2557cf26..5ea68178 100644 --- a/libs/knex-schema/src/SchemaQueryBuilder.ts +++ b/libs/knex-schema/src/SchemaQueryBuilder.ts @@ -1610,7 +1610,7 @@ export class SchemaQueryBuilder< private writer(filtered = true): QuerySource> { this.assertWritable(); - let base = this.knex(getTableName(this.source)); + let base: Knex.QueryBuilder = this.knex(getTableName(this.source)); if (filtered) { const keys = getPrimaryKeyColumns(this.source).columnNames; if (keys.length) { diff --git a/libs/knex-schema/src/property-navigation.test.ts b/libs/knex-schema/src/property-navigation.test.ts index 4cb5b56d..78d317ae 100644 --- a/libs/knex-schema/src/property-navigation.test.ts +++ b/libs/knex-schema/src/property-navigation.test.ts @@ -1,8 +1,16 @@ +import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { LanguageVariant, SyntaxKind } from 'typescript/unstable/ast'; +import { createScanner } from 'typescript/unstable/ast/scanner'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { typescriptCli } from '../../../scripts/typescript-cli.mjs'; +import { + type Location, + NativeLanguageService, + positionAt +} from '../test-fixtures/native-language-service.js'; // Exercise the whole public type pipeline in one language service, including // downstream ORM, mapper, form and client consumers of the emitted declarations. @@ -94,37 +102,14 @@ const references = [...sources].flatMap(([file, source]) => })) ); -let service: ts.LanguageService; -beforeAll(() => { - const options: ts.CompilerOptions = { - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, - strict: true, - skipLibCheck: true, - allowJs: true, - checkJs: true, - noEmit: true, - esModuleInterop: true, - types: ['node'] - }; - service = ts.createLanguageService({ - ...ts.sys, - useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, - getScriptFileNames: () => [...sources.keys()], - getScriptVersion: () => '0', - getScriptSnapshot: file => { - const text = sources.get(file) ?? ts.sys.readFile(file); - return text === undefined - ? undefined - : ts.ScriptSnapshot.fromString(text); - }, - getCompilationSettings: () => options, - getCurrentDirectory: () => dirname(model), - getDefaultLibFileName: ts.getDefaultLibFilePath - }); +let service: NativeLanguageService; +beforeAll(async () => { + service = new NativeLanguageService(); + await service.open(dirname(model), sources); +}); +afterAll(async () => { + await service?.close(); }); -afterAll(() => service?.dispose()); test('navigation fixtures typecheck against public package declarations', () => { // Formatting must not silently stop the marker parser from finding cases. @@ -132,16 +117,23 @@ test('navigation fixtures typecheck against public package declarations', () => expect(new Set(references.map(reference => reference.name))).toEqual( new Set(Object.keys(documentation)) ); - const diagnostics = [...sources.keys()].flatMap(file => [ - ...service.getSyntacticDiagnostics(file), - ...service.getSemanticDiagnostics(file) - ]); - expect( - diagnostics.map(d => - ts.flattenDiagnosticMessageText(d.messageText, '\n') - ) - ).toEqual([]); - const files = service.getProgram()!.getSourceFiles(); + const result = spawnSync( + process.execPath, + [ + typescriptCli(), + '-p', + fileURLToPath( + new URL('../test-fixtures/tsconfig.json', import.meta.url) + ), + '--listFiles', + '--pretty', + 'false' + ], + { encoding: 'utf8', timeout: 120_000 } + ); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(0); + const files = result.stdout.split('\n').map(file => file.trim()); for (const name of [ 'knex-schema', 'orm', @@ -154,9 +146,7 @@ test('navigation fixtures typecheck against public package declarations', () => 'client' ]) { expect( - files.some(file => - file.fileName.endsWith(`/libs/${name}/dist/index.d.ts`) - ), + files.some(file => file.endsWith(`/libs/${name}/dist/index.d.ts`)), `${name} must be tested through its published declarations` ).toBe(true); } @@ -165,33 +155,40 @@ test('navigation fixtures typecheck against public package declarations', () => }, 120_000); describe.each(references)('$language $name ($property)', reference => { - test('goes to the original property declaration', () => { + test('goes to the original property declaration', async () => { const doc = documentation[reference.name]; expect(doc).toBeDefined(); const comment = `/** ${doc} */`; const offset = modelSource.indexOf(comment); expect(offset).toBeGreaterThanOrEqual(0); - const scanner = ts.createScanner( - ts.ScriptTarget.ES2022, + const scanner = createScanner( true, - ts.LanguageVariant.Standard, + LanguageVariant.Standard, modelSource ); - scanner.setTextPos(offset + comment.length); + scanner.resetTokenState(offset + comment.length); scanner.scan(); - if (scanner.getToken() === ts.SyntaxKind.ReadonlyKeyword) - scanner.scan(); + if (scanner.getToken() === SyntaxKind.ReadonlyKeyword) scanner.scan(); expect(scanner.getTokenText()).toBe(reference.property); - const definitions = service.getDefinitionAtPosition( - reference.file, - reference.position + const definitions = await service.request( + 'textDocument/definition', + { + textDocument: { uri: pathToFileURL(reference.file).href }, + position: positionAt( + sources.get(reference.file)!, + reference.position + ) + } ); expect(definitions).toContainEqual( expect.objectContaining({ - fileName: model, - textSpan: { - start: scanner.getTokenPos(), - length: reference.property.length + uri: pathToFileURL(model).href, + range: { + start: positionAt(modelSource, scanner.getTokenStart()), + end: positionAt( + modelSource, + scanner.getTokenStart() + reference.property.length + ) } }) ); @@ -200,14 +197,21 @@ describe.each(references)('$language $name ($property)', reference => { // TypeScript omits hover docs for this union-context object literal even // when both branches navigate to the same schema. Keep the accepted union. if (reference.name !== 'update-key') { - test('shows the original JSDoc on hover', () => { - const info = service.getQuickInfoAtPosition( - reference.file, - reference.position - ); - expect(ts.displayPartsToString(info?.documentation)).toBe( - documentation[reference.name] + test('shows the original JSDoc on hover', async () => { + const info = await service.request<{ contents: { value: string } }>( + 'textDocument/hover', + { + textDocument: { uri: pathToFileURL(reference.file).href }, + position: positionAt( + sources.get(reference.file)!, + reference.position + ) + } ); + // Markdown hover separates the signature from the original documentation. + expect( + info.contents.value.replace(/^```[\s\S]*?```\s*/, '').trim() + ).toBe(documentation[reference.name]); }); } }); diff --git a/libs/knex-schema/src/public-api-docs.test.ts b/libs/knex-schema/src/public-api-docs.test.ts index bc3039e4..bc642d6b 100644 --- a/libs/knex-schema/src/public-api-docs.test.ts +++ b/libs/knex-schema/src/public-api-docs.test.ts @@ -1,26 +1,35 @@ import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; -import { describe, expect, it } from 'vitest'; +import type { Node, SourceFile } from 'typescript/unstable/ast'; +import { ModifierFlags } from 'typescript/unstable/ast'; +import * as ts from 'typescript/unstable/ast/is'; +import { getCombinedModifierFlags } from 'typescript/unstable/ast/utils'; +import { API } from 'typescript/unstable/sync'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +let api: API; +beforeAll(() => { + api = new API(); +}); +afterAll(() => api?.close()); /** Inspect emitted declarations, since those are what installed consumers read. */ -function declaration(path: string): ts.SourceFile { +function declaration(path: string): SourceFile { const url = new URL(path, import.meta.url); - return ts.createSourceFile( - fileURLToPath(url), - readFileSync(url, 'utf8'), - ts.ScriptTarget.Latest, - true - ); + return api.createSourceFile(fileURLToPath(url), readFileSync(url, 'utf8')) + .sourceFile; } -function hasSummary(node: ts.Node): boolean { - const docs = (node as ts.Node & { jsDoc?: readonly ts.JSDoc[] }).jsDoc; - return !!docs?.some(doc => doc.comment); +function hasSummary(node: Node): boolean { + return !!node.jsDoc?.some(doc => ts.isJSDoc(doc) && doc.comment.length); } -function isInternal(node: ts.Node): boolean { - return ts.getJSDocTags(node).some(tag => tag.tagName.text === 'internal'); +function isInternal(node: Node): boolean { + return !!node.jsDoc?.some( + doc => + ts.isJSDoc(doc) && + doc.tags?.some(tag => tag.tagName.getText() === 'internal') + ); } describe('published query/ORM API documentation', () => { @@ -60,7 +69,7 @@ describe('published query/ORM API documentation', () => { ); expect(missing).toEqual([]); - function check(source: ts.SourceFile, names: Set): void { + function check(source: SourceFile, names: Set): void { for (const node of source.statements) { if ( ts.isVariableStatement(node) && @@ -87,9 +96,9 @@ describe('published query/ORM API documentation', () => { if (ts.isClassDeclaration(node)) { for (const member of node.members) { if ( - ts.getCombinedModifierFlags(member) & - (ts.ModifierFlags.Private | - ts.ModifierFlags.Protected) || + getCombinedModifierFlags(member) & + (ModifierFlags.Private | + ModifierFlags.Protected) || (member.name && ts.isPrivateIdentifier(member.name)) || isInternal(member) @@ -117,17 +126,17 @@ describe('published query/ORM API documentation', () => { ]) { const source = declaration(path); const missing: string[] = []; - function visit(node: ts.Node): void { + function visit(node: Node): void { if ( - (ts.isMethodSignature(node) || - ts.isPropertySignature(node) || + (ts.isMethodSignatureDeclaration(node) || + ts.isPropertySignatureDeclaration(node) || ts.isCallSignatureDeclaration(node)) && !isInternal(node) && !hasSummary(node) ) { missing.push(node.getText(source).split('\n')[0]); } - ts.forEachChild(node, visit); + node.forEachChild(visit); } const node = source.statements.find( n => diff --git a/libs/knex-schema/src/read-consumer.test.ts b/libs/knex-schema/src/read-consumer.test.ts index fc612086..9839d42f 100644 --- a/libs/knex-schema/src/read-consumer.test.ts +++ b/libs/knex-schema/src/read-consumer.test.ts @@ -10,6 +10,7 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { expect, test } from 'vitest'; +import { typescriptCli } from '../../../scripts/typescript-cli.mjs'; const repository = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); @@ -145,11 +146,7 @@ services.addSingletonFromSchema(Result, factory, (value: number) => value); ); const result = spawnSync( process.execPath, - [ - join(repository, 'node_modules/typescript/lib/tsc.js'), - '-p', - join(directory, 'tsconfig.json') - ], + [typescriptCli(), '-p', join(directory, 'tsconfig.json')], { encoding: 'utf8', timeout: 60000 } ); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); diff --git a/libs/knex-schema/test-fixtures/native-language-service.ts b/libs/knex-schema/test-fixtures/native-language-service.ts new file mode 100644 index 00000000..821d9a3e --- /dev/null +++ b/libs/knex-schema/test-fixtures/native-language-service.ts @@ -0,0 +1,156 @@ +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { pathToFileURL } from 'node:url'; +import { typescriptCli } from '../../../scripts/typescript-cli.mjs'; + +export type Position = { line: number; character: number }; +export type Location = { + uri: string; + range: { start: Position; end: Position }; +}; + +/** UTF-16 positions, as used by LSP and JavaScript string offsets. */ +export function positionAt(text: string, offset: number): Position { + const lines = text.slice(0, offset).split('\n'); + return { line: lines.length - 1, character: lines.at(-1)!.length }; +} + +/** Minimal stdio client for the native compiler's editor regression tests. */ +export class NativeLanguageService { + private child = spawn( + process.execPath, + [typescriptCli(), '--lsp', '--stdio'], + { stdio: ['pipe', 'pipe', 'pipe'] } + ); + private buffer = Buffer.alloc(0); + private stderr = ''; + private nextId = 0; + private pending = new Map< + number, + { + resolve: (value: any) => void; + reject: (error: Error) => void; + timer: ReturnType; + } + >(); + + constructor() { + this.child.stdout.on('data', chunk => { + this.buffer = Buffer.concat([this.buffer, chunk]); + while (true) { + const headerEnd = this.buffer.indexOf('\r\n\r\n'); + if (headerEnd < 0) break; + const header = this.buffer.subarray(0, headerEnd).toString(); + const length = Number( + /Content-Length: (\d+)/i.exec(header)?.[1] + ); + if (!Number.isFinite(length)) { + this.fail(new Error('Invalid language server frame')); + this.child.kill(); + break; + } + const end = headerEnd + 4 + length; + if (this.buffer.length < end) break; + const message = JSON.parse( + this.buffer.subarray(headerEnd + 4, end).toString() + ); + this.buffer = this.buffer.subarray(end); + if (message.method && message.id !== undefined) { + // This client requests no dynamic registration or configuration. + this.send({ jsonrpc: '2.0', id: message.id, result: null }); + } else if (message.id !== undefined) { + const pending = this.pending.get(message.id); + if (!pending) continue; + clearTimeout(pending.timer); + this.pending.delete(message.id); + if (message.error) + pending.reject( + new Error(JSON.stringify(message.error)) + ); + else pending.resolve(message.result); + } + } + }); + this.child.stderr.on('data', chunk => { + this.stderr += chunk; + }); + this.child.on('error', error => this.fail(error)); + this.child.on('exit', code => + this.fail( + new Error(`Language server exited (${code}): ${this.stderr}`) + ) + ); + } + + private fail(error: Error) { + for (const pending of this.pending.values()) { + clearTimeout(pending.timer); + pending.reject(error); + } + this.pending.clear(); + } + + private send(message: object) { + const body = JSON.stringify(message); + this.child.stdin.write( + `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}` + ); + } + + request(method: string, params: unknown): Promise { + const id = ++this.nextId; + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(id); + reject(new Error(`Language server timed out: ${method}`)); + }, 30_000); + this.pending.set(id, { resolve, reject, timer }); + this.send({ jsonrpc: '2.0', id, method, params }); + }); + } + + async open(directory: string, sources: Map) { + await this.request('initialize', { + processId: process.pid, + rootUri: pathToFileURL(directory).href, + capabilities: { + textDocument: { + hover: { + contentFormat: ['markdown'] + } + } + } + }); + this.send({ jsonrpc: '2.0', method: 'initialized', params: {} }); + for (const [file, text] of sources) { + this.send({ + jsonrpc: '2.0', + method: 'textDocument/didOpen', + params: { + textDocument: { + uri: pathToFileURL(file).href, + languageId: file.endsWith('.ts') + ? 'typescript' + : 'javascript', + version: 1, + text + } + } + }); + } + } + + async close() { + if (this.child.exitCode !== null || this.child.signalCode !== null) + return; + const exited = once(this.child, 'exit'); + try { + await this.request('shutdown', undefined); + this.send({ jsonrpc: '2.0', method: 'exit' }); + } finally { + this.child.stdin.end(); + this.child.kill(); + await exited; + } + } +} diff --git a/libs/knex-schema/test-fixtures/tsconfig.json b/libs/knex-schema/test-fixtures/tsconfig.json new file mode 100644 index 00000000..70270cce --- /dev/null +++ b/libs/knex-schema/test-fixtures/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "allowJs": true, + "checkJs": true, + "noEmit": true, + "esModuleInterop": true, + "types": ["node"] + }, + "files": [ + "property-navigation.model.ts", + "property-navigation.consumer.js", + "property-navigation.types.ts" + ] +} diff --git a/libs/react-form/src/declarations.test.ts b/libs/react-form/src/declarations.test.ts index b2e18595..cef08622 100644 --- a/libs/react-form/src/declarations.test.ts +++ b/libs/react-form/src/declarations.test.ts @@ -1,39 +1,50 @@ // @vitest-environment node +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import ts from 'typescript'; import { expect, test } from 'vitest'; +import { typescriptCli } from '../../../scripts/typescript-cli.mjs'; test('consumers can emit declarations for exported inferred form systems', () => { const fixture = fileURLToPath( new URL('../test-fixtures/exported-system.tsx', import.meta.url) ); - const options: ts.CompilerOptions = { - declaration: true, - emitDeclarationOnly: true, - strict: true, - skipLibCheck: true, - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, - jsx: ts.JsxEmit.ReactJSX - }; - const host = ts.createCompilerHost(options); - const output: string[] = []; - host.writeFile = (_fileName, content) => { - output.push(content); - }; - const program = ts.createProgram([fixture], options, host); - const diagnostics = [ - ...ts.getPreEmitDiagnostics(program), - ...program.emit().diagnostics - ]; - expect( - diagnostics.map(diagnostic => - ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') - ) - ).toEqual([]); - expect(output.join('\n')).toContain('TypedFormSystem'); - // In-process TypeScript declaration emit is substantially slower with V8 - // coverage on shared CI runners than ordinary form behavior tests. + const output = mkdtempSync(join(tmpdir(), 'framework-form-declarations-')); + try { + const result = spawnSync( + process.execPath, + [ + typescriptCli(), + '--ignoreConfig', + '--declaration', + '--emitDeclarationOnly', + '--strict', + '--skipLibCheck', + '--target', + 'ES2022', + '--module', + 'ESNext', + '--moduleResolution', + 'bundler', + '--jsx', + 'react-jsx', + '--outDir', + output, + '--rootDir', + dirname(fixture), + fixture + ], + { encoding: 'utf8', timeout: 60_000 } + ); + expect(result.error).toBeUndefined(); + expect(result.status, result.stdout + result.stderr).toBe(0); + expect( + readFileSync(join(output, 'exported-system.d.ts'), 'utf8') + ).toContain('TypedFormSystem'); + } finally { + rmSync(output, { recursive: true, force: true }); + } }, 60_000); diff --git a/libs/scheduler-postgres/src/schema.ts b/libs/scheduler-postgres/src/schema.ts index 3ef7f12b..38e6518a 100644 --- a/libs/scheduler-postgres/src/schema.ts +++ b/libs/scheduler-postgres/src/schema.ts @@ -40,23 +40,26 @@ const attemptFields = { }; // Named field maps keep declaration emission portable without duplicating row types. +// Explicitly project composite schemas to their existing unbranded storage type: +// comparing the branded recursive `this` type through a variable annotation +// triggers TS5114 in the native compiler. Only the phantom brand is erased. const runs: ReturnType> = object(runFields).hasTableName('cb_jobs_runs'); -const schedules: ReturnType> = object( - scheduleFields -) +const schedules = object(scheduleFields) .hasTableName('cb_jobs_schedules') - .hasPrimaryKey(['namespace', 'id']); -const events: ReturnType> = object( - eventFields -) + .hasPrimaryKey(['namespace', 'id']) as unknown as ReturnType< + typeof object +>; +const events = object(eventFields) .hasTableName('cb_jobs_events') - .hasPrimaryKey(['runId', 'sequence']); -const attempts: ReturnType> = object( - attemptFields -) + .hasPrimaryKey(['runId', 'sequence']) as unknown as ReturnType< + typeof object +>; +const attempts = object(attemptFields) .hasTableName('cb_jobs_attempts') - .hasPrimaryKey(['runId', 'attempt']); + .hasPrimaryKey(['runId', 'attempt']) as unknown as ReturnType< + typeof object +>; type StorageSchemas = { runs: typeof runs; diff --git a/libs/server/src/Endpoint.ts b/libs/server/src/Endpoint.ts index db07f5f5..60730d96 100644 --- a/libs/server/src/Endpoint.ts +++ b/libs/server/src/Endpoint.ts @@ -707,17 +707,20 @@ type CacheTagSelector = { : Record; }; +// Explicit variance lets the native compiler compare contracts by their type +// arguments instead of expanding every method for each operation in a large API. +// Body/response descriptors also expose setters, so those states are invariant. export class EndpointBuilder< - TParams = {}, - TBody = undefined, - TQuery = {}, - THeaders = {}, - TServices = {}, - TPrincipal = undefined, - TRoles extends string = string, - TResponse = any, - TResponses extends Record = {}, - TUpload extends UploadContract = false + out TParams = {}, + in out TBody = undefined, + out TQuery = {}, + out THeaders = {}, + out TServices = {}, + out TPrincipal = undefined, + out TRoles extends string = string, + in out TResponse = any, + out TResponses extends Record = {}, + out TUpload extends UploadContract = false > { readonly #method: string; readonly #basePath: string; diff --git a/libs/server/src/Endpoint.variance.test-d.ts b/libs/server/src/Endpoint.variance.test-d.ts new file mode 100644 index 00000000..dd15d5bf --- /dev/null +++ b/libs/server/src/Endpoint.variance.test-d.ts @@ -0,0 +1,53 @@ +import { expectTypeOf } from 'vitest'; +import type { EndpointBuilder } from './Endpoint.js'; + +// Large contracts rely on argument-based comparisons. Covariance must still +// reject narrowing request, service, principal and response-map types. +type Narrow = EndpointBuilder< + { id: string }, + { title: string }, + { search: string }, + { token: string }, + { db: { name: string } }, + { role: 'admin' }, + 'admin', + { title: string }, + { 200: { title: string } }, + false +>; +type Wide = EndpointBuilder< + { id: string | number }, + { title: string }, + { search: string | number }, + { token: string | number }, + { db: { name: string | number } }, + { role: string }, + string, + { title: string }, + { 200: { title: string | number } }, + boolean +>; +expectTypeOf().toExtend(); +expectTypeOf().not.toExtend(); +expectTypeOf>().not.toExtend< + EndpointBuilder<{ id: string }> +>(); + +// Body and response descriptors expose setters as well as getters. +expectTypeOf>().not.toExtend< + EndpointBuilder<{}, string | number> +>(); +expectTypeOf< + EndpointBuilder<{}, undefined, {}, {}, {}, undefined, string, string> +>().not.toExtend< + EndpointBuilder< + {}, + undefined, + {}, + {}, + {}, + undefined, + string, + string | number + > +>(); diff --git a/libs/server/src/Implementation.consumer.test.ts b/libs/server/src/Implementation.consumer.test.ts index 5b09f6bd..c559cdbc 100644 --- a/libs/server/src/Implementation.consumer.test.ts +++ b/libs/server/src/Implementation.consumer.test.ts @@ -11,6 +11,7 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { expect, it } from 'vitest'; +import { typescriptCli } from '../../../scripts/typescript-cli.mjs'; const repository = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); @@ -136,7 +137,7 @@ export const registration = mapHandlers({ ${groups.map(g => `${g}: ${g}.endpoint const result = spawnSync( process.execPath, [ - join(repository, 'node_modules/typescript/lib/tsc.js'), + typescriptCli(), '-p', join(temporary, style, 'tsconfig.json'), '--extendedDiagnostics' diff --git a/package-lock.json b/package-lock.json index b5dd75c6..00562ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "./demos/*", "./websites/shared", "./websites/schema", - "./websites/docs" + "./websites/docs", + "./scripts/api-docs" ], "devDependencies": { "@biomejs/biome": "^2.4.9", @@ -26,8 +27,7 @@ "jsdom": "^29.0.1", "tsup": "^8.5.1", "turbo": "^2.9.14", - "typedoc": "^0.28.18", - "typescript": "^6.0.2", + "typescript": "7.1.0-dev.20261007.1", "vitest": "^4.1.11" }, "engines": { @@ -46,7 +46,7 @@ "@types/node": "^22.0.0", "@types/pg": "^8.11.10", "@types/ws": "^8.5.13", - "typescript": "^6.0.2", + "typescript": "7.1.0-dev.20261007.1", "vitest": "^4.1.11" } }, @@ -106,7 +106,7 @@ "@types/node": "^22.0.0", "tsup": "^8.5.1", "tsx": "^4.19.0", - "typescript": "^6.0.2" + "typescript": "7.1.0-dev.20261007.1" } }, "demos/todo-frontend": { @@ -130,7 +130,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.4", - "typescript": "^6.0.2", + "typescript": "7.1.0-dev.20261007.1", "vite": "^6.4.3" } }, @@ -1818,6 +1818,10 @@ "node": ">= 20.12.0" } }, + "node_modules/@cleverbrush/api-docs-tooling": { + "resolved": "scripts/api-docs", + "link": true + }, "node_modules/@cleverbrush/async": { "resolved": "libs/async", "link": true @@ -3322,15 +3326,15 @@ } }, "node_modules/@next/env": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz", - "integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.0.tgz", + "integrity": "sha512-pyLlT5oafGlX8X7xjgGNXz5dNIDSxREm+qD6i9VLSZiZFopA/XWl8NM/KPmLx/8EG/++c49iid3gNWSQW1+2dQ==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz", - "integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.0.tgz", + "integrity": "sha512-yyZ+9MgViAKLLb5l1gPBGjRwnq3teNLb5Zx6JEw8LzNSTjlsnp+baUHFxTgl7PpjCmKf48VQfyipG1eBflb48Q==", "cpu": [ "arm64" ], @@ -3344,9 +3348,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz", - "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.0.tgz", + "integrity": "sha512-RePEEejEj1Auf5s9qEcG0JbcDP3ceNUS/ofNX8AhxzgIjTVlZIHMatlp16YplcSMpsVo/ZMQHJTjbMYJznjGUg==", "cpu": [ "x64" ], @@ -3360,9 +3364,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz", - "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.0.tgz", + "integrity": "sha512-HhsyCqd+NpmvDDVY/LqkAN24f+QVvXh3ie+PfcwKinWL2Mu826JymQxJXGjAIfmJ4VlHjp4Pe/nZtXtUoEMAqw==", "cpu": [ "arm64" ], @@ -3379,9 +3383,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz", - "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.0.tgz", + "integrity": "sha512-sN9Pf+Zh+RpZAOmbR7d8KAA8T61CVhawtbEJ2Bk6QrRzx6Z/6haaF4UiwBzytM96GRRRWZh9/WALIEO3npMamA==", "cpu": [ "arm64" ], @@ -3398,9 +3402,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz", - "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.0.tgz", + "integrity": "sha512-D5M5xLWQVd1CvR+L7BickM1weRfmM41x3L1NWcnKY/oyJJqr1qwaCgmu3dMAhLBOMdZwSuDCRV6DJRRpiUkbDQ==", "cpu": [ "x64" ], @@ -3417,9 +3421,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz", - "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.0.tgz", + "integrity": "sha512-8d5WLS6a9YalqpOgXdlbvonFu+sIgjA+CO6tGzetrpPVcUL56foa7B7664h41rAlbLi5fJwK4vWji3zf4QDy+w==", "cpu": [ "x64" ], @@ -3436,9 +3440,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz", - "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.0.tgz", + "integrity": "sha512-sQuOIiW3snb48UwrBcDMvFA/u03qtw2LeuSIxFIQ9AJuW9UG/4Ddsfo+eV73PnZwBhyncMtsKVh7PrzMol2/fA==", "cpu": [ "arm64" ], @@ -3452,9 +3456,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz", - "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.0.tgz", + "integrity": "sha512-6/33lCuxpdHUy27RegyGM/5uTpF7YZd3UR4D0gpkOA/tWY5YR7msbQjqPgCZmSmxrrTLGCXYA4A8vT2H5DPGTQ==", "cpu": [ "x64" ], @@ -6761,6 +6765,118 @@ "@types/node": "*" } }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-uW+MMuwJQoxK1CMGkmiYQ1jNdUsS0MUsMh6bP3V3XHHoyZHbqSJShcF30dVB2WAW+p5kh+6I4za05w2Z7Y6Axg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-M92aWIg2vh1xdHLAQrZlTAe/BvwgnQwrtJ4BgErZg1Rc6CuiB1zzVz33nmrVcMOmGSTuGtqpVsBGh+jkFJIGrw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-fH8JM8rpF78AsK1wA3WmhBgeUEULensthFv5yvktNvR9CqAk7PMS7hHTLRZkRxHL/kRq6BxjkvhLvJHGtXtbaQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-wFjFvZJyhoAsH8IVquPmXbpWP6IAO+JP5a2gdMgvUsGW4b3vXdky/LyGM7kMBsTh+aHdk4EsCIwg1XZbN/cnOw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-vs4yDJgDegYd0Xa2kTzH3TfgUsAQzASlZH6AV804wZakIwuDRqCFhlqi9dRHbQ5q9OvjCRX1W3ZIbXx/XpfZVg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-4V6rSStaYRcOJMN6umnvJdTxCiEXpujpA3IGC8naApW5EQAMivslxKbQ2UEWA46eNFacw7K5aG8x3eNl4roZNw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-doGojvo5Weo9T3ES6Ur0XWm61oP+2IQPDmdceA0dm/5vQsmE9pVv7+yay/Wql/igZ8YvVEyVHdP56PS6MtRlAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -7336,15 +7452,15 @@ } }, "node_modules/concurrently": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", - "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.5.tgz", + "integrity": "sha512-YMZlvWrTPQEhjKp60XSmMGt60+pffbv6DyRnOMvVO98+J7aF8w8Ui9FhdcOwvSixc/kyR6SoVvmejcn/2lZBqg==", "dev": true, "license": "MIT", "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", - "shell-quote": "1.9.0", + "shell-quote": "1.12.0", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" @@ -8580,12 +8696,12 @@ } }, "node_modules/next": { - "version": "16.3.8", - "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz", - "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.0.tgz", + "integrity": "sha512-N2C+9q89Z/oLrsepSRS4jRMtwmUZAoZuAHU0e9T6+sAB5DdhADmrnOW+zjYijlrYFmVfmQhGVGy9eMFoB7UgKg==", "license": "MIT", "dependencies": { - "@next/env": "16.3.8", + "@next/env": "16.4.0", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -8599,14 +8715,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.3.8", - "@next/swc-darwin-x64": "16.3.8", - "@next/swc-linux-arm64-gnu": "16.3.8", - "@next/swc-linux-arm64-musl": "16.3.8", - "@next/swc-linux-x64-gnu": "16.3.8", - "@next/swc-linux-x64-musl": "16.3.8", - "@next/swc-win32-arm64-msvc": "16.3.8", - "@next/swc-win32-x64-msvc": "16.3.8", + "@next/swc-darwin-arm64": "16.4.0", + "@next/swc-darwin-x64": "16.4.0", + "@next/swc-linux-arm64-gnu": "16.4.0", + "@next/swc-linux-arm64-musl": "16.4.0", + "@next/swc-linux-x64-gnu": "16.4.0", + "@next/swc-linux-x64-musl": "16.4.0", + "@next/swc-win32-arm64-msvc": "16.4.0", + "@next/swc-win32-x64-msvc": "16.4.0", "sharp": "^0.35.4" }, "peerDependencies": { @@ -9605,9 +9721,9 @@ } }, "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", "dev": true, "license": "MIT", "engines": { @@ -10622,42 +10738,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typedoc": { - "version": "0.28.20", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", - "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@gerrit0/mini-shiki": "^3.23.0", - "lunr": "^2.3.9", - "markdown-it": "^14.3.0", - "minimatch": "^10.2.5", - "yaml": "^2.9.0" - }, - "bin": { - "typedoc": "bin/typedoc" - }, - "engines": { - "node": ">= 18", - "pnpm": ">= 10" - }, - "peerDependencies": { - "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" - } - }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "7.1.0-dev.20261007.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.1.0-dev.20261007.1.tgz", + "integrity": "sha512-mk5RKgMkHFknAWDeplGRnduMttCCgLrJW+vUXxMJ8itcZ8Y17OZUByQYxVf3ZEHqqtQ3nmen25wqqplBaiOQGg==", "devOptional": true, "license": "Apache-2.0", "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "tsc": "bin/tsc" }, "engines": { - "node": ">=14.17" + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-darwin-arm64": "7.1.0-dev.20261007.1", + "@typescript/typescript-darwin-x64": "7.1.0-dev.20261007.1", + "@typescript/typescript-linux-arm": "7.1.0-dev.20261007.1", + "@typescript/typescript-linux-arm64": "7.1.0-dev.20261007.1", + "@typescript/typescript-linux-x64": "7.1.0-dev.20261007.1", + "@typescript/typescript-win32-arm64": "7.1.0-dev.20261007.1", + "@typescript/typescript-win32-x64": "7.1.0-dev.20261007.1" } }, "node_modules/uc.micro": { @@ -11681,12 +11781,58 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "scripts/api-docs": { + "name": "@cleverbrush/api-docs-tooling", + "devDependencies": { + "typedoc-legacy": "npm:typedoc@0.28.20", + "typescript": "6.0.3" + } + }, + "scripts/api-docs/node_modules/typedoc-legacy": { + "name": "typedoc", + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.23.0", + "lunr": "^2.3.9", + "markdown-it": "^14.3.0", + "minimatch": "^10.2.5", + "yaml": "^2.9.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" + } + }, + "scripts/api-docs/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "websites/docs": { "name": "website-docs", "version": "0.1.0", "dependencies": { "@cleverbrush/website-shared": "*", - "next": "^16.3.6", + "next": "16.4.0", "performative-ui": "^0.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" @@ -11695,21 +11841,7 @@ "@types/node": "^22.15.19", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.6", - "typescript": "^5.8.3" - } - }, - "websites/docs/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" + "typescript": "7.1.0-dev.20261007.1" } }, "websites/schema": { @@ -11723,7 +11855,7 @@ "@t3-oss/env-core": "^0.13.11", "@t3-oss/env-nextjs": "^0.13.11", "@tanstack/react-form": "^1.28.6", - "next": "^16.3.6", + "next": "16.4.0", "performative-ui": "^0.3.0", "react": "^19.2.4", "react-dom": "^19.2.4" @@ -11732,7 +11864,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^6.0.2" + "typescript": "7.1.0-dev.20261007.1" } }, "websites/schema/node_modules/@types/node": { diff --git a/package.json b/package.json index ac7d08b2..8679f826 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "build:docs-site": "cd websites/docs && npm run build && cd ../..", "clean": "turbo run clean", "watch": "tsc --build --watch tsconfig.build.json", - "docs": "typedoc && ./scripts/inject-gtm.sh websites/docs/public/api-docs/latest", + "docs": "npm run docs:generate && ./scripts/inject-gtm.sh websites/docs/public/api-docs/latest", "docs:snapshot": "./scripts/snapshot-docs.sh", "lint": "biome check ./libs ./websites/schema ./websites/docs", "typecheck:schema-site": "tsc --noEmit -p websites/schema/tsconfig.json", @@ -41,14 +41,16 @@ "test:e2e:api": "npm run test:api -w @cleverbrush/demo-e2e", "test:e2e:ui": "npm run test:ui -w @cleverbrush/demo-e2e", "test:e2e:reset": "RESET=1 npm run test -w @cleverbrush/demo-e2e", - "test:storage:integration": "node scripts/test-storage.mjs" + "test:storage:integration": "node scripts/test-storage.mjs", + "docs:generate": "node scripts/generate-api-docs.mjs" }, "workspaces": [ "./libs/*", "./demos/*", "./websites/shared", "./websites/schema", - "./websites/docs" + "./websites/docs", + "./scripts/api-docs" ], "keywords": [], "author": "Andrew Zolotukhin ", @@ -65,8 +67,18 @@ "jsdom": "^29.0.1", "tsup": "^8.5.1", "turbo": "^2.9.14", - "typedoc": "^0.28.18", - "typescript": "^6.0.2", + "typescript": "7.1.0-dev.20261007.1", "vitest": "^4.1.11" + }, + "overrides": { + "tsup": { + "typescript": "$typescript" + }, + "@t3-oss/env-core": { + "typescript": "$typescript" + }, + "@t3-oss/env-nextjs": { + "typescript": "$typescript" + } } } diff --git a/scripts/api-docs/package.json b/scripts/api-docs/package.json new file mode 100644 index 00000000..af812f6e --- /dev/null +++ b/scripts/api-docs/package.json @@ -0,0 +1,9 @@ +{ + "name": "@cleverbrush/api-docs-tooling", + "private": true, + "type": "module", + "devDependencies": { + "typedoc-legacy": "npm:typedoc@0.28.20", + "typescript": "6.0.3" + } +} diff --git a/scripts/benchmark-build.mjs b/scripts/benchmark-build.mjs new file mode 100644 index 00000000..368f123b --- /dev/null +++ b/scripts/benchmark-build.mjs @@ -0,0 +1,143 @@ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { cpus, platform, arch, totalmem } from 'node:os'; +import { join, resolve } from 'node:path'; +import { parseArgs } from 'node:util'; +import { typescriptCli } from './typescript-cli.mjs'; + +const { values } = parseArgs({ options: { + baseline: { type: 'string' }, + candidate: { type: 'string' }, + output: { type: 'string' }, + runs: { type: 'string', default: '5' } +} }); +const runs = Number(values.runs); +if (!values.baseline || !values.candidate || !values.output || + !Number.isInteger(runs) || runs < 1) { + throw new Error('Usage: node scripts/benchmark-build.mjs --baseline --candidate --output [--runs 5]'); +} +const output = resolve(values.output); +const logs = `${output}.logs`; +mkdirSync(logs, { recursive: true }); +const readJson = path => JSON.parse(readFileSync(path, 'utf8')); +const execute = (root, command, args, log) => { + const start = performance.now(); + const result = spawnSync(command, args, { + cwd: root, + encoding: 'utf8', + env: { ...process.env, CI: 'true', TURBO_TELEMETRY_DISABLED: '1' }, + timeout: 600_000, + maxBuffer: 32 * 1024 * 1024 + }); + const seconds = (performance.now() - start) / 1000; + if (log) writeFileSync(log, `${result.stdout ?? ''}${result.stderr ?? ''}`); + if (result.error || result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed: ${result.error ?? result.status}; see ${log}`); + } + return { seconds, stdout: result.stdout }; +}; +const checkouts = Object.entries({ baseline: values.baseline, candidate: values.candidate }) + .map(([name, directory]) => { + const root = resolve(directory); + const pkg = readJson(join(root, 'package.json')); + if (pkg.name !== 'cleverbrush-framework') throw new Error(`Unexpected checkout: ${root}`); + const compiler = typescriptCli(join(root, 'package.json')); + const libraries = readdirSync(join(root, 'libs')).filter(name => { + const manifest = readJson(join(root, 'libs', name, 'package.json')); + return !manifest.private && manifest.scripts?.build?.includes('tsc'); + }).sort(); + return { name, root, compiler, libraries }; + }); +if (checkouts[0].root === checkouts[1].root) throw new Error('Use separate checkouts'); + +const result = { + recordedAt: new Date().toISOString(), + machine: { platform: platform(), architecture: arch(), cpu: cpus()[0]?.model, + logicalCpus: cpus().length, memoryBytes: totalmem(), node: process.version, + npm: execute(checkouts[0].root, 'npm', ['--version']).stdout.trim() }, + methodology: { + runs, + warmups: 1, + ordering: 'alternating baseline/candidate, reversed on odd iterations', + cleanBuild: 'remove package dist and tsbuildinfo; npm run build -- --force', + declarations: 'sequential tsc --project /tsconfig.build.json --emitDeclarationOnly --incremental false --composite false for every published library', + incremental: 'schema declaration-only incremental build immediately after an untimed priming build', + installationExcluded: true, + caches: 'Turbo cache reads bypassed with --force; filesystem cache warm' + }, + checkouts: checkouts.map(({ name, root, compiler, libraries }) => ({ + name, + root, + commit: execute(root, 'git', ['rev-parse', 'HEAD']).stdout.trim(), + diffSha256: createHash('sha256').update(execute(root, 'git', ['diff', 'HEAD']).stdout).digest('hex'), + lockSha256: createHash('sha256').update(readFileSync(join(root, 'package-lock.json'))).digest('hex'), + compiler: execute(root, process.execPath, [compiler, '--version']).stdout.trim(), + libraries + })), + samples: [], + summary: {} +}; + +function clean(root) { + // Restrict removal to generated package artifacts, and refuse tracked ones. + const directories = ['libs', 'demos'].flatMap(parent => + readdirSync(join(root, parent), { withFileTypes: true }) + .filter(entry => entry.isDirectory()).map(entry => join(parent, entry.name)) + ); + const targets = directories.flatMap(directory => [ + join(directory, 'dist'), + ...readdirSync(join(root, directory)).filter(file => file.endsWith('.tsbuildinfo')) + .map(file => join(directory, file)) + ]); + for (const target of targets) { + const tracked = execute(root, 'git', ['ls-files', '--', target]).stdout.trim(); + if (tracked) throw new Error(`Refusing to remove tracked output: ${target}`); + rmSync(join(root, target), { recursive: true, force: true }); + } +} + +for (let iteration = 0; iteration <= runs; iteration++) { + const order = iteration % 2 ? [...checkouts].reverse() : checkouts; + for (const checkout of order) { + const { name, root, compiler, libraries } = checkout; + const prefix = join(logs, `${iteration}-${name}`); + clean(root); + const build = execute(root, 'npm', ['run', 'build', '--', '--force'], `${prefix}-build.log`); + const declarationStart = performance.now(); + for (const library of libraries) { + execute(root, process.execPath, [compiler, '--project', `libs/${library}/tsconfig.build.json`, + '--emitDeclarationOnly', '--incremental', 'false', '--composite', 'false'], `${prefix}-${library}.log`); + } + const declarations = (performance.now() - declarationStart) / 1000; + const buildInfo = `${prefix}.tsbuildinfo`; + rmSync(buildInfo, { force: true }); + const incrementalArgs = [compiler, '--project', 'libs/schema/tsconfig.build.json', + '--emitDeclarationOnly', '--incremental', '--tsBuildInfoFile', buildInfo]; + execute(root, process.execPath, incrementalArgs, `${prefix}-incremental-prime.log`); + const incremental = execute(root, process.execPath, incrementalArgs, `${prefix}-incremental.log`); + const sample = { iteration, warmup: iteration === 0, checkout: name, + buildSeconds: build.seconds, declarationSeconds: declarations, + incrementalSeconds: incremental.seconds }; + result.samples.push(sample); + writeFileSync(output, JSON.stringify(result, null, 2) + '\n'); + process.stdout.write(JSON.stringify(sample) + '\n'); + } +} +const median = numbers => { + const sorted = [...numbers].sort((a, b) => a - b); + const middle = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2; +}; +for (const metric of ['buildSeconds', 'declarationSeconds', 'incrementalSeconds']) { + const baseline = result.samples.filter(s => !s.warmup && s.checkout === 'baseline').map(s => s[metric]); + const candidate = result.samples.filter(s => !s.warmup && s.checkout === 'candidate').map(s => s[metric]); + result.summary[metric] = { + baseline: { median: median(baseline), min: Math.min(...baseline), max: Math.max(...baseline) }, + candidate: { median: median(candidate), min: Math.min(...candidate), max: Math.max(...candidate) }, + improvementPercent: (1 - median(candidate) / median(baseline)) * 100, + fasterPairs: candidate.filter((value, i) => value < baseline[i]).length + }; +} +writeFileSync(output, JSON.stringify(result, null, 2) + '\n'); +process.stdout.write(JSON.stringify(result.summary, null, 2) + '\n'); diff --git a/scripts/generate-api-docs.mjs b/scripts/generate-api-docs.mjs new file mode 100644 index 00000000..aeb737f6 --- /dev/null +++ b/scripts/generate-api-docs.mjs @@ -0,0 +1,21 @@ +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +// TypeDoc still needs the TypeScript 6 compiler API. Keep its dependency +// resolution separate from the native compiler used by builds and tests. +const tooling = createRequire(new URL('./api-docs/package.json', import.meta.url)); +const manifest = tooling.resolve('typedoc-legacy/package.json'); +const { bin } = JSON.parse(readFileSync(manifest, 'utf8')); +const result = spawnSync( + process.execPath, + [resolve(dirname(manifest), bin.typedoc), ...process.argv.slice(2)], + { + cwd: fileURLToPath(new URL('../', import.meta.url)), + stdio: 'inherit' + } +); +if (result.error) throw result.error; +process.exitCode = result.status ?? 1; diff --git a/scripts/test-packages.mjs b/scripts/test-packages.mjs index f45b677c..e10408c8 100644 --- a/scripts/test-packages.mjs +++ b/scripts/test-packages.mjs @@ -10,6 +10,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { build } from 'esbuild'; +import { typescriptCli } from './typescript-cli.mjs'; import { assertPackageLicense } from './package-license.mjs'; const root = fileURLToPath(new URL('../', import.meta.url)); @@ -134,7 +135,7 @@ try { declarations + '\n' + readmeExample ); execute(process.execPath, [ - join(root, 'node_modules/typescript/bin/tsc'), + typescriptCli(), '--noEmit', '--strict', '--skipLibCheck', diff --git a/scripts/typescript-cli.d.mts b/scripts/typescript-cli.d.mts new file mode 100644 index 00000000..f7d5eb33 --- /dev/null +++ b/scripts/typescript-cli.d.mts @@ -0,0 +1 @@ +export function typescriptCli(from?: string | URL): string; diff --git a/scripts/typescript-cli.mjs b/scripts/typescript-cli.mjs new file mode 100644 index 00000000..77f43105 --- /dev/null +++ b/scripts/typescript-cli.mjs @@ -0,0 +1,10 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { dirname, resolve } from 'node:path'; + +/** Resolve the installed compiler through its public package metadata. */ +export function typescriptCli(from = import.meta.url) { + const manifest = createRequire(from).resolve('typescript/package.json'); + const { bin } = JSON.parse(readFileSync(manifest, 'utf8')); + return resolve(dirname(manifest), bin.tsc); +} diff --git a/scripts/typescript-cli.test.mjs b/scripts/typescript-cli.test.mjs new file mode 100644 index 00000000..ff26ec8e --- /dev/null +++ b/scripts/typescript-cli.test.mjs @@ -0,0 +1,29 @@ +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { expect, test } from 'vitest'; +import { typescriptCli } from './typescript-cli.mjs'; + +test('builds and consumer checks resolve the pinned native compiler', () => { + const result = spawnSync(process.execPath, [typescriptCli(), '--version'], { + encoding: 'utf8' + }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim()).toBe('Version 7.1.0-dev.20261007.1'); +}); + +test('the legacy compiler is confined to the TypeDoc dependency tree', () => { + const tooling = createRequire(new URL('./api-docs/package.json', import.meta.url)); + const typedoc = tooling.resolve('typedoc-legacy/package.json'); + const compiler = createRequire(typedoc).resolve('typescript/package.json'); + expect(JSON.parse(readFileSync(compiler, 'utf8')).version).toBe('6.0.3'); + expect(resolve(dirname(compiler), 'bin/tsc')).not.toBe(typescriptCli()); + const lock = JSON.parse(readFileSync(new URL('../package-lock.json', import.meta.url), 'utf8')); + const legacy = Object.entries(lock.packages).filter(([name, pkg]) => + name.endsWith('node_modules/typescript') && pkg.version !== '7.1.0-dev.20261007.1' + ); + expect(legacy.map(([name]) => name)).toEqual([ + 'scripts/api-docs/node_modules/typescript' + ]); +}); diff --git a/websites/docs/next.config.ts b/websites/docs/next.config.ts index 3699712a..8c315fe4 100644 --- a/websites/docs/next.config.ts +++ b/websites/docs/next.config.ts @@ -49,6 +49,7 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { + experimental: { useTypeScriptCli: true }, output: 'standalone', transpilePackages: [ '@t3-oss/env-nextjs', diff --git a/websites/docs/package.json b/websites/docs/package.json index add66155..9431b505 100644 --- a/websites/docs/package.json +++ b/websites/docs/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@cleverbrush/website-shared": "*", - "next": "^16.3.6", + "next": "16.4.0", "performative-ui": "^0.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" @@ -18,6 +18,6 @@ "@types/node": "^22.15.19", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.6", - "typescript": "^5.8.3" + "typescript": "7.1.0-dev.20261007.1" } } diff --git a/websites/schema/app/playground/schemaDeclarations.ts b/websites/schema/app/playground/schemaDeclarations.ts index aaff6960..86b459e7 100644 --- a/websites/schema/app/playground/schemaDeclarations.ts +++ b/websites/schema/app/playground/schemaDeclarations.ts @@ -11,8 +11,8 @@ type AnySchemaBuilderCreateProps = Partial & TExtensions; /** - * Performs synchronous validation of the schema over \`object\`. {@inheritDoc SchemaBuilder.validate} + * {@inheritDoc SchemaBuilder.validate} */ validate(object: TResult, context?: ValidationContext): ArraySchemaValidationResult; /** - * Performs asynchronous validation of the schema over \`object\`. {@inheritDoc SchemaBuilder.validateAsync} + * {@inheritDoc SchemaBuilder.validateAsync} */ validateAsync(object: TResult, context?: ValidationContext): Promise>; /** @@ -365,23 +365,26 @@ type BooleanSchemaBuilderCreateProps = Partial): BooleanSchemaBuilder; protected constructor(props: BooleanSchemaBuilderCreateProps); introspect(): { - /** - * If set, restrict object to be equal to a certain value. - */ - equalsTo: boolean | undefined; - /** - * Equals to validation error message provider. - * If not provided, default error message will be used. - */ - equalsToValidationErrorMessageProvider: ValidationErrorMessageProvider>; type: string; isRequired: boolean; isNullable: boolean; @@ -435,6 +429,15 @@ export declare class BooleanSchemaBuilder TFinalResult) | undefined; example: unknown; + /** + * If set, restrict object to be equal to a certain value. + */ + equalsTo: boolean | undefined; + /** + * Equals to validation error message provider. + * If not provided, default error message will be used. + */ + equalsToValidationErrorMessageProvider: ValidationErrorMessageProvider>; }; /** * @inheritdoc @@ -508,7 +511,8 @@ export declare class BooleanSchemaBuilder = Partial< * It can be restricted to be: equal to a certain value, in future, in past, in a certain range. * Supports parsing from JSON string and UNIX epoch (using preprocessors). * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use {@link date | date()} function instead. * - * @example \`\`\`ts - * const date = new Date(2020, 0, 2); + * @example + * \`\`\`ts + * const value = new Date(2020, 0, 2); * const schema = date().min(new Date(2020, 0, 1)); - * const result = schema.validate(date); + * const result = schema.validate(value); * // result.valid === true - * // result.object === date + * // result.object === value * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = date(); * const result = schema.validate('2020-01-01'); * // result.valid === false * // result.errors[0].message === 'is expected to be a date' * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = date().parseFromJson(); * const result = schema.validate('2020-01-01T00:00:00.000Z'); * // result.valid === true * // result.object is equal to corresponding Date object * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = date().parseFromEpoch(); * const result = schema.validate(1577836800000); * // result.valid === true @@ -589,6 +597,22 @@ export declare class DateSchemaBuilder; protected constructor(props: DateSchemaBuilderCreateProps); introspect(): { + type: string; + isRequired: boolean; + isNullable: boolean; + isReadonly: boolean; + requiredValidationErrorMessageProvider: ValidationErrorMessageProvider>; + extensions: { + [x: string]: unknown; + }; + hasDefault: boolean; + defaultValue: TResult | (() => TResult) | undefined; + description: string | undefined; + schemaName: string | undefined; + referenceTarget: SchemaBuilder | undefined; + hasCatch: boolean; + catchValue: TResult | (() => TResult) | undefined; + example: unknown; /** * Min valid value (if defined). */ @@ -652,22 +676,6 @@ export declare class DateSchemaBuilder[]; - type: string; - isRequired: boolean; - isNullable: boolean; - isReadonly: boolean; - requiredValidationErrorMessageProvider: ValidationErrorMessageProvider>; - extensions: { - [x: string]: unknown; - }; - hasDefault: boolean; - defaultValue: TResult | (() => TResult) | undefined; - description: string | undefined; - schemaName: string | undefined; - referenceTarget: SchemaBuilder | undefined; - hasCatch: boolean; - catchValue: TResult | (() => TResult) | undefined; - example: unknown; }; /** * @inheritdoc @@ -797,7 +805,8 @@ export declare class DateSchemaBuilder = Partial t.order.id)\` works without any additional * configuration. * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use the {@link extern | extern()} factory function instead. * * @example @@ -1052,8 +1061,8 @@ type InferParameters[]> = * It can be required or optional, and may carry typed parameter and return-type * schemas so that the inferred TypeScript function signature is fully typed. * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use {@link func | func()} function instead. * * @example Basic validation @@ -1143,10 +1152,6 @@ export declare class FunctionSchemaBuilder[]; - /** Return type schema set via {@link hasReturnType}, or \`undefined\` if not set. */ - returnType: SchemaBuilder | undefined; type: string; isRequired: boolean; isNullable: boolean; @@ -1165,6 +1170,10 @@ export declare class FunctionSchemaBuilder TResult) | undefined; example: unknown; + /** List of parameter schemas added via {@link addParameter}. */ + parameters: SchemaBuilder[]; + /** Return type schema set via {@link hasReturnType}, or \`undefined\` if not set. */ + returnType: SchemaBuilder | undefined; }; /** {@inheritDoc SchemaBuilder.validate} */ validate(object: TResult, context?: ValidationContext): ValidationResult; @@ -1312,8 +1321,8 @@ type GenericSchemaBuilderCreateProps = Partial * builder whose TypeScript type is inferred from the template function's * generic signature. * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use {@link generic | generic()} function instead. * * @example Single type parameter @@ -1355,7 +1364,7 @@ type GenericSchemaBuilderCreateProps = Partial * // InferType → { ok: boolean; value?: string; error?: number } * \`\`\` * - * @example With default arguments (enables direct \`.validate()\` on the template) + * @example With default arguments * \`\`\`ts * const AnyList = generic( * [any()], // default args — one per template parameter @@ -1449,10 +1458,6 @@ export declare class GenericSchemaBuilder Schema * \`\`\` */ introspect(): { - /** Template function passed to {@link generic}. */ - templateFn: ((...args: any[]) => SchemaBuilder) | undefined; - /** Default positional arguments for the template function, or \`undefined\`. */ - defaults: readonly any[] | undefined; type: string; isRequired: boolean; isNullable: boolean; @@ -1471,6 +1476,10 @@ export declare class GenericSchemaBuilder Schema hasCatch: boolean; catchValue: TResult | (() => TResult) | undefined; example: unknown; + /** Template function passed to {@link generic}. */ + templateFn: ((...args: any[]) => SchemaBuilder) | undefined; + /** Default positional arguments for the template function, or \`undefined\`. */ + defaults: readonly any[] | undefined; }; /** {@inheritDoc SchemaBuilder.validate} */ validate(object: TResult, context?: ValidationContext): ValidationResult; @@ -1621,8 +1630,6 @@ export declare class IntersectionSchemaBuilder): IntersectionSchemaBuilder; protected constructor(props: IntersectionSchemaBuilderCreateProps); introspect(): { - left: TLeft; - right: TRight; type: string; isRequired: boolean; isNullable: boolean; @@ -1634,13 +1641,15 @@ export declare class IntersectionSchemaBuilder : TExplicitType) | (() => TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | undefined; + defaultValue: (() => TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | (TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | undefined; description: string | undefined; schemaName: string | undefined; referenceTarget: SchemaBuilder | undefined; hasCatch: boolean; - catchValue: (TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | (() => TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | undefined; + catchValue: (() => TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | (TExplicitType extends undefined ? SchemaIntersection : TExplicitType) | undefined; example: unknown; + left: TLeft; + right: TRight; }; /** * @override @@ -1787,11 +1796,6 @@ export declare class LazySchemaBuilder SchemaBuilder; type: string; isRequired: boolean; isNullable: boolean; @@ -1810,6 +1814,11 @@ export declare class LazySchemaBuilder TResult) | undefined; example: unknown; + /** + * The getter function that returns the lazily-resolved schema. + * Call {@link LazySchemaBuilder.resolve} to obtain the schema instance. + */ + getter: () => SchemaBuilder; }; /** {@inheritDoc SchemaBuilder.validate} */ validate(object: TResult, context?: ValidationContext): ValidationResult; @@ -1919,8 +1928,8 @@ type NullSchemaBuilderCreateProps = Partial>; /** * Performs synchronous validation of the schema over \`object\`. - * @param context Optional \`ValidationContext\` settings. */ protected _validate(object: null, _context?: ValidationContext): ValidationResult; /** * Performs async validation of the schema over \`object\`. - * @param context Optional \`ValidationContext\` settings. */ protected _validateAsync(object: null, _context?: ValidationContext): Promise>; protected createFromProps(props: NullSchemaBuilderCreateProps): this; @@ -2076,30 +2083,34 @@ type NumberSchemaBuilderCreateProps = Part * Can be required or optional, can be restricted to be equal to a certain value, * can be restricted to be in a certain range, can be restricted to be integer. * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use {@link number | number()} function instead. * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = number().equals(42); * const result = schema.validate(42); * // result.valid === true * // result.object === 42 * \`\`\` - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = number(); * const result = schema.validate('42'); * // result.valid === false * // result.errors[0].message === 'is expected to be a number' * \`\`\` - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = number().min(0).max(100); * const result = schema.validate(42); * // result.valid === true * // result.object === 42 * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = number().min(0).max(100); * const result = schema.validate(142.5); * // result.valid === false @@ -2124,6 +2135,22 @@ export declare class NumberSchemaBuilder; protected constructor(props: NumberSchemaBuilderCreateProps); introspect(): { + type: string; + isRequired: boolean; + isNullable: boolean; + isReadonly: boolean; + requiredValidationErrorMessageProvider: ValidationErrorMessageProvider>; + extensions: { + [x: string]: unknown; + }; + hasDefault: boolean; + defaultValue: TResult | (() => TResult) | undefined; + description: string | undefined; + schemaName: string | undefined; + referenceTarget: SchemaBuilder | undefined; + hasCatch: boolean; + catchValue: TResult | (() => TResult) | undefined; + example: unknown; /** * Min valid value (if defined). */ @@ -2185,22 +2212,6 @@ export declare class NumberSchemaBuilder[]; - type: string; - isRequired: boolean; - isNullable: boolean; - isReadonly: boolean; - requiredValidationErrorMessageProvider: ValidationErrorMessageProvider>; - extensions: { - [x: string]: unknown; - }; - hasDefault: boolean; - defaultValue: TResult | (() => TResult) | undefined; - description: string | undefined; - schemaName: string | undefined; - referenceTarget: SchemaBuilder | undefined; - hasCatch: boolean; - catchValue: TResult | (() => TResult) | undefined; - example: unknown; }; /** * @inheritdoc @@ -2316,7 +2327,8 @@ export declare class NumberSchemaBuilder> : TExplicitType) | (() => undefined extends TExplicitType ? WithConstructors> : TExplicitType) | undefined; + defaultValue: (() => undefined extends TExplicitType ? WithConstructors> : TExplicitType) | (undefined extends TExplicitType ? WithConstructors> : TExplicitType) | undefined; description: string | undefined; schemaName: string | undefined; referenceTarget: SchemaBuilder | undefined; hasCatch: boolean; - catchValue: (undefined extends TExplicitType ? WithConstructors> : TExplicitType) | (() => undefined extends TExplicitType ? WithConstructors> : TExplicitType) | undefined; + catchValue: (() => undefined extends TExplicitType ? WithConstructors> : TExplicitType) | (undefined extends TExplicitType ? WithConstructors> : TExplicitType) | undefined; example: unknown; }; /** @@ -3188,10 +3200,6 @@ export declare class ParseStringSchemaBuilder; - /** The template definition (literals + segments). */ - templateDefinition: ParseStringTemplateDefinition; type: string; isRequired: boolean; isNullable: boolean; @@ -3210,6 +3218,10 @@ export declare class ParseStringSchemaBuilder TResult) | undefined; example: unknown; + /** The object schema defining the result shape. */ + objectSchema: ObjectSchemaBuilder; + /** The template definition (literals + segments). */ + templateDefinition: ParseStringTemplateDefinition; }; /** * Builds a string from the template by substituting parameter values. @@ -3340,8 +3352,8 @@ type PromiseSchemaBuilderCreateProps = Partial\` instead of \`Promise\`. * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use {@link promise | promise()} function instead. * * @example Basic validation @@ -3418,8 +3430,6 @@ export declare class PromiseSchemaBuilder | undefined; type: string; isRequired: boolean; isNullable: boolean; @@ -3438,6 +3448,8 @@ export declare class PromiseSchemaBuilder TResult) | undefined; example: unknown; + /** Resolved-value schema set via {@link hasResolvedType}, or \`undefined\` if not set. */ + resolvedType: SchemaBuilder | undefined; }; /** {@inheritDoc SchemaBuilder.validate} */ validate(object: TResult, context?: ValidationContext): ValidationResult; @@ -3868,14 +3880,6 @@ export declare class RecordSchemaBuilder TResult) | undefined; example: unknown; + /** + * The schema every key must satisfy (a \`StringSchemaBuilder\`). + */ + keySchema: TKeySchema; + /** + * The schema every value must satisfy. + */ + valueSchema: TValueSchema; }; /** * Core sync validation. {@inheritDoc SchemaBuilder.validate} @@ -4035,7 +4047,7 @@ export type InferType = T extends { * * When returned from an object-level validator (via {@link SchemaBuilder.addValidator | addValidator}), * the optional \`property\` selector can route the error to a specific property - * so that {@link ObjectSchemaValidationResult.getErrorsFor | getErrorsFor()} reports it + * so that \`ObjectSchemaValidationResult.getErrorsFor()\` reports it * on that property rather than only on the root object. * * \`\`\`ts @@ -4437,8 +4449,8 @@ type ResolvedSchemaType { #private; @@ -5082,46 +5094,52 @@ type StringSchemaBuilderCreateProps = Part * length, restricted to start with a certain value, restricted to end with * a certain value, restricted to match a certain regular expression. * - * **NOTE** this class is exported only to give opportunity to extend it - * by inheriting. It is not recommended to create an instance of this class + * **NOTE** this class is exported for type annotations and advanced use. + * Customize via extensions rather than subclassing. Avoid instantiating it * directly. Use {@link string | string()} function instead. * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = string().equals('hello'); * const result = schema.validate('hello'); * // result.valid === true * // result.object === 'hello' * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = string().equals('hello'); * const result = schema.validate('world'); * // result.valid === false * // result.errors[0].message === "is expected to be equal to 'hello'" * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = string().minLength(5); * const result = schema.validate('hello'); * // result.valid === true * // result.object === 'hello' * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = string().minLength(5); * const result = schema.validate('hi'); * // result.valid === false * // result.errors[0].message === 'is expected to have a length of at least 5' * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = string().minLength(2).maxLength(5); * const result = schema.validate('yes'); * // result.valid === true * // result.object === 'yes' * \`\`\` * - * @example \`\`\`ts + * @example + * \`\`\`ts * const schema = string('no'); * const result = schema.validate('yes'); * // result.valid === false @@ -5146,6 +5164,22 @@ export declare class StringSchemaBuilder; protected constructor(props: StringSchemaBuilderCreateProps); introspect(): { + type: string; + isRequired: boolean; + isNullable: boolean; + isReadonly: boolean; + requiredValidationErrorMessageProvider: ValidationErrorMessageProvider>; + extensions: { + [x: string]: unknown; + }; + hasDefault: boolean; + defaultValue: TResult | (() => TResult) | undefined; + description: string | undefined; + schemaName: string | undefined; + referenceTarget: SchemaBuilder | undefined; + hasCatch: boolean; + catchValue: TResult | (() => TResult) | undefined; + example: unknown; /** * Min length of the string (if defined). */ @@ -5208,22 +5242,6 @@ export declare class StringSchemaBuilder[]; - type: string; - isRequired: boolean; - isNullable: boolean; - isReadonly: boolean; - requiredValidationErrorMessageProvider: ValidationErrorMessageProvider>; - extensions: { - [x: string]: unknown; - }; - hasDefault: boolean; - defaultValue: TResult | (() => TResult) | undefined; - description: string | undefined; - schemaName: string | undefined; - referenceTarget: SchemaBuilder | undefined; - hasCatch: boolean; - catchValue: TResult | (() => TResult) | undefined; - example: unknown; }; /** * @inheritdoc @@ -5414,8 +5432,8 @@ type TupleSchemaBuilderCreateProps & TExtensions; /** - * Performs synchronous validation of the schema over \`object\`. {@inheritDoc SchemaBuilder.validate} + * {@inheritDoc SchemaBuilder.validate} */ validate(object: TResult, context?: ValidationContext): TupleSchemaValidationResult; /** - * Performs asynchronous validation of the schema over \`object\`. {@inheritDoc SchemaBuilder.validateAsync} + * {@inheritDoc SchemaBuilder.validateAsync} */ validateAsync(object: TResult, context?: ValidationContext): Promise>; /** @@ -5536,16 +5554,6 @@ export declare class TupleSchemaBuilder & TExtensions; introspect(): { - /** - * Per-position element schemas defining the fixed tuple structure. - */ - elements: TElements; - /** - * Optional schema for elements beyond the fixed positions. - * When set, additional elements are validated against this schema. - * Mirrors TypeScript's rest element syntax: \`[string, number, ...boolean[]]\`. - */ - restSchema: (TRestSchema & SchemaBuilder) | undefined; type: string; isRequired: boolean; isNullable: boolean; @@ -5564,6 +5572,16 @@ export declare class TupleSchemaBuilder TResult) | undefined; example: unknown; + /** + * Per-position element schemas defining the fixed tuple structure. + */ + elements: TElements; + /** + * Optional schema for elements beyond the fixed positions. + * When set, additional elements are validated against this schema. + * Mirrors TypeScript's rest element syntax: \`[string, number, ...boolean[]]\`. + */ + restSchema: (TRestSchema & SchemaBuilder) | undefined; }; /** * Sets a schema that all elements beyond the fixed positions must satisfy. @@ -5691,8 +5709,8 @@ type TakeExceptIndex : TExplicitType) | (() => TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | undefined; + defaultValue: (() => TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | (TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | undefined; description: string | undefined; schemaName: string | undefined; referenceTarget: SchemaBuilder | undefined; hasCatch: boolean; - catchValue: (TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | (() => TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | undefined; + catchValue: (() => TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | (TExplicitType extends undefined ? SchemaArrayToUnion : TExplicitType) | undefined; example: unknown; }; /** @@ -6110,7 +6128,7 @@ type MergeExtensionMethods[], TT * * This powers scope-name autocomplete in \`SchemaQueryBuilder.scoped()\`. */ -declare const METHOD_LITERAL_BRAND: "__cleverbrush_method_literal_brand__"; +declare const METHOD_LITERAL_BRAND: '__cleverbrush_method_literal_brand__'; export { METHOD_LITERAL_BRAND }; export type MethodLiteralBrandSymbol = typeof METHOD_LITERAL_BRAND; /** @@ -6133,7 +6151,7 @@ export type MethodLiteralBrandSymbol = typeof METHOD_LITERAL_BRAND; * * @see {@link FixedMethods} for how the accumulation works. */ -declare const EXTRA_TYPE_BRAND: "__cleverbrush_extra_type_brand__"; +declare const EXTRA_TYPE_BRAND: '__cleverbrush_extra_type_brand__'; export { EXTRA_TYPE_BRAND }; export type ExtraTypeBrandSymbol = typeof EXTRA_TYPE_BRAND; /** @internal Drops the first element of a tuple type. */ @@ -7141,8 +7159,7 @@ export interface StringBuiltinExtensions { * By default only \`http\` and \`https\` protocols are accepted. * Pass \`opts.protocols\` to restrict or expand the allowed set. * - * @param opts - optional configuration - * @param opts.protocols - allowed URL protocols (default: \`['http', 'https']\`) + * The options overload accepts a \`protocols\` array (default: http/https). * @param errorMessage - custom error message or function to generate one * @returns a new schema builder with the URL validator applied * @@ -7319,8 +7336,7 @@ export declare const stringExtensions: import("../extension.js").ExtensionDescri * By default only \`http\` and \`https\` protocols are accepted. * Pass \`opts.protocols\` to restrict or expand the allowed set. * - * @param opts - optional configuration - * @param opts.protocols - allowed URL protocols (default: \`['http', 'https']\`) + * @param optsOrError - protocol options or a custom error message * @param errorMessage - custom error message or function to generate one * @returns a new schema builder with the URL validator applied * @@ -7366,7 +7382,7 @@ export declare const stringExtensions: import("../extension.js").ExtensionDescri * \`\`\` */ ip(this: StringSchemaBuilder, opts?: { - version?: "v4" | "v6"; + version?: 'v4' | 'v6'; }, errorMessage?: ValidationErrorMessageProvider): StringSchemaBuilder; /** * Preprocessor that trims leading and trailing whitespace before validation. diff --git a/websites/schema/next.config.ts b/websites/schema/next.config.ts index 5eb2eae8..44feb892 100644 --- a/websites/schema/next.config.ts +++ b/websites/schema/next.config.ts @@ -50,6 +50,7 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { + experimental: { useTypeScriptCli: true }, output: 'standalone', transpilePackages: [ '@t3-oss/env-nextjs', diff --git a/websites/schema/package.json b/websites/schema/package.json index 717ec8fc..38cca4db 100644 --- a/websites/schema/package.json +++ b/websites/schema/package.json @@ -18,7 +18,7 @@ "@t3-oss/env-core": "^0.13.11", "@t3-oss/env-nextjs": "^0.13.11", "@tanstack/react-form": "^1.28.6", - "next": "^16.3.6", + "next": "16.4.0", "performative-ui": "^0.3.0", "react": "^19.2.4", "react-dom": "^19.2.4" @@ -27,6 +27,6 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^6.0.2" + "typescript": "7.1.0-dev.20261007.1" } }