carrick 0.3.58 → 0.3.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -26
- package/bin/carrick.mjs +13 -2
- package/dist/auth/credentials.d.ts +14 -0
- package/dist/auth/credentials.js +86 -0
- package/dist/auth/credentials.js.map +1 -0
- package/dist/auth/oauth.d.ts +11 -0
- package/dist/auth/oauth.js +119 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/read.d.ts +139 -0
- package/dist/auth/read.js +44 -0
- package/dist/auth/read.js.map +1 -0
- package/dist/auth/run.d.ts +12 -0
- package/dist/auth/run.js +68 -0
- package/dist/auth/run.js.map +1 -0
- package/dist/contract.d.ts +10 -0
- package/dist/contract.js +3 -0
- package/dist/contract.js.map +1 -1
- package/dist/definition.js +2 -2
- package/dist/definition.js.map +1 -1
- package/dist/diagnostics.d.ts +35 -1
- package/dist/diagnostics.js +52 -11
- package/dist/diagnostics.js.map +1 -1
- package/dist/hook/refresh.d.ts +32 -0
- package/dist/hook/refresh.js +138 -0
- package/dist/hook/refresh.js.map +1 -0
- package/dist/hook/session-start.js +13 -0
- package/dist/hook/session-start.js.map +1 -1
- package/dist/init/connect.d.ts +17 -0
- package/dist/init/connect.js +120 -0
- package/dist/init/connect.js.map +1 -0
- package/dist/init/mcp.d.ts +50 -0
- package/dist/init/mcp.js +235 -0
- package/dist/init/mcp.js.map +1 -0
- package/dist/init/projects.d.ts +46 -0
- package/dist/init/projects.js +137 -0
- package/dist/init/projects.js.map +1 -0
- package/dist/init/repos.d.ts +113 -19
- package/dist/init/repos.js +60 -39
- package/dist/init/repos.js.map +1 -1
- package/dist/init/run.d.ts +16 -0
- package/dist/init/run.js +243 -61
- package/dist/init/run.js.map +1 -1
- package/dist/init/settings.d.ts +1 -1
- package/dist/init/settings.js +1 -1
- package/dist/init/settings.js.map +1 -1
- package/dist/server.js +173 -16
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/capture/deno-project.d.ts +41 -0
- package/sidecar/dist/src/capture/deno-project.js +513 -0
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +60 -19
- package/sidecar/dist/src/capture/self-check.d.ts +2 -0
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/project-loader.js +22 -1
- package/sidecar/dist/src/type-inferrer.d.ts +52 -0
- package/sidecar/dist/src/type-inferrer.js +130 -9
- package/dist/init/identity.d.ts +0 -20
- package/dist/init/identity.js +0 -60
- package/dist/init/identity.js.map +0 -1
|
@@ -37,6 +37,8 @@ import { installedVersions, lockfileVersions } from './lockfile.js';
|
|
|
37
37
|
import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
|
|
38
38
|
import { selfCheckStub } from './self-check.js';
|
|
39
39
|
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
40
|
+
import { DenoProject, findDenoConfig } from './deno-project.js';
|
|
41
|
+
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
40
42
|
// v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
|
|
41
43
|
// reaches it only through this door (index.js).
|
|
42
44
|
export { runCheck } from './check.js';
|
|
@@ -90,10 +92,23 @@ export function captureStub(opts) {
|
|
|
90
92
|
const stubDir = path.resolve(opts.outDir);
|
|
91
93
|
const errors = [];
|
|
92
94
|
const configPath = opts.tsconfigPath
|
|
93
|
-
? path.resolve(opts.tsconfigPath)
|
|
95
|
+
? path.resolve(repoRoot, opts.tsconfigPath)
|
|
94
96
|
: path.join(repoRoot, 'tsconfig.json');
|
|
95
97
|
let parsed;
|
|
96
|
-
|
|
98
|
+
let deno;
|
|
99
|
+
try {
|
|
100
|
+
const config = findDenoConfig(repoRoot, opts.tsconfigPath);
|
|
101
|
+
if (config)
|
|
102
|
+
deno = new DenoProject(config, repoRoot);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
106
|
+
}
|
|
107
|
+
if (deno) {
|
|
108
|
+
parsed = deno.parsed;
|
|
109
|
+
errors.push(...deno.diagnostics);
|
|
110
|
+
}
|
|
111
|
+
else if (!fs.existsSync(configPath)) {
|
|
97
112
|
if (opts.tsconfigPath) {
|
|
98
113
|
// An explicitly named tsconfig that does not exist is a caller bug.
|
|
99
114
|
return fail(stubDir, packageName, [`tsconfig not found at ${configPath}`]);
|
|
@@ -137,17 +152,20 @@ export function captureStub(opts) {
|
|
|
137
152
|
const entryDir = parsed.options.rootDir
|
|
138
153
|
? path.resolve(path.dirname(configPath), parsed.options.rootDir)
|
|
139
154
|
: repoRoot;
|
|
140
|
-
const entryPath =
|
|
155
|
+
const entryPath = deno
|
|
156
|
+
? path.join(deno.cacheDir, `${SURFACE_ENTRY_BASENAME}.ts`)
|
|
157
|
+
: path.join(entryDir, `${SURFACE_ENTRY_BASENAME}.ts`);
|
|
158
|
+
fs.mkdirSync(path.dirname(entryPath), { recursive: true });
|
|
141
159
|
// ---- Phase A: analysis program over placeholder entry + anchor sources ----
|
|
142
160
|
let resolved;
|
|
143
161
|
try {
|
|
144
|
-
resolved = resolveAnchors(opts, parsed, { repoRoot, entryDir, entryPath });
|
|
162
|
+
resolved = resolveAnchors(opts, parsed, { repoRoot, entryDir: path.dirname(entryPath), entryPath }, deno);
|
|
145
163
|
}
|
|
146
164
|
catch (err) {
|
|
147
165
|
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
148
166
|
}
|
|
149
167
|
// ---- Augmentation detection over the tsconfig's full file list ----
|
|
150
|
-
const augmentationSources = findAugmentationFiles(parsed.fileNames.filter((f) => !f.includes(`${path.sep}node_modules${path.sep}`)));
|
|
168
|
+
const augmentationSources = [...new Set([...findAugmentationFiles(parsed.fileNames.filter((f) => !f.includes(`${path.sep}node_modules${path.sep}`))), ...(deno?.globals ?? [])])];
|
|
151
169
|
// ---- Phase B: declaration emit of the final entry ----
|
|
152
170
|
const entryLines = ['// Generated by Carrick capture v2. Deleted after emit.'];
|
|
153
171
|
for (const anchor of resolved) {
|
|
@@ -162,6 +180,7 @@ export function captureStub(opts) {
|
|
|
162
180
|
// hand-written declarations in the import closure) are never re-emitted by
|
|
163
181
|
// tsc; they must ship verbatim or the tree's references to them dangle.
|
|
164
182
|
const declarationSources = new Map();
|
|
183
|
+
const sourceByEmitted = new Map();
|
|
165
184
|
let emitPartial = false;
|
|
166
185
|
try {
|
|
167
186
|
fs.writeFileSync(entryPath, entryLines.join('\n') + '\n');
|
|
@@ -179,8 +198,12 @@ export function captureStub(opts) {
|
|
|
179
198
|
outDir: staging,
|
|
180
199
|
rootDir: entryDir,
|
|
181
200
|
};
|
|
182
|
-
const program = ts.createProgram([entryPath, ...augmentationSources], emitOptions);
|
|
183
|
-
const emitResult = program.emit(undefined, (fileName, text
|
|
201
|
+
const program = ts.createProgram([entryPath, ...augmentationSources], emitOptions, deno?.host(emitOptions));
|
|
202
|
+
const emitResult = program.emit(undefined, (fileName, text, _bom, _error, sources) => {
|
|
203
|
+
emitted.set(fileName, text);
|
|
204
|
+
if (sources?.[0])
|
|
205
|
+
sourceByEmitted.set(path.relative(staging, fileName).split(path.sep).join('/'), sources[0].fileName);
|
|
206
|
+
}, undefined,
|
|
184
207
|
/* emitOnlyDtsFiles */ true);
|
|
185
208
|
// emitSkipped is PER-PROGRAM even when only one file's declaration emit
|
|
186
209
|
// failed (e.g. TS4023 from a hand-rolled ambient stub shadowing a real
|
|
@@ -202,6 +225,7 @@ export function captureStub(opts) {
|
|
|
202
225
|
if (rel.startsWith('..') || rel.includes('node_modules/'))
|
|
203
226
|
continue;
|
|
204
227
|
declarationSources.set(rel, sourceFile.getFullText());
|
|
228
|
+
sourceByEmitted.set(rel, abs);
|
|
205
229
|
}
|
|
206
230
|
}
|
|
207
231
|
catch (err) {
|
|
@@ -235,8 +259,13 @@ export function captureStub(opts) {
|
|
|
235
259
|
let surfaceAbsPath = '';
|
|
236
260
|
for (const [fileName, text] of emitted) {
|
|
237
261
|
let rel = path.relative(staging, fileName).split(path.sep).join('/');
|
|
238
|
-
if (rel === `${SURFACE_ENTRY_BASENAME}.d.ts`)
|
|
262
|
+
if (path.basename(rel) === `${SURFACE_ENTRY_BASENAME}.d.ts`) {
|
|
263
|
+
const source = sourceByEmitted.get(rel);
|
|
264
|
+
sourceByEmitted.delete(rel);
|
|
239
265
|
rel = 'surface.d.ts';
|
|
266
|
+
if (source)
|
|
267
|
+
sourceByEmitted.set(rel, source);
|
|
268
|
+
}
|
|
240
269
|
const dest = path.join(typesDir, rel);
|
|
241
270
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
242
271
|
fs.writeFileSync(dest, text);
|
|
@@ -268,7 +297,14 @@ export function captureStub(opts) {
|
|
|
268
297
|
.filter((rel) => emittedFiles.includes(rel))
|
|
269
298
|
.map((rel) => `types/${rel}`);
|
|
270
299
|
// ---- Post-emit specifier rewrite (paths mappings + absolute internals) ----
|
|
271
|
-
|
|
300
|
+
let denoRewrites = 0;
|
|
301
|
+
try {
|
|
302
|
+
denoRewrites = deno?.rewrite(typesDir, emittedFiles, sourceByEmitted) ?? 0;
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
306
|
+
}
|
|
307
|
+
const specifierRewrites = denoRewrites + rewriteEmittedSpecifiers({
|
|
272
308
|
typesDir,
|
|
273
309
|
files: emittedFiles,
|
|
274
310
|
options: parsed.options,
|
|
@@ -297,16 +333,19 @@ export function captureStub(opts) {
|
|
|
297
333
|
// only transitives resolve").
|
|
298
334
|
const installed = installedVersions(repoRoot, externalSpecs);
|
|
299
335
|
const lockVersions = lockfileVersions(repoRoot);
|
|
336
|
+
for (const name of Object.keys(deno?.pinned ?? {}))
|
|
337
|
+
externalSpecs.add(name);
|
|
300
338
|
const pinned = {};
|
|
301
339
|
const unpinned = [];
|
|
302
340
|
for (const name of [...externalSpecs].sort()) {
|
|
303
|
-
const version = installed.get(name) ?? lockVersions.get(name);
|
|
341
|
+
const version = deno?.pinned[name] ?? installed.get(name) ?? lockVersions.get(name);
|
|
304
342
|
if (version)
|
|
305
343
|
pinned[name] = version;
|
|
306
344
|
else
|
|
307
345
|
unpinned.push(name);
|
|
308
346
|
}
|
|
309
|
-
const
|
|
347
|
+
const dependencyRoot = deno?.config.workspaceRoot ?? repoRoot;
|
|
348
|
+
const bareCheckout = !deno && !fs.existsSync(path.join(dependencyRoot, 'node_modules'));
|
|
310
349
|
fs.writeFileSync(path.join(stubDir, 'package.json'), JSON.stringify({
|
|
311
350
|
name: packageName,
|
|
312
351
|
version: '0.0.0-carrick',
|
|
@@ -329,7 +368,8 @@ export function captureStub(opts) {
|
|
|
329
368
|
resolved,
|
|
330
369
|
pinned,
|
|
331
370
|
bareCheckout,
|
|
332
|
-
repoRoot,
|
|
371
|
+
repoRoot: dependencyRoot,
|
|
372
|
+
compilerHost: deno ? options => deno.host(options) : undefined,
|
|
333
373
|
});
|
|
334
374
|
const fidelity = computeFidelity(aliases);
|
|
335
375
|
fs.writeFileSync(path.join(stubDir, 'carrick-manifest.json'), JSON.stringify({
|
|
@@ -371,7 +411,7 @@ function demoteDanglingAliases(args) {
|
|
|
371
411
|
let surfaceKey;
|
|
372
412
|
for (const fileName of args.emitted.keys()) {
|
|
373
413
|
const rel = path.relative(args.staging, fileName).split(path.sep).join('/');
|
|
374
|
-
if (rel === `${SURFACE_ENTRY_BASENAME}.d.ts`)
|
|
414
|
+
if (path.basename(rel) === `${SURFACE_ENTRY_BASENAME}.d.ts`)
|
|
375
415
|
surfaceKey = fileName;
|
|
376
416
|
if (rel.endsWith('.d.ts'))
|
|
377
417
|
treeModules.add(rel.slice(0, -'.d.ts'.length));
|
|
@@ -380,10 +420,10 @@ function demoteDanglingAliases(args) {
|
|
|
380
420
|
if (rel.endsWith('.d.ts'))
|
|
381
421
|
treeModules.add(rel.slice(0, -'.d.ts'.length));
|
|
382
422
|
}
|
|
383
|
-
//
|
|
384
|
-
|
|
423
|
+
// Deno's temporary surface lives in the cache inside the emitted tree.
|
|
424
|
+
const surfaceDir = surfaceKey ? path.posix.dirname(path.relative(args.staging, surfaceKey).split(path.sep).join('/')) : '.';
|
|
385
425
|
const moduleInTree = (spec) => {
|
|
386
|
-
const id = path.posix.normalize(spec);
|
|
426
|
+
const id = path.posix.normalize(path.posix.join(surfaceDir, spec));
|
|
387
427
|
if (id.startsWith('..'))
|
|
388
428
|
return false;
|
|
389
429
|
return treeModules.has(id) || treeModules.has(`${id}/index`);
|
|
@@ -429,7 +469,7 @@ function rewriteSurfaceAliasesToUnknown(text, demoted) {
|
|
|
429
469
|
return out;
|
|
430
470
|
}
|
|
431
471
|
/** Phase A: build the placeholder entry, then resolve every anchor. */
|
|
432
|
-
function resolveAnchors(opts, parsed, ctx) {
|
|
472
|
+
function resolveAnchors(opts, parsed, ctx, deno) {
|
|
433
473
|
const placeholderLines = ['// Carrick capture v2 analysis placeholder.'];
|
|
434
474
|
for (const anchor of opts.anchors) {
|
|
435
475
|
placeholderLines.push(`export type ${anchor.alias} = unknown;`);
|
|
@@ -441,10 +481,11 @@ function resolveAnchors(opts, parsed, ctx) {
|
|
|
441
481
|
.filter((a) => a.kind !== 'literal')
|
|
442
482
|
.map((a) => path.join(ctx.repoRoot, a.source_file))),
|
|
443
483
|
].filter((f) => fs.existsSync(f));
|
|
444
|
-
const
|
|
484
|
+
const options = {
|
|
445
485
|
...parsed.options,
|
|
446
486
|
noEmit: true,
|
|
447
|
-
}
|
|
487
|
+
};
|
|
488
|
+
const program = ts.createProgram([ctx.entryPath, ...anchorSources, ...(deno?.globals ?? [])], options, deno?.host(options));
|
|
448
489
|
const entrySource = program.getSourceFile(ctx.entryPath);
|
|
449
490
|
const placeholders = new Map();
|
|
450
491
|
if (entrySource) {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* (import-type seeds, then BFS over relative imports). The spike's
|
|
25
25
|
* file-granularity shortcut is gone.
|
|
26
26
|
*/
|
|
27
|
+
import ts from 'typescript';
|
|
27
28
|
import type { CaptureAliasRecord } from './api.js';
|
|
28
29
|
import type { ResolvedAnchor } from './anchors.js';
|
|
29
30
|
export interface SelfCheckArgs {
|
|
@@ -34,5 +35,6 @@ export interface SelfCheckArgs {
|
|
|
34
35
|
bareCheckout: boolean;
|
|
35
36
|
/** Producer repo root; its node_modules (if any) backs resolution. */
|
|
36
37
|
repoRoot: string;
|
|
38
|
+
compilerHost?: (options: ts.CompilerOptions) => ts.CompilerHost;
|
|
37
39
|
}
|
|
38
40
|
export declare function selfCheckStub(args: SelfCheckArgs): CaptureAliasRecord[];
|
|
@@ -61,7 +61,7 @@ export function selfCheckStub(args) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
function runSelfCheck(args, treeFiles) {
|
|
64
|
-
const
|
|
64
|
+
const options = {
|
|
65
65
|
noEmit: true,
|
|
66
66
|
strict: true,
|
|
67
67
|
// MUST be false: the whole stub tree is .d.ts (see module header).
|
|
@@ -69,7 +69,8 @@ function runSelfCheck(args, treeFiles) {
|
|
|
69
69
|
module: ts.ModuleKind.ESNext,
|
|
70
70
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
71
71
|
types: [],
|
|
72
|
-
}
|
|
72
|
+
};
|
|
73
|
+
const program = ts.createProgram(treeFiles, options, args.compilerHost?.(options));
|
|
73
74
|
const checker = program.getTypeChecker();
|
|
74
75
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
75
76
|
// Failed module specifiers, split external-pinned vs internal, per FILE.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { Project } from 'ts-morph';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
|
+
import { DenoProject, findDenoConfig } from './capture/index.js';
|
|
15
16
|
/**
|
|
16
17
|
* Source-file patterns used when the repo declares no tsconfig, relative to
|
|
17
18
|
* the repo root. `node_modules` is excluded explicitly: a glob that matches
|
|
@@ -169,7 +170,27 @@ export class ProjectLoader {
|
|
|
169
170
|
// Priority 2: Use tsconfig.json file
|
|
170
171
|
else {
|
|
171
172
|
const tsconfigPath = this.findTsConfig();
|
|
172
|
-
|
|
173
|
+
const denoConfig = findDenoConfig(this.repoRoot, this.tsconfigPath);
|
|
174
|
+
if (denoConfig) {
|
|
175
|
+
this.log(`Project will load with Deno config: ${denoConfig.configPath}`);
|
|
176
|
+
this.buildProject = () => {
|
|
177
|
+
const deno = new DenoProject(denoConfig, this.repoRoot);
|
|
178
|
+
const project = new Project({
|
|
179
|
+
compilerOptions: deno.parsed.options,
|
|
180
|
+
skipAddingFilesFromTsConfig: true,
|
|
181
|
+
resolutionHost: (host, getOptions) => ({
|
|
182
|
+
resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
|
|
183
|
+
resolveTypeReferenceDirectives: (names, from) => names.map(name => deno.resolveTypeReference(typeof name === 'string' ? name : name.fileName, from, getOptions(), host)),
|
|
184
|
+
}),
|
|
185
|
+
});
|
|
186
|
+
for (const file of deno.parsed.fileNames)
|
|
187
|
+
project.addSourceFileAtPath(file);
|
|
188
|
+
for (const diagnostic of deno.diagnostics)
|
|
189
|
+
this.logError(diagnostic);
|
|
190
|
+
return project;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
else if (tsconfigPath) {
|
|
173
194
|
this.log(`Project will load with tsconfig: ${tsconfigPath}`);
|
|
174
195
|
this.buildProject = () => new Project({
|
|
175
196
|
tsConfigFilePath: tsconfigPath,
|
|
@@ -272,6 +272,24 @@ export declare class TypeInferrer {
|
|
|
272
272
|
* Check if a type string is "useless" for payload purposes.
|
|
273
273
|
*/
|
|
274
274
|
private isUselessType;
|
|
275
|
+
/**
|
|
276
|
+
* A type that can be CALLED or constructed is machinery, never a payload
|
|
277
|
+
* (carrick#964).
|
|
278
|
+
*
|
|
279
|
+
* A context-object framework hands the handler one object that both reads the
|
|
280
|
+
* request and sends the response, so that object has a `body` MEMBER whose
|
|
281
|
+
* type is the response sender. Every anchor that reads a member off the
|
|
282
|
+
* handler's parameter, and every locator that lands on such a member, can
|
|
283
|
+
* therefore resolve to a perfectly concrete type that is the wrong side of
|
|
284
|
+
* the exchange — and, being concrete, it sails past the useless-type guard and
|
|
285
|
+
* publishes as an explicit contract.
|
|
286
|
+
*
|
|
287
|
+
* Nothing crosses an HTTP (or queue, or topic) boundary as a function, so the
|
|
288
|
+
* presence of call or construct signatures is a structural, framework-free
|
|
289
|
+
* proof that the resolution landed on machinery. The caller abstains, which
|
|
290
|
+
* leaves the row honestly unresolved instead of confidently wrong.
|
|
291
|
+
*/
|
|
292
|
+
private isCallableType;
|
|
275
293
|
private unwrapExpressionNode;
|
|
276
294
|
/**
|
|
277
295
|
* If `node` is a `JSON.stringify(arg)` call, return the (expression-unwrapped)
|
|
@@ -797,6 +815,18 @@ export declare class TypeInferrer {
|
|
|
797
815
|
* the route declares its request nowhere we can read.
|
|
798
816
|
*/
|
|
799
817
|
private requestContractFromRegistration;
|
|
818
|
+
/**
|
|
819
|
+
* The request contract a route DECLARES, in anchor order: the handler's
|
|
820
|
+
* parameter annotation (a), the registration's schema object (b), then a
|
|
821
|
+
* validator middleware bound to the request body (b2).
|
|
822
|
+
*
|
|
823
|
+
* Both callers — the registration locator and the expression locator whose
|
|
824
|
+
* line is a registration — consult exactly this set, so the two cannot drift.
|
|
825
|
+
* The typed request READ inside the handler body is deliberately not part of
|
|
826
|
+
* it: that anchor is itself an expression, and the expression path keeps its
|
|
827
|
+
* own locator authoritative when a route declares nothing.
|
|
828
|
+
*/
|
|
829
|
+
private declaredRequestContract;
|
|
800
830
|
/**
|
|
801
831
|
* Anchor (a): the request contract declared on the handler's own signature.
|
|
802
832
|
*
|
|
@@ -810,6 +840,28 @@ export declare class TypeInferrer {
|
|
|
810
840
|
* anchor runs.
|
|
811
841
|
*/
|
|
812
842
|
private requestBodyFromHandlerParams;
|
|
843
|
+
/**
|
|
844
|
+
* Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
|
|
845
|
+
* registration (carrick#964).
|
|
846
|
+
*
|
|
847
|
+
* A route can declare its body by binding a schema to a named part of the
|
|
848
|
+
* request in a middleware the registration carries alongside the handler:
|
|
849
|
+
*
|
|
850
|
+
* router.post('/search', validate('json', PayloadSchema), async (c) => …)
|
|
851
|
+
*
|
|
852
|
+
* The shape is a CALL among the registration's arguments whose own arguments
|
|
853
|
+
* are a request part and a schema value. Neither the middleware's name nor
|
|
854
|
+
* the schema library is checked: the part is read off the string literal, and
|
|
855
|
+
* the schema is whatever exposes a parsed output (`schemaOutputTypeText`) —
|
|
856
|
+
* the same test anchor (b) already applies to a `schema: { body: … }` object.
|
|
857
|
+
*
|
|
858
|
+
* `REQUEST_BODY_PARTS` is HTTP vocabulary for how a body is carried, not a
|
|
859
|
+
* framework list. A middleware bound to any other part (`query`, `param`,
|
|
860
|
+
* `header`, `cookie`) declares no BODY, and a middleware that names no part
|
|
861
|
+
* at all is not read: publishing a query schema as the request contract would
|
|
862
|
+
* be the same confident-and-wrong answer this anchor exists to remove.
|
|
863
|
+
*/
|
|
864
|
+
private validatedBodyContractText;
|
|
813
865
|
/**
|
|
814
866
|
* Anchor (b): the contract declared in the route's validation schema.
|
|
815
867
|
*
|
|
@@ -123,6 +123,16 @@ const RESPONSE_HELPER_MAX_DEPTH = 4;
|
|
|
123
123
|
* the branch an error path, whose shape is not the endpoint's contract.
|
|
124
124
|
*/
|
|
125
125
|
const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
|
|
126
|
+
/**
|
|
127
|
+
* The parts of a request that ARE the body, as validator middleware names them
|
|
128
|
+
* (`validate('json', Schema)`). HTTP vocabulary for how a body is carried — the
|
|
129
|
+
* same class of generic route vocabulary as the `schema` / `body` / `response` /
|
|
130
|
+
* `handler` keys the schema anchors already read, and deliberately not a list of
|
|
131
|
+
* libraries. Every other part a validator can bind (`query`, `param`, `header`,
|
|
132
|
+
* `cookie`) is NOT the body, so a schema bound to one of those declares no
|
|
133
|
+
* request contract.
|
|
134
|
+
*/
|
|
135
|
+
const REQUEST_BODY_PARTS = new Set(['json', 'form', 'body']);
|
|
126
136
|
/**
|
|
127
137
|
* Print a `Type` to its string form WITHOUT the compiler's default truncation.
|
|
128
138
|
*
|
|
@@ -790,16 +800,15 @@ export class TypeInferrer {
|
|
|
790
800
|
// ships as machinery and decays to `any` in the cross-repo surface.
|
|
791
801
|
//
|
|
792
802
|
// A DECLARED contract outranks whatever expression the locator picked,
|
|
793
|
-
// exactly as it does on the response side, so read
|
|
794
|
-
// first and fall through to the expression only when the
|
|
795
|
-
// its request nowhere. The
|
|
796
|
-
// READ inside the handler body — is deliberately not
|
|
797
|
-
// one is itself an expression, so the locator's own
|
|
798
|
-
// authoritative when nothing is declared.
|
|
803
|
+
// exactly as it does on the response side, so read the DECLARED anchors
|
|
804
|
+
// (a), (b) and (b2) first and fall through to the expression only when the
|
|
805
|
+
// route declares its request nowhere. The remaining registration anchor —
|
|
806
|
+
// the typed request READ inside the handler body — is deliberately not
|
|
807
|
+
// consulted here: that one is itself an expression, so the locator's own
|
|
808
|
+
// expression stays authoritative when nothing is declared.
|
|
799
809
|
const declaredAt = this.registrationAtLine(sourceFile, request.line_number);
|
|
800
810
|
if (declaredAt) {
|
|
801
|
-
const declared = this.
|
|
802
|
-
this.routeSchemaContractText(declaredAt.registration, 'body');
|
|
811
|
+
const declared = this.declaredRequestContract(declaredAt.registration, declaredAt.handler);
|
|
803
812
|
if (declared) {
|
|
804
813
|
this.log(`Route registration at ${request.file_path}:${request.line_number} declares its ` +
|
|
805
814
|
'request contract; using the declaration over the located expression');
|
|
@@ -895,6 +904,19 @@ export class TypeInferrer {
|
|
|
895
904
|
typeString = explicitType;
|
|
896
905
|
isExplicit = true;
|
|
897
906
|
}
|
|
907
|
+
// Publication guard (carrick#964): the locator landed on machinery — a
|
|
908
|
+
// callable member of the handler's context, a method reference, a handler
|
|
909
|
+
// binding — and neither a wrapper rule nor a declared type recovered a
|
|
910
|
+
// payload from it. Publishing that would assert an explicit contract for a
|
|
911
|
+
// type nothing can send over the wire, so abstain and let the row say
|
|
912
|
+
// unknown.
|
|
913
|
+
if (this.isCallableType(payloadType) &&
|
|
914
|
+
!unwrapResult.wasUnwrapped &&
|
|
915
|
+
!explicitType) {
|
|
916
|
+
this.log(`Request locator at ${request.file_path}:${request.line_number} resolved a callable ` +
|
|
917
|
+
`(${typeString}); a function is not a payload, leaving unresolved`);
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
898
920
|
typeString = this.unwrapPromise(typeString, payloadType);
|
|
899
921
|
return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(node), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined);
|
|
900
922
|
}
|
|
@@ -1611,6 +1633,32 @@ export class TypeInferrer {
|
|
|
1611
1633
|
const trimmed = typeString.trim();
|
|
1612
1634
|
return useless.includes(trimmed) || trimmed === '';
|
|
1613
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* A type that can be CALLED or constructed is machinery, never a payload
|
|
1638
|
+
* (carrick#964).
|
|
1639
|
+
*
|
|
1640
|
+
* A context-object framework hands the handler one object that both reads the
|
|
1641
|
+
* request and sends the response, so that object has a `body` MEMBER whose
|
|
1642
|
+
* type is the response sender. Every anchor that reads a member off the
|
|
1643
|
+
* handler's parameter, and every locator that lands on such a member, can
|
|
1644
|
+
* therefore resolve to a perfectly concrete type that is the wrong side of
|
|
1645
|
+
* the exchange — and, being concrete, it sails past the useless-type guard and
|
|
1646
|
+
* publishes as an explicit contract.
|
|
1647
|
+
*
|
|
1648
|
+
* Nothing crosses an HTTP (or queue, or topic) boundary as a function, so the
|
|
1649
|
+
* presence of call or construct signatures is a structural, framework-free
|
|
1650
|
+
* proof that the resolution landed on machinery. The caller abstains, which
|
|
1651
|
+
* leaves the row honestly unresolved instead of confidently wrong.
|
|
1652
|
+
*/
|
|
1653
|
+
isCallableType(type) {
|
|
1654
|
+
try {
|
|
1655
|
+
return (type.getCallSignatures().length > 0 ||
|
|
1656
|
+
type.getConstructSignatures().length > 0);
|
|
1657
|
+
}
|
|
1658
|
+
catch {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1614
1662
|
unwrapExpressionNode(node) {
|
|
1615
1663
|
let current = node;
|
|
1616
1664
|
while (current) {
|
|
@@ -2799,6 +2847,9 @@ export class TypeInferrer {
|
|
|
2799
2847
|
structuralTextFromTypeNode(typeNode) {
|
|
2800
2848
|
try {
|
|
2801
2849
|
const resolved = this.unwrapPromiseType(typeNode.getType());
|
|
2850
|
+
if (this.isCallableType(resolved)) {
|
|
2851
|
+
return null;
|
|
2852
|
+
}
|
|
2802
2853
|
const bare = typeText(resolved, typeNode);
|
|
2803
2854
|
if (this.isUselessType(bare)) {
|
|
2804
2855
|
return null;
|
|
@@ -2817,6 +2868,9 @@ export class TypeInferrer {
|
|
|
2817
2868
|
structuralTextFromType(type, at) {
|
|
2818
2869
|
try {
|
|
2819
2870
|
const resolved = this.unwrapPromiseType(type);
|
|
2871
|
+
if (this.isCallableType(resolved)) {
|
|
2872
|
+
return null;
|
|
2873
|
+
}
|
|
2820
2874
|
const bare = typeText(resolved, at);
|
|
2821
2875
|
if (this.isUselessType(bare)) {
|
|
2822
2876
|
return null;
|
|
@@ -2891,9 +2945,24 @@ export class TypeInferrer {
|
|
|
2891
2945
|
* the route declares its request nowhere we can read.
|
|
2892
2946
|
*/
|
|
2893
2947
|
requestContractFromRegistration(registration, handler) {
|
|
2948
|
+
return (this.declaredRequestContract(registration, handler) ??
|
|
2949
|
+
this.inferRequestReadFromHandler(handler));
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* The request contract a route DECLARES, in anchor order: the handler's
|
|
2953
|
+
* parameter annotation (a), the registration's schema object (b), then a
|
|
2954
|
+
* validator middleware bound to the request body (b2).
|
|
2955
|
+
*
|
|
2956
|
+
* Both callers — the registration locator and the expression locator whose
|
|
2957
|
+
* line is a registration — consult exactly this set, so the two cannot drift.
|
|
2958
|
+
* The typed request READ inside the handler body is deliberately not part of
|
|
2959
|
+
* it: that anchor is itself an expression, and the expression path keeps its
|
|
2960
|
+
* own locator authoritative when a route declares nothing.
|
|
2961
|
+
*/
|
|
2962
|
+
declaredRequestContract(registration, handler) {
|
|
2894
2963
|
return (this.requestBodyFromHandlerParams(handler) ??
|
|
2895
2964
|
this.routeSchemaContractText(registration, 'body') ??
|
|
2896
|
-
this.
|
|
2965
|
+
this.validatedBodyContractText(registration));
|
|
2897
2966
|
}
|
|
2898
2967
|
/**
|
|
2899
2968
|
* Anchor (a): the request contract declared on the handler's own signature.
|
|
@@ -2934,6 +3003,58 @@ export class TypeInferrer {
|
|
|
2934
3003
|
}
|
|
2935
3004
|
return null;
|
|
2936
3005
|
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
|
|
3008
|
+
* registration (carrick#964).
|
|
3009
|
+
*
|
|
3010
|
+
* A route can declare its body by binding a schema to a named part of the
|
|
3011
|
+
* request in a middleware the registration carries alongside the handler:
|
|
3012
|
+
*
|
|
3013
|
+
* router.post('/search', validate('json', PayloadSchema), async (c) => …)
|
|
3014
|
+
*
|
|
3015
|
+
* The shape is a CALL among the registration's arguments whose own arguments
|
|
3016
|
+
* are a request part and a schema value. Neither the middleware's name nor
|
|
3017
|
+
* the schema library is checked: the part is read off the string literal, and
|
|
3018
|
+
* the schema is whatever exposes a parsed output (`schemaOutputTypeText`) —
|
|
3019
|
+
* the same test anchor (b) already applies to a `schema: { body: … }` object.
|
|
3020
|
+
*
|
|
3021
|
+
* `REQUEST_BODY_PARTS` is HTTP vocabulary for how a body is carried, not a
|
|
3022
|
+
* framework list. A middleware bound to any other part (`query`, `param`,
|
|
3023
|
+
* `header`, `cookie`) declares no BODY, and a middleware that names no part
|
|
3024
|
+
* at all is not read: publishing a query schema as the request contract would
|
|
3025
|
+
* be the same confident-and-wrong answer this anchor exists to remove.
|
|
3026
|
+
*/
|
|
3027
|
+
validatedBodyContractText(registration) {
|
|
3028
|
+
if (!Node.isCallExpression(registration)) {
|
|
3029
|
+
return null;
|
|
3030
|
+
}
|
|
3031
|
+
for (const argument of registration.getArguments()) {
|
|
3032
|
+
const middleware = this.unwrapExpressionNode(argument);
|
|
3033
|
+
if (!Node.isCallExpression(middleware)) {
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
const middlewareArgs = middleware
|
|
3037
|
+
.getArguments()
|
|
3038
|
+
.map((arg) => this.unwrapExpressionNode(arg));
|
|
3039
|
+
const parts = middlewareArgs.filter((arg) => Node.isStringLiteral(arg));
|
|
3040
|
+
if (parts.length === 0) {
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
if (!parts.some((part) => REQUEST_BODY_PARTS.has(part.getLiteralValue().toLowerCase()))) {
|
|
3044
|
+
continue;
|
|
3045
|
+
}
|
|
3046
|
+
for (const candidate of middlewareArgs) {
|
|
3047
|
+
if (Node.isStringLiteral(candidate)) {
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
const declared = this.schemaOutputTypeText(candidate);
|
|
3051
|
+
if (declared) {
|
|
3052
|
+
return declared;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return null;
|
|
3057
|
+
}
|
|
2937
3058
|
/**
|
|
2938
3059
|
* Anchor (b): the contract declared in the route's validation schema.
|
|
2939
3060
|
*
|
package/dist/init/identity.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export type Identity = {
|
|
2
|
-
/** The GitHub login, when the source could name one. */
|
|
3
|
-
login: string | null;
|
|
4
|
-
source: "gh" | "token";
|
|
5
|
-
};
|
|
6
|
-
export type IdentityLookup = {
|
|
7
|
-
identity: Identity | null;
|
|
8
|
-
/** What to do about it, ready to print. Null when there is an identity. */
|
|
9
|
-
problem: string | null;
|
|
10
|
-
};
|
|
11
|
-
export type IdentityOptions = {
|
|
12
|
-
env?: NodeJS.ProcessEnv;
|
|
13
|
-
/** Injectable for tests: runs `gh auth status` and returns its output. */
|
|
14
|
-
ghStatus?: () => string;
|
|
15
|
-
};
|
|
16
|
-
/** The login out of `gh auth status`, whatever wording it used. */
|
|
17
|
-
export declare function loginFromGhStatus(output: string): string | null;
|
|
18
|
-
export declare function githubIdentity(options?: IdentityOptions): IdentityLookup;
|
|
19
|
-
/** One line for the summary. */
|
|
20
|
-
export declare function describeIdentity(identity: Identity): string;
|
package/dist/init/identity.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// The GitHub identity `carrick init` requires.
|
|
2
|
-
//
|
|
3
|
-
// There is no anonymous tier (carrick#729, ruled 2026-09-08): the local index
|
|
4
|
-
// is built from the identified free tier's allowance, so setup asks who you are
|
|
5
|
-
// before it writes anything. The check is offline and reuses whatever the
|
|
6
|
-
// machine already has — the GitHub CLI's login, or a token in the environment —
|
|
7
|
-
// rather than opening a browser flow of its own.
|
|
8
|
-
//
|
|
9
|
-
// This states the identity. Spending an allowance against it is the cloud's
|
|
10
|
-
// side (carrick-cloud#601) and is not wired here.
|
|
11
|
-
import { spawnSync } from "node:child_process";
|
|
12
|
-
function defaultGhStatus() {
|
|
13
|
-
// Both streams: `gh auth status` writes to stderr on older versions and to
|
|
14
|
-
// stdout on newer ones, and a run that reads one of them finds nothing on
|
|
15
|
-
// half the machines it runs on.
|
|
16
|
-
const run = spawnSync("gh", ["auth", "status"], { encoding: "utf8" });
|
|
17
|
-
if (run.error)
|
|
18
|
-
throw run.error;
|
|
19
|
-
return `${run.stdout ?? ""}\n${run.stderr ?? ""}`;
|
|
20
|
-
}
|
|
21
|
-
/** The login out of `gh auth status`, whatever wording it used. */
|
|
22
|
-
export function loginFromGhStatus(output) {
|
|
23
|
-
const match = /Logged in to \S+ account (\S+)/.exec(output) ?? /Logged in to \S+ as (\S+)/.exec(output);
|
|
24
|
-
return match?.[1] ?? null;
|
|
25
|
-
}
|
|
26
|
-
export function githubIdentity(options = {}) {
|
|
27
|
-
const env = options.env ?? process.env;
|
|
28
|
-
const ghStatus = options.ghStatus ?? defaultGhStatus;
|
|
29
|
-
try {
|
|
30
|
-
const login = loginFromGhStatus(ghStatus());
|
|
31
|
-
if (login)
|
|
32
|
-
return { identity: { login, source: "gh" }, problem: null };
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// No gh, or gh is not logged in. The token below is the other way in.
|
|
36
|
-
}
|
|
37
|
-
const token = env["GITHUB_TOKEN"] || env["GH_TOKEN"];
|
|
38
|
-
if (token)
|
|
39
|
-
return { identity: { login: null, source: "token" }, problem: null };
|
|
40
|
-
return {
|
|
41
|
-
identity: null,
|
|
42
|
-
problem: [
|
|
43
|
-
"carrick init needs your GitHub identity, and this machine has none it can use.",
|
|
44
|
-
"",
|
|
45
|
-
"Either:",
|
|
46
|
-
" gh auth login (the GitHub CLI: https://cli.github.com)",
|
|
47
|
-
" export GITHUB_TOKEN=<token> (any token that identifies you)",
|
|
48
|
-
"",
|
|
49
|
-
"Carrick indexes what you already have access to, and the free tier's",
|
|
50
|
-
"allowance is counted against the account that asks for it.",
|
|
51
|
-
].join("\n"),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/** One line for the summary. */
|
|
55
|
-
export function describeIdentity(identity) {
|
|
56
|
-
return identity.login
|
|
57
|
-
? `GitHub identity: ${identity.login} (from the GitHub CLI)`
|
|
58
|
-
: "GitHub identity: the token in this environment";
|
|
59
|
-
}
|
|
60
|
-
//# sourceMappingURL=identity.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/init/identity.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,0EAA0E;AAC1E,gFAAgF;AAChF,iDAAiD;AACjD,EAAE;AACF,4EAA4E;AAC5E,kDAAkD;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAoB/C,SAAS,eAAe;IACtB,2EAA2E;IAC3E,0EAA0E;IAC1E,gCAAgC;IAChC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACtE,IAAI,GAAG,CAAC,KAAK;QAAE,MAAM,GAAG,CAAC,KAAK,CAAC;IAC/B,OAAO,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AACpD,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,MAAM,KAAK,GACT,gCAAgC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5F,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,UAA2B,EAAE;IAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,eAAe,CAAC;IACrD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5C,IAAI,KAAK;YAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;IACxE,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACrD,IAAI,KAAK;QAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAEhF,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE;YACP,gFAAgF;YAChF,EAAE;YACF,SAAS;YACT,0EAA0E;YAC1E,iEAAiE;YACjE,EAAE;YACF,sEAAsE;YACtE,4DAA4D;SAC7D,CAAC,IAAI,CAAC,IAAI,CAAC;KACb,CAAC;AACJ,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,OAAO,QAAQ,CAAC,KAAK;QACnB,CAAC,CAAC,oBAAoB,QAAQ,CAAC,KAAK,wBAAwB;QAC5D,CAAC,CAAC,gDAAgD,CAAC;AACvD,CAAC"}
|