@tenonhq/dovetail-core 0.0.126 → 0.0.127
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/dist/appUtils.js +17 -9
- package/dist/commands.js +16 -2
- package/dist/createRecordCommand.js +7 -1
- package/dist/tests/refreshFailedScopes.test.js +194 -0
- package/package.json +1 -1
- package/release-manifest.json +36 -27
package/dist/appUtils.js
CHANGED
|
@@ -424,6 +424,7 @@ const narrowManifestToRecord = (manifest, table, sysId) => {
|
|
|
424
424
|
};
|
|
425
425
|
exports.narrowManifestToRecord = narrowManifestToRecord;
|
|
426
426
|
const syncManifest = async (scope, options = {}) => {
|
|
427
|
+
var failedScopes = [];
|
|
427
428
|
// Top-level entry owns the collector lifecycle. Recursive calls (all-scopes
|
|
428
429
|
// → per-scope) inherit the collector via options._benchmarkCollector.
|
|
429
430
|
var isBenchmarkOwner = false;
|
|
@@ -448,7 +449,7 @@ const syncManifest = async (scope, options = {}) => {
|
|
|
448
449
|
Logger_1.logger.warn("Skipping scope '" + scope + "' — not declared in dove.config.js `scopes`. " +
|
|
449
450
|
"Add it to config.scopes to sync, or remove its manifest file.");
|
|
450
451
|
FileLogger_1.fileLogger.debug("syncManifest: skipped undeclared scope '" + scope + "'");
|
|
451
|
-
return;
|
|
452
|
+
return { failedScopes: failedScopes };
|
|
452
453
|
}
|
|
453
454
|
Logger_1.logger.info("Refreshing scope: " + scope + "...");
|
|
454
455
|
const client = (0, snClient_1.defaultClient)();
|
|
@@ -555,20 +556,25 @@ const syncManifest = async (scope, options = {}) => {
|
|
|
555
556
|
tables: options.tables,
|
|
556
557
|
_benchmarkCollector: collector,
|
|
557
558
|
};
|
|
559
|
+
// Each child call catches its own failure and reports it in its result,
|
|
560
|
+
// so one broken scope never stops the rest — collect, then report.
|
|
561
|
+
var childScopes = [];
|
|
558
562
|
if (declaredScopes.length > 0) {
|
|
559
|
-
|
|
560
|
-
await (0, exports.syncManifest)(declaredScopes[d], childOptions);
|
|
561
|
-
}
|
|
563
|
+
childScopes = declaredScopes;
|
|
562
564
|
}
|
|
563
565
|
else if (ConfigManager.isMultiScopeManifest(curManifest)) {
|
|
564
566
|
// No declared scopes — fall back to the persisted manifest's scopes.
|
|
565
|
-
|
|
566
|
-
await (0, exports.syncManifest)(scopeName, childOptions);
|
|
567
|
-
}
|
|
567
|
+
childScopes = Object.keys(curManifest);
|
|
568
568
|
}
|
|
569
569
|
else if (curManifest.scope) {
|
|
570
570
|
// Single scope manifest
|
|
571
|
-
|
|
571
|
+
childScopes = [curManifest.scope];
|
|
572
|
+
}
|
|
573
|
+
for (var d = 0; d < childScopes.length; d++) {
|
|
574
|
+
var childResult = await (0, exports.syncManifest)(childScopes[d], childOptions);
|
|
575
|
+
if (childResult && Array.isArray(childResult.failedScopes)) {
|
|
576
|
+
failedScopes = failedScopes.concat(childResult.failedScopes);
|
|
577
|
+
}
|
|
572
578
|
}
|
|
573
579
|
}
|
|
574
580
|
}
|
|
@@ -578,7 +584,8 @@ const syncManifest = async (scope, options = {}) => {
|
|
|
578
584
|
message = e.message;
|
|
579
585
|
else
|
|
580
586
|
message = String(e);
|
|
581
|
-
Logger_1.logger.error("Refresh failed: " + message);
|
|
587
|
+
Logger_1.logger.error("Refresh failed" + (scope ? " for " + scope : "") + ": " + message);
|
|
588
|
+
failedScopes.push({ scope: scope || "(all scopes)", error: message });
|
|
582
589
|
}
|
|
583
590
|
finally {
|
|
584
591
|
if (isBenchmarkOwner && collector) {
|
|
@@ -586,6 +593,7 @@ const syncManifest = async (scope, options = {}) => {
|
|
|
586
593
|
Logger_1.logger.info(collector.formatSummary());
|
|
587
594
|
}
|
|
588
595
|
}
|
|
596
|
+
return { failedScopes: failedScopes };
|
|
589
597
|
};
|
|
590
598
|
exports.syncManifest = syncManifest;
|
|
591
599
|
const markFileMissing = (missingObj) => (table) => (recordId) => (file) => {
|
package/dist/commands.js
CHANGED
|
@@ -134,13 +134,27 @@ async function refreshCommand(args, log = true) {
|
|
|
134
134
|
", metadataOnly=" + !!args.metadataOnly +
|
|
135
135
|
", benchmark=" + !!args.benchmark +
|
|
136
136
|
", tables=" + (tables.length > 0 ? tables.join(",") : "all") + ")");
|
|
137
|
-
await AppUtils.syncManifest(args.scope, {
|
|
137
|
+
const result = await AppUtils.syncManifest(args.scope, {
|
|
138
138
|
force: !!args.force,
|
|
139
139
|
metadataOnly: !!args.metadataOnly,
|
|
140
140
|
benchmark: !!args.benchmark,
|
|
141
141
|
tables: tables.length > 0 ? tables : undefined,
|
|
142
142
|
});
|
|
143
|
-
|
|
143
|
+
const failed = (result && Array.isArray(result.failedScopes)) ? result.failedScopes : [];
|
|
144
|
+
if (failed.length > 0) {
|
|
145
|
+
// Every remaining scope was still attempted (syncManifest keeps going
|
|
146
|
+
// past a broken scope); now say so loudly and exit non-zero so scripts
|
|
147
|
+
// and CI can't mistake a partial refresh for a clean one.
|
|
148
|
+
Logger_1.logger.error("Refresh finished with " + failed.length + " failed scope" +
|
|
149
|
+
(failed.length === 1 ? "" : "s") + ":");
|
|
150
|
+
for (const f of failed) {
|
|
151
|
+
Logger_1.logger.error(" - " + f.scope + ": " + f.error);
|
|
152
|
+
}
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
Logger_1.logger.success("Refresh complete!");
|
|
157
|
+
}
|
|
144
158
|
setLogLevel(args);
|
|
145
159
|
}
|
|
146
160
|
catch (e) {
|
|
@@ -536,9 +536,15 @@ async function createRecordCommand(args) {
|
|
|
536
536
|
Logger_1.logger.info("Syncing record to local files...");
|
|
537
537
|
FileLogger_1.fileLogger.debug("Starting single-record sync for scope:", scope);
|
|
538
538
|
try {
|
|
539
|
-
await AppUtils.syncManifest(scope, {
|
|
539
|
+
var syncResult = await AppUtils.syncManifest(scope, {
|
|
540
540
|
record: { table: table, sysId: newSysId },
|
|
541
541
|
});
|
|
542
|
+
// syncManifest reports a failed scope in its result instead of
|
|
543
|
+
// throwing; route it to the catch below so we never claim the local
|
|
544
|
+
// files were created when the refresh failed.
|
|
545
|
+
if (syncResult && syncResult.failedScopes && syncResult.failedScopes.length > 0) {
|
|
546
|
+
throw new Error(syncResult.failedScopes[0].error);
|
|
547
|
+
}
|
|
542
548
|
// Resolve the scope's own source directory — syncManifest writes into
|
|
543
549
|
// getSourcePathForScope(scope), which a scope config can override. Using
|
|
544
550
|
// the top-level getSourcePath() here would print a wrong path in
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `dove refresh` must fail loudly when a scope fails.
|
|
4
|
+
*
|
|
5
|
+
* Regression target: against an instance without the sync API, every scope's
|
|
6
|
+
* getManifest returned HTTP 400, each failure was caught and logged inside
|
|
7
|
+
* syncManifest, and refreshCommand still printed "Refresh complete!" and
|
|
8
|
+
* exited 0 — so scripts and CI read a total failure as success.
|
|
9
|
+
*
|
|
10
|
+
* Contract pinned here:
|
|
11
|
+
* 1. An all-scopes refresh keeps going past a failed scope (the rest still
|
|
12
|
+
* refresh) and returns every failure in `failedScopes`.
|
|
13
|
+
* 2. refreshCommand prints a per-scope failure summary, sets a non-zero exit
|
|
14
|
+
* code, and does NOT print "Refresh complete!".
|
|
15
|
+
* 3. A clean refresh still prints "Refresh complete!" and leaves the exit
|
|
16
|
+
* code alone.
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
var mockLogger = {
|
|
53
|
+
info: jest.fn(),
|
|
54
|
+
debug: jest.fn(),
|
|
55
|
+
warn: jest.fn(),
|
|
56
|
+
error: jest.fn(),
|
|
57
|
+
success: jest.fn(),
|
|
58
|
+
setLogLevel: jest.fn(),
|
|
59
|
+
getLogLevel: function () { return "info"; },
|
|
60
|
+
};
|
|
61
|
+
var mockClient = {
|
|
62
|
+
getManifest: jest.fn(),
|
|
63
|
+
getMissingFiles: jest.fn().mockResolvedValue({}),
|
|
64
|
+
};
|
|
65
|
+
var mockFUtils = {
|
|
66
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
67
|
+
writeFileForce: jest.fn().mockResolvedValue(undefined),
|
|
68
|
+
writeSNFileCurry: jest.fn(() => jest.fn().mockResolvedValue(undefined)),
|
|
69
|
+
createDirRecursively: jest.fn().mockResolvedValue(undefined),
|
|
70
|
+
};
|
|
71
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
72
|
+
jest.mock("../FileLogger", function () {
|
|
73
|
+
return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
|
|
74
|
+
});
|
|
75
|
+
jest.mock("../FileUtils", function () { return mockFUtils; });
|
|
76
|
+
jest.mock("../snClient", function () {
|
|
77
|
+
return {
|
|
78
|
+
defaultClient: function () { return mockClient; },
|
|
79
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p); },
|
|
80
|
+
processPushResponse: jest.fn(),
|
|
81
|
+
retryOnErr: jest.fn(),
|
|
82
|
+
retryOnHttpErr: jest.fn(),
|
|
83
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
var mockConfig = {
|
|
87
|
+
getConfig: jest.fn(),
|
|
88
|
+
getManifest: jest.fn(),
|
|
89
|
+
getSourcePathForScope: jest.fn().mockReturnValue("/tmp/src"),
|
|
90
|
+
getSourcePath: jest.fn().mockReturnValue("/tmp/src"),
|
|
91
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
|
|
92
|
+
resolveConfigForScope: jest.fn().mockReturnValue({
|
|
93
|
+
tables: ["sys_script_include"],
|
|
94
|
+
fieldOverrides: {},
|
|
95
|
+
apiIncludes: {},
|
|
96
|
+
apiExcludes: {},
|
|
97
|
+
}),
|
|
98
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
99
|
+
updateManifest: jest.fn(),
|
|
100
|
+
};
|
|
101
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
102
|
+
jest.mock("progress", function () {
|
|
103
|
+
return jest.fn().mockImplementation(function () { return { tick: jest.fn() }; });
|
|
104
|
+
});
|
|
105
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
106
|
+
const commands_1 = require("../commands");
|
|
107
|
+
var SCOPES = ["x_cadso_core", "x_cadso_work", "x_cadso_journey"];
|
|
108
|
+
function declareScopes() {
|
|
109
|
+
var scopes = {};
|
|
110
|
+
var manifest = {};
|
|
111
|
+
SCOPES.forEach(function (s) {
|
|
112
|
+
scopes[s] = {};
|
|
113
|
+
manifest[s] = { scope: s, tables: {} };
|
|
114
|
+
});
|
|
115
|
+
mockConfig.getConfig.mockReturnValue({ scopes: scopes });
|
|
116
|
+
mockConfig.getManifest.mockResolvedValue(manifest);
|
|
117
|
+
}
|
|
118
|
+
function errorLines() {
|
|
119
|
+
return mockLogger.error.mock.calls.map(function (c) { return String(c[0]); });
|
|
120
|
+
}
|
|
121
|
+
describe("syncManifest — failed scopes are collected, not swallowed", function () {
|
|
122
|
+
beforeEach(function () {
|
|
123
|
+
jest.clearAllMocks();
|
|
124
|
+
mockConfig.isMultiScopeManifest.mockReturnValue(true);
|
|
125
|
+
declareScopes();
|
|
126
|
+
});
|
|
127
|
+
test("keeps refreshing past a failed scope and reports it", async function () {
|
|
128
|
+
mockClient.getManifest.mockImplementation(function (scope) {
|
|
129
|
+
if (scope === "x_cadso_work") {
|
|
130
|
+
return Promise.reject(new Error("HTTP 400 from api/cadso/dovetail_sync/getManifest/x_cadso_work"));
|
|
131
|
+
}
|
|
132
|
+
return Promise.resolve({ scope: scope, tables: {} });
|
|
133
|
+
});
|
|
134
|
+
var result = await AppUtils.syncManifest();
|
|
135
|
+
var attempted = mockClient.getManifest.mock.calls.map(function (c) { return c[0]; });
|
|
136
|
+
expect(attempted).toEqual(SCOPES);
|
|
137
|
+
expect(result.failedScopes).toHaveLength(1);
|
|
138
|
+
expect(result.failedScopes[0].scope).toBe("x_cadso_work");
|
|
139
|
+
expect(result.failedScopes[0].error).toContain("HTTP 400");
|
|
140
|
+
// The scopes either side of the failure still wrote their manifests.
|
|
141
|
+
expect(mockFUtils.writeScopeManifest).toHaveBeenCalledTimes(2);
|
|
142
|
+
});
|
|
143
|
+
test("every scope failing reports every scope", async function () {
|
|
144
|
+
mockClient.getManifest.mockRejectedValue(new Error("HTTP 400"));
|
|
145
|
+
var result = await AppUtils.syncManifest();
|
|
146
|
+
expect(result.failedScopes.map(function (f) { return f.scope; })).toEqual(SCOPES);
|
|
147
|
+
});
|
|
148
|
+
test("a clean refresh reports no failures", async function () {
|
|
149
|
+
mockClient.getManifest.mockImplementation(function (scope) {
|
|
150
|
+
return Promise.resolve({ scope: scope, tables: {} });
|
|
151
|
+
});
|
|
152
|
+
var result = await AppUtils.syncManifest();
|
|
153
|
+
expect(result.failedScopes).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
test("a top-level failure (no manifest) is reported too", async function () {
|
|
156
|
+
mockConfig.getManifest.mockResolvedValue(undefined);
|
|
157
|
+
var result = await AppUtils.syncManifest();
|
|
158
|
+
expect(result.failedScopes).toHaveLength(1);
|
|
159
|
+
expect(result.failedScopes[0].error).toContain("No manifest file loaded");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("refreshCommand — exit code reflects failed scopes", function () {
|
|
163
|
+
var originalExitCode;
|
|
164
|
+
beforeEach(function () {
|
|
165
|
+
jest.clearAllMocks();
|
|
166
|
+
originalExitCode = process.exitCode;
|
|
167
|
+
process.exitCode = undefined;
|
|
168
|
+
});
|
|
169
|
+
afterEach(function () {
|
|
170
|
+
process.exitCode = originalExitCode;
|
|
171
|
+
jest.restoreAllMocks();
|
|
172
|
+
});
|
|
173
|
+
test("failed scopes → summary, non-zero exit, no 'Refresh complete!'", async function () {
|
|
174
|
+
jest.spyOn(AppUtils, "syncManifest").mockResolvedValue({
|
|
175
|
+
failedScopes: [
|
|
176
|
+
{ scope: "x_cadso_core", error: "HTTP 400" },
|
|
177
|
+
{ scope: "x_cadso_work", error: "HTTP 400" },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
await (0, commands_1.refreshCommand)({});
|
|
181
|
+
expect(process.exitCode).toBe(1);
|
|
182
|
+
expect(mockLogger.success).not.toHaveBeenCalled();
|
|
183
|
+
var lines = errorLines();
|
|
184
|
+
expect(lines.some(function (l) { return l.indexOf("2 failed scopes") !== -1; })).toBe(true);
|
|
185
|
+
expect(lines.some(function (l) { return l.indexOf("x_cadso_core: HTTP 400") !== -1; })).toBe(true);
|
|
186
|
+
expect(lines.some(function (l) { return l.indexOf("x_cadso_work: HTTP 400") !== -1; })).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
test("clean refresh → 'Refresh complete!' and exit code untouched", async function () {
|
|
189
|
+
jest.spyOn(AppUtils, "syncManifest").mockResolvedValue({ failedScopes: [] });
|
|
190
|
+
await (0, commands_1.refreshCommand)({});
|
|
191
|
+
expect(process.exitCode).toBeUndefined();
|
|
192
|
+
expect(mockLogger.success).toHaveBeenCalledWith("Refresh complete!");
|
|
193
|
+
});
|
|
194
|
+
});
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "dovetail-manifest/v1",
|
|
3
|
-
"generated_at": "2026-10-
|
|
3
|
+
"generated_at": "2026-10-09T03:55:11.874Z",
|
|
4
4
|
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-core@0.0.126",
|
|
8
|
+
"package": "@tenonhq/dovetail-core",
|
|
9
|
+
"version": "0.0.126",
|
|
10
|
+
"prev_version": "0.0.125",
|
|
11
|
+
"published_at": "2026-10-09T03:55:11.868Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "ca4fdead0bbd5c4f7ed3dd544ac3ae19a4037e42",
|
|
14
|
+
"tag": "dovetail-core@0.0.126"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "ca4fdead0",
|
|
20
|
+
"subject": "fix(core): dove --env accepts a bare instance name like dove-sn (#325)",
|
|
21
|
+
"pr": 325
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "435341abf",
|
|
25
|
+
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"sha": "301dfbb75",
|
|
29
|
+
"subject": "fix: review fixes for the Oct 1-8 merges, re-land reverted #288 and #311 (#322)",
|
|
30
|
+
"pr": 322
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"sha": "16fd9248d",
|
|
34
|
+
"subject": "chore(release): @tenonhq/dovetail-core@0.0.125 [skip ci]"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"exports_delta": null,
|
|
38
|
+
"narrative": null
|
|
39
|
+
},
|
|
5
40
|
{
|
|
6
41
|
"$schema": "dovetail-release-event/v1",
|
|
7
42
|
"event_id": "dovetail-servicenow@0.0.52",
|
|
@@ -1312,32 +1347,6 @@
|
|
|
1312
1347
|
],
|
|
1313
1348
|
"exports_delta": null,
|
|
1314
1349
|
"narrative": null
|
|
1315
|
-
},
|
|
1316
|
-
{
|
|
1317
|
-
"$schema": "dovetail-release-event/v1",
|
|
1318
|
-
"event_id": "dovetail-servicenow@0.0.38",
|
|
1319
|
-
"package": "@tenonhq/dovetail-servicenow",
|
|
1320
|
-
"version": "0.0.38",
|
|
1321
|
-
"prev_version": "0.0.37",
|
|
1322
|
-
"published_at": "2026-09-15T23:33:16.570Z",
|
|
1323
|
-
"git": {
|
|
1324
|
-
"sha": "925c0ca40cc6407fa78f28b25b82f8dc0fbd460f",
|
|
1325
|
-
"tag": "dovetail-servicenow@0.0.38"
|
|
1326
|
-
},
|
|
1327
|
-
"semver_bump": "patch",
|
|
1328
|
-
"commits": [
|
|
1329
|
-
{
|
|
1330
|
-
"sha": "925c0ca40",
|
|
1331
|
-
"subject": "feat(servicenow): headless update-set / app export with mandatory secret stripping (#276)",
|
|
1332
|
-
"pr": 276
|
|
1333
|
-
},
|
|
1334
|
-
{
|
|
1335
|
-
"sha": "83df94645",
|
|
1336
|
-
"subject": "chore(release): publish 2 packages [skip ci]"
|
|
1337
|
-
}
|
|
1338
|
-
],
|
|
1339
|
-
"exports_delta": null,
|
|
1340
|
-
"narrative": null
|
|
1341
1350
|
}
|
|
1342
1351
|
]
|
|
1343
1352
|
}
|