d365fo-mcp 1.17.4 → 1.18.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.
- package/.github/copilot-instructions.md +1 -0
- package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +13 -11
- package/dist/bridge/bridgeAdapter.d.ts +25 -0
- package/dist/bridge/bridgeAdapter.js +42 -8
- package/dist/bridge/bridgeTypes.d.ts +14 -0
- package/dist/cli/ui.d.ts +9 -2
- package/dist/cli/ui.js +8 -1
- package/dist/config/settings.js +18 -0
- package/dist/metadata/modelDescriptor.d.ts +139 -2
- package/dist/metadata/modelDescriptor.js +249 -3
- package/dist/scripts/build-database.js +15 -0
- package/dist/scripts/build-fts.js +15 -0
- package/dist/scripts/extract-metadata.js +15 -0
- package/dist/server/toolSchemas/d365foFile.js +2 -1
- package/dist/tools/analysis/findReferences.js +75 -25
- package/dist/tools/analysis/validateObjectNaming.js +15 -4
- package/dist/tools/d365foFile.js +27 -8
- package/dist/tools/readers/classInfo.js +58 -6
- package/dist/tools/readers/getMethodSource.d.ts +12 -1
- package/dist/tools/readers/getMethodSource.js +75 -20
- package/dist/tools/readers/getWorkspaceInfo.js +53 -6
- package/dist/tools/sdlc/buildProject.js +90 -0
- package/dist/tools/sdlc/compilerMetadataPrune.d.ts +50 -0
- package/dist/tools/sdlc/compilerMetadataPrune.js +129 -0
- package/dist/tools/sdlc/updateSymbolIndex.d.ts +1 -0
- package/dist/tools/sdlc/updateSymbolIndex.js +26 -0
- package/dist/tools/smart/codeGen.js +47 -50
- package/dist/tools/specs/d365foFileOpSpecs.js +37 -0
- package/dist/tools/write/directXmlWriters.d.ts +31 -0
- package/dist/tools/write/directXmlWriters.js +129 -0
- package/dist/tools/write/formDataSourceNote.d.ts +22 -0
- package/dist/tools/write/formDataSourceNote.js +64 -0
- package/dist/tools/write/inlineIndexUpsert.js +6 -0
- package/dist/tools/write/inlineWriteVerification.d.ts +1 -1
- package/dist/tools/write/inlineWriteVerification.js +10 -1
- package/dist/tools/write/modifyD365File.d.ts +11 -0
- package/dist/tools/write/modifyD365File.js +150 -15
- package/dist/tools/write/preserveMetadataElements.js +1 -0
- package/dist/utils/indexedMethodSource.d.ts +76 -0
- package/dist/utils/indexedMethodSource.js +108 -0
- package/dist/utils/methodBodyHint.d.ts +12 -0
- package/dist/utils/methodBodyHint.js +12 -0
- package/dist/utils/modelClassifier.d.ts +26 -0
- package/dist/utils/modelClassifier.js +52 -3
- package/dist/utils/objectFileLookup.d.ts +41 -0
- package/dist/utils/objectFileLookup.js +84 -0
- package/dist/utils/objectNaming.js +12 -5
- package/dist/utils/objectNamingRules.d.ts +2 -0
- package/dist/utils/objectNamingRules.js +73 -5
- package/dist/utils/pathContainment.d.ts +7 -1
- package/dist/utils/pathContainment.js +39 -1
- package/dist/utils/toolProgressMessage.js +4 -0
- package/dist/utils/xmlScan.js +7 -1
- package/dist/workspace/projectMembership.d.ts +15 -0
- package/dist/workspace/projectMembership.js +17 -0
- package/package.json +1 -1
|
@@ -74,6 +74,7 @@ PowerShell / any terminal command **WILL HANG** in VS 2022 / VS 2026 MCP integra
|
|
|
74
74
|
- `model-name` → class `{Target}_{ModelName}_Extension`, element `{Target}.{ModelName}`
|
|
75
75
|
|
|
76
76
|
Pass the BASE object name to `d365fo_file(action="create")` and let the tool inject the token — don't hand-build the infix.
|
|
77
|
+
These patterns describe how the tool names extensions, and the tool applies them itself — editing them here does not change the names it writes. Change the style with `d365fo-mcp config naming`.
|
|
77
78
|
|
|
78
79
|
### Reuse & diff safety
|
|
79
80
|
|
|
@@ -3483,14 +3483,11 @@ namespace D365MetadataBridge.Services
|
|
|
3483
3483
|
?? throw new ArgumentException($"Form extension '{objectName}' not found");
|
|
3484
3484
|
var msi = GetModelSaveInfoForObject(_provider.FormExtensions, objectName);
|
|
3485
3485
|
|
|
3486
|
-
// Same idempotency rule as the form branch below: skip on a
|
|
3487
|
-
// and on a different-named data source already bound to the same table.
|
|
3486
|
+
// Same idempotency rule as the form branch below: skip on a NAME match only.
|
|
3488
3487
|
foreach (AxFormDataSourceRoot existing in axExt.DataSources)
|
|
3489
3488
|
{
|
|
3490
3489
|
if (string.Equals(existing.Name, dsName, StringComparison.OrdinalIgnoreCase))
|
|
3491
3490
|
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, skipped = true, reason = $"data source '{dsName}' already exists", api = "IMetaFormExtensionProvider.Update" };
|
|
3492
|
-
if (string.Equals(existing.Table, table, StringComparison.OrdinalIgnoreCase))
|
|
3493
|
-
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, skipped = true, reason = $"data source '{existing.Name}' already binds table '{table}'", api = "IMetaFormExtensionProvider.Update" };
|
|
3494
3491
|
}
|
|
3495
3492
|
|
|
3496
3493
|
axExt.DataSources.Add((AxFormDataSourceRoot)CreateFormDataSourceRoot(dsName, table, joinSource, linkType));
|
|
@@ -3506,18 +3503,23 @@ namespace D365MetadataBridge.Services
|
|
|
3506
3503
|
?? throw new ArgumentException($"Form '{objectName}' not found");
|
|
3507
3504
|
var msi = GetModelSaveInfoForObject(_provider.Forms, objectName);
|
|
3508
3505
|
|
|
3509
|
-
// Idempotency: don't append a duplicate.
|
|
3510
|
-
//
|
|
3511
|
-
// right table)
|
|
3512
|
-
//
|
|
3513
|
-
//
|
|
3506
|
+
// Idempotency: don't append a duplicate. A data source with the same NAME
|
|
3507
|
+
// already there is a real conflict (it may be a template stub already bound
|
|
3508
|
+
// to the right table), so skip that one.
|
|
3509
|
+
//
|
|
3510
|
+
// A different-named data source already bound to the same TABLE is NOT a
|
|
3511
|
+
// conflict and must still be written. This used to skip too, on the theory
|
|
3512
|
+
// that "adding a second binding to the same table is almost always an
|
|
3513
|
+
// accident" — which is simply untrue: a form routinely joins one table into
|
|
3514
|
+
// several branches of its query. Microsoft's own PurchLineBackOrder carries
|
|
3515
|
+
// PurchTable and PurchTable1, both over PurchTable, with different link
|
|
3516
|
+
// types. The rule silently dropped three legitimate data sources and, because
|
|
3517
|
+
// `skipped` never reached the caller, reported all three as added.
|
|
3514
3518
|
foreach (var existing in axForm.DataSources)
|
|
3515
3519
|
{
|
|
3516
3520
|
dynamic dyn = existing;
|
|
3517
3521
|
if (string.Equals((string)dyn.Name, dsName, StringComparison.OrdinalIgnoreCase))
|
|
3518
3522
|
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, skipped = true, reason = $"data source '{dsName}' already exists", api = "IMetaFormProvider.Update" };
|
|
3519
|
-
if (string.Equals((string)dyn.Table, table, StringComparison.OrdinalIgnoreCase))
|
|
3520
|
-
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, skipped = true, reason = $"data source '{(string)dyn.Name}' already binds table '{table}'", api = "IMetaFormProvider.Update" };
|
|
3521
3523
|
}
|
|
3522
3524
|
|
|
3523
3525
|
axForm.AddDataSource(CreateFormDataSourceRoot(dsName, table, joinSource, linkType));
|
|
@@ -234,6 +234,27 @@ export declare function bridgeResolveObject(bridge: BridgeClient | undefined, ob
|
|
|
234
234
|
export declare function unappliedSuffix(result: {
|
|
235
235
|
unsupportedProperties?: string[];
|
|
236
236
|
}): string;
|
|
237
|
+
/** What a bridge write returns when it declined to do anything. */
|
|
238
|
+
export interface BridgeSkippable {
|
|
239
|
+
skipped?: boolean;
|
|
240
|
+
reason?: string;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Renders a bridge result that declined the write, or null when it did write.
|
|
244
|
+
*
|
|
245
|
+
* The bridge has long been able to answer `{ success: true, skipped: true, reason }`
|
|
246
|
+
* — it did the honest thing and said it changed nothing. Nothing on this side read
|
|
247
|
+
* `skipped`, so every decline fell through the caller's `result.success` branch and
|
|
248
|
+
* was rendered "✅ … added": a write that never happened, reported as one, with the
|
|
249
|
+
* bridge's own explanation discarded on the way past.
|
|
250
|
+
*
|
|
251
|
+
* `success` from the bridge means "this did not fail", NOT "the object changed".
|
|
252
|
+
* Any wrapper whose operation can skip must ask this before claiming a write.
|
|
253
|
+
*
|
|
254
|
+
* Exactly the lesson unappliedSuffix() above was added for, one field over — which
|
|
255
|
+
* is why both live here together.
|
|
256
|
+
*/
|
|
257
|
+
export declare function skippedMessage(result: BridgeSkippable, subject: string): string | null;
|
|
237
258
|
/**
|
|
238
259
|
* Checks if bridge can handle this create operation.
|
|
239
260
|
*/
|
|
@@ -319,6 +340,7 @@ export declare function bridgeAddField(bridge: BridgeClient | undefined, tableNa
|
|
|
319
340
|
}): Promise<{
|
|
320
341
|
success: boolean;
|
|
321
342
|
message: string;
|
|
343
|
+
skipped?: boolean;
|
|
322
344
|
} | null>;
|
|
323
345
|
/**
|
|
324
346
|
* Sets a property via the C# bridge (IMetadataProvider.Update()).
|
|
@@ -425,6 +447,7 @@ export declare function bridgeRemoveFieldGroup(bridge: BridgeClient | undefined,
|
|
|
425
447
|
export declare function bridgeAddFieldToFieldGroup(bridge: BridgeClient | undefined, tableName: string, groupName: string, fieldName: string, extendBaseFieldGroup?: boolean): Promise<{
|
|
426
448
|
success: boolean;
|
|
427
449
|
message: string;
|
|
450
|
+
skipped?: boolean;
|
|
428
451
|
} | null>;
|
|
429
452
|
/**
|
|
430
453
|
* Modifies field properties on a table via the C# bridge.
|
|
@@ -488,6 +511,7 @@ export declare function bridgeAddControl(bridge: BridgeClient | undefined, formN
|
|
|
488
511
|
export declare function bridgeAddDataSource(bridge: BridgeClient | undefined, objectType: string, objectName: string, dsName: string, table: string, joinSource?: string, linkType?: string): Promise<{
|
|
489
512
|
success: boolean;
|
|
490
513
|
message: string;
|
|
514
|
+
skipped?: boolean;
|
|
491
515
|
} | null>;
|
|
492
516
|
/**
|
|
493
517
|
* Adds or updates a FieldModification entry in a table-extension via the C# bridge.
|
|
@@ -503,6 +527,7 @@ export declare function bridgeAddFieldModification(bridge: BridgeClient | undefi
|
|
|
503
527
|
export declare function bridgeAddMenuItemToMenu(bridge: BridgeClient | undefined, menuName: string, menuItemToAdd: string, menuItemToAddType?: string): Promise<{
|
|
504
528
|
success: boolean;
|
|
505
529
|
message: string;
|
|
530
|
+
skipped?: boolean;
|
|
506
531
|
} | null>;
|
|
507
532
|
/**
|
|
508
533
|
* Executes multiple write operations on a single object in one bridge call.
|
|
@@ -1254,6 +1254,9 @@ const BRIDGE_MODIFY_OPS = new Set([
|
|
|
1254
1254
|
// no concept of it — so this is XML-only for the same structural reason as the
|
|
1255
1255
|
// two above.
|
|
1256
1256
|
'remove-diagnostic-suppression', 'add-diagnostic-suppression',
|
|
1257
|
+
// A model descriptor is not an AOT object either — it is the package's own
|
|
1258
|
+
// manifest, outside the AOT entirely — so the same XML-only reasoning applies.
|
|
1259
|
+
'add-module-reference', 'remove-module-reference',
|
|
1257
1260
|
'add-display-method', 'add-table-method',
|
|
1258
1261
|
'add-field-modification', 'add-menu-item-to-menu',
|
|
1259
1262
|
// No C# op exists for query ranges on entities — served entirely by a
|
|
@@ -1296,6 +1299,8 @@ const XML_ONLY_MODIFY_PAIRS = {
|
|
|
1296
1299
|
'remove-entry-point': new Set(['security-privilege']),
|
|
1297
1300
|
'remove-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1298
1301
|
'add-diagnostic-suppression': new Set(['ignore-diagnostic-list']),
|
|
1302
|
+
'add-module-reference': new Set(['model-descriptor']),
|
|
1303
|
+
'remove-module-reference': new Set(['model-descriptor']),
|
|
1299
1304
|
// An AxReport has no bridge write path at all — IMetadataProvider cannot express
|
|
1300
1305
|
// a dataset field or a report parameter through a Dictionary<string,string> — and
|
|
1301
1306
|
// `report` is deliberately absent from BRIDGE_MODIFY_TYPES for that reason. Without
|
|
@@ -1317,6 +1322,30 @@ export function unappliedSuffix(result) {
|
|
|
1317
1322
|
return '';
|
|
1318
1323
|
return `\n⚠️ NOT applied — this object type has nowhere to store them: ${dropped.join(', ')}`;
|
|
1319
1324
|
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Renders a bridge result that declined the write, or null when it did write.
|
|
1327
|
+
*
|
|
1328
|
+
* The bridge has long been able to answer `{ success: true, skipped: true, reason }`
|
|
1329
|
+
* — it did the honest thing and said it changed nothing. Nothing on this side read
|
|
1330
|
+
* `skipped`, so every decline fell through the caller's `result.success` branch and
|
|
1331
|
+
* was rendered "✅ … added": a write that never happened, reported as one, with the
|
|
1332
|
+
* bridge's own explanation discarded on the way past.
|
|
1333
|
+
*
|
|
1334
|
+
* `success` from the bridge means "this did not fail", NOT "the object changed".
|
|
1335
|
+
* Any wrapper whose operation can skip must ask this before claiming a write.
|
|
1336
|
+
*
|
|
1337
|
+
* Exactly the lesson unappliedSuffix() above was added for, one field over — which
|
|
1338
|
+
* is why both live here together.
|
|
1339
|
+
*/
|
|
1340
|
+
export function skippedMessage(result, subject) {
|
|
1341
|
+
if (result.skipped !== true)
|
|
1342
|
+
return null;
|
|
1343
|
+
const reason = result.reason?.trim();
|
|
1344
|
+
return (`⏭️ NOT written — ${subject} was skipped by the bridge` +
|
|
1345
|
+
(reason ? `: ${reason}` : ' (no reason given)') +
|
|
1346
|
+
`\n Nothing changed on disk. If you meant to replace what is already there, ` +
|
|
1347
|
+
`remove or rename it first — re-sending this operation will skip again.`);
|
|
1348
|
+
}
|
|
1320
1349
|
/**
|
|
1321
1350
|
* Checks if bridge can handle this create operation.
|
|
1322
1351
|
*/
|
|
@@ -1480,9 +1509,10 @@ export async function bridgeAddField(bridge, tableName, fieldName, fieldType, ed
|
|
|
1480
1509
|
const result = await bridge.addField(tableName, fieldName, fieldType, edt, mandatory, label, mapped?.dataField, mapped?.dataSource, mapped?.fieldGroupName);
|
|
1481
1510
|
return {
|
|
1482
1511
|
success: result.success,
|
|
1483
|
-
|
|
1512
|
+
skipped: result.skipped === true,
|
|
1513
|
+
message: skippedMessage(result, `Field '${fieldName}'`) ?? (result.success
|
|
1484
1514
|
? `✅ Field '${fieldName}' added via ${result.api}`
|
|
1485
|
-
: `Bridge addField returned success=false
|
|
1515
|
+
: `Bridge addField returned success=false`),
|
|
1486
1516
|
};
|
|
1487
1517
|
}
|
|
1488
1518
|
catch (e) {
|
|
@@ -1839,9 +1869,10 @@ export async function bridgeAddFieldToFieldGroup(bridge, tableName, groupName, f
|
|
|
1839
1869
|
: '<FieldGroups>';
|
|
1840
1870
|
return {
|
|
1841
1871
|
success: result.success,
|
|
1842
|
-
|
|
1872
|
+
skipped: result.skipped === true,
|
|
1873
|
+
message: skippedMessage(result, `Field '${fieldName}' → group '${groupName}'`) ?? (result.success
|
|
1843
1874
|
? `✅ Field '${fieldName}' added to group '${groupName}' in ${target} via ${result.api}`
|
|
1844
|
-
: `Bridge addFieldToFieldGroup returned success=false
|
|
1875
|
+
: `Bridge addFieldToFieldGroup returned success=false`),
|
|
1845
1876
|
};
|
|
1846
1877
|
}
|
|
1847
1878
|
catch (e) {
|
|
@@ -2053,11 +2084,13 @@ export async function bridgeAddDataSource(bridge, objectType, objectName, dsName
|
|
|
2053
2084
|
return null;
|
|
2054
2085
|
try {
|
|
2055
2086
|
const result = await bridge.addDataSource(objectType, objectName, dsName, table, joinSource, linkType);
|
|
2087
|
+
const skip = skippedMessage(result, `DataSource '${dsName}'`);
|
|
2056
2088
|
return {
|
|
2057
2089
|
success: result.success,
|
|
2058
|
-
|
|
2090
|
+
skipped: result.skipped === true,
|
|
2091
|
+
message: skip ?? (result.success
|
|
2059
2092
|
? `✅ DataSource '${dsName}' added via ${result.api}`
|
|
2060
|
-
: `Bridge addDataSource returned success=false
|
|
2093
|
+
: `Bridge addDataSource returned success=false`),
|
|
2061
2094
|
};
|
|
2062
2095
|
}
|
|
2063
2096
|
catch (e) {
|
|
@@ -2106,9 +2139,10 @@ export async function bridgeAddMenuItemToMenu(bridge, menuName, menuItemToAdd, m
|
|
|
2106
2139
|
const result = await bridge.addMenuItemToMenu(menuName, menuItemToAdd, menuItemToAddType);
|
|
2107
2140
|
return {
|
|
2108
2141
|
success: result.success,
|
|
2109
|
-
|
|
2142
|
+
skipped: result.skipped === true,
|
|
2143
|
+
message: skippedMessage(result, `Menu item '${menuItemToAdd}'`) ?? (result.success
|
|
2110
2144
|
? `✅ Menu item '${menuItemToAdd}' added via ${result.api}`
|
|
2111
|
-
: `Bridge addMenuItemToMenu returned success=false
|
|
2145
|
+
: `Bridge addMenuItemToMenu returned success=false`),
|
|
2112
2146
|
};
|
|
2113
2147
|
}
|
|
2114
2148
|
catch (e) {
|
|
@@ -348,6 +348,20 @@ export interface BridgeWriteResult {
|
|
|
348
348
|
* create ops and on add-control; the write itself still succeeded.
|
|
349
349
|
*/
|
|
350
350
|
unsupportedProperties?: string[];
|
|
351
|
+
/**
|
|
352
|
+
* The bridge declined the operation and changed NOTHING — an element with that
|
|
353
|
+
* name is already there. `success` stays true because nothing failed, so a
|
|
354
|
+
* caller that reads only `success` reports a write that did not happen. Render
|
|
355
|
+
* it with skippedMessage() (bridgeAdapter.ts) rather than the success branch.
|
|
356
|
+
*
|
|
357
|
+
* Returned today by add-field (data-entity-extension), add-field-to-field-group,
|
|
358
|
+
* add-menu-item-to-menu and add-data-source. It was absent from this interface
|
|
359
|
+
* for as long as the bridge has been sending it, which is precisely why four
|
|
360
|
+
* wrappers could ignore it without so much as a type error.
|
|
361
|
+
*/
|
|
362
|
+
skipped?: boolean;
|
|
363
|
+
/** Why the bridge skipped, in its own words. Only set alongside `skipped`. */
|
|
364
|
+
reason?: string;
|
|
351
365
|
api?: string;
|
|
352
366
|
}
|
|
353
367
|
/** Result from createSmartTable — includes BP defaults summary */
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
7
|
import type { Option } from '@clack/prompts';
|
|
8
8
|
export { p };
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Unwrap a clack result; exit gracefully when the user cancelled.
|
|
11
|
+
*
|
|
12
|
+
* Typed as `Exclude<T, symbol>` rather than `(value: T | symbol): T`: since
|
|
13
|
+
* @clack/prompts 1.8.1 the prompts return `T | typeof CANCEL_SYMBOL`, a
|
|
14
|
+
* `unique symbol` that the `T | symbol` form no longer peeled off T, so every
|
|
15
|
+
* caller got the cancel symbol back in its type.
|
|
16
|
+
*/
|
|
17
|
+
export declare function ensure<T>(value: T): Exclude<T, symbol>;
|
|
11
18
|
export declare function askText(opts: {
|
|
12
19
|
message: string;
|
|
13
20
|
placeholder?: string;
|
package/dist/cli/ui.js
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
7
|
import { installOneLiner, isFullInstall } from './context.js';
|
|
8
8
|
export { p };
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Unwrap a clack result; exit gracefully when the user cancelled.
|
|
11
|
+
*
|
|
12
|
+
* Typed as `Exclude<T, symbol>` rather than `(value: T | symbol): T`: since
|
|
13
|
+
* @clack/prompts 1.8.1 the prompts return `T | typeof CANCEL_SYMBOL`, a
|
|
14
|
+
* `unique symbol` that the `T | symbol` form no longer peeled off T, so every
|
|
15
|
+
* caller got the cancel symbol back in its type.
|
|
16
|
+
*/
|
|
10
17
|
export function ensure(value) {
|
|
11
18
|
if (p.isCancel(value)) {
|
|
12
19
|
p.cancel('Cancelled.');
|
package/dist/config/settings.js
CHANGED
|
@@ -273,6 +273,24 @@ export const SETTINGS = [
|
|
|
273
273
|
{ value: 'model-name', hint: 'CustTable.ContosoRobotics — embeds the model name (VS default)' },
|
|
274
274
|
],
|
|
275
275
|
},
|
|
276
|
+
{
|
|
277
|
+
path: 'naming.extensionClassStyle',
|
|
278
|
+
env: 'EXTENSION_CLASS_NAMING_STYLE',
|
|
279
|
+
section: 'naming',
|
|
280
|
+
tier: 'advanced',
|
|
281
|
+
type: 'enum',
|
|
282
|
+
label: 'How extension CLASSES are named',
|
|
283
|
+
description: 'Overrides naming.extensionStyle for CoC extension classes only, leaving element extensions alone. ' +
|
|
284
|
+
'Some conventions spell the two differently — Visual Studio elements (CustTable.ContosoRobotics) with ' +
|
|
285
|
+
'prefix-style classes (CustTableCtso_Extension) — which a single style cannot express: it renames one ' +
|
|
286
|
+
'or the other. Leave on inherit unless your convention names classes differently from elements.',
|
|
287
|
+
default: 'inherit',
|
|
288
|
+
choices: [
|
|
289
|
+
{ value: 'inherit', hint: 'follow naming.extensionStyle — the default, behaviour unchanged' },
|
|
290
|
+
{ value: 'prefix', hint: 'CustTableCtso_Extension — embeds the extension prefix' },
|
|
291
|
+
{ value: 'model-name', hint: 'CustTable_ContosoRobotics_Extension — embeds the model name' },
|
|
292
|
+
],
|
|
293
|
+
},
|
|
276
294
|
// ── index ────────────────────────────────────────────────────────────────
|
|
277
295
|
{
|
|
278
296
|
path: 'index.extractMode',
|
|
@@ -16,8 +16,32 @@
|
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
18
|
* Extract every `<d2p1:string>` entry of a descriptor's `<ModuleReferences>`.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* Scoped to that ELEMENT, not scanned flat over the document. The flat scan this
|
|
21
|
+
* replaces reasoned only about `<ModelReferences>` — `i:nil` in every descriptor
|
|
22
|
+
* observed on a real box, so it contributes nothing — and missed the OTHER
|
|
23
|
+
* `<d2p1:string>` arrays a descriptor carries: `<InternalsVisibleTo>` and
|
|
24
|
+
* `<AppliedUpdates>`. Both are ordinary string lists, and `<InternalsVisibleTo>`
|
|
25
|
+
* sorts BEFORE `<ModuleReferences>` in the serialized document, so its entries
|
|
26
|
+
* came back as module references.
|
|
27
|
+
*
|
|
28
|
+
* Measured on a 10.0.2527 PackagesLocalDirectory plus the custom roots beside
|
|
29
|
+
* it: 112 of 176 descriptors over-reported, 606 phantom references in total.
|
|
30
|
+
* `Foundation.xml` answered 157 for a model that references 46;
|
|
31
|
+
* `ApplicationPlatform.xml` answered 19 for a model that references NONE.
|
|
32
|
+
*
|
|
33
|
+
* That is not cosmetic. Both readers of this function treat the result as the
|
|
34
|
+
* set of packages a model may see: build_d365fo_project orders its build queue
|
|
35
|
+
* by it, and getModelVisibility turns it into `visiblePackages`, the oracle
|
|
36
|
+
* resolve_references uses to decide an indexed type is invisible to the model
|
|
37
|
+
* being compiled. Every phantom entry made that oracle MORE permissive, so the
|
|
38
|
+
* missing-reference defect it exists to catch was silently waved through —
|
|
39
|
+
* which is the exact compile-time `classStr`/type failure this module's own
|
|
40
|
+
* header describes.
|
|
41
|
+
*
|
|
42
|
+
* Returns [] for a nil or absent element: a model that references nothing. The
|
|
43
|
+
* "descriptor unreadable" case is null, and it is readModuleReferences's to
|
|
44
|
+
* report, not this one's.
|
|
21
45
|
*/
|
|
22
46
|
export declare function parseModuleReferences(descriptorXml: string): string[];
|
|
23
47
|
/**
|
|
@@ -46,6 +70,119 @@ export interface ModelVisibility {
|
|
|
46
70
|
* descriptor turning into a wall of errors is worse than the gap it closes.
|
|
47
71
|
*/
|
|
48
72
|
export declare function getModelVisibility(packagesRoot: string | null | undefined, modelName: string | null | undefined): ModelVisibility | null;
|
|
73
|
+
/**
|
|
74
|
+
* Drop the memoised visibility of `modelName` under every root.
|
|
75
|
+
*
|
|
76
|
+
* The descriptor writers call this after every successful write. Without it the
|
|
77
|
+
* oracle keeps the reference set the model had when it was first asked, so
|
|
78
|
+
* resolve_references goes on reporting `not-visible-from-model` — and telling
|
|
79
|
+
* the agent to add the very reference it has just added — until the server
|
|
80
|
+
* restarts; a remove leaves it too permissive the same way.
|
|
81
|
+
*/
|
|
82
|
+
export declare function invalidateModelVisibility(modelName: string): void;
|
|
49
83
|
/** Uncached form — exported for tests, which need a fresh fixture each time. */
|
|
50
84
|
export declare function buildModelVisibility(packagesRoot: string | null | undefined, modelName: string | null | undefined): ModelVisibility | null;
|
|
85
|
+
export type AddModuleReferenceResult =
|
|
86
|
+
/** Added. `xml` is the updated document. */
|
|
87
|
+
{
|
|
88
|
+
kind: 'added';
|
|
89
|
+
xml: string;
|
|
90
|
+
}
|
|
91
|
+
/** Already referenced — refuse rather than write a second `<d2p1:string>`. */
|
|
92
|
+
| {
|
|
93
|
+
kind: 'duplicate';
|
|
94
|
+
existing: string;
|
|
95
|
+
}
|
|
96
|
+
/** No `<ModuleReferences>` element at all; the caller declines rather than inventing one. */
|
|
97
|
+
| {
|
|
98
|
+
kind: 'no-element';
|
|
99
|
+
}
|
|
100
|
+
/** Not a package folder name — see isValidModuleReferenceName. */
|
|
101
|
+
| {
|
|
102
|
+
kind: 'invalid-name';
|
|
103
|
+
};
|
|
104
|
+
export type RemoveModuleReferenceResult =
|
|
105
|
+
/** Removed. `xml` is the updated document, `removed` the entry as it was spelled. */
|
|
106
|
+
{
|
|
107
|
+
kind: 'removed';
|
|
108
|
+
xml: string;
|
|
109
|
+
removed: string;
|
|
110
|
+
}
|
|
111
|
+
/** Not referenced. `present` is what IS referenced, for the message. */
|
|
112
|
+
| {
|
|
113
|
+
kind: 'not-found';
|
|
114
|
+
present: string[];
|
|
115
|
+
}
|
|
116
|
+
/** No `<ModuleReferences>` element at all. */
|
|
117
|
+
| {
|
|
118
|
+
kind: 'no-element';
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Could `name` be a package folder name?
|
|
122
|
+
*
|
|
123
|
+
* Checked before anything is written, because the entry is interpolated into
|
|
124
|
+
* `<d2p1:string>…</d2p1:string>` verbatim: whitespace inside the name writes an
|
|
125
|
+
* entry parseModuleReferences cannot read back (so it is neither a detected
|
|
126
|
+
* duplicate nor removable), an all-blank name writes an empty entry, and `&`/`<`
|
|
127
|
+
* write a malformed descriptor. Package folders on a real install use letters,
|
|
128
|
+
* digits and `_`, plus `-` in custom ones (`fm-mcp`); `.` is admitted for
|
|
129
|
+
* dotted ISV names, but never leading, so the name cannot step out of a
|
|
130
|
+
* packages root when probed as a folder.
|
|
131
|
+
*/
|
|
132
|
+
export declare function isValidModuleReferenceName(name: string): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Add one `<d2p1:string>` entry to `<ModuleReferences>`.
|
|
135
|
+
*
|
|
136
|
+
* Idempotent by refusal, matching add-diagnostic-suppression: a duplicate comes
|
|
137
|
+
* back as 'duplicate' rather than as a silent no-op, because a caller who does
|
|
138
|
+
* not know the reference is already there learns more from being told than from
|
|
139
|
+
* a ✅ over an unchanged file. Comparison is case-insensitive — a package folder
|
|
140
|
+
* name is, and two entries differing only in case are one reference to xppc and
|
|
141
|
+
* two lines to a reviewer.
|
|
142
|
+
*
|
|
143
|
+
* Placement follows the file's OWN convention, the same way the indentation
|
|
144
|
+
* does. A descriptor that is already sorted gets the entry in sorted position;
|
|
145
|
+
* one that is not gets it appended.
|
|
146
|
+
*
|
|
147
|
+
* An earlier version of this docblock asserted that no shipped descriptor is
|
|
148
|
+
* sorted and always appended. That was wrong, and measurably so: across the 176
|
|
149
|
+
* descriptors of a 10.0.2527 PackagesLocalDirectory plus the custom and ISV
|
|
150
|
+
* roots beside it, 137 of the 173 with two or more references are in ascending
|
|
151
|
+
* case-insensitive order, and every custom model on the box was among them —
|
|
152
|
+
* i.e. the sorted convention is the one the files a write actually targets keep.
|
|
153
|
+
* Appending to a sorted list puts the new entry out of order, which is a line a
|
|
154
|
+
* reviewer stops at and which the next Visual Studio save silently re-sorts,
|
|
155
|
+
* turning one intended change into two diffs.
|
|
156
|
+
*
|
|
157
|
+
* The 36 unsorted ones (Foundation, SCMControls, several *Integration models)
|
|
158
|
+
* are left alone rather than tidied: re-sorting a file to add one line to it is
|
|
159
|
+
* the larger diff, not the smaller one.
|
|
160
|
+
*/
|
|
161
|
+
export declare function addModuleReference(xml: string, moduleName: string): AddModuleReferenceResult;
|
|
162
|
+
/**
|
|
163
|
+
* Remove one `<d2p1:string>` entry from `<ModuleReferences>`, matched
|
|
164
|
+
* case-insensitively on its text.
|
|
165
|
+
*
|
|
166
|
+
* Removing the LAST entry collapses the element to `<ModuleReferences … />`,
|
|
167
|
+
* keeping its attributes — the shape a shipped descriptor with no references
|
|
168
|
+
* actually has, and the one that makes add-then-remove a byte-identical round
|
|
169
|
+
* trip on a file that started empty. (The one nuance: a list that started
|
|
170
|
+
* `i:nil="true"` comes back as the self-closing form WITHOUT the nil, because
|
|
171
|
+
* adding a reference is what dropped it. "No list" and "empty list" both mean
|
|
172
|
+
* the model references nothing, so nothing is lost.)
|
|
173
|
+
*/
|
|
174
|
+
export declare function removeModuleReference(xml: string, moduleName: string): RemoveModuleReferenceResult;
|
|
175
|
+
/**
|
|
176
|
+
* Where a model's descriptor actually is under `packagesRoot`, or null when it
|
|
177
|
+
* is not there — the write side's equivalent of readModuleReferences, which
|
|
178
|
+
* collapses "no such file" into the same null as "unreadable".
|
|
179
|
+
*
|
|
180
|
+
* Reuses findDescriptor's probe order (package == model first, then a bounded
|
|
181
|
+
* sweep for an ISV model inside a differently-named package) so a write lands on
|
|
182
|
+
* the file the readers read.
|
|
183
|
+
*/
|
|
184
|
+
export declare function findDescriptorPath(packagesRoot: string, modelName: string): {
|
|
185
|
+
filePath: string;
|
|
186
|
+
packageName: string;
|
|
187
|
+
} | null;
|
|
51
188
|
//# sourceMappingURL=modelDescriptor.d.ts.map
|