carrick 0.3.76 → 0.3.78

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.76",
3
+ "version": "0.3.78",
4
4
  "description": "The API contract index for a TypeScript workspace: what the other services do with the routes and calls in the file you are editing, in your editor and in your agent's context",
5
5
  "keywords": [
6
6
  "typescript",
@@ -57,11 +57,11 @@
57
57
  "zod": "^3.23.0"
58
58
  },
59
59
  "optionalDependencies": {
60
- "@carrick-tools/cli-darwin-arm64": "0.3.76",
61
- "@carrick-tools/cli-darwin-x64": "0.3.76",
62
- "@carrick-tools/cli-linux-arm64": "0.3.76",
63
- "@carrick-tools/cli-linux-x64": "0.3.76",
64
- "@carrick-tools/cli-win32-x64": "0.3.76"
60
+ "@carrick-tools/cli-darwin-arm64": "0.3.78",
61
+ "@carrick-tools/cli-darwin-x64": "0.3.78",
62
+ "@carrick-tools/cli-linux-arm64": "0.3.78",
63
+ "@carrick-tools/cli-linux-x64": "0.3.78",
64
+ "@carrick-tools/cli-win32-x64": "0.3.78"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^24.13.3",
@@ -19,6 +19,7 @@ export declare class DenoProject {
19
19
  readonly pinned: Record<string, string>;
20
20
  private readonly modules;
21
21
  private readonly localPaths;
22
+ private readonly specifiersByPath;
22
23
  private readonly edges;
23
24
  private readonly redirects;
24
25
  private readonly npmPackages;
@@ -27,6 +28,16 @@ export declare class DenoProject {
27
28
  private targetPath;
28
29
  resolve(spec: string, from: string, options: ts.CompilerOptions, host?: ts.ModuleResolutionHost): ts.ResolvedModule | undefined;
29
30
  private npmOwner;
31
+ /** Which registry package a resolved file belongs to (carrick#1260).
32
+ *
33
+ * The inverse of resolution, and the only answer available for a Deno
34
+ * project: nothing it resolves sits under `node_modules`, so reading a
35
+ * package name off the path cannot work. An npm dependency is owned by the
36
+ * graph entry whose cache directory contains the file; a JSR one is named by
37
+ * its own specifier, since its cached copy is content-addressed and carries
38
+ * no identity. A file the workspace itself owns belongs to no package and
39
+ * answers nothing, exactly as a workspace-declared type does under Node. */
40
+ packageOf(file: string): string | undefined;
30
41
  private isNpmFile;
31
42
  /** Let TypeScript interpret real package exports/types using Deno's exact
32
43
  * dependency graph. Only the resolution host sees virtual node_modules;
@@ -12,6 +12,22 @@ function readConfig(file) {
12
12
  throw new Error(ts.flattenDiagnosticMessageText(parsed.error.messageText, '\n'));
13
13
  return parsed.config;
14
14
  }
15
+ /** The package a JSR module specifier names, in the form the registry uses
16
+ * (`@scope/name`). Both the specifier Deno records after resolution
17
+ * (`https://jsr.io/@scope/name/1.2.3/mod.ts`) and the one an import map writes
18
+ * (`jsr:@scope/name@1`) name it in their first two segments. Anything else —
19
+ * a `file:` module, an `https://` module from a host that is not a package
20
+ * registry — names no package. */
21
+ function jsrPackageName(specifier) {
22
+ const tail = /^jsr:\/{0,2}(.*)$/.exec(specifier)?.[1]
23
+ ?? /^https:\/\/jsr\.io\/(.*)$/.exec(specifier)?.[1];
24
+ if (!tail)
25
+ return undefined;
26
+ const [scope, name] = tail.split('/');
27
+ if (!scope?.startsWith('@') || !name)
28
+ return undefined;
29
+ return `${scope}/${name.split('@')[0]}`;
30
+ }
15
31
  /** Explicit TS configs keep their existing behaviour, including mixed repos. */
16
32
  export function findDenoConfig(repoRoot, explicit) {
17
33
  if (explicit && !/^deno\.jsonc?$/.test(path.basename(explicit)))
@@ -78,6 +94,7 @@ export class DenoProject {
78
94
  pinned = {};
79
95
  modules = new Map();
80
96
  localPaths = new Map();
97
+ specifiersByPath = new Map();
81
98
  edges = new Map();
82
99
  redirects;
83
100
  npmPackages;
@@ -168,6 +185,7 @@ export class DenoProject {
168
185
  fs.copyFileSync(module.local, local);
169
186
  }
170
187
  this.localPaths.set(module.specifier, path.resolve(local));
188
+ this.specifiersByPath.set(path.resolve(local), module.specifier);
171
189
  }
172
190
  for (const module of graph.modules) {
173
191
  const local = this.localPaths.get(module.specifier);
@@ -236,8 +254,26 @@ export class DenoProject {
236
254
  return this.resolveNpm(spec, from, options) ?? ts.resolveModuleName(spec, from, options, host).resolvedModule;
237
255
  }
238
256
  npmOwner(file) {
257
+ const target = path.normalize(file);
239
258
  return Object.values(this.npmPackages).find(pkg => pkg.localPath &&
240
- (file === pkg.localPath || file.startsWith(pkg.localPath + path.sep)));
259
+ (target === pkg.localPath || target.startsWith(pkg.localPath + path.sep)));
260
+ }
261
+ /** Which registry package a resolved file belongs to (carrick#1260).
262
+ *
263
+ * The inverse of resolution, and the only answer available for a Deno
264
+ * project: nothing it resolves sits under `node_modules`, so reading a
265
+ * package name off the path cannot work. An npm dependency is owned by the
266
+ * graph entry whose cache directory contains the file; a JSR one is named by
267
+ * its own specifier, since its cached copy is content-addressed and carries
268
+ * no identity. A file the workspace itself owns belongs to no package and
269
+ * answers nothing, exactly as a workspace-declared type does under Node. */
270
+ packageOf(file) {
271
+ const resolved = path.resolve(file);
272
+ const owner = this.npmOwner(resolved);
273
+ if (owner)
274
+ return owner.name;
275
+ const specifier = this.specifiersByPath.get(resolved);
276
+ return specifier ? jsrPackageName(specifier) : undefined;
241
277
  }
242
278
  isNpmFile(file) {
243
279
  return !!this.npmOwner(file) || file.split(path.sep).includes('node_modules');
@@ -32,6 +32,25 @@ export type { CaptureStubOptions, CaptureStubResult } from './api.js';
32
32
  export { DenoProject, findDenoConfig } from './deno-project.js';
33
33
  export { runCheck } from './check.js';
34
34
  export type { CheckProgress } from './check.js';
35
+ /**
36
+ * The name of THIS capture's surface entry, without an extension
37
+ * (carrick#1046).
38
+ *
39
+ * The entry has to live inside the effective `rootDir` — an entry beside a
40
+ * `rootDir` of `src` fails TS6059 — so it is written into the scanned tree and
41
+ * unlinked afterwards. One name per repo root made two captures of the same
42
+ * tree share a single file: whichever finished first unlinked it while the
43
+ * other's program was still reading it, and every alias whose print anchors in
44
+ * that destination then demoted to `structural_fallback` with an accessibility
45
+ * reason that described the harness rather than the code. Concurrent captures
46
+ * of one tree are ordinary — the test suite does it on every run, and two
47
+ * services of a monorepo can share a root — so the name, not the locking, is
48
+ * what has to give.
49
+ *
50
+ * A leftover from an interrupted capture is also identifiable as one process's
51
+ * (carrick#1069), rather than a fixed name the next scan reads as source.
52
+ */
53
+ export declare function surfaceEntryFileName(): string;
35
54
  /** Same normalization intent as bundle_file_stems on the Rust side. */
36
55
  export declare function sanitizeServiceName(name: string): string;
37
56
  export declare function captureStub(opts: CaptureStubOptions): CaptureStubResult;
@@ -44,6 +44,31 @@ export { DenoProject, findDenoConfig } from './deno-project.js';
44
44
  // reaches it only through this door (index.js).
45
45
  export { runCheck } from './check.js';
46
46
  const SURFACE_ENTRY_BASENAME = '__carrick_surface__';
47
+ /** Captures made by this process, so each one's entry file has a name of its
48
+ * own. Paired with the pid it is unique across processes too. */
49
+ let surfaceEntrySequence = 0;
50
+ /**
51
+ * The name of THIS capture's surface entry, without an extension
52
+ * (carrick#1046).
53
+ *
54
+ * The entry has to live inside the effective `rootDir` — an entry beside a
55
+ * `rootDir` of `src` fails TS6059 — so it is written into the scanned tree and
56
+ * unlinked afterwards. One name per repo root made two captures of the same
57
+ * tree share a single file: whichever finished first unlinked it while the
58
+ * other's program was still reading it, and every alias whose print anchors in
59
+ * that destination then demoted to `structural_fallback` with an accessibility
60
+ * reason that described the harness rather than the code. Concurrent captures
61
+ * of one tree are ordinary — the test suite does it on every run, and two
62
+ * services of a monorepo can share a root — so the name, not the locking, is
63
+ * what has to give.
64
+ *
65
+ * A leftover from an interrupted capture is also identifiable as one process's
66
+ * (carrick#1069), rather than a fixed name the next scan reads as source.
67
+ */
68
+ export function surfaceEntryFileName() {
69
+ surfaceEntrySequence += 1;
70
+ return `${SURFACE_ENTRY_BASENAME}.${process.pid}.${surfaceEntrySequence}`;
71
+ }
47
72
  /** Same normalization intent as bundle_file_stems on the Rust side. */
48
73
  export function sanitizeServiceName(name) {
49
74
  return name.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
@@ -153,9 +178,11 @@ export function captureStub(opts) {
153
178
  const entryDir = parsed.options.rootDir
154
179
  ? path.resolve(path.dirname(configPath), parsed.options.rootDir)
155
180
  : repoRoot;
181
+ const surfaceEntry = surfaceEntryFileName();
182
+ const surfaceDeclaration = `${surfaceEntry}.d.ts`;
156
183
  const entryPath = deno
157
- ? path.join(deno.cacheDir, `${SURFACE_ENTRY_BASENAME}.ts`)
158
- : path.join(entryDir, `${SURFACE_ENTRY_BASENAME}.ts`);
184
+ ? path.join(deno.cacheDir, `${surfaceEntry}.ts`)
185
+ : path.join(entryDir, `${surfaceEntry}.ts`);
159
186
  fs.mkdirSync(path.dirname(entryPath), { recursive: true });
160
187
  // ---- Phase A: analysis program over placeholder entry + anchor sources ----
161
188
  let resolved;
@@ -250,7 +277,7 @@ export function captureStub(opts) {
250
277
  if (emitPartial) {
251
278
  errors.push(`declaration emit was partial: kept ${emitted.size} emitted file(s); ` +
252
279
  'aliases referencing unemitted modules are demoted to structural_fallback');
253
- resolved = demoteDanglingAliases({ resolved, emitted, declarationSources, staging });
280
+ resolved = demoteDanglingAliases({ resolved, emitted, declarationSources, staging, surfaceDeclaration });
254
281
  }
255
282
  // ---- Relocate the emitted tree into the stub package ----
256
283
  const typesDir = path.join(stubDir, 'types');
@@ -260,7 +287,7 @@ export function captureStub(opts) {
260
287
  let surfaceAbsPath = '';
261
288
  for (const [fileName, text] of emitted) {
262
289
  let rel = path.relative(staging, fileName).split(path.sep).join('/');
263
- if (path.basename(rel) === `${SURFACE_ENTRY_BASENAME}.d.ts`) {
290
+ if (path.basename(rel) === surfaceDeclaration) {
264
291
  const source = sourceByEmitted.get(rel);
265
292
  sourceByEmitted.delete(rel);
266
293
  rel = 'surface.d.ts';
@@ -422,7 +449,7 @@ function demoteDanglingAliases(args) {
422
449
  let surfaceKey;
423
450
  for (const fileName of args.emitted.keys()) {
424
451
  const rel = path.relative(args.staging, fileName).split(path.sep).join('/');
425
- if (path.basename(rel) === `${SURFACE_ENTRY_BASENAME}.d.ts`)
452
+ if (path.basename(rel) === args.surfaceDeclaration)
426
453
  surfaceKey = fileName;
427
454
  if (rel.endsWith('.d.ts'))
428
455
  treeModules.add(rel.slice(0, -'.d.ts'.length));
@@ -37,12 +37,22 @@ function projectComponents() {
37
37
  throw new Error('Sidecar not initialized. Call init first.');
38
38
  }
39
39
  if (!components) {
40
- const project = projectLoader.getProject();
41
- const repoRoot = projectLoader.getRepoRoot();
40
+ // Bound here rather than read from the module slot inside the components:
41
+ // a re-init drops `components` and points the slot at another service, and
42
+ // nothing built over this project may follow it there.
43
+ const loader = projectLoader;
44
+ const project = loader.getProject();
45
+ const repoRoot = loader.getRepoRoot();
42
46
  components = {
43
47
  typeBundler: new TypeBundler({ project, repoRoot }),
44
48
  surfaceEmitter: new SurfaceEmitter({ project, repoRoot }),
45
- typeInferrer: new TypeInferrer({ project }),
49
+ // The module graph, where the project resolved through one, is the only
50
+ // thing that can name the package a file belongs to: a Deno service
51
+ // resolves nothing under `node_modules` (carrick#1260).
52
+ typeInferrer: new TypeInferrer({
53
+ project,
54
+ packageOf: (filePath) => loader.packageOf(filePath),
55
+ }),
46
56
  definitionResolver: new DefinitionResolver({ project }),
47
57
  };
48
58
  }
@@ -54,6 +54,13 @@ export declare class ProjectLoader {
54
54
  private project;
55
55
  /** Set by `load()`; builds the ts-morph project on first `getProject()`. */
56
56
  private buildProject;
57
+ /**
58
+ * The Deno graph this project resolved through, when it did (carrick#1260).
59
+ * It is the only thing that can name the package a resolved file belongs to,
60
+ * because a Deno project resolves nothing under `node_modules`. Set by the
61
+ * build, so it is there from the first `getProject()` onwards.
62
+ */
63
+ private denoProject;
57
64
  private readonly repoRoot;
58
65
  private readonly tsconfigPath;
59
66
  private readonly tsconfigSnapshot;
@@ -82,6 +89,12 @@ export declare class ProjectLoader {
82
89
  * @throws Error if load() has not succeeded, or if the build fails
83
90
  */
84
91
  getProject(): Project;
92
+ /**
93
+ * The registry package a resolved file belongs to, when the module graph
94
+ * names one (carrick#1260). `undefined` for a project that did not resolve
95
+ * through Deno, and for a file the workspace itself owns.
96
+ */
97
+ packageOf(filePath: string): string | undefined;
85
98
  /**
86
99
  * Check if the project has been successfully initialized
87
100
  */
@@ -108,6 +108,13 @@ export class ProjectLoader {
108
108
  project = null;
109
109
  /** Set by `load()`; builds the ts-morph project on first `getProject()`. */
110
110
  buildProject = null;
111
+ /**
112
+ * The Deno graph this project resolved through, when it did (carrick#1260).
113
+ * It is the only thing that can name the package a resolved file belongs to,
114
+ * because a Deno project resolves nothing under `node_modules`. Set by the
115
+ * build, so it is there from the first `getProject()` onwards.
116
+ */
117
+ denoProject;
111
118
  repoRoot;
112
119
  tsconfigPath;
113
120
  tsconfigSnapshot;
@@ -175,6 +182,7 @@ export class ProjectLoader {
175
182
  this.log(`Project will load with Deno config: ${denoConfig.configPath}`);
176
183
  this.buildProject = () => {
177
184
  const deno = new DenoProject(denoConfig, this.repoRoot);
185
+ this.denoProject = deno;
178
186
  const project = new Project({
179
187
  compilerOptions: deno.parsed.options,
180
188
  skipAddingFilesFromTsConfig: true,
@@ -317,6 +325,14 @@ export class ProjectLoader {
317
325
  }
318
326
  return this.project;
319
327
  }
328
+ /**
329
+ * The registry package a resolved file belongs to, when the module graph
330
+ * names one (carrick#1260). `undefined` for a project that did not resolve
331
+ * through Deno, and for a file the workspace itself owns.
332
+ */
333
+ packageOf(filePath) {
334
+ return this.denoProject?.packageOf(filePath);
335
+ }
320
336
  /**
321
337
  * Check if the project has been successfully initialized
322
338
  */
@@ -62,6 +62,13 @@ export declare function isExternalOrigin(filePath: string): boolean;
62
62
  export interface TypeInferrerOptions {
63
63
  /** The ts-morph Project instance */
64
64
  project: Project;
65
+ /**
66
+ * The registry package a resolved file belongs to, when the module graph
67
+ * that built the project names one (carrick#1260). Supplied for a project
68
+ * whose resolution went through a graph rather than through `node_modules`;
69
+ * absent otherwise, and the path itself is then the only thing to read.
70
+ */
71
+ packageOf?: (filePath: string) => string | undefined;
65
72
  }
66
73
  /**
67
74
  * TypeInferrer - Extracts types from source code, both explicit and inferred
@@ -72,6 +79,7 @@ export interface TypeInferrerOptions {
72
79
  */
73
80
  export declare class TypeInferrer {
74
81
  private readonly project;
82
+ private readonly packageOf;
75
83
  constructor(options: TypeInferrerOptions);
76
84
  /**
77
85
  * Infer types for the given requests
@@ -727,14 +735,25 @@ export declare class TypeInferrer {
727
735
  */
728
736
  private inferReceiverType;
729
737
  /**
730
- * The npm package name that declares a type, read off its declaration's file
731
- * path. `undefined` when the type has no declaration to read (a top type, a
732
- * primitive, an anonymous object literal) or when its declaration is not
733
- * under a `node_modules` tree — a type the workspace itself declares.
734
- *
735
- * The LAST `node_modules` segment wins, which is what a nested or
736
- * content-addressed store (`node_modules/.store/pkg@1.0.0/node_modules/pkg`)
737
- * requires. Scoped names keep both segments.
738
+ * The npm package name that declares a type. `undefined` when the type has
739
+ * no declaration to read (a top type, a primitive, an anonymous object
740
+ * literal) or when nothing owns its declaration but the workspace itself.
741
+ *
742
+ * The module graph is asked first, where there is one, because it is the
743
+ * only thing that can answer for a project whose resolution does not go
744
+ * through `node_modules`. Deno resolves an npm dependency's types straight
745
+ * out of its own cache
746
+ * (`$DENO_DIR/npm/<registry-host>/<name>/<version>/…`), a path with no
747
+ * `node_modules` segment anywhere in it, so the scan below found no package
748
+ * for ANY dependency-declared type on such a repo and every receiver the
749
+ * compiler had typed correctly went unclassified (carrick#1260). It runs
750
+ * first rather than second because a mixed checkout has both, and the graph
751
+ * is what actually resolved the module.
752
+ *
753
+ * The path scan is the Node answer: the LAST `node_modules` segment wins,
754
+ * which is what a nested or content-addressed store
755
+ * (`node_modules/.store/pkg@1.0.0/node_modules/pkg`) requires. Scoped names
756
+ * keep both segments.
738
757
  */
739
758
  private declaringPackageOf;
740
759
  /**
@@ -249,8 +249,10 @@ function typeText(type, enclosingNode) {
249
249
  */
250
250
  export class TypeInferrer {
251
251
  project;
252
+ packageOf;
252
253
  constructor(options) {
253
254
  this.project = options.project;
255
+ this.packageOf = options.packageOf;
254
256
  }
255
257
  /**
256
258
  * Infer types for the given requests
@@ -2942,14 +2944,25 @@ export class TypeInferrer {
2942
2944
  };
2943
2945
  }
2944
2946
  /**
2945
- * The npm package name that declares a type, read off its declaration's file
2946
- * path. `undefined` when the type has no declaration to read (a top type, a
2947
- * primitive, an anonymous object literal) or when its declaration is not
2948
- * under a `node_modules` tree — a type the workspace itself declares.
2947
+ * The npm package name that declares a type. `undefined` when the type has
2948
+ * no declaration to read (a top type, a primitive, an anonymous object
2949
+ * literal) or when nothing owns its declaration but the workspace itself.
2949
2950
  *
2950
- * The LAST `node_modules` segment wins, which is what a nested or
2951
- * content-addressed store (`node_modules/.store/pkg@1.0.0/node_modules/pkg`)
2952
- * requires. Scoped names keep both segments.
2951
+ * The module graph is asked first, where there is one, because it is the
2952
+ * only thing that can answer for a project whose resolution does not go
2953
+ * through `node_modules`. Deno resolves an npm dependency's types straight
2954
+ * out of its own cache
2955
+ * (`$DENO_DIR/npm/<registry-host>/<name>/<version>/…`), a path with no
2956
+ * `node_modules` segment anywhere in it, so the scan below found no package
2957
+ * for ANY dependency-declared type on such a repo and every receiver the
2958
+ * compiler had typed correctly went unclassified (carrick#1260). It runs
2959
+ * first rather than second because a mixed checkout has both, and the graph
2960
+ * is what actually resolved the module.
2961
+ *
2962
+ * The path scan is the Node answer: the LAST `node_modules` segment wins,
2963
+ * which is what a nested or content-addressed store
2964
+ * (`node_modules/.store/pkg@1.0.0/node_modules/pkg`) requires. Scoped names
2965
+ * keep both segments.
2953
2966
  */
2954
2967
  declaringPackageOf(type) {
2955
2968
  const symbol = type.getSymbol() ?? type.getAliasSymbol();
@@ -2958,6 +2971,10 @@ export class TypeInferrer {
2958
2971
  return undefined;
2959
2972
  }
2960
2973
  const filePath = declaration.getSourceFile().getFilePath().replace(/\\/g, '/');
2974
+ const named = this.packageOf?.(filePath);
2975
+ if (named) {
2976
+ return named;
2977
+ }
2961
2978
  const marker = '/node_modules/';
2962
2979
  const index = filePath.lastIndexOf(marker);
2963
2980
  if (index < 0) {