proteum 2.1.9 → 2.2.0

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.
Files changed (79) hide show
  1. package/.codex/environments/environment.toml +11 -0
  2. package/AGENTS.md +25 -11
  3. package/README.md +19 -9
  4. package/agents/project/AGENTS.md +165 -120
  5. package/agents/project/CODING_STYLE.md +1 -1
  6. package/agents/project/app-root/AGENTS.md +16 -0
  7. package/agents/project/client/AGENTS.md +5 -5
  8. package/agents/project/client/pages/AGENTS.md +13 -13
  9. package/agents/project/diagnostics.md +19 -10
  10. package/agents/project/optimizations.md +5 -6
  11. package/agents/project/root/AGENTS.md +295 -0
  12. package/agents/project/server/routes/AGENTS.md +2 -2
  13. package/agents/project/server/services/AGENTS.md +4 -2
  14. package/agents/project/tests/AGENTS.md +2 -2
  15. package/cli/app/index.ts +31 -7
  16. package/cli/commands/configure.ts +226 -0
  17. package/cli/commands/dev.ts +0 -2
  18. package/cli/commands/diagnose.ts +33 -1
  19. package/cli/commands/explain.ts +1 -1
  20. package/cli/commands/migrate.ts +51 -0
  21. package/cli/commands/orient.ts +169 -0
  22. package/cli/commands/perf.ts +8 -1
  23. package/cli/commands/verify.ts +1003 -49
  24. package/cli/compiler/artifacts/manifest.ts +4 -4
  25. package/cli/compiler/artifacts/routing.ts +2 -2
  26. package/cli/compiler/artifacts/services.ts +12 -3
  27. package/cli/compiler/client/index.ts +65 -19
  28. package/cli/compiler/common/files/style.ts +47 -2
  29. package/cli/compiler/common/generatedRouteModules.ts +31 -38
  30. package/cli/compiler/common/index.ts +10 -0
  31. package/cli/compiler/common/proteumManifest.ts +1 -0
  32. package/cli/compiler/server/index.ts +34 -9
  33. package/cli/context.ts +6 -1
  34. package/cli/index.ts +7 -8
  35. package/cli/migrate/pageContract.ts +516 -0
  36. package/cli/paths.ts +47 -6
  37. package/cli/presentation/commands.ts +100 -10
  38. package/cli/presentation/devSession.ts +4 -6
  39. package/cli/presentation/help.ts +2 -2
  40. package/cli/presentation/ink.ts +10 -5
  41. package/cli/presentation/welcome.ts +2 -4
  42. package/cli/runtime/commands.ts +94 -1
  43. package/cli/scaffold/index.ts +2 -2
  44. package/cli/scaffold/templates.ts +4 -2
  45. package/cli/utils/agents.ts +273 -58
  46. package/client/dev/profiler/index.tsx +3 -2
  47. package/client/router.ts +10 -2
  48. package/client/services/router/index.tsx +6 -22
  49. package/common/dev/connect.ts +20 -4
  50. package/common/dev/console.ts +7 -0
  51. package/common/dev/contractsDoctor.ts +354 -0
  52. package/common/dev/diagnostics.ts +10 -7
  53. package/common/dev/inspection.ts +830 -38
  54. package/common/dev/performance.ts +19 -5
  55. package/common/dev/profiler.ts +1 -0
  56. package/common/dev/proteumManifest.ts +5 -4
  57. package/common/dev/requestTrace.ts +12 -1
  58. package/common/router/contracts.ts +8 -11
  59. package/common/router/index.ts +2 -2
  60. package/common/router/pageData.ts +72 -0
  61. package/common/router/register.ts +10 -46
  62. package/common/router/response/page.ts +28 -16
  63. package/docs/dev-sessions.md +8 -4
  64. package/docs/diagnostics.md +77 -11
  65. package/docs/migrate-from-2.1.3.md +388 -0
  66. package/docs/request-tracing.md +25 -6
  67. package/package.json +6 -1
  68. package/scripts/update-codex-agents.ts +2 -2
  69. package/server/app/container/console/index.ts +11 -1
  70. package/server/app/container/trace/index.ts +117 -0
  71. package/server/app/devDiagnostics.ts +1 -1
  72. package/server/app/index.ts +5 -1
  73. package/server/services/auth/index.ts +9 -0
  74. package/server/services/router/index.ts +64 -14
  75. package/server/services/router/request/api.ts +7 -1
  76. package/server/services/router/response/index.ts +8 -28
  77. package/types/global/vendors.d.ts +12 -0
  78. package/types/vendors.d.ts +12 -0
  79. package/common/router/pageSetup.ts +0 -51
@@ -0,0 +1,516 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { parse } from '@babel/parser';
4
+ import generate from '@babel/generator';
5
+ import traverse, { type Binding, type NodePath } from '@babel/traverse';
6
+ import * as t from '@babel/types';
7
+
8
+ import { routeOptionKeys } from '../../common/router/pageData';
9
+
10
+ const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.cjs', '.mjs']);
11
+ const routeOptionKeysSet: ReadonlySet<string> = new Set(routeOptionKeys);
12
+
13
+ export type TPageContractManualFix = {
14
+ filepath: string;
15
+ routeLabel: string;
16
+ line: number;
17
+ column: number;
18
+ reason: string;
19
+ };
20
+
21
+ export type TPageContractMigrationSummary = {
22
+ appRoot: string;
23
+ changedFiles: string[];
24
+ dryRun: boolean;
25
+ manualFixes: TPageContractManualFix[];
26
+ scannedFiles: number;
27
+ };
28
+
29
+ type TDataAnalysis =
30
+ | {
31
+ kind: 'ok';
32
+ hasDataAfterMigration: boolean;
33
+ movedOptionProperties: t.ObjectProperty[];
34
+ }
35
+ | {
36
+ kind: 'manual';
37
+ reason: string;
38
+ };
39
+
40
+ const findFiles = (dir: string): string[] => {
41
+ if (!fs.existsSync(dir)) return [];
42
+
43
+ const files: string[] = [];
44
+
45
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
46
+ const filepath = path.join(dir, dirent.name);
47
+
48
+ if (dirent.isDirectory()) {
49
+ files.push(...findFiles(filepath));
50
+ continue;
51
+ }
52
+
53
+ if (dirent.isFile() && SUPPORTED_EXTENSIONS.has(path.extname(filepath))) files.push(filepath);
54
+ }
55
+
56
+ return files;
57
+ };
58
+
59
+ const isRouterPageCall = (callPath: NodePath<t.CallExpression>) => {
60
+ const calleePath = callPath.get('callee');
61
+
62
+ return (
63
+ calleePath.isMemberExpression() &&
64
+ calleePath.get('object').isIdentifier({ name: 'Router' }) &&
65
+ calleePath.get('property').isIdentifier({ name: 'page' })
66
+ );
67
+ };
68
+
69
+ const unwrapExpressionPath = (expressionPath: NodePath<t.Expression>): NodePath<t.Expression> => {
70
+ let currentPath = expressionPath;
71
+
72
+ while (true) {
73
+ if (currentPath.isTSAsExpression() || currentPath.isTSTypeAssertion()) {
74
+ currentPath = currentPath.get('expression') as NodePath<t.Expression>;
75
+ continue;
76
+ }
77
+
78
+ if (currentPath.isTSNonNullExpression() || currentPath.isParenthesizedExpression()) {
79
+ currentPath = currentPath.get('expression') as NodePath<t.Expression>;
80
+ continue;
81
+ }
82
+
83
+ return currentPath;
84
+ }
85
+ };
86
+
87
+ const getObjectPropertyKeyName = (property: t.ObjectProperty) => {
88
+ if (t.isIdentifier(property.key)) return property.key.name;
89
+ if (t.isStringLiteral(property.key)) return property.key.value;
90
+ return null;
91
+ };
92
+
93
+ const normalizeLegacyRouteOptionKey = (key: string) => {
94
+ if (routeOptionKeysSet.has(key)) return key;
95
+
96
+ if (key.startsWith('_')) {
97
+ const normalizedKey = key.slice(1);
98
+ if (routeOptionKeysSet.has(normalizedKey)) return normalizedKey;
99
+ }
100
+
101
+ return null;
102
+ };
103
+
104
+ const createRouteOptionProperty = (property: t.ObjectProperty, normalizedKey: string) => {
105
+ const value = t.cloneNode(property.value, true);
106
+ const shorthand = t.isIdentifier(value) && value.name === normalizedKey;
107
+
108
+ return t.objectProperty(t.identifier(normalizedKey), value, false, shorthand);
109
+ };
110
+
111
+ const getRouteLabel = (pathNode: t.Expression) =>
112
+ (t.isStringLiteral(pathNode) || t.isNumericLiteral(pathNode) ? String(pathNode.value) : generate(pathNode).code).trim();
113
+
114
+ const getRouteLocation = (callPath: NodePath<t.CallExpression>) => ({
115
+ line: callPath.node.loc?.start.line || 1,
116
+ column: callPath.node.loc?.start.column ? callPath.node.loc.start.column + 1 : 1,
117
+ });
118
+
119
+ const resolveFunctionPathFromBinding = (binding: Binding) => {
120
+ if (binding.path.isFunctionDeclaration()) {
121
+ return binding.path as NodePath<t.FunctionDeclaration>;
122
+ }
123
+
124
+ if (!binding.path.isVariableDeclarator()) return null;
125
+
126
+ const initPath = binding.path.get('init');
127
+ if (!initPath.node) return null;
128
+
129
+ const unwrappedInitPath = unwrapExpressionPath(initPath as NodePath<t.Expression>);
130
+ if (unwrappedInitPath.isFunctionExpression() || unwrappedInitPath.isArrowFunctionExpression()) return unwrappedInitPath;
131
+
132
+ return null;
133
+ };
134
+
135
+ const resolveObjectExpressionFromBinding = (binding: Binding) => {
136
+ if (!binding.path.isVariableDeclarator()) return null;
137
+
138
+ const initPath = binding.path.get('init');
139
+ if (!initPath.node) return null;
140
+
141
+ const unwrappedInitPath = unwrapExpressionPath(initPath as NodePath<t.Expression>);
142
+ if (unwrappedInitPath.isObjectExpression()) return unwrappedInitPath;
143
+
144
+ return null;
145
+ };
146
+
147
+ const classifyLegacySecondArg = (expressionPath: NodePath<t.Expression>) => {
148
+ const unwrappedPath = unwrapExpressionPath(expressionPath);
149
+
150
+ if (unwrappedPath.isObjectExpression()) return 'options' as const;
151
+ if (unwrappedPath.isArrowFunctionExpression() || unwrappedPath.isFunctionExpression()) return 'data' as const;
152
+
153
+ if (!unwrappedPath.isIdentifier()) return 'manual' as const;
154
+
155
+ const binding = expressionPath.scope.getBinding(unwrappedPath.node.name);
156
+ if (!binding) return 'manual' as const;
157
+
158
+ if (resolveFunctionPathFromBinding(binding)) return 'data' as const;
159
+ if (resolveObjectExpressionFromBinding(binding)) return 'options' as const;
160
+
161
+ return 'manual' as const;
162
+ };
163
+
164
+ const findReturnedObjectPath = (
165
+ functionPath: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
166
+ ) => {
167
+ if (functionPath.isArrowFunctionExpression() && !functionPath.get('body').isBlockStatement()) {
168
+ const bodyPath = unwrapExpressionPath(functionPath.get('body') as NodePath<t.Expression>);
169
+ if (bodyPath.isObjectExpression()) return { kind: 'ok' as const, objectPath: bodyPath };
170
+
171
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
172
+ }
173
+
174
+ const bodyPath = functionPath.get('body');
175
+ if (!bodyPath.isBlockStatement()) {
176
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
177
+ }
178
+
179
+ let returnedObjectPath: NodePath<t.ObjectExpression> | null = null;
180
+
181
+ for (const statementPath of bodyPath.get('body')) {
182
+ if (!statementPath.isReturnStatement()) continue;
183
+
184
+ const argumentPath = statementPath.get('argument');
185
+ if (!argumentPath.node) {
186
+ return { kind: 'manual' as const, reason: 'Data function cannot return without an object value.' };
187
+ }
188
+
189
+ const unwrappedArgumentPath = unwrapExpressionPath(argumentPath as NodePath<t.Expression>);
190
+ if (!unwrappedArgumentPath.isObjectExpression()) {
191
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
192
+ }
193
+
194
+ if (returnedObjectPath) {
195
+ return { kind: 'manual' as const, reason: 'Data function with multiple direct returns requires a manual rewrite.' };
196
+ }
197
+
198
+ returnedObjectPath = unwrappedArgumentPath;
199
+ }
200
+
201
+ if (!returnedObjectPath) {
202
+ return { kind: 'manual' as const, reason: 'Data function must directly return an object expression.' };
203
+ }
204
+
205
+ return { kind: 'ok' as const, objectPath: returnedObjectPath };
206
+ };
207
+
208
+ const analyzeFunctionPath = (
209
+ functionPath: NodePath<t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression>,
210
+ analysisCache: Map<string, TDataAnalysis>,
211
+ ): TDataAnalysis => {
212
+ const cacheKey = `${functionPath.node.start || 0}:${functionPath.node.end || 0}`;
213
+ const cachedResult = analysisCache.get(cacheKey);
214
+ if (cachedResult) return cachedResult;
215
+
216
+ const objectResult = findReturnedObjectPath(functionPath);
217
+ if (objectResult.kind === 'manual') {
218
+ const result: TDataAnalysis = { kind: 'manual', reason: objectResult.reason };
219
+ analysisCache.set(cacheKey, result);
220
+ return result;
221
+ }
222
+
223
+ const keptProperties: Array<t.ObjectProperty | t.SpreadElement> = [];
224
+ const movedOptionProperties: t.ObjectProperty[] = [];
225
+
226
+ for (const propertyPath of objectResult.objectPath.get('properties')) {
227
+ if (propertyPath.isSpreadElement()) {
228
+ const result: TDataAnalysis = {
229
+ kind: 'manual',
230
+ reason: 'Data function using object spreads requires a manual rewrite.',
231
+ };
232
+ analysisCache.set(cacheKey, result);
233
+ return result;
234
+ }
235
+
236
+ if (!propertyPath.isObjectProperty() || propertyPath.node.computed) {
237
+ const result: TDataAnalysis = {
238
+ kind: 'manual',
239
+ reason: 'Data function must return a plain object with explicit property keys.',
240
+ };
241
+ analysisCache.set(cacheKey, result);
242
+ return result;
243
+ }
244
+
245
+ const propertyKeyName = getObjectPropertyKeyName(propertyPath.node);
246
+ if (!propertyKeyName) {
247
+ const result: TDataAnalysis = {
248
+ kind: 'manual',
249
+ reason: 'Data function must return a plain object with explicit property keys.',
250
+ };
251
+ analysisCache.set(cacheKey, result);
252
+ return result;
253
+ }
254
+
255
+ const normalizedRouteOptionKey = normalizeLegacyRouteOptionKey(propertyKeyName);
256
+ if (normalizedRouteOptionKey) {
257
+ movedOptionProperties.push(createRouteOptionProperty(propertyPath.node, normalizedRouteOptionKey));
258
+ continue;
259
+ }
260
+
261
+ keptProperties.push(t.cloneNode(propertyPath.node, true));
262
+ }
263
+
264
+ if (movedOptionProperties.length > 0) objectResult.objectPath.node.properties = keptProperties;
265
+
266
+ const result: TDataAnalysis = {
267
+ kind: 'ok',
268
+ hasDataAfterMigration: keptProperties.length > 0,
269
+ movedOptionProperties,
270
+ };
271
+ analysisCache.set(cacheKey, result);
272
+ return result;
273
+ };
274
+
275
+ const analyzeDataExpression = (
276
+ expressionPath: NodePath<t.Expression>,
277
+ analysisCache: Map<string, TDataAnalysis>,
278
+ ): TDataAnalysis => {
279
+ const unwrappedPath = unwrapExpressionPath(expressionPath);
280
+
281
+ if (unwrappedPath.isNullLiteral()) {
282
+ return {
283
+ kind: 'ok',
284
+ hasDataAfterMigration: false,
285
+ movedOptionProperties: [],
286
+ };
287
+ }
288
+
289
+ if (unwrappedPath.isArrowFunctionExpression() || unwrappedPath.isFunctionExpression()) {
290
+ return analyzeFunctionPath(unwrappedPath, analysisCache);
291
+ }
292
+
293
+ if (!unwrappedPath.isIdentifier()) {
294
+ return {
295
+ kind: 'manual',
296
+ reason: 'Data provider must be a local function expression/reference or null.',
297
+ };
298
+ }
299
+
300
+ const binding = expressionPath.scope.getBinding(unwrappedPath.node.name);
301
+ if (!binding) {
302
+ return {
303
+ kind: 'manual',
304
+ reason: `Could not resolve local data provider "${unwrappedPath.node.name}".`,
305
+ };
306
+ }
307
+
308
+ const functionPath = resolveFunctionPathFromBinding(binding);
309
+ if (!functionPath) {
310
+ return {
311
+ kind: 'manual',
312
+ reason: `Data provider "${unwrappedPath.node.name}" is not a directly analyzable local function.`,
313
+ };
314
+ }
315
+
316
+ return analyzeFunctionPath(functionPath, analysisCache);
317
+ };
318
+
319
+ const cloneObjectProperties = (properties: t.ObjectProperty[]) =>
320
+ properties.map((property) => t.cloneNode(property, true));
321
+
322
+ const buildNextOptionsExpression = (optionsNode: t.Expression, movedOptionProperties: t.ObjectProperty[]) => {
323
+ if (movedOptionProperties.length === 0) return t.cloneNode(optionsNode, true);
324
+
325
+ if (t.isObjectExpression(optionsNode) && optionsNode.properties.length === 0) {
326
+ return t.objectExpression(cloneObjectProperties(movedOptionProperties));
327
+ }
328
+
329
+ return t.objectExpression([
330
+ t.spreadElement(t.cloneNode(optionsNode, true)),
331
+ ...cloneObjectProperties(movedOptionProperties),
332
+ ]);
333
+ };
334
+
335
+ const transformFile = (filepath: string) => {
336
+ const source = fs.readFileSync(filepath, 'utf8');
337
+ if (!source.includes('Router.page(')) {
338
+ return {
339
+ changed: false,
340
+ manualFixes: [] as TPageContractManualFix[],
341
+ output: source,
342
+ };
343
+ }
344
+
345
+ const ast = parse(source, {
346
+ sourceType: 'module',
347
+ errorRecovery: true,
348
+ plugins: ['typescript', 'jsx', 'decorators-legacy', 'classProperties'],
349
+ });
350
+
351
+ const routeCalls: NodePath<t.CallExpression>[] = [];
352
+ const manualFixes: TPageContractManualFix[] = [];
353
+ const analysisCache = new Map<string, TDataAnalysis>();
354
+
355
+ traverse(ast, {
356
+ CallExpression(callPath: NodePath<t.CallExpression>) {
357
+ if (!isRouterPageCall(callPath)) return;
358
+ routeCalls.push(callPath);
359
+ },
360
+ });
361
+
362
+ let mutated = false;
363
+
364
+ for (const callPath of routeCalls) {
365
+ const argumentPaths = callPath.get('arguments') as NodePath<t.Expression>[];
366
+ if (argumentPaths.length < 2 || argumentPaths.length > 4) {
367
+ const pathArgument = argumentPaths[0]?.node;
368
+ const routeLabel = pathArgument ? getRouteLabel(pathArgument) : '(unknown route)';
369
+ const location = getRouteLocation(callPath);
370
+
371
+ manualFixes.push({
372
+ filepath,
373
+ routeLabel,
374
+ line: location.line,
375
+ column: location.column,
376
+ reason: 'Unsupported Router.page signature.',
377
+ });
378
+ continue;
379
+ }
380
+
381
+ const pathArgument = argumentPaths[0].node;
382
+ const routeLabel = getRouteLabel(pathArgument);
383
+ const location = getRouteLocation(callPath);
384
+
385
+ let optionsNode: t.Expression;
386
+ let dataPath: NodePath<t.Expression> | null;
387
+ let renderNode: t.Expression;
388
+ const isExplicitSignature = argumentPaths.length === 4;
389
+
390
+ if (argumentPaths.length === 2) {
391
+ optionsNode = t.objectExpression([]);
392
+ dataPath = null;
393
+ renderNode = t.cloneNode(argumentPaths[1].node, true);
394
+ } else if (argumentPaths.length === 3) {
395
+ const secondArgKind = classifyLegacySecondArg(argumentPaths[1]);
396
+ if (secondArgKind === 'manual') {
397
+ manualFixes.push({
398
+ filepath,
399
+ routeLabel,
400
+ line: location.line,
401
+ column: location.column,
402
+ reason: 'Could not classify the legacy second Router.page argument as options or data.',
403
+ });
404
+ continue;
405
+ }
406
+
407
+ if (secondArgKind === 'options') {
408
+ optionsNode = t.cloneNode(argumentPaths[1].node, true);
409
+ dataPath = null;
410
+ renderNode = t.cloneNode(argumentPaths[2].node, true);
411
+ } else {
412
+ optionsNode = t.objectExpression([]);
413
+ dataPath = argumentPaths[1];
414
+ renderNode = t.cloneNode(argumentPaths[2].node, true);
415
+ }
416
+ } else {
417
+ optionsNode = t.cloneNode(argumentPaths[1].node, true);
418
+ dataPath = argumentPaths[2];
419
+ renderNode = t.cloneNode(argumentPaths[3].node, true);
420
+ }
421
+
422
+ let dataNode: t.Expression = t.nullLiteral();
423
+ let movedOptionProperties: t.ObjectProperty[] = [];
424
+ let shouldRewrite = !isExplicitSignature;
425
+
426
+ if (dataPath) {
427
+ const analysis = analyzeDataExpression(dataPath, analysisCache);
428
+ if (analysis.kind === 'manual') {
429
+ if (isExplicitSignature) continue;
430
+
431
+ manualFixes.push({
432
+ filepath,
433
+ routeLabel,
434
+ line: location.line,
435
+ column: location.column,
436
+ reason: analysis.reason,
437
+ });
438
+ continue;
439
+ }
440
+
441
+ movedOptionProperties = analysis.movedOptionProperties;
442
+ dataNode = analysis.hasDataAfterMigration ? t.cloneNode(dataPath.node, true) : t.nullLiteral();
443
+ shouldRewrite =
444
+ shouldRewrite ||
445
+ movedOptionProperties.length > 0 ||
446
+ (!analysis.hasDataAfterMigration && !unwrapExpressionPath(dataPath).isNullLiteral());
447
+ }
448
+
449
+ if (!shouldRewrite) continue;
450
+
451
+ callPath.node.arguments = [
452
+ t.cloneNode(pathArgument, true),
453
+ buildNextOptionsExpression(optionsNode, movedOptionProperties),
454
+ dataNode,
455
+ renderNode,
456
+ ];
457
+ mutated = true;
458
+ }
459
+
460
+ if (manualFixes.length > 0) {
461
+ return {
462
+ changed: false,
463
+ manualFixes,
464
+ output: source,
465
+ };
466
+ }
467
+
468
+ if (!mutated) {
469
+ return {
470
+ changed: false,
471
+ manualFixes,
472
+ output: source,
473
+ };
474
+ }
475
+
476
+ return {
477
+ changed: true,
478
+ manualFixes,
479
+ output: generate(ast, {
480
+ comments: true,
481
+ compact: false,
482
+ retainLines: false,
483
+ }).code,
484
+ };
485
+ };
486
+
487
+ export const runPageContractMigration = ({
488
+ appRoot,
489
+ dryRun,
490
+ }: {
491
+ appRoot: string;
492
+ dryRun: boolean;
493
+ }): TPageContractMigrationSummary => {
494
+ const pagesRoot = path.join(appRoot, 'client', 'pages');
495
+ const changedFiles: string[] = [];
496
+ const manualFixes: TPageContractManualFix[] = [];
497
+ const files = findFiles(pagesRoot);
498
+
499
+ for (const filepath of files) {
500
+ const result = transformFile(filepath);
501
+ manualFixes.push(...result.manualFixes);
502
+
503
+ if (!result.changed) continue;
504
+
505
+ changedFiles.push(filepath);
506
+ if (!dryRun) fs.writeFileSync(filepath, result.output);
507
+ }
508
+
509
+ return {
510
+ appRoot,
511
+ changedFiles,
512
+ dryRun,
513
+ manualFixes,
514
+ scannedFiles: files.length,
515
+ };
516
+ };
package/cli/paths.ts CHANGED
@@ -114,6 +114,20 @@ const findVisibleNodeModulesRoot = (startPath: string): string | undefined => {
114
114
  }
115
115
  };
116
116
 
117
+ const findVisibleNodeModulesRoots = (startPath: string): string[] => {
118
+ const roots: string[] = [];
119
+ let currentPath = path.resolve(startPath);
120
+
121
+ while (true) {
122
+ const candidate = path.join(currentPath, 'node_modules');
123
+ if (fs.existsSync(candidate) && !roots.includes(candidate)) roots.push(candidate);
124
+
125
+ const parentPath = path.dirname(currentPath);
126
+ if (parentPath === currentPath) return roots;
127
+ currentPath = parentPath;
128
+ }
129
+ };
130
+
117
131
  const findVisiblePackageInstall = (startPath: string, packageName: string): string | undefined => {
118
132
  let currentPath = path.resolve(startPath);
119
133
 
@@ -127,6 +141,20 @@ const findVisiblePackageInstall = (startPath: string, packageName: string): stri
127
141
  }
128
142
  };
129
143
 
144
+ const findVisiblePackageInstalls = (startPath: string, packageName: string): string[] => {
145
+ const installs: string[] = [];
146
+ let currentPath = path.resolve(startPath);
147
+
148
+ while (true) {
149
+ const candidate = path.join(currentPath, 'node_modules', packageName);
150
+ if (fs.existsSync(candidate) && !installs.includes(candidate)) installs.push(candidate);
151
+
152
+ const parentPath = path.dirname(currentPath);
153
+ if (parentPath === currentPath) return installs;
154
+ currentPath = parentPath;
155
+ }
156
+ };
157
+
130
158
  const resolveCoreRoot = (appRoot: string): string => {
131
159
  const currentPackageRoot = path.resolve(__dirname, '..');
132
160
  const currentBin = path.join(currentPackageRoot, 'cli', 'bin.js');
@@ -346,9 +374,18 @@ export default class Paths {
346
374
  return [
347
375
  this.framework.activeRoot,
348
376
  ...(this.framework.installedRoot ? [this.framework.installedRoot] : []),
377
+ ...this.getVisiblePackageInstallRoots('proteum'),
349
378
  ].filter((rootPath, index, list) => list.indexOf(rootPath) === index && fs.existsSync(rootPath));
350
379
  }
351
380
 
381
+ public getVisibleNodeModulesRootsForPath(startPath: string): string[] {
382
+ return findVisibleNodeModulesRoots(startPath);
383
+ }
384
+
385
+ public getVisiblePackageInstallRoots(packageName: string, startPath = this.appRoot): string[] {
386
+ return findVisiblePackageInstalls(startPath, packageName);
387
+ }
388
+
352
389
  public getFrameworkInstallRoot(): string {
353
390
  return this.getFrameworkInstallRootForAppRoot(this.appRoot);
354
391
  }
@@ -419,16 +456,20 @@ export default class Paths {
419
456
  }
420
457
 
421
458
  public resolvePackageRoot(packageName: string, { preferApp = true }: { preferApp?: boolean } = {}): string {
422
- const searchPaths = preferApp
423
- ? [this.appRoot, this.framework.activeRoot]
424
- : [this.framework.activeRoot, this.appRoot];
459
+ const installedRoot = this.framework.installedRoot;
460
+ const searchPaths = (preferApp
461
+ ? [this.appRoot, ...(installedRoot ? [installedRoot] : []), this.framework.activeRoot]
462
+ : [this.framework.activeRoot, ...(installedRoot ? [installedRoot] : []), this.appRoot]
463
+ ).filter((searchPath, index, list) => list.indexOf(searchPath) === index);
425
464
  return path.dirname(resolvePackageJsonPath(packageName, searchPaths));
426
465
  }
427
466
 
428
467
  public resolveRequest(request: string, { preferApp = true }: { preferApp?: boolean } = {}): string {
429
- const searchPaths = preferApp
430
- ? [this.appRoot, this.framework.activeRoot]
431
- : [this.framework.activeRoot, this.appRoot];
468
+ const installedRoot = this.framework.installedRoot;
469
+ const searchPaths = (preferApp
470
+ ? [this.appRoot, ...(installedRoot ? [installedRoot] : []), this.framework.activeRoot]
471
+ : [this.framework.activeRoot, ...(installedRoot ? [installedRoot] : []), this.appRoot]
472
+ ).filter((searchPath, index, list) => list.indexOf(searchPath) === index);
432
473
  return require.resolve(request, { paths: searchPaths });
433
474
  }
434
475