@memberjunction/codegen-lib 4.4.0 → 5.1.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 (47) hide show
  1. package/README.md +163 -0
  2. package/dist/Angular/angular-codegen.d.ts +12 -0
  3. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  4. package/dist/Angular/angular-codegen.js +78 -12
  5. package/dist/Angular/angular-codegen.js.map +1 -1
  6. package/dist/Angular/related-entity-components.d.ts.map +1 -1
  7. package/dist/Angular/related-entity-components.js +10 -3
  8. package/dist/Angular/related-entity-components.js.map +1 -1
  9. package/dist/Database/manage-metadata.d.ts +40 -0
  10. package/dist/Database/manage-metadata.d.ts.map +1 -1
  11. package/dist/Database/manage-metadata.js +103 -13
  12. package/dist/Database/manage-metadata.js.map +1 -1
  13. package/dist/Database/sql_codegen.d.ts +10 -3
  14. package/dist/Database/sql_codegen.d.ts.map +1 -1
  15. package/dist/Database/sql_codegen.js +79 -15
  16. package/dist/Database/sql_codegen.js.map +1 -1
  17. package/dist/EntityNameScanner/EntityNameScanner.d.ts +166 -0
  18. package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -0
  19. package/dist/EntityNameScanner/EntityNameScanner.js +758 -0
  20. package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -0
  21. package/dist/EntityNameScanner/HtmlEntityNameScanner.d.ts +86 -0
  22. package/dist/EntityNameScanner/HtmlEntityNameScanner.d.ts.map +1 -0
  23. package/dist/EntityNameScanner/HtmlEntityNameScanner.js +262 -0
  24. package/dist/EntityNameScanner/HtmlEntityNameScanner.js.map +1 -0
  25. package/dist/EntityNameScanner/MetadataNameScanner.d.ts +90 -0
  26. package/dist/EntityNameScanner/MetadataNameScanner.d.ts.map +1 -0
  27. package/dist/EntityNameScanner/MetadataNameScanner.js +426 -0
  28. package/dist/EntityNameScanner/MetadataNameScanner.js.map +1 -0
  29. package/dist/EntityNameScanner/entity-rename-map.d.ts +31 -0
  30. package/dist/EntityNameScanner/entity-rename-map.d.ts.map +1 -0
  31. package/dist/EntityNameScanner/entity-rename-map.js +3012 -0
  32. package/dist/EntityNameScanner/entity-rename-map.js.map +1 -0
  33. package/dist/Misc/action_subclasses_codegen.d.ts +2 -2
  34. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  35. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  36. package/dist/Misc/createNewUser.js +6 -6
  37. package/dist/Misc/createNewUser.js.map +1 -1
  38. package/dist/Misc/entity_subclasses_codegen.js +2 -2
  39. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +4 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/runCodeGen.d.ts.map +1 -1
  45. package/dist/runCodeGen.js +24 -3
  46. package/dist/runCodeGen.js.map +1 -1
  47. package/package.json +15 -15
@@ -0,0 +1,758 @@
1
+ /**
2
+ * AST-based scanner/fixer for hardcoded entity names AND class name references
3
+ * in TypeScript source code, for the MemberJunction v5.0 migration.
4
+ *
5
+ * Three replacement strategies (ported from tools/migrate-entity-refs.js):
6
+ *
7
+ * 1. **Class names** (regex with word boundaries):
8
+ * `ActionEntity` → `MJActionEntity`, `ActionSchema` → `MJActionSchema`,
9
+ * `ActionEntityType` → `MJActionEntityType`
10
+ *
11
+ * 2. **Multi-word entity names** (regex with quote boundaries):
12
+ * `'Action Categories'` → `'MJ: Action Categories'`
13
+ *
14
+ * 3. **Single-word entity names** (TypeScript AST):
15
+ * Only replaces string literals in confirmed entity-name contexts
16
+ * (EntityName properties, GetEntityObject args, RegisterClass decorators, etc.)
17
+ *
18
+ * Usage (via MJCLI):
19
+ * mj codegen 5-0-fix-entity-names --path packages/Angular
20
+ * mj codegen 5-0-fix-entity-names --path packages/Angular --fix
21
+ *
22
+ * Or programmatically:
23
+ * import { scanEntityNames } from '@memberjunction/codegen-lib';
24
+ * const result = await scanEntityNames({ TargetPath: './packages' });
25
+ */
26
+ import ts from 'typescript';
27
+ import * as fs from 'fs';
28
+ import * as path from 'path';
29
+ import { glob } from 'glob';
30
+ import { ENTITY_RENAME_MAP } from './entity-rename-map.js';
31
+ export { ENTITY_RENAME_MAP } from './entity-rename-map.js';
32
+ // ============================================================================
33
+ // Default Configuration
34
+ // ============================================================================
35
+ const DEFAULT_EXCLUDE_PATTERNS = [
36
+ '**/node_modules/**',
37
+ '**/dist/**',
38
+ '**/build/**',
39
+ '**/.git/**',
40
+ '**/__tests__/**',
41
+ '**/*.d.ts',
42
+ '**/*.spec.ts',
43
+ '**/*.test.ts',
44
+ '**/generated/**',
45
+ '**/Demos/**',
46
+ ];
47
+ /**
48
+ * Method names whose first string-literal argument is treated as an entity name.
49
+ */
50
+ const ENTITY_NAME_METHODS = new Set([
51
+ 'GetEntityObject',
52
+ 'GetEntityObjectByRecord',
53
+ 'GetEntityByName',
54
+ 'EntityByName',
55
+ 'OpenEntityRecord',
56
+ 'navigateToEntity',
57
+ 'BuildRelationshipViewParamsByEntityName',
58
+ 'NewRecordValues',
59
+ 'IsCurrentTab',
60
+ ]);
61
+ /**
62
+ * Property names that hold entity name strings in property assignments.
63
+ * Matches the original migrate-entity-refs.js ENTITY_NAME_PROPERTIES set.
64
+ */
65
+ const ENTITY_NAME_ASSIGNMENT_PROPS = new Set([
66
+ 'EntityName',
67
+ 'entityName',
68
+ 'Entity',
69
+ ]);
70
+ /**
71
+ * Property names that, when used in a `=== 'OldName'` or `!== 'OldName'`
72
+ * comparison, unambiguously indicate the string literal is an entity name.
73
+ *
74
+ * NOTE: `Name` is intentionally excluded — it's too generic and produces
75
+ * false positives. Instead, `.Name` comparisons are handled by targeted
76
+ * AST checks in classifyParentContext (Cases 5 and 6).
77
+ */
78
+ const ENTITY_NAME_COMPARISON_PROPS = new Set([
79
+ 'Entity',
80
+ 'EntityName',
81
+ 'LinkedEntity',
82
+ ]);
83
+ // ============================================================================
84
+ // Rename Map Construction
85
+ // ============================================================================
86
+ /** Escapes special regex characters in a string. */
87
+ function escapeRegExp(str) {
88
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
89
+ }
90
+ /**
91
+ * Builds class rename rules from the rename map entries.
92
+ * For each entry with classNameChanged=true, creates regex rules for:
93
+ * - OldClassNameEntityType → NewClassNameEntityType (zod inferred type)
94
+ * - OldClassNameSchema → NewClassNameSchema (zod schema constant)
95
+ * - OldClassNameEntity → NewClassNameEntity (class name itself)
96
+ *
97
+ * Uses negative lookbehind for / and . to avoid matching inside file paths
98
+ * (e.g., `import { Foo } from './custom/OldClassNameEntity'` should NOT rename the path).
99
+ */
100
+ export function buildClassRenameRules(entries) {
101
+ const rules = [];
102
+ for (const entry of entries) {
103
+ if (!entry.classNameChanged)
104
+ continue;
105
+ // Suffixes in longest-first order to avoid partial matches
106
+ const suffixes = ['EntityType', 'Schema', 'Entity'];
107
+ for (const suffix of suffixes) {
108
+ const oldName = entry.oldClassName + suffix;
109
+ const newName = entry.newClassName + suffix;
110
+ rules.push({
111
+ old: oldName,
112
+ new: newName,
113
+ pattern: new RegExp(`(?<![/.])\\b${escapeRegExp(oldName)}\\b`, 'g'),
114
+ });
115
+ }
116
+ }
117
+ // Sort longest-first to prevent shorter patterns from matching inside longer names
118
+ rules.sort((a, b) => b.old.length - a.old.length);
119
+ return rules;
120
+ }
121
+ /**
122
+ * Builds multi-word entity name regex rules.
123
+ * For each entry with nameChanged=true AND a multi-word old name,
124
+ * creates patterns that match the old name inside quotes.
125
+ */
126
+ export function buildMultiWordNameRules(entries) {
127
+ const rules = [];
128
+ for (const entry of entries) {
129
+ if (!entry.nameChanged)
130
+ continue;
131
+ if (!entry.oldName.includes(' '))
132
+ continue;
133
+ const escaped = escapeRegExp(entry.oldName);
134
+ rules.push({
135
+ old: entry.oldName,
136
+ new: entry.newName,
137
+ singleQuotePattern: new RegExp(`(?<=')${escaped}(?=')`, 'g'),
138
+ doubleQuotePattern: new RegExp(`(?<=")${escaped}(?=")`, 'g'),
139
+ backtickPattern: new RegExp(`(?<=\`)${escaped}(?=\`)`, 'g'),
140
+ });
141
+ }
142
+ // Sort longest-first
143
+ rules.sort((a, b) => b.old.length - a.old.length);
144
+ return rules;
145
+ }
146
+ /**
147
+ * Builds an entity name rename map (old → new) from the embedded data.
148
+ * This is the simple entity-name-only map used by the HTML and metadata scanners.
149
+ */
150
+ export function loadEmbeddedRenameMap() {
151
+ const map = new Map();
152
+ for (const entry of ENTITY_RENAME_MAP) {
153
+ if (entry.nameChanged) {
154
+ map.set(entry.oldName, entry.newName);
155
+ }
156
+ }
157
+ return map;
158
+ }
159
+ /**
160
+ * Parses entity_subclasses.ts to build a map of old entity names to new
161
+ * (MJ:-prefixed) entity names.
162
+ *
163
+ * Scans for `@RegisterClass(BaseEntity, 'MJ: SomeName')` decorators and
164
+ * creates a mapping: `'SomeName' -> 'MJ: SomeName'`.
165
+ */
166
+ export function buildEntityNameMap(entitySubclassesPath) {
167
+ const renameMap = new Map();
168
+ if (!fs.existsSync(entitySubclassesPath)) {
169
+ throw new Error(`Entity subclasses file not found: ${entitySubclassesPath}`);
170
+ }
171
+ const sourceText = fs.readFileSync(entitySubclassesPath, 'utf-8');
172
+ // Quick check
173
+ if (!sourceText.includes('RegisterClass')) {
174
+ return renameMap;
175
+ }
176
+ const sourceFile = ts.createSourceFile(entitySubclassesPath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
177
+ function visit(node) {
178
+ if (ts.isClassDeclaration(node)) {
179
+ const decorators = ts.getDecorators(node);
180
+ if (decorators) {
181
+ for (const decorator of decorators) {
182
+ if (!ts.isCallExpression(decorator.expression))
183
+ continue;
184
+ const callExpr = decorator.expression;
185
+ if (!ts.isIdentifier(callExpr.expression))
186
+ continue;
187
+ if (callExpr.expression.text !== 'RegisterClass')
188
+ continue;
189
+ const args = callExpr.arguments;
190
+ if (args.length < 2)
191
+ continue;
192
+ if (!ts.isIdentifier(args[0]) || args[0].text !== 'BaseEntity')
193
+ continue;
194
+ if (!ts.isStringLiteral(args[1]))
195
+ continue;
196
+ const newName = args[1].text;
197
+ if (!newName.startsWith('MJ: '))
198
+ continue;
199
+ const oldName = newName.substring(4); // Strip 'MJ: '
200
+ renameMap.set(oldName, newName);
201
+ }
202
+ }
203
+ }
204
+ ts.forEachChild(node, visit);
205
+ }
206
+ visit(sourceFile);
207
+ // Filter to only entries that actually changed names (some entities always
208
+ // had the MJ: prefix). Cross-reference against the embedded rename map.
209
+ const knownRenames = new Set();
210
+ for (const entry of ENTITY_RENAME_MAP) {
211
+ if (entry.nameChanged) {
212
+ knownRenames.add(entry.oldName);
213
+ }
214
+ }
215
+ for (const oldName of [...renameMap.keys()]) {
216
+ if (!knownRenames.has(oldName)) {
217
+ renameMap.delete(oldName);
218
+ }
219
+ }
220
+ return renameMap;
221
+ }
222
+ // ============================================================================
223
+ // Rename Map Resolution
224
+ // ============================================================================
225
+ /**
226
+ * Resolves the path to entity_subclasses.ts, trying common locations.
227
+ * Returns null if not found (caller should fall back to embedded data).
228
+ */
229
+ function resolveEntitySubclassesPath(basePath, explicitPath) {
230
+ if (explicitPath) {
231
+ const resolved = path.resolve(explicitPath);
232
+ if (fs.existsSync(resolved))
233
+ return resolved;
234
+ throw new Error(`Specified entity subclasses path does not exist: ${explicitPath}`);
235
+ }
236
+ // Try common locations relative to the target path
237
+ const candidates = [
238
+ // Monorepo layout (running from repo root or subdir)
239
+ path.resolve(basePath, 'packages/MJCoreEntities/src/generated/entity_subclasses.ts'),
240
+ path.resolve(basePath, '../packages/MJCoreEntities/src/generated/entity_subclasses.ts'),
241
+ path.resolve(basePath, '../../packages/MJCoreEntities/src/generated/entity_subclasses.ts'),
242
+ // npm consumer layout (node_modules)
243
+ path.resolve(basePath, 'node_modules/@memberjunction/core-entities/src/generated/entity_subclasses.ts'),
244
+ path.resolve(basePath, '../node_modules/@memberjunction/core-entities/src/generated/entity_subclasses.ts'),
245
+ ];
246
+ for (const candidate of candidates) {
247
+ if (fs.existsSync(candidate))
248
+ return candidate;
249
+ }
250
+ return null;
251
+ }
252
+ /**
253
+ * Builds the entity name rename map, trying entity_subclasses.ts first and
254
+ * falling back to the embedded rename map compiled into this package.
255
+ */
256
+ export function resolveEntityNameMap(basePath, explicitPath, verbose) {
257
+ const entitySubclassesPath = resolveEntitySubclassesPath(basePath, explicitPath);
258
+ if (entitySubclassesPath) {
259
+ if (verbose) {
260
+ console.log(`Building rename map from: ${entitySubclassesPath}`);
261
+ }
262
+ const renameMap = buildEntityNameMap(entitySubclassesPath);
263
+ if (verbose) {
264
+ console.log(`Loaded ${renameMap.size} entity name mappings`);
265
+ }
266
+ return renameMap;
267
+ }
268
+ // Fall back to the embedded data compiled into this package
269
+ if (verbose) {
270
+ console.log(`entity_subclasses.ts not found on disk, using embedded rename map`);
271
+ }
272
+ const renameMap = loadEmbeddedRenameMap();
273
+ if (verbose) {
274
+ console.log(`Loaded ${renameMap.size} entity name mappings from embedded data`);
275
+ }
276
+ return renameMap;
277
+ }
278
+ // ============================================================================
279
+ // AST Scanner (Strategy 3: Single-word entity names)
280
+ // ============================================================================
281
+ /**
282
+ * Extracts the method name from a call expression's callee.
283
+ * Handles both direct calls `GetEntityObject(...)` and member access
284
+ * `md.GetEntityObject(...)` or `this.service.OpenEntityRecord(...)`.
285
+ */
286
+ function getMethodName(expression) {
287
+ if (ts.isIdentifier(expression))
288
+ return expression.text;
289
+ if (ts.isPropertyAccessExpression(expression))
290
+ return expression.name.text;
291
+ return null;
292
+ }
293
+ /**
294
+ * Determines if a string literal node is in a relevant AST context
295
+ * (i.e., an argument to a known method, an EntityName property assignment,
296
+ * or a comparison against a known entity-name property).
297
+ */
298
+ function classifyParentContext(node) {
299
+ const parent = node.parent;
300
+ if (!parent)
301
+ return null;
302
+ // Case 1: Argument to a method call like GetEntityObject('Name') or OpenEntityRecord('Name')
303
+ if (ts.isCallExpression(parent)) {
304
+ const methodName = getMethodName(parent.expression);
305
+ if (methodName && ENTITY_NAME_METHODS.has(methodName)) {
306
+ if (methodName === 'GetEntityObject')
307
+ return 'GetEntityObject';
308
+ if (methodName === 'OpenEntityRecord')
309
+ return 'OpenEntityRecord';
310
+ return 'EntityNameMethod';
311
+ }
312
+ }
313
+ // Case 2: Property assignment like EntityName: 'Name' or entityName: 'Name' or Entity: 'Name'
314
+ if (ts.isPropertyAssignment(parent)) {
315
+ const propName = parent.name;
316
+ if (ts.isIdentifier(propName) && ENTITY_NAME_ASSIGNMENT_PROPS.has(propName.text)) {
317
+ return 'EntityNameProperty';
318
+ }
319
+ }
320
+ // Case 2b: Binary assignment like item.EntityName = 'Users' or item.entityName = 'Users'
321
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
322
+ if (parent.right === node && ts.isPropertyAccessExpression(parent.left)) {
323
+ const propName = parent.left.name.text;
324
+ if (ENTITY_NAME_ASSIGNMENT_PROPS.has(propName)) {
325
+ return 'EntityNameProperty';
326
+ }
327
+ }
328
+ }
329
+ // Case 3: Argument to @RegisterClass decorator
330
+ if (ts.isCallExpression(parent) && parent.parent && ts.isDecorator(parent.parent)) {
331
+ const callee = parent.expression;
332
+ if (ts.isIdentifier(callee) && callee.text === 'RegisterClass') {
333
+ return 'RegisterClass';
334
+ }
335
+ }
336
+ // Case 4: Comparison like .Entity === 'OldName' or .LinkedEntity === 'OldName'
337
+ if (ts.isBinaryExpression(parent)) {
338
+ const op = parent.operatorToken.kind;
339
+ if (op === ts.SyntaxKind.EqualsEqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsEqualsToken) {
340
+ const otherSide = parent.left === node ? parent.right : parent.left;
341
+ const propName = getTrailingPropertyName(otherSide);
342
+ if (propName && ENTITY_NAME_COMPARISON_PROPS.has(propName)) {
343
+ return 'NameComparison';
344
+ }
345
+ // Case 5: .EntityInfo.Name === 'OldName'
346
+ if (propName === 'Name' && isEntityInfoNameChain(otherSide)) {
347
+ return 'NameComparison';
348
+ }
349
+ // Case 6: .Entities.find(e => e.Name === 'OldName')
350
+ if (propName === 'Name' && isEntitiesArrayCallback(parent)) {
351
+ return 'NameComparison';
352
+ }
353
+ }
354
+ }
355
+ return null;
356
+ }
357
+ /**
358
+ * Extracts the trailing property name from an expression.
359
+ * For `foo.bar.Name` returns 'Name', for `e.Name` returns 'Name'.
360
+ */
361
+ function getTrailingPropertyName(expr) {
362
+ if (ts.isPropertyAccessExpression(expr)) {
363
+ return expr.name.text;
364
+ }
365
+ return null;
366
+ }
367
+ /**
368
+ * Checks if an expression is a `.EntityInfo.Name` property chain.
369
+ */
370
+ function isEntityInfoNameChain(expr) {
371
+ if (!ts.isPropertyAccessExpression(expr))
372
+ return false;
373
+ if (expr.name.text !== 'Name')
374
+ return false;
375
+ const parent = expr.expression;
376
+ if (ts.isPropertyAccessExpression(parent) && parent.name.text === 'EntityInfo') {
377
+ return true;
378
+ }
379
+ return false;
380
+ }
381
+ /**
382
+ * Checks if a binary expression is inside a `.find()` or `.filter()` callback
383
+ * on an array property named `Entities`.
384
+ */
385
+ function isEntitiesArrayCallback(binaryExpr) {
386
+ let current = binaryExpr;
387
+ while (current.parent) {
388
+ current = current.parent;
389
+ if (ts.isArrowFunction(current)) {
390
+ const callParent = current.parent;
391
+ if (ts.isCallExpression(callParent)) {
392
+ const callee = callParent.expression;
393
+ if (ts.isPropertyAccessExpression(callee)) {
394
+ const methodName = callee.name.text;
395
+ if (methodName === 'find' || methodName === 'filter') {
396
+ const obj = callee.expression;
397
+ const objProp = getTrailingPropertyName(obj);
398
+ if (objProp === 'Entities') {
399
+ return true;
400
+ }
401
+ }
402
+ }
403
+ }
404
+ return false;
405
+ }
406
+ if (ts.isBlock(current) || ts.isSourceFile(current)) {
407
+ return false;
408
+ }
409
+ }
410
+ return false;
411
+ }
412
+ // ============================================================================
413
+ // File Scanning
414
+ // ============================================================================
415
+ /**
416
+ * Scans a single TypeScript file for entity name and class name references
417
+ * that need updating for the v5.0 migration.
418
+ *
419
+ * Applies all three strategies:
420
+ * 1. Regex-based class name scanning
421
+ * 2. Regex-based multi-word entity name scanning
422
+ * 3. AST-based single-word entity name scanning
423
+ */
424
+ export function scanFile(filePath, sourceText, renameMap, classRules, multiWordRules) {
425
+ const findings = [];
426
+ // Quick check: does this file contain any old entity name or class name?
427
+ let hasRelevantContent = false;
428
+ for (const oldName of renameMap.keys()) {
429
+ if (sourceText.includes(oldName)) {
430
+ hasRelevantContent = true;
431
+ break;
432
+ }
433
+ }
434
+ // Also check for class names if rules are provided
435
+ if (!hasRelevantContent && classRules) {
436
+ for (const rule of classRules) {
437
+ if (sourceText.includes(rule.old)) {
438
+ hasRelevantContent = true;
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ // Also check for multi-word entity names if rules are provided
444
+ if (!hasRelevantContent && multiWordRules) {
445
+ for (const rule of multiWordRules) {
446
+ if (sourceText.includes(rule.old)) {
447
+ hasRelevantContent = true;
448
+ break;
449
+ }
450
+ }
451
+ }
452
+ if (!hasRelevantContent)
453
+ return findings;
454
+ // ── Strategy 1: Regex-based class name scanning ──────────────────────────
455
+ if (classRules) {
456
+ for (const rule of classRules) {
457
+ let match;
458
+ // Reset lastIndex for each file
459
+ rule.pattern.lastIndex = 0;
460
+ while ((match = rule.pattern.exec(sourceText)) !== null) {
461
+ const startPos = match.index;
462
+ const endPos = startPos + match[0].length;
463
+ const line = getLineNumber(sourceText, startPos);
464
+ const column = getColumnNumber(sourceText, startPos);
465
+ findings.push({
466
+ FilePath: filePath,
467
+ Line: line,
468
+ Column: column,
469
+ OldName: rule.old,
470
+ NewName: rule.new,
471
+ QuoteChar: '',
472
+ StartPos: startPos,
473
+ EndPos: endPos,
474
+ PatternKind: 'ClassName',
475
+ });
476
+ }
477
+ }
478
+ }
479
+ // ── Strategy 2: Regex-based multi-word entity name scanning ──────────────
480
+ if (multiWordRules) {
481
+ for (const rule of multiWordRules) {
482
+ const quotePatterns = [
483
+ { quoteChar: "'", pattern: rule.singleQuotePattern },
484
+ { quoteChar: '"', pattern: rule.doubleQuotePattern },
485
+ { quoteChar: '`', pattern: rule.backtickPattern },
486
+ ];
487
+ for (const { quoteChar, pattern } of quotePatterns) {
488
+ let match;
489
+ pattern.lastIndex = 0;
490
+ while ((match = pattern.exec(sourceText)) !== null) {
491
+ const startPos = match.index;
492
+ const endPos = startPos + match[0].length;
493
+ const line = getLineNumber(sourceText, startPos);
494
+ const column = getColumnNumber(sourceText, startPos);
495
+ findings.push({
496
+ FilePath: filePath,
497
+ Line: line,
498
+ Column: column,
499
+ OldName: rule.old,
500
+ NewName: rule.new,
501
+ QuoteChar: quoteChar,
502
+ StartPos: startPos,
503
+ EndPos: endPos,
504
+ PatternKind: 'MultiWordEntityName',
505
+ });
506
+ }
507
+ }
508
+ }
509
+ }
510
+ // ── Strategy 3: AST-based single-word entity name scanning ───────────────
511
+ const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, // setParentNodes
512
+ ts.ScriptKind.TS);
513
+ function visit(node) {
514
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
515
+ const text = node.text;
516
+ // Direct match: the entire string literal is an old entity name
517
+ if (renameMap.has(text)) {
518
+ const patternKind = classifyParentContext(node);
519
+ if (patternKind) {
520
+ const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
521
+ const startPos = node.getStart(sourceFile);
522
+ const endPos = node.getEnd();
523
+ const quoteChar = sourceText[startPos];
524
+ findings.push({
525
+ FilePath: filePath,
526
+ Line: line + 1,
527
+ Column: character,
528
+ OldName: text,
529
+ NewName: renameMap.get(text),
530
+ QuoteChar: quoteChar,
531
+ StartPos: startPos,
532
+ EndPos: endPos,
533
+ PatternKind: patternKind,
534
+ });
535
+ }
536
+ }
537
+ }
538
+ ts.forEachChild(node, visit);
539
+ }
540
+ visit(sourceFile);
541
+ return findings;
542
+ }
543
+ // ============================================================================
544
+ // Fixer
545
+ // ============================================================================
546
+ /** Priority map: AST-based findings are more precise than regex-based ones. */
547
+ const PATTERN_PRIORITY = {
548
+ 'ClassName': 1,
549
+ 'MultiWordEntityName': 2,
550
+ };
551
+ /**
552
+ * Removes overlapping findings to prevent output corruption when multiple
553
+ * strategies match the same text region. Keeps the highest-priority finding
554
+ * (AST-based patterns win over regex-based ones since they have proper
555
+ * context validation).
556
+ */
557
+ function deduplicateFindings(findings) {
558
+ if (findings.length <= 1)
559
+ return findings;
560
+ // Sort by StartPos ascending, then by priority descending (higher = better)
561
+ const sorted = [...findings].sort((a, b) => {
562
+ if (a.StartPos !== b.StartPos)
563
+ return a.StartPos - b.StartPos;
564
+ const aPri = PATTERN_PRIORITY[a.PatternKind] ?? 10;
565
+ const bPri = PATTERN_PRIORITY[b.PatternKind] ?? 10;
566
+ return bPri - aPri; // higher priority first
567
+ });
568
+ const result = [sorted[0]];
569
+ for (let i = 1; i < sorted.length; i++) {
570
+ const current = sorted[i];
571
+ const last = result[result.length - 1];
572
+ // If this finding overlaps with the previous kept finding, skip it
573
+ if (current.StartPos < last.EndPos)
574
+ continue;
575
+ result.push(current);
576
+ }
577
+ return result;
578
+ }
579
+ /**
580
+ * Applies fixes to a file by replacing old names with new names
581
+ * at the exact positions identified by the scanner.
582
+ *
583
+ * Processes findings from end to start to preserve byte offsets.
584
+ */
585
+ export function fixFile(sourceText, findings) {
586
+ if (findings.length === 0)
587
+ return sourceText;
588
+ // Deduplicate overlapping findings to prevent output corruption.
589
+ // When multiple strategies find the same entity name at overlapping positions,
590
+ // keep only the most specific one (AST > MultiWordEntityName > ClassName).
591
+ const deduped = deduplicateFindings(findings);
592
+ // Sort by StartPos descending so we fix from end to start
593
+ const sorted = [...deduped].sort((a, b) => b.StartPos - a.StartPos);
594
+ let result = sourceText;
595
+ for (const finding of sorted) {
596
+ if (finding.PatternKind === 'ClassName') {
597
+ // Class names: replace the identifier directly (no quotes)
598
+ const before = result.substring(0, finding.StartPos);
599
+ const after = result.substring(finding.EndPos);
600
+ result = before + finding.NewName + after;
601
+ }
602
+ else if (finding.PatternKind === 'MultiWordEntityName') {
603
+ // Multi-word names: replace just the entity name text (inside quotes)
604
+ const before = result.substring(0, finding.StartPos);
605
+ const after = result.substring(finding.EndPos);
606
+ result = before + finding.NewName + after;
607
+ }
608
+ else {
609
+ // AST-based: the StartPos/EndPos include quotes
610
+ const newLiteral = finding.QuoteChar + finding.NewName + finding.QuoteChar;
611
+ const before = result.substring(0, finding.StartPos);
612
+ const after = result.substring(finding.EndPos);
613
+ result = before + newLiteral + after;
614
+ }
615
+ }
616
+ return result;
617
+ }
618
+ // ============================================================================
619
+ // Utility Functions
620
+ // ============================================================================
621
+ /** Gets the 1-based line number for a character position in text. */
622
+ function getLineNumber(text, pos) {
623
+ let line = 1;
624
+ for (let i = 0; i < pos && i < text.length; i++) {
625
+ if (text[i] === '\n')
626
+ line++;
627
+ }
628
+ return line;
629
+ }
630
+ /** Gets the 0-based column number for a character position in text. */
631
+ function getColumnNumber(text, pos) {
632
+ let lastNewline = -1;
633
+ for (let i = 0; i < pos && i < text.length; i++) {
634
+ if (text[i] === '\n')
635
+ lastNewline = i;
636
+ }
637
+ return pos - lastNewline - 1;
638
+ }
639
+ // ============================================================================
640
+ // Main Entry Point
641
+ // ============================================================================
642
+ /**
643
+ * Scans TypeScript files for hardcoded entity names and class name references
644
+ * that need updating for the MemberJunction v5.0 migration, and optionally
645
+ * fixes them in place.
646
+ *
647
+ * Three strategies are applied:
648
+ * 1. Class name renames (regex): ActionEntity → MJActionEntity
649
+ * 2. Multi-word entity name renames (regex): 'AI Models' → 'MJ: AI Models'
650
+ * 3. Single-word entity name renames (AST): 'Actions' → 'MJ: Actions' (context-verified)
651
+ */
652
+ export async function scanEntityNames(options) {
653
+ const errors = [];
654
+ const verbose = options.Verbose !== false;
655
+ // Resolve target path
656
+ const targetPath = path.resolve(options.TargetPath);
657
+ if (!fs.existsSync(targetPath)) {
658
+ return {
659
+ Success: false,
660
+ Findings: [],
661
+ FixedFiles: [],
662
+ FilesScanned: 0,
663
+ RenameMapSize: 0,
664
+ Errors: [`Target path does not exist: ${targetPath}`],
665
+ };
666
+ }
667
+ // Build rename maps from embedded data
668
+ const renameEntries = ENTITY_RENAME_MAP;
669
+ const classRules = buildClassRenameRules(renameEntries);
670
+ const multiWordRules = buildMultiWordNameRules(renameEntries);
671
+ // The entity name map for AST scanning — SINGLE-WORD ONLY to avoid duplicate
672
+ // findings with Strategy 2 (multi-word names are handled exclusively by regex).
673
+ // When entity_subclasses.ts is available, buildEntityNameMap returns all entries
674
+ // (including multi-word). We filter to single-word to prevent overlapping matches.
675
+ let entityNameMap;
676
+ try {
677
+ const fullMap = resolveEntityNameMap(targetPath, options.EntitySubclassesPath, verbose);
678
+ // Filter to single-word names only for AST scanning
679
+ entityNameMap = new Map();
680
+ for (const [oldName, newName] of fullMap) {
681
+ if (!oldName.includes(' ')) {
682
+ entityNameMap.set(oldName, newName);
683
+ }
684
+ }
685
+ }
686
+ catch (err) {
687
+ return {
688
+ Success: false,
689
+ Findings: [],
690
+ FixedFiles: [],
691
+ FilesScanned: 0,
692
+ RenameMapSize: 0,
693
+ Errors: [err.message],
694
+ };
695
+ }
696
+ if (verbose) {
697
+ console.log(`Loaded ${classRules.length} class rename rules, ${multiWordRules.length} multi-word entity name rules, and ${entityNameMap.size} single-word AST rules`);
698
+ }
699
+ // Find TypeScript files
700
+ const isFile = fs.statSync(targetPath).isFile();
701
+ let tsFiles;
702
+ if (isFile) {
703
+ tsFiles = [targetPath];
704
+ }
705
+ else {
706
+ const excludePatterns = [
707
+ ...DEFAULT_EXCLUDE_PATTERNS,
708
+ ...(options.ExcludePatterns ?? []),
709
+ ];
710
+ tsFiles = await glob('**/*.ts', {
711
+ cwd: targetPath,
712
+ absolute: true,
713
+ ignore: excludePatterns,
714
+ });
715
+ }
716
+ if (verbose) {
717
+ console.log(`Scanning ${tsFiles.length} TypeScript files...`);
718
+ }
719
+ // Scan files
720
+ const allFindings = [];
721
+ const fixedFiles = [];
722
+ for (const filePath of tsFiles) {
723
+ try {
724
+ const sourceText = fs.readFileSync(filePath, 'utf-8');
725
+ const findings = scanFile(filePath, sourceText, entityNameMap, classRules, multiWordRules);
726
+ if (findings.length > 0) {
727
+ allFindings.push(...findings);
728
+ if (options.Fix) {
729
+ const fixedText = fixFile(sourceText, findings);
730
+ fs.writeFileSync(filePath, fixedText, 'utf-8');
731
+ fixedFiles.push(filePath);
732
+ if (verbose) {
733
+ console.log(` Fixed ${findings.length} reference(s) in ${filePath}`);
734
+ }
735
+ }
736
+ else if (verbose) {
737
+ console.log(` Found ${findings.length} reference(s) in ${filePath}`);
738
+ }
739
+ }
740
+ }
741
+ catch (err) {
742
+ const message = `Error scanning ${filePath}: ${err.message}`;
743
+ errors.push(message);
744
+ if (verbose) {
745
+ console.error(` ${message}`);
746
+ }
747
+ }
748
+ }
749
+ return {
750
+ Success: errors.length === 0,
751
+ Findings: allFindings,
752
+ FixedFiles: fixedFiles,
753
+ FilesScanned: tsFiles.length,
754
+ RenameMapSize: renameEntries.length,
755
+ Errors: errors,
756
+ };
757
+ }
758
+ //# sourceMappingURL=EntityNameScanner.js.map