carrick 0.3.53
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/LICENSE.md +99 -0
- package/README.md +79 -0
- package/bin/carrick.mjs +168 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +37 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +141 -0
- package/dist/contract.js.map +1 -0
- package/dist/diagnostics.d.ts +54 -0
- package/dist/diagnostics.js +199 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hook/post-edit.d.ts +13 -0
- package/dist/hook/post-edit.js +76 -0
- package/dist/hook/post-edit.js.map +1 -0
- package/dist/hook/session-start.d.ts +2 -0
- package/dist/hook/session-start.js +44 -0
- package/dist/hook/session-start.js.map +1 -0
- package/dist/init/identity.d.ts +20 -0
- package/dist/init/identity.js +60 -0
- package/dist/init/identity.js.map +1 -0
- package/dist/init/repos.d.ts +19 -0
- package/dist/init/repos.js +46 -0
- package/dist/init/repos.js.map +1 -0
- package/dist/init/run.d.ts +11 -0
- package/dist/init/run.js +228 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/settings.d.ts +50 -0
- package/dist/init/settings.js +138 -0
- package/dist/init/settings.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.js +35 -0
- package/dist/log.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.js +334 -0
- package/dist/render.js.map +1 -0
- package/dist/root.d.ts +30 -0
- package/dist/root.js +82 -0
- package/dist/root.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +255 -0
- package/dist/server.js.map +1 -0
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +66 -0
- package/dist/templates.js.map +1 -0
- package/package.json +72 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/.lsp.json +14 -0
- package/plugin/hooks/hooks.json +27 -0
- package/sidecar/dist/src/bundler.d.ts +141 -0
- package/sidecar/dist/src/bundler.js +680 -0
- package/sidecar/dist/src/capture/anchors.d.ts +61 -0
- package/sidecar/dist/src/capture/anchors.js +1132 -0
- package/sidecar/dist/src/capture/api.d.ts +378 -0
- package/sidecar/dist/src/capture/api.js +10 -0
- package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
- package/sidecar/dist/src/capture/augmentations.js +60 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
- package/sidecar/dist/src/capture/check-classify.js +189 -0
- package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
- package/sidecar/dist/src/capture/check-deep.js +91 -0
- package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
- package/sidecar/dist/src/capture/check-poison.js +155 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
- package/sidecar/dist/src/capture/check-probe.js +147 -0
- package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
- package/sidecar/dist/src/capture/check-scrub.js +71 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
- package/sidecar/dist/src/capture/check-workspace.js +218 -0
- package/sidecar/dist/src/capture/check.d.ts +39 -0
- package/sidecar/dist/src/capture/check.js +450 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
- package/sidecar/dist/src/capture/deep-walk.js +243 -0
- package/sidecar/dist/src/capture/index.d.ts +36 -0
- package/sidecar/dist/src/capture/index.js +477 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
- package/sidecar/dist/src/capture/lockfile.js +490 -0
- package/sidecar/dist/src/capture/machinery.d.ts +59 -0
- package/sidecar/dist/src/capture/machinery.js +160 -0
- package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
- package/sidecar/dist/src/capture/node-builder.js +123 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
- package/sidecar/dist/src/capture/self-check.d.ts +38 -0
- package/sidecar/dist/src/capture/self-check.js +317 -0
- package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
- package/sidecar/dist/src/capture/specifiers.js +56 -0
- package/sidecar/dist/src/definition-resolver.d.ts +57 -0
- package/sidecar/dist/src/definition-resolver.js +153 -0
- package/sidecar/dist/src/index.d.ts +14 -0
- package/sidecar/dist/src/index.js +564 -0
- package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
- package/sidecar/dist/src/monorepo-builder.js +584 -0
- package/sidecar/dist/src/project-loader.d.ts +130 -0
- package/sidecar/dist/src/project-loader.js +399 -0
- package/sidecar/dist/src/type-inferrer.d.ts +940 -0
- package/sidecar/dist/src/type-inferrer.js +3540 -0
- package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
- package/sidecar/dist/src/type-structural-expander.js +283 -0
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
- package/sidecar/dist/src/types.d.ts +669 -0
- package/sidecar/dist/src/types.js +5 -0
- package/sidecar/dist/src/validators.d.ts +2214 -0
- package/sidecar/dist/src/validators.js +336 -0
- package/sidecar/package.json +6 -0
- package/templates/carrick.json +7 -0
- package/templates/carrick.yml +45 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monorepo Builder - Builds synthetic monorepo workspaces for type compatibility checking
|
|
3
|
+
*
|
|
4
|
+
* This module implements the "Synthetic Monorepo (Stub Snapshot)" architecture:
|
|
5
|
+
* - Creates a workspace with stub packages for each repo
|
|
6
|
+
* - Each stub package has its own isolated node_modules with pinned dependencies
|
|
7
|
+
* - Generates path mappings so @carrick/{repoName}/{spec} resolves correctly
|
|
8
|
+
* - Creates a checker package that imports surfaces and asserts type compatibility
|
|
9
|
+
*
|
|
10
|
+
* Key design decisions:
|
|
11
|
+
* - Uses pnpm for better isolation between packages
|
|
12
|
+
* - Does NOT copy full repos - only uses captured artifacts
|
|
13
|
+
* - Deterministic: uses pinned dependency snapshots
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { execSync } from 'node:child_process';
|
|
18
|
+
/**
|
|
19
|
+
* Default workspace root path
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_WORKSPACE_ROOT = '.carrick/workspace';
|
|
22
|
+
/**
|
|
23
|
+
* MonorepoBuilder - Creates synthetic workspaces for cross-repo type checking
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* const builder = new MonorepoBuilder();
|
|
27
|
+
* const result = builder.build(repos);
|
|
28
|
+
* const checkResult = builder.checkCompatibility(workspacePath, checks);
|
|
29
|
+
*/
|
|
30
|
+
export class MonorepoBuilder {
|
|
31
|
+
/**
|
|
32
|
+
* Build the synthetic monorepo workspace
|
|
33
|
+
*
|
|
34
|
+
* @param repos - Metadata for each repository to include
|
|
35
|
+
* @param workspaceRoot - Root directory for the workspace (defaults to .carrick/workspace)
|
|
36
|
+
* @returns WorkspaceBuildResult
|
|
37
|
+
*/
|
|
38
|
+
build(repos, workspaceRoot) {
|
|
39
|
+
const root = workspaceRoot || DEFAULT_WORKSPACE_ROOT;
|
|
40
|
+
const errors = [];
|
|
41
|
+
const stubPackages = [];
|
|
42
|
+
try {
|
|
43
|
+
// Phase 1: Clean and create workspace root
|
|
44
|
+
this.log(`Creating workspace at: ${root}`);
|
|
45
|
+
this.ensureCleanDirectory(root);
|
|
46
|
+
// Phase 2: Create root workspace files
|
|
47
|
+
this.createRootPackageJson(root);
|
|
48
|
+
this.createPnpmWorkspaceYaml(root);
|
|
49
|
+
this.createNpmrc(root);
|
|
50
|
+
// Phase 3: Create stub packages for each repo
|
|
51
|
+
for (const repo of repos) {
|
|
52
|
+
const packagePath = this.createStubPackage(root, repo);
|
|
53
|
+
stubPackages.push(packagePath);
|
|
54
|
+
}
|
|
55
|
+
// Phase 4: Create checker package
|
|
56
|
+
const checkerPath = this.createCheckerPackage(root, repos);
|
|
57
|
+
// Phase 5: Install dependencies
|
|
58
|
+
this.installDependencies(root);
|
|
59
|
+
this.log(`Workspace built successfully at: ${root}`);
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
workspace_path: path.resolve(root),
|
|
63
|
+
stub_packages: stubPackages.map((p) => path.resolve(p)),
|
|
64
|
+
checker_path: path.resolve(checkerPath),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
69
|
+
this.logError(`Workspace build failed: ${error}`);
|
|
70
|
+
errors.push(error);
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
errors,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Run type compatibility checks in the workspace
|
|
79
|
+
*
|
|
80
|
+
* @param workspaceRoot - Path to the workspace root
|
|
81
|
+
* @param checks - Compatibility checks to run
|
|
82
|
+
* @returns CompatibilityCheckResult
|
|
83
|
+
*/
|
|
84
|
+
checkCompatibility(workspaceRoot, checks) {
|
|
85
|
+
const errors = [];
|
|
86
|
+
const results = [];
|
|
87
|
+
const diagnostics = [];
|
|
88
|
+
try {
|
|
89
|
+
// Generate check assertions file
|
|
90
|
+
const checkerDir = path.join(workspaceRoot, 'checker');
|
|
91
|
+
const checkFilePath = path.join(checkerDir, 'src', 'checks.ts');
|
|
92
|
+
this.generateCheckFile(checkFilePath, checks);
|
|
93
|
+
// Run tsc on the checker package
|
|
94
|
+
const tscResult = this.runTypeCheck(checkerDir);
|
|
95
|
+
// Parse results
|
|
96
|
+
for (const check of checks) {
|
|
97
|
+
const result = this.parseCheckResult(check, tscResult);
|
|
98
|
+
results.push(result);
|
|
99
|
+
if (!result.compatible && result.diagnostic) {
|
|
100
|
+
diagnostics.push(result.diagnostic);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
results,
|
|
106
|
+
diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
111
|
+
this.logError(`Compatibility check failed: ${error}`);
|
|
112
|
+
errors.push(error);
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
errors,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ===========================================================================
|
|
120
|
+
// Workspace Creation
|
|
121
|
+
// ===========================================================================
|
|
122
|
+
/**
|
|
123
|
+
* Ensure a clean directory exists at the given path
|
|
124
|
+
*/
|
|
125
|
+
ensureCleanDirectory(dirPath) {
|
|
126
|
+
if (fs.existsSync(dirPath)) {
|
|
127
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Create the root package.json for the workspace
|
|
133
|
+
*/
|
|
134
|
+
createRootPackageJson(root) {
|
|
135
|
+
const packageJson = {
|
|
136
|
+
name: '@carrick/workspace',
|
|
137
|
+
version: '0.0.0',
|
|
138
|
+
private: true,
|
|
139
|
+
description: 'Synthetic workspace for Carrick type compatibility checking',
|
|
140
|
+
workspaces: [
|
|
141
|
+
'packages/*',
|
|
142
|
+
'checker',
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
const filePath = path.join(root, 'package.json');
|
|
146
|
+
fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
147
|
+
this.log(`Created: ${filePath}`);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create pnpm-workspace.yaml
|
|
151
|
+
*/
|
|
152
|
+
createPnpmWorkspaceYaml(root) {
|
|
153
|
+
const content = `# Carrick synthetic workspace
|
|
154
|
+
packages:
|
|
155
|
+
- 'packages/*'
|
|
156
|
+
- 'checker'
|
|
157
|
+
`;
|
|
158
|
+
const filePath = path.join(root, 'pnpm-workspace.yaml');
|
|
159
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
160
|
+
this.log(`Created: ${filePath}`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Create .npmrc to prevent hoisting that would blur package boundaries
|
|
164
|
+
*/
|
|
165
|
+
createNpmrc(root) {
|
|
166
|
+
const content = `# Prevent hoisting to maintain per-package isolation
|
|
167
|
+
node-linker=isolated
|
|
168
|
+
hoist=false
|
|
169
|
+
shamefully-hoist=false
|
|
170
|
+
`;
|
|
171
|
+
const filePath = path.join(root, '.npmrc');
|
|
172
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
173
|
+
this.log(`Created: ${filePath}`);
|
|
174
|
+
}
|
|
175
|
+
// ===========================================================================
|
|
176
|
+
// Stub Package Creation
|
|
177
|
+
// ===========================================================================
|
|
178
|
+
/**
|
|
179
|
+
* Create a stub package for a repository
|
|
180
|
+
*
|
|
181
|
+
* @returns Path to the created package
|
|
182
|
+
*/
|
|
183
|
+
createStubPackage(root, repo) {
|
|
184
|
+
const packageDir = path.join(root, 'packages', repo.repoName);
|
|
185
|
+
const srcDir = path.join(packageDir, 'src');
|
|
186
|
+
// Create directories
|
|
187
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
188
|
+
// Create package.json from pinned dependencies
|
|
189
|
+
this.createStubPackageJson(packageDir, repo);
|
|
190
|
+
// Create tsconfig.json from snapshot
|
|
191
|
+
this.createStubTsconfig(packageDir, repo);
|
|
192
|
+
// Create surface.d.ts if provided
|
|
193
|
+
if (repo.surfaceContent) {
|
|
194
|
+
this.createSurfaceFile(srcDir, repo.surfaceContent);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Create placeholder
|
|
198
|
+
this.createPlaceholderSurface(srcDir, repo.repoName);
|
|
199
|
+
}
|
|
200
|
+
this.log(`Created stub package: ${packageDir}`);
|
|
201
|
+
return packageDir;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Create package.json for a stub package
|
|
205
|
+
*/
|
|
206
|
+
createStubPackageJson(packageDir, repo) {
|
|
207
|
+
const packageJson = {
|
|
208
|
+
name: `@carrick/${repo.repoName}`,
|
|
209
|
+
version: '0.0.0',
|
|
210
|
+
private: true,
|
|
211
|
+
description: `Stub package for ${repo.repoName} type checking`,
|
|
212
|
+
main: './src/surface.d.ts',
|
|
213
|
+
types: './src/surface.d.ts',
|
|
214
|
+
dependencies: {},
|
|
215
|
+
};
|
|
216
|
+
// Add pinned dependencies
|
|
217
|
+
if (repo.dependencies) {
|
|
218
|
+
packageJson.dependencies = { ...repo.dependencies };
|
|
219
|
+
}
|
|
220
|
+
const filePath = path.join(packageDir, 'package.json');
|
|
221
|
+
fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create tsconfig.json for a stub package from the snapshot
|
|
225
|
+
*/
|
|
226
|
+
createStubTsconfig(packageDir, repo) {
|
|
227
|
+
const snapshot = repo.tsconfig || this.getDefaultTsconfigSnapshot();
|
|
228
|
+
const tsconfig = {
|
|
229
|
+
compilerOptions: {
|
|
230
|
+
...snapshot.compilerOptions,
|
|
231
|
+
// Override for surface checking
|
|
232
|
+
declaration: true,
|
|
233
|
+
declarationMap: false,
|
|
234
|
+
emitDeclarationOnly: true,
|
|
235
|
+
noEmit: false,
|
|
236
|
+
skipLibCheck: true,
|
|
237
|
+
// Ensure we can import the surface
|
|
238
|
+
rootDir: './src',
|
|
239
|
+
outDir: './dist',
|
|
240
|
+
},
|
|
241
|
+
include: ['src/**/*'],
|
|
242
|
+
exclude: ['node_modules'],
|
|
243
|
+
};
|
|
244
|
+
const filePath = path.join(packageDir, 'tsconfig.json');
|
|
245
|
+
fs.writeFileSync(filePath, JSON.stringify(tsconfig, null, 2), 'utf-8');
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Create the surface.d.ts file
|
|
249
|
+
*/
|
|
250
|
+
createSurfaceFile(srcDir, content) {
|
|
251
|
+
const filePath = path.join(srcDir, 'surface.d.ts');
|
|
252
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Create a placeholder surface file
|
|
256
|
+
*/
|
|
257
|
+
createPlaceholderSurface(srcDir, repoName) {
|
|
258
|
+
const content = `// Placeholder surface for ${repoName}
|
|
259
|
+
// This file should be replaced with actual extracted types
|
|
260
|
+
|
|
261
|
+
export type Placeholder = unknown;
|
|
262
|
+
`;
|
|
263
|
+
const filePath = path.join(srcDir, 'surface.d.ts');
|
|
264
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get default tsconfig snapshot
|
|
268
|
+
*/
|
|
269
|
+
getDefaultTsconfigSnapshot() {
|
|
270
|
+
return {
|
|
271
|
+
compilerOptions: {
|
|
272
|
+
module: 'NodeNext',
|
|
273
|
+
moduleResolution: 'NodeNext',
|
|
274
|
+
target: 'ES2022',
|
|
275
|
+
lib: ['ES2022'],
|
|
276
|
+
strict: true,
|
|
277
|
+
esModuleInterop: true,
|
|
278
|
+
skipLibCheck: true,
|
|
279
|
+
declaration: true,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
// ===========================================================================
|
|
284
|
+
// Checker Package Creation
|
|
285
|
+
// ===========================================================================
|
|
286
|
+
/**
|
|
287
|
+
* Create the checker package that runs type compatibility checks
|
|
288
|
+
*
|
|
289
|
+
* @returns Path to the checker package
|
|
290
|
+
*/
|
|
291
|
+
createCheckerPackage(root, repos) {
|
|
292
|
+
const checkerDir = path.join(root, 'checker');
|
|
293
|
+
const srcDir = path.join(checkerDir, 'src');
|
|
294
|
+
// Create directories
|
|
295
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
296
|
+
// Create package.json
|
|
297
|
+
this.createCheckerPackageJson(checkerDir, repos);
|
|
298
|
+
// Create tsconfig.json with path mappings
|
|
299
|
+
this.createCheckerTsconfig(checkerDir, repos);
|
|
300
|
+
// Create initial checks file
|
|
301
|
+
this.createInitialChecksFile(srcDir);
|
|
302
|
+
this.log(`Created checker package: ${checkerDir}`);
|
|
303
|
+
return checkerDir;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Create package.json for the checker package
|
|
307
|
+
*/
|
|
308
|
+
createCheckerPackageJson(checkerDir, repos) {
|
|
309
|
+
const dependencies = {};
|
|
310
|
+
// Add workspace dependencies to each stub package
|
|
311
|
+
for (const repo of repos) {
|
|
312
|
+
dependencies[`@carrick/${repo.repoName}`] = 'workspace:*';
|
|
313
|
+
}
|
|
314
|
+
const packageJson = {
|
|
315
|
+
name: '@carrick/checker',
|
|
316
|
+
version: '0.0.0',
|
|
317
|
+
private: true,
|
|
318
|
+
description: 'Type compatibility checker',
|
|
319
|
+
dependencies,
|
|
320
|
+
devDependencies: {
|
|
321
|
+
typescript: '^5.0.0',
|
|
322
|
+
},
|
|
323
|
+
scripts: {
|
|
324
|
+
check: 'tsc --noEmit',
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
const filePath = path.join(checkerDir, 'package.json');
|
|
328
|
+
fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Create tsconfig.json for the checker package with path mappings
|
|
332
|
+
*/
|
|
333
|
+
createCheckerTsconfig(checkerDir, repos) {
|
|
334
|
+
const paths = {};
|
|
335
|
+
for (const repo of repos) {
|
|
336
|
+
const repoName = repo.repoName;
|
|
337
|
+
// Surface import: @carrick/{repoName}/surface -> packages/{repoName}/src/surface.d.ts
|
|
338
|
+
paths[`@carrick/${repoName}/surface`] = [
|
|
339
|
+
`../packages/${repoName}/src/surface.d.ts`,
|
|
340
|
+
];
|
|
341
|
+
// Unscoped packages: @carrick/{repoName}/* -> packages/{repoName}/node_modules/*
|
|
342
|
+
paths[`@carrick/${repoName}/*`] = [
|
|
343
|
+
`../packages/${repoName}/node_modules/*`,
|
|
344
|
+
`../packages/${repoName}/node_modules/*/index.d.ts`,
|
|
345
|
+
`../packages/${repoName}/node_modules/@types/*`,
|
|
346
|
+
];
|
|
347
|
+
// Scoped packages: @carrick/{repoName}/@*/* -> packages/{repoName}/node_modules/@*/*
|
|
348
|
+
paths[`@carrick/${repoName}/@*/*`] = [
|
|
349
|
+
`../packages/${repoName}/node_modules/@*/*`,
|
|
350
|
+
`../packages/${repoName}/node_modules/@*/*/index.d.ts`,
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
const tsconfig = {
|
|
354
|
+
compilerOptions: {
|
|
355
|
+
module: 'NodeNext',
|
|
356
|
+
moduleResolution: 'NodeNext',
|
|
357
|
+
target: 'ES2022',
|
|
358
|
+
lib: ['ES2022'],
|
|
359
|
+
strict: true,
|
|
360
|
+
esModuleInterop: true,
|
|
361
|
+
skipLibCheck: true,
|
|
362
|
+
noEmit: true,
|
|
363
|
+
baseUrl: '.',
|
|
364
|
+
paths,
|
|
365
|
+
},
|
|
366
|
+
include: ['src/**/*'],
|
|
367
|
+
exclude: ['node_modules'],
|
|
368
|
+
};
|
|
369
|
+
const filePath = path.join(checkerDir, 'tsconfig.json');
|
|
370
|
+
fs.writeFileSync(filePath, JSON.stringify(tsconfig, null, 2), 'utf-8');
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Create initial checks.ts file
|
|
374
|
+
*/
|
|
375
|
+
createInitialChecksFile(srcDir) {
|
|
376
|
+
const content = `// Carrick Type Compatibility Checks
|
|
377
|
+
// This file is auto-generated - do not edit manually
|
|
378
|
+
|
|
379
|
+
// Type assertion helpers
|
|
380
|
+
type Assert<T extends true> = T;
|
|
381
|
+
type IsAssignable<X, Y> = [X] extends [Y] ? true : false;
|
|
382
|
+
type IsBidirectional<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;
|
|
383
|
+
|
|
384
|
+
// Checks will be generated here
|
|
385
|
+
export {};
|
|
386
|
+
`;
|
|
387
|
+
const filePath = path.join(srcDir, 'checks.ts');
|
|
388
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
389
|
+
}
|
|
390
|
+
// ===========================================================================
|
|
391
|
+
// Dependency Installation
|
|
392
|
+
// ===========================================================================
|
|
393
|
+
/**
|
|
394
|
+
* Install dependencies using pnpm
|
|
395
|
+
*/
|
|
396
|
+
installDependencies(root) {
|
|
397
|
+
this.log('Installing dependencies with pnpm...');
|
|
398
|
+
try {
|
|
399
|
+
// Check if pnpm is available
|
|
400
|
+
execSync('pnpm --version', { stdio: 'pipe' });
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
this.log('pnpm not found, trying npm...');
|
|
404
|
+
this.installWithNpm(root);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
execSync('pnpm install --frozen-lockfile=false', {
|
|
409
|
+
cwd: root,
|
|
410
|
+
stdio: 'pipe',
|
|
411
|
+
env: { ...process.env, CI: 'false' },
|
|
412
|
+
});
|
|
413
|
+
this.log('Dependencies installed successfully');
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// If frozen lockfile fails, try without it
|
|
417
|
+
try {
|
|
418
|
+
execSync('pnpm install', {
|
|
419
|
+
cwd: root,
|
|
420
|
+
stdio: 'pipe',
|
|
421
|
+
env: { ...process.env, CI: 'false' },
|
|
422
|
+
});
|
|
423
|
+
this.log('Dependencies installed successfully');
|
|
424
|
+
}
|
|
425
|
+
catch (installErr) {
|
|
426
|
+
const error = installErr instanceof Error ? installErr.message : String(installErr);
|
|
427
|
+
throw new Error(`Failed to install dependencies: ${error}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Fallback: Install dependencies using npm
|
|
433
|
+
*/
|
|
434
|
+
installWithNpm(root) {
|
|
435
|
+
try {
|
|
436
|
+
// Remove pnpm-workspace.yaml since npm doesn't understand it
|
|
437
|
+
const workspaceFile = path.join(root, 'pnpm-workspace.yaml');
|
|
438
|
+
if (fs.existsSync(workspaceFile)) {
|
|
439
|
+
fs.unlinkSync(workspaceFile);
|
|
440
|
+
}
|
|
441
|
+
execSync('npm install', {
|
|
442
|
+
cwd: root,
|
|
443
|
+
stdio: 'pipe',
|
|
444
|
+
env: { ...process.env, CI: 'false' },
|
|
445
|
+
});
|
|
446
|
+
this.log('Dependencies installed successfully with npm');
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
450
|
+
throw new Error(`Failed to install dependencies with npm: ${error}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ===========================================================================
|
|
454
|
+
// Compatibility Checking
|
|
455
|
+
// ===========================================================================
|
|
456
|
+
/**
|
|
457
|
+
* Generate the checks.ts file with type assertions
|
|
458
|
+
*/
|
|
459
|
+
generateCheckFile(filePath, checks) {
|
|
460
|
+
const lines = [
|
|
461
|
+
'// Carrick Type Compatibility Checks',
|
|
462
|
+
'// Auto-generated - do not edit manually',
|
|
463
|
+
'',
|
|
464
|
+
'// Type assertion helpers',
|
|
465
|
+
'type Assert<T extends true> = T;',
|
|
466
|
+
'type IsAssignable<X, Y> = [X] extends [Y] ? true : false;',
|
|
467
|
+
'type IsBidirectional<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false;',
|
|
468
|
+
'',
|
|
469
|
+
];
|
|
470
|
+
// Generate imports
|
|
471
|
+
const repoImports = new Map();
|
|
472
|
+
for (const check of checks) {
|
|
473
|
+
if (!repoImports.has(check.source_repo)) {
|
|
474
|
+
repoImports.set(check.source_repo, new Set());
|
|
475
|
+
}
|
|
476
|
+
repoImports.get(check.source_repo).add(check.source_alias);
|
|
477
|
+
if (!repoImports.has(check.target_repo)) {
|
|
478
|
+
repoImports.set(check.target_repo, new Set());
|
|
479
|
+
}
|
|
480
|
+
repoImports.get(check.target_repo).add(check.target_alias);
|
|
481
|
+
}
|
|
482
|
+
for (const [repo, aliases] of repoImports) {
|
|
483
|
+
const aliasesArray = Array.from(aliases);
|
|
484
|
+
// Create unique import names to avoid conflicts
|
|
485
|
+
const importNames = aliasesArray.map((alias) => `${alias} as ${this.makeImportName(repo, alias)}`);
|
|
486
|
+
lines.push(`import type { ${importNames.join(', ')} } from '@carrick/${repo}/surface';`);
|
|
487
|
+
}
|
|
488
|
+
lines.push('');
|
|
489
|
+
lines.push('// Compatibility checks');
|
|
490
|
+
// Generate type assertions
|
|
491
|
+
for (let i = 0; i < checks.length; i++) {
|
|
492
|
+
const check = checks[i];
|
|
493
|
+
const sourceType = this.makeImportName(check.source_repo, check.source_alias);
|
|
494
|
+
const targetType = this.makeImportName(check.target_repo, check.target_alias);
|
|
495
|
+
const checkName = `_check${i}`;
|
|
496
|
+
lines.push('');
|
|
497
|
+
lines.push(`// Check: ${check.source_repo}/${check.source_alias} vs ${check.target_repo}/${check.target_alias}`);
|
|
498
|
+
switch (check.direction) {
|
|
499
|
+
case 'source_extends_target':
|
|
500
|
+
lines.push(`type ${checkName} = Assert<IsAssignable<${sourceType}, ${targetType}>>;`);
|
|
501
|
+
break;
|
|
502
|
+
case 'target_extends_source':
|
|
503
|
+
lines.push(`type ${checkName} = Assert<IsAssignable<${targetType}, ${sourceType}>>;`);
|
|
504
|
+
break;
|
|
505
|
+
case 'bidirectional':
|
|
506
|
+
lines.push(`type ${checkName} = Assert<IsBidirectional<${sourceType}, ${targetType}>>;`);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
lines.push('');
|
|
511
|
+
lines.push('export {};');
|
|
512
|
+
const content = lines.join('\n');
|
|
513
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
514
|
+
this.log(`Generated check file: ${filePath}`);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Create a unique import name for a repo/alias combination
|
|
518
|
+
*/
|
|
519
|
+
makeImportName(repo, alias) {
|
|
520
|
+
const safeRepo = repo.replace(/[^a-zA-Z0-9]/g, '_');
|
|
521
|
+
const safeAlias = alias.replace(/[^a-zA-Z0-9]/g, '_');
|
|
522
|
+
return `${safeRepo}_${safeAlias}`;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Run TypeScript type checking on the checker package
|
|
526
|
+
*
|
|
527
|
+
* @returns Object with success status and any error output
|
|
528
|
+
*/
|
|
529
|
+
runTypeCheck(checkerDir) {
|
|
530
|
+
try {
|
|
531
|
+
execSync('npx tsc --noEmit', {
|
|
532
|
+
cwd: checkerDir,
|
|
533
|
+
stdio: 'pipe',
|
|
534
|
+
encoding: 'utf-8',
|
|
535
|
+
});
|
|
536
|
+
return { success: true, output: '' };
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
const error = err;
|
|
540
|
+
const output = error.stdout || error.stderr || error.message || 'Unknown error';
|
|
541
|
+
return { success: false, output };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Parse the result of a type check for a specific compatibility check
|
|
546
|
+
*/
|
|
547
|
+
parseCheckResult(check, tscResult) {
|
|
548
|
+
// If tsc succeeded, all checks passed
|
|
549
|
+
if (tscResult.success) {
|
|
550
|
+
return {
|
|
551
|
+
source_repo: check.source_repo,
|
|
552
|
+
source_alias: check.source_alias,
|
|
553
|
+
target_repo: check.target_repo,
|
|
554
|
+
target_alias: check.target_alias,
|
|
555
|
+
compatible: true,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// Look for errors related to this specific check
|
|
559
|
+
const escSrc = check.source_alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
560
|
+
const escTgt = check.target_alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
561
|
+
const checkPattern = new RegExp(`${escSrc}.*${escTgt}|${escTgt}.*${escSrc}`, 'i');
|
|
562
|
+
const isRelated = checkPattern.test(tscResult.output);
|
|
563
|
+
// If tsc failed but the error doesn't mention our aliases, treat as
|
|
564
|
+
// incompatible rather than silently reporting compatible (the check
|
|
565
|
+
// was not actually validated).
|
|
566
|
+
return {
|
|
567
|
+
source_repo: check.source_repo,
|
|
568
|
+
source_alias: check.source_alias,
|
|
569
|
+
target_repo: check.target_repo,
|
|
570
|
+
target_alias: check.target_alias,
|
|
571
|
+
compatible: false,
|
|
572
|
+
diagnostic: tscResult.output,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// ===========================================================================
|
|
576
|
+
// Logging
|
|
577
|
+
// ===========================================================================
|
|
578
|
+
log(message) {
|
|
579
|
+
console.error(`[sidecar:monorepo-builder] ${message}`);
|
|
580
|
+
}
|
|
581
|
+
logError(message) {
|
|
582
|
+
console.error(`[sidecar:monorepo-builder:error] ${message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|