atmn 1.1.25 → 2.0.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/README.md +120 -0
- package/dist/bin.js +23102 -0
- package/dist/index.js +2906 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -74
- package/src/actions/api/callApi.ts +280 -0
- package/src/actions/api/registerApiCommands.ts +121 -0
- package/src/actions/env/fetchOrgInfo.ts +32 -0
- package/src/actions/env/types/orgInfo.ts +18 -0
- package/src/actions/env.ts +94 -0
- package/src/actions/init/runInit.ts +401 -0
- package/src/actions/login/keyless.ts +135 -0
- package/src/actions/login.ts +144 -0
- package/src/actions/pull/appendPlanVersionFixture.ts +373 -0
- package/src/actions/pull/applyPreview.ts +553 -0
- package/src/actions/pull/applySettingsPreview.ts +114 -0
- package/src/actions/pull/changedFixtureKeys.ts +88 -0
- package/src/actions/pull/listSourceFiles.ts +29 -0
- package/src/actions/pull/locateFixture.ts +78 -0
- package/src/actions/pull/resolveCollectionTarget.ts +198 -0
- package/src/actions/pull/rewriteConfig.ts +57 -0
- package/src/actions/pull/scaffoldConfig.ts +118 -0
- package/src/actions/pull.ts +399 -0
- package/src/actions/push/backfillInternalIds.ts +394 -0
- package/src/actions/push/deprecatedFields.ts +55 -0
- package/src/actions/push.ts +294 -0
- package/src/actions/reset/runReset.ts +58 -0
- package/src/actions/sandbox/createSandbox.ts +83 -0
- package/src/actions/sandbox/deleteSandbox.ts +91 -0
- package/src/actions/sandbox/listSandboxes.ts +28 -0
- package/src/actions/sandbox/types/sandboxClient.ts +16 -0
- package/src/actions/sandbox/useSandbox.ts +157 -0
- package/src/actions/sandbox/withSandboxScopeHint.ts +27 -0
- package/src/actions/skills/skills.ts +246 -0
- package/src/auth/announceAuthorizationUrl.ts +26 -0
- package/src/auth/browser/openSystemBrowser.ts +7 -0
- package/src/auth/browser/tryOpenBrowser.ts +17 -0
- package/src/auth/browser/watchLauncher.ts +46 -0
- package/src/auth/buildAuthorizationUrl.ts +36 -0
- package/src/auth/callbackPages.ts +126 -0
- package/src/auth/createOrgApiKeys.ts +46 -0
- package/src/auth/keyless.ts +119 -0
- package/src/auth/oauthConfig.ts +63 -0
- package/src/auth/runOAuthFlow.ts +230 -0
- package/src/auth/types/browserOpener.ts +5 -0
- package/src/auth/types/impersonationTokens.ts +23 -0
- package/src/auth/types/oauthTokens.ts +13 -0
- package/src/auth/types/orgApiKeys.ts +6 -0
- package/src/bin.ts +9 -0
- package/src/cli.ts +648 -0
- package/src/config/configPackageName.ts +9 -0
- package/src/config/legacyConfig.ts +25 -0
- package/src/config/loadConfig.ts +231 -0
- package/src/env/assertSandboxTarget.ts +20 -0
- package/src/env/loadEnv.ts +184 -0
- package/src/env/resolveTarget.ts +134 -0
- package/src/env/sandboxKeyName.ts +18 -0
- package/src/generated/apiRoutes.ts +3787 -0
- package/src/generated/client.ts +51564 -0
- package/src/generated/emit.ts +1163 -0
- package/src/generated/emitRuntime.ts +522 -0
- package/src/generated/features.ts +146 -0
- package/src/generated/labels.ts +29 -0
- package/src/generated/licenses.ts +287 -0
- package/src/generated/lintRules.ts +2207 -0
- package/src/generated/lintRuntime.ts +865 -0
- package/src/generated/plans.ts +1628 -0
- package/src/generated/referralPrograms.ts +22 -0
- package/src/generated/rewards.ts +58 -0
- package/src/generated/settings.ts +21 -0
- package/src/generated/skills.ts +305 -0
- package/src/generated/variants.ts +934 -0
- package/src/generated/wire.ts +334 -0
- package/src/http/autumnFetch.ts +30 -0
- package/src/index.ts +20 -0
- package/src/project/chooseConfigDir.ts +41 -0
- package/src/project/resolveProject.ts +115 -0
- package/src/project/rootMarker.ts +40 -0
- package/src/prompt/prompt.ts +186 -0
- package/src/prompt/select.ts +162 -0
- package/src/render/renderEnv.ts +77 -0
- package/src/render/renderPreview.ts +945 -0
- package/src/render/renderSandboxes.ts +92 -0
- package/src/render/stripTerminalControls.ts +19 -0
- package/src/repo/findRepoRoot.ts +79 -0
- package/src/surgery/appendPropertyEdit.ts +67 -0
- package/src/surgery/appendToArray.ts +87 -0
- package/src/surgery/appendToBinding.ts +19 -0
- package/src/surgery/appendToCollection.ts +40 -0
- package/src/surgery/appendToFixtureArray.ts +71 -0
- package/src/surgery/arrayBinding.ts +30 -0
- package/src/surgery/deleteFixtureLiteral.ts +81 -0
- package/src/surgery/deleteReference.ts +48 -0
- package/src/surgery/ensureBuilderImport.ts +65 -0
- package/src/surgery/findFixture.ts +238 -0
- package/src/surgery/fixtureEdit.ts +156 -0
- package/src/surgery/fixtureLocation.ts +32 -0
- package/src/surgery/insertCollection.ts +86 -0
- package/src/surgery/insertFirstProperty.ts +73 -0
- package/src/surgery/patchFixtureProperty.ts +152 -0
- package/src/surgery/patchSingletonProperty.ts +221 -0
- package/src/surgery/replaceFixture.ts +28 -0
- package/src/surgery/setFixtureProperty.ts +55 -0
- package/src/surgery/staticFixtureRule.ts +48 -0
- package/src/version.ts +5 -0
- package/dist/cli.js +0 -146296
- package/dist/compose/index.js +0 -122
- package/dist/src/compose/builders/builderFunctions.d.ts +0 -84
- package/dist/src/compose/builders/rewardFunctions.d.ts +0 -5
- package/dist/src/compose/builders/variantFunctions.d.ts +0 -2
- package/dist/src/compose/index.d.ts +0 -19
- package/dist/src/compose/models/featureModels.d.ts +0 -262
- package/dist/src/compose/models/index.d.ts +0 -3
- package/dist/src/compose/models/planModels.d.ts +0 -562
- package/dist/src/compose/models/rewardModels.d.ts +0 -52
- package/dist/src/compose/models/variantModels.d.ts +0 -34
- package/readme.md +0 -186
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { NESTED_FIXTURES } from "../../generated/emit";
|
|
2
|
+
import {
|
|
3
|
+
branchSpecs,
|
|
4
|
+
type CollectionSpec,
|
|
5
|
+
emitFixture,
|
|
6
|
+
emitFixtureProperty,
|
|
7
|
+
emitNestedFixture,
|
|
8
|
+
resolveBranch,
|
|
9
|
+
} from "../../generated/emitRuntime";
|
|
10
|
+
import { appendToBinding } from "../../surgery/appendToBinding";
|
|
11
|
+
import { appendToCollection } from "../../surgery/appendToCollection";
|
|
12
|
+
import { appendToFixtureArray } from "../../surgery/appendToFixtureArray";
|
|
13
|
+
import { deleteFixtureLiteral } from "../../surgery/deleteFixtureLiteral";
|
|
14
|
+
import { deleteReference } from "../../surgery/deleteReference";
|
|
15
|
+
import { ensureBuilderImport } from "../../surgery/ensureBuilderImport";
|
|
16
|
+
import { leadingIndentOfLine } from "../../surgery/fixtureEdit";
|
|
17
|
+
import {
|
|
18
|
+
insertCollection,
|
|
19
|
+
rootSpreadNames,
|
|
20
|
+
} from "../../surgery/insertCollection";
|
|
21
|
+
import {
|
|
22
|
+
fixturePropertyString,
|
|
23
|
+
patchFixtureProperty,
|
|
24
|
+
} from "../../surgery/patchFixtureProperty";
|
|
25
|
+
import { replaceFixture } from "../../surgery/replaceFixture";
|
|
26
|
+
import { changedFixtureKeys } from "./changedFixtureKeys";
|
|
27
|
+
import { type FixtureConstraint, locateFixture } from "./locateFixture";
|
|
28
|
+
import { resolveCollectionTarget } from "./resolveCollectionTarget";
|
|
29
|
+
|
|
30
|
+
export type PreviewEntry = { action?: string } & Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
const NESTED_VARIANT = NESTED_FIXTURES.variants;
|
|
33
|
+
const nestedVariantEdges = (
|
|
34
|
+
row: Record<string, unknown>,
|
|
35
|
+
): Record<string, unknown>[] =>
|
|
36
|
+
Array.isArray(row[NESTED_VARIANT.path])
|
|
37
|
+
? (row[NESTED_VARIANT.path] as Record<string, unknown>[])
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
export type ApplyPreviewArgs = {
|
|
41
|
+
collection: string;
|
|
42
|
+
spec: CollectionSpec;
|
|
43
|
+
/** The preview's rows for this collection. */
|
|
44
|
+
entries: PreviewEntry[];
|
|
45
|
+
/** The catalog's server rows for this collection. */
|
|
46
|
+
catalogRows: Record<string, unknown>[];
|
|
47
|
+
configPath: string;
|
|
48
|
+
/** In-memory file sources, mutated in place; nothing touches disk here. */
|
|
49
|
+
files: Map<string, string>;
|
|
50
|
+
includeMappings: boolean;
|
|
51
|
+
featureTypes?: Readonly<Record<string, string>>;
|
|
52
|
+
nestedBuilders?: Readonly<Record<string, string>>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type ApplyPreviewResult = {
|
|
56
|
+
appended: string[];
|
|
57
|
+
replaced: string[];
|
|
58
|
+
deleted: string[];
|
|
59
|
+
/** One printed line per applied change, in the order applied. */
|
|
60
|
+
lines: string[];
|
|
61
|
+
/** Fixtures the preview named that are not plain literals anywhere. */
|
|
62
|
+
unlocated: { id: string; action: string }[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The reversal of one collection's preview: a create in the config is deleted
|
|
67
|
+
* from code, a delete on the server is appended, an update is replaced. Pure
|
|
68
|
+
* over the `files` map so tests can assert without touching disk.
|
|
69
|
+
*/
|
|
70
|
+
export const applyPreview = ({
|
|
71
|
+
collection,
|
|
72
|
+
spec,
|
|
73
|
+
entries,
|
|
74
|
+
catalogRows,
|
|
75
|
+
configPath,
|
|
76
|
+
files,
|
|
77
|
+
includeMappings,
|
|
78
|
+
featureTypes,
|
|
79
|
+
nestedBuilders,
|
|
80
|
+
}: ApplyPreviewArgs): ApplyPreviewResult => {
|
|
81
|
+
const result: ApplyPreviewResult = {
|
|
82
|
+
appended: [],
|
|
83
|
+
replaced: [],
|
|
84
|
+
deleted: [],
|
|
85
|
+
lines: [],
|
|
86
|
+
unlocated: [],
|
|
87
|
+
};
|
|
88
|
+
// Versioned collections key rows by id AND slug; a row without a slug is v1.
|
|
89
|
+
const versioned = spec.versioned === true;
|
|
90
|
+
const slugOf = (row: Record<string, unknown>): string =>
|
|
91
|
+
typeof row.versionSlug === "string" ? row.versionSlug : "v1";
|
|
92
|
+
const keyOf = ({ id, slug }: { id: string; slug: string }): string =>
|
|
93
|
+
versioned ? `${id}@${slug}` : id;
|
|
94
|
+
|
|
95
|
+
// A union item is addressed through its branch: the builder, the id field
|
|
96
|
+
// and the fixture keys are the branch's, not the collection's.
|
|
97
|
+
const specFor = (row: Record<string, unknown>): CollectionSpec =>
|
|
98
|
+
resolveBranch({ spec, row }).spec;
|
|
99
|
+
const bodyOf = (row: Record<string, unknown>): Record<string, unknown> =>
|
|
100
|
+
resolveBranch({ spec, row }).row;
|
|
101
|
+
const idOfRow = (row: Record<string, unknown>): unknown =>
|
|
102
|
+
bodyOf(row)[specFor(row).responseIdField];
|
|
103
|
+
|
|
104
|
+
const rowsById = new Map<string, Record<string, unknown>>();
|
|
105
|
+
// A variant row lives in its parent's `variants[]`, never top-level: the
|
|
106
|
+
// preview still names it by its own plan id and slug, so it is indexed too.
|
|
107
|
+
const nestedRowsById = new Map<
|
|
108
|
+
string,
|
|
109
|
+
{ edge: Record<string, unknown>; parent: Record<string, unknown> }
|
|
110
|
+
>();
|
|
111
|
+
for (const row of catalogRows) {
|
|
112
|
+
// Archived rows are server history, not something a pull should write.
|
|
113
|
+
if (row.archived === true) continue;
|
|
114
|
+
const id = idOfRow(row);
|
|
115
|
+
if (typeof id !== "string") continue;
|
|
116
|
+
rowsById.set(keyOf({ id, slug: slugOf(row) }), row);
|
|
117
|
+
for (const edge of nestedVariantEdges(row)) {
|
|
118
|
+
const variantId = edge[NESTED_VARIANT.idField];
|
|
119
|
+
if (typeof variantId !== "string") continue;
|
|
120
|
+
nestedRowsById.set(keyOf({ id: variantId, slug: slugOf(edge) }), {
|
|
121
|
+
edge,
|
|
122
|
+
parent: row,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const constraintsFor = (
|
|
128
|
+
entry: PreviewEntry,
|
|
129
|
+
): FixtureConstraint[] | undefined =>
|
|
130
|
+
versioned
|
|
131
|
+
? [{ field: "versionSlug", equals: slugOf(entry), absentMeans: "v1" }]
|
|
132
|
+
: undefined;
|
|
133
|
+
const internalIdOf = (entry: PreviewEntry): string | null =>
|
|
134
|
+
typeof entry.internalId === "string" ? entry.internalId : null;
|
|
135
|
+
const ensureNestedBuilderImports = ({
|
|
136
|
+
file,
|
|
137
|
+
row,
|
|
138
|
+
}: {
|
|
139
|
+
file: string;
|
|
140
|
+
row: Record<string, unknown>;
|
|
141
|
+
}): void => {
|
|
142
|
+
for (const [path, builder] of Object.entries(nestedBuilders ?? {})) {
|
|
143
|
+
if (!Array.isArray(row[path]) || row[path].length === 0) continue;
|
|
144
|
+
files.set(
|
|
145
|
+
file,
|
|
146
|
+
ensureBuilderImport({
|
|
147
|
+
source: files.get(file) ?? "",
|
|
148
|
+
builder,
|
|
149
|
+
collection: path,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const deleteExportReferences = ({ name }: { name: string }): void => {
|
|
155
|
+
const referenceFiles = new Set([configPath]);
|
|
156
|
+
const target = resolveCollectionTarget({ configPath, files, collection });
|
|
157
|
+
if (target !== null) referenceFiles.add(target.file);
|
|
158
|
+
for (const file of referenceFiles) {
|
|
159
|
+
files.set(file, deleteReference({ source: files.get(file) ?? "", name }));
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const removeFixture = ({
|
|
164
|
+
id,
|
|
165
|
+
entry,
|
|
166
|
+
}: {
|
|
167
|
+
id: string;
|
|
168
|
+
entry: PreviewEntry;
|
|
169
|
+
}): void => {
|
|
170
|
+
let entrySpec = spec;
|
|
171
|
+
let located: ReturnType<typeof locateFixture> = null;
|
|
172
|
+
for (const candidate of branchSpecs({ spec })) {
|
|
173
|
+
located = locateFixture({
|
|
174
|
+
configPath,
|
|
175
|
+
files,
|
|
176
|
+
builder: candidate.builder,
|
|
177
|
+
idField: candidate.idField,
|
|
178
|
+
id,
|
|
179
|
+
internalId: internalIdOf(entry),
|
|
180
|
+
where: constraintsFor(entry),
|
|
181
|
+
});
|
|
182
|
+
if (located !== null) {
|
|
183
|
+
entrySpec = candidate;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (located === null) {
|
|
188
|
+
result.unlocated.push({ id, action: "delete from your config" });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const removed = deleteFixtureLiteral({
|
|
192
|
+
source: located.source,
|
|
193
|
+
builder: entrySpec.builder,
|
|
194
|
+
idField: located.idField,
|
|
195
|
+
id: located.id,
|
|
196
|
+
where: located.where,
|
|
197
|
+
});
|
|
198
|
+
if (removed === null) return;
|
|
199
|
+
files.set(located.file, removed.source);
|
|
200
|
+
if (removed.exportedName !== undefined)
|
|
201
|
+
deleteExportReferences({ name: removed.exportedName });
|
|
202
|
+
result.deleted.push(id);
|
|
203
|
+
result.lines.push(`- ${id}`);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** A server-only variant version goes back where the server holds it: its parent's `variants[]`. */
|
|
207
|
+
const appendNestedVariant = ({
|
|
208
|
+
key,
|
|
209
|
+
entry,
|
|
210
|
+
}: {
|
|
211
|
+
key: string;
|
|
212
|
+
entry: PreviewEntry;
|
|
213
|
+
}): boolean => {
|
|
214
|
+
const nested = nestedRowsById.get(key);
|
|
215
|
+
if (nested === undefined) return false;
|
|
216
|
+
const { edge, parent } = nested;
|
|
217
|
+
const parentId = idOfRow(parent);
|
|
218
|
+
if (typeof parentId !== "string") return false;
|
|
219
|
+
const parentSpec = specFor(parent);
|
|
220
|
+
const alreadyStated =
|
|
221
|
+
locateFixture({
|
|
222
|
+
configPath,
|
|
223
|
+
files,
|
|
224
|
+
builder: {
|
|
225
|
+
parentBuilder: parentSpec.builder,
|
|
226
|
+
arrayProperty: NESTED_VARIANT.path,
|
|
227
|
+
},
|
|
228
|
+
idField: NESTED_VARIANT.idField,
|
|
229
|
+
id: String(edge[NESTED_VARIANT.idField]),
|
|
230
|
+
internalId:
|
|
231
|
+
typeof edge.internalId === "string" ? edge.internalId : null,
|
|
232
|
+
where: constraintsFor(entry),
|
|
233
|
+
}) !== null;
|
|
234
|
+
if (alreadyStated) return false;
|
|
235
|
+
const located = locateFixture({
|
|
236
|
+
configPath,
|
|
237
|
+
files,
|
|
238
|
+
builder: parentSpec.builder,
|
|
239
|
+
idField: parentSpec.idField,
|
|
240
|
+
id: parentId,
|
|
241
|
+
internalId:
|
|
242
|
+
typeof parent.internalId === "string" ? parent.internalId : null,
|
|
243
|
+
where: [
|
|
244
|
+
{ field: "versionSlug", equals: slugOf(parent), absentMeans: "v1" },
|
|
245
|
+
],
|
|
246
|
+
allowDynamic: true,
|
|
247
|
+
});
|
|
248
|
+
if (located === null) return false;
|
|
249
|
+
const text = (elementIndent: string) =>
|
|
250
|
+
emitNestedFixture({
|
|
251
|
+
spec: parentSpec,
|
|
252
|
+
path: NESTED_VARIANT.path,
|
|
253
|
+
row: edge,
|
|
254
|
+
includeMappings,
|
|
255
|
+
indent: elementIndent,
|
|
256
|
+
context: { featureTypes, nestedBuilders },
|
|
257
|
+
});
|
|
258
|
+
const updated = appendToFixtureArray({
|
|
259
|
+
source: located.source,
|
|
260
|
+
builder: located.builder,
|
|
261
|
+
idField: located.idField,
|
|
262
|
+
id: located.id,
|
|
263
|
+
where: located.where,
|
|
264
|
+
property: NESTED_VARIANT.path,
|
|
265
|
+
text,
|
|
266
|
+
});
|
|
267
|
+
if (updated === null) return false;
|
|
268
|
+
files.set(
|
|
269
|
+
located.file,
|
|
270
|
+
ensureBuilderImport({
|
|
271
|
+
source: updated,
|
|
272
|
+
builder: NESTED_VARIANT.builder,
|
|
273
|
+
collection: NESTED_VARIANT.path,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
result.appended.push(key);
|
|
277
|
+
result.lines.push(`+ ${key}`);
|
|
278
|
+
return true;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const appendRow = ({
|
|
282
|
+
id,
|
|
283
|
+
entry,
|
|
284
|
+
}: {
|
|
285
|
+
id: string;
|
|
286
|
+
entry: PreviewEntry;
|
|
287
|
+
}): boolean => {
|
|
288
|
+
const key = keyOf({ id, slug: slugOf(entry) });
|
|
289
|
+
const row = rowsById.get(key);
|
|
290
|
+
if (row === undefined) return appendNestedVariant({ key, entry });
|
|
291
|
+
const rowSpec = specFor(row);
|
|
292
|
+
const locatedEntry =
|
|
293
|
+
typeof row.internalId === "string"
|
|
294
|
+
? { ...entry, internalId: row.internalId }
|
|
295
|
+
: entry;
|
|
296
|
+
if (
|
|
297
|
+
versioned &&
|
|
298
|
+
locateFixture({
|
|
299
|
+
configPath,
|
|
300
|
+
files,
|
|
301
|
+
builder: rowSpec.builder,
|
|
302
|
+
idField: rowSpec.idField,
|
|
303
|
+
id,
|
|
304
|
+
internalId: internalIdOf(locatedEntry),
|
|
305
|
+
where: constraintsFor(locatedEntry),
|
|
306
|
+
}) !== null
|
|
307
|
+
) {
|
|
308
|
+
replaceRow({ id, entry: locatedEntry });
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
// A config that never mentioned the collection gets the key, then the row.
|
|
312
|
+
// Unless the object spreads another one: the key may live there, and a
|
|
313
|
+
// second `features:` after the spread would silently override it.
|
|
314
|
+
const configSource = files.get(configPath) ?? "";
|
|
315
|
+
const withKey = insertCollection({ source: configSource, collection });
|
|
316
|
+
if (withKey === null) return false;
|
|
317
|
+
const spreads = rootSpreadNames({ source: configSource });
|
|
318
|
+
if (withKey !== configSource && spreads.length > 0) {
|
|
319
|
+
result.unlocated.push({
|
|
320
|
+
id: key,
|
|
321
|
+
action: `append to \`${collection}\` by hand: atmn() spreads \`${spreads.join("`, `")}\`, which may already hold it`,
|
|
322
|
+
});
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
files.set(configPath, withKey);
|
|
326
|
+
// The array may be a const the config names, in this file or an imported one.
|
|
327
|
+
const resolved = resolveCollectionTarget({ configPath, files, collection });
|
|
328
|
+
if (resolved === null) {
|
|
329
|
+
result.unlocated.push({
|
|
330
|
+
id: key,
|
|
331
|
+
action: `append to \`${collection}\` by hand: it is not an array literal`,
|
|
332
|
+
});
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
// The surgery indents the first line; the emitter indents the rest.
|
|
336
|
+
const text = (elementIndent: string) =>
|
|
337
|
+
emitFixture({
|
|
338
|
+
spec,
|
|
339
|
+
row,
|
|
340
|
+
includeMappings,
|
|
341
|
+
indent: elementIndent,
|
|
342
|
+
context: { featureTypes, nestedBuilders },
|
|
343
|
+
});
|
|
344
|
+
const targetSource = files.get(resolved.file) ?? "";
|
|
345
|
+
const updated =
|
|
346
|
+
resolved.kind === "inline"
|
|
347
|
+
? appendToCollection({ source: targetSource, collection, text })
|
|
348
|
+
: appendToBinding({ source: targetSource, name: resolved.name, text });
|
|
349
|
+
if (updated === null) {
|
|
350
|
+
result.unlocated.push({
|
|
351
|
+
id: key,
|
|
352
|
+
action: `append to \`${collection}\` by hand: it is not an array literal`,
|
|
353
|
+
});
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
// The file now holds an inline literal, so it must import the builder.
|
|
357
|
+
files.set(
|
|
358
|
+
resolved.file,
|
|
359
|
+
ensureBuilderImport({
|
|
360
|
+
source: updated,
|
|
361
|
+
builder: rowSpec.builder,
|
|
362
|
+
collection,
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
ensureNestedBuilderImports({ file: resolved.file, row });
|
|
366
|
+
result.appended.push(key);
|
|
367
|
+
result.lines.push(`+ ${key}`);
|
|
368
|
+
return true;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const patchChangedProperties = ({
|
|
372
|
+
entry,
|
|
373
|
+
row,
|
|
374
|
+
indent,
|
|
375
|
+
located,
|
|
376
|
+
rowSpec,
|
|
377
|
+
}: {
|
|
378
|
+
entry: PreviewEntry;
|
|
379
|
+
row: Record<string, unknown>;
|
|
380
|
+
indent: string;
|
|
381
|
+
located: NonNullable<ReturnType<typeof locateFixture>>;
|
|
382
|
+
rowSpec: CollectionSpec;
|
|
383
|
+
}): string | null => {
|
|
384
|
+
// The fixture may have been found by its stable id; the public id it
|
|
385
|
+
// states is what a rename has to move.
|
|
386
|
+
const keys = changedFixtureKeys({
|
|
387
|
+
spec: rowSpec,
|
|
388
|
+
entry,
|
|
389
|
+
includeMappings,
|
|
390
|
+
fixtureId: fixturePropertyString({
|
|
391
|
+
call: located.node,
|
|
392
|
+
property: rowSpec.idField,
|
|
393
|
+
}),
|
|
394
|
+
rowId: bodyOf(row)[rowSpec.responseIdField],
|
|
395
|
+
});
|
|
396
|
+
if (keys === null) return null;
|
|
397
|
+
let source = located.source;
|
|
398
|
+
// Identity keys come last, and the slug moves before the id, so every
|
|
399
|
+
// lookup still finds the fixture by what the config said before.
|
|
400
|
+
let where = located.where;
|
|
401
|
+
for (const property of keys) {
|
|
402
|
+
const text = emitFixtureProperty({
|
|
403
|
+
spec,
|
|
404
|
+
row,
|
|
405
|
+
key: property,
|
|
406
|
+
includeMappings,
|
|
407
|
+
indent,
|
|
408
|
+
context: { featureTypes, nestedBuilders },
|
|
409
|
+
});
|
|
410
|
+
const next = patchFixtureProperty({
|
|
411
|
+
source,
|
|
412
|
+
builder: rowSpec.builder,
|
|
413
|
+
idField: located.idField,
|
|
414
|
+
id: located.id,
|
|
415
|
+
where,
|
|
416
|
+
property,
|
|
417
|
+
text,
|
|
418
|
+
});
|
|
419
|
+
if (next === null) return null;
|
|
420
|
+
source = next;
|
|
421
|
+
if (property === "versionSlug" && versioned) {
|
|
422
|
+
const slug =
|
|
423
|
+
typeof row.versionSlug === "string" ? row.versionSlug : "v1";
|
|
424
|
+
where = [{ field: "versionSlug", equals: slug, absentMeans: "v1" }];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return source;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const replaceRow = ({
|
|
431
|
+
id,
|
|
432
|
+
entry,
|
|
433
|
+
}: {
|
|
434
|
+
id: string;
|
|
435
|
+
entry: PreviewEntry;
|
|
436
|
+
}): void => {
|
|
437
|
+
const row = rowsById.get(keyOf({ id, slug: slugOf(entry) }));
|
|
438
|
+
if (row === undefined) return;
|
|
439
|
+
const rowSpec = specFor(row);
|
|
440
|
+
const located = locateFixture({
|
|
441
|
+
configPath,
|
|
442
|
+
files,
|
|
443
|
+
builder: rowSpec.builder,
|
|
444
|
+
idField: rowSpec.idField,
|
|
445
|
+
id,
|
|
446
|
+
internalId: internalIdOf(entry),
|
|
447
|
+
where: constraintsFor(entry),
|
|
448
|
+
});
|
|
449
|
+
if (located === null) {
|
|
450
|
+
// A literal built from spreads or calls is there but cannot be edited;
|
|
451
|
+
// only a version the config never had is missing rather than unlocatable.
|
|
452
|
+
const dynamic = locateFixture({
|
|
453
|
+
configPath,
|
|
454
|
+
files,
|
|
455
|
+
builder: rowSpec.builder,
|
|
456
|
+
idField: rowSpec.idField,
|
|
457
|
+
id,
|
|
458
|
+
internalId: internalIdOf(entry),
|
|
459
|
+
where: constraintsFor(entry),
|
|
460
|
+
allowDynamic: true,
|
|
461
|
+
});
|
|
462
|
+
if (versioned && dynamic === null) {
|
|
463
|
+
appendRow({ id, entry });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
result.unlocated.push({ id, action: "replace with the server's copy" });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const key = keyOf({ id, slug: slugOf(entry) });
|
|
470
|
+
// A sibling version rides the preview for context; only its own diff is work.
|
|
471
|
+
if (entry.siblingOf !== undefined && entry.planChange == null) return;
|
|
472
|
+
// The emitter's indent is the found call's own line indent, so the
|
|
473
|
+
// closing `})` lines up with the text it replaces.
|
|
474
|
+
const indent = leadingIndentOfLine(
|
|
475
|
+
located.source,
|
|
476
|
+
located.node.range().start.index,
|
|
477
|
+
);
|
|
478
|
+
// When the preview names the fields, only those move; the rest of the
|
|
479
|
+
// fixture keeps its bytes. A diff without fields rewrites the whole call.
|
|
480
|
+
const patched = patchChangedProperties({
|
|
481
|
+
entry,
|
|
482
|
+
row,
|
|
483
|
+
indent,
|
|
484
|
+
located,
|
|
485
|
+
rowSpec,
|
|
486
|
+
});
|
|
487
|
+
if (patched !== null) {
|
|
488
|
+
files.set(located.file, patched);
|
|
489
|
+
ensureNestedBuilderImports({ file: located.file, row });
|
|
490
|
+
result.replaced.push(key);
|
|
491
|
+
result.lines.push(`~ ${key}`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const text = emitFixture({
|
|
495
|
+
spec,
|
|
496
|
+
row,
|
|
497
|
+
includeMappings,
|
|
498
|
+
indent,
|
|
499
|
+
context: { featureTypes },
|
|
500
|
+
});
|
|
501
|
+
const updated = replaceFixture({
|
|
502
|
+
source: located.source,
|
|
503
|
+
builder: rowSpec.builder,
|
|
504
|
+
idField: located.idField,
|
|
505
|
+
id: located.id,
|
|
506
|
+
where: located.where,
|
|
507
|
+
text,
|
|
508
|
+
});
|
|
509
|
+
if (updated === null) return;
|
|
510
|
+
files.set(located.file, updated);
|
|
511
|
+
ensureNestedBuilderImports({ file: located.file, row });
|
|
512
|
+
result.replaced.push(key);
|
|
513
|
+
result.lines.push(`~ ${key}`);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// The preview speaks for what the config states. A version the config never
|
|
517
|
+
// mentions is still the server's truth, so versioned rows the config has no
|
|
518
|
+
// fixture for are appended straight from the catalog.
|
|
519
|
+
const appendUnstatedVersions = (): void => {
|
|
520
|
+
if (!versioned) return;
|
|
521
|
+
for (const row of rowsById.values()) {
|
|
522
|
+
const id = idOfRow(row);
|
|
523
|
+
if (typeof id !== "string") continue;
|
|
524
|
+
const slug = slugOf(row);
|
|
525
|
+
const stated = entries.some(
|
|
526
|
+
(entry) => entry[spec.idField] === id && slugOf(entry) === slug,
|
|
527
|
+
);
|
|
528
|
+
if (stated) continue;
|
|
529
|
+
const located = locateFixture({
|
|
530
|
+
configPath,
|
|
531
|
+
files,
|
|
532
|
+
builder: spec.builder,
|
|
533
|
+
idField: spec.idField,
|
|
534
|
+
id,
|
|
535
|
+
internalId: typeof row.internalId === "string" ? row.internalId : null,
|
|
536
|
+
where: [{ field: "versionSlug", equals: slug, absentMeans: "v1" }],
|
|
537
|
+
});
|
|
538
|
+
if (located === null) appendRow({ id, entry: { versionSlug: slug } });
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
for (const entry of entries) {
|
|
543
|
+
const id = entry[spec.idField];
|
|
544
|
+
if (typeof id !== "string") continue;
|
|
545
|
+
// `none` and `skip` mean the code already matches the server.
|
|
546
|
+
if (entry.action === "create") removeFixture({ id, entry });
|
|
547
|
+
else if (entry.action === "delete") appendRow({ id, entry });
|
|
548
|
+
else if (entry.action === "update") replaceRow({ id, entry });
|
|
549
|
+
}
|
|
550
|
+
appendUnstatedVersions();
|
|
551
|
+
|
|
552
|
+
return result;
|
|
553
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { SingletonSpec } from "../../generated/emitRuntime";
|
|
2
|
+
import type { SettingChange } from "../../render/renderPreview";
|
|
3
|
+
import {
|
|
4
|
+
insertSingleton,
|
|
5
|
+
patchSingletonProperty,
|
|
6
|
+
type SingletonBlock,
|
|
7
|
+
} from "../../surgery/patchSingletonProperty";
|
|
8
|
+
import { resolveCollectionTarget } from "./resolveCollectionTarget";
|
|
9
|
+
|
|
10
|
+
export type ApplySettingsResult = {
|
|
11
|
+
/** One printed line per key changed, in the order applied. */
|
|
12
|
+
lines: string[];
|
|
13
|
+
/** Keys the config could not take an edit for. */
|
|
14
|
+
unlocated: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** The file and literal the block lives in: inline, a local const, or an
|
|
18
|
+
* imported one — like a collection's array. Null when it is none of these. */
|
|
19
|
+
const locateBlock = ({
|
|
20
|
+
singleton,
|
|
21
|
+
configPath,
|
|
22
|
+
files,
|
|
23
|
+
}: {
|
|
24
|
+
singleton: string;
|
|
25
|
+
configPath: string;
|
|
26
|
+
files: Map<string, string>;
|
|
27
|
+
}): { file: string; block: SingletonBlock } | null => {
|
|
28
|
+
const target = resolveCollectionTarget({
|
|
29
|
+
configPath,
|
|
30
|
+
files,
|
|
31
|
+
collection: singleton,
|
|
32
|
+
kind: "object",
|
|
33
|
+
});
|
|
34
|
+
if (target === null) return null;
|
|
35
|
+
return target.kind === "inline"
|
|
36
|
+
? { file: target.file, block: { kind: "inline", singleton } }
|
|
37
|
+
: { file: target.file, block: { kind: "binding", name: target.name } };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The reversal of a settings preview: the block ends up stating every
|
|
42
|
+
* non-default flag the server holds, and none at its default — an absent
|
|
43
|
+
* flag already reads as its default. `update` says the config states the
|
|
44
|
+
* wrong value; `unmanaged` says it states none and the server holds a
|
|
45
|
+
* non-default; a stated flag the preview is silent on matches the server,
|
|
46
|
+
* and goes only when that shared value is the default.
|
|
47
|
+
*/
|
|
48
|
+
export const applySettingsPreview = ({
|
|
49
|
+
singleton,
|
|
50
|
+
spec,
|
|
51
|
+
changes,
|
|
52
|
+
stated,
|
|
53
|
+
configPath,
|
|
54
|
+
files,
|
|
55
|
+
}: {
|
|
56
|
+
singleton: string;
|
|
57
|
+
spec: SingletonSpec;
|
|
58
|
+
changes: SettingChange[];
|
|
59
|
+
/** The block's wire body as the config states it, if any. */
|
|
60
|
+
stated: Record<string, unknown> | undefined;
|
|
61
|
+
configPath: string;
|
|
62
|
+
files: Map<string, string>;
|
|
63
|
+
}): ApplySettingsResult => {
|
|
64
|
+
const result: ApplySettingsResult = { lines: [], unlocated: [] };
|
|
65
|
+
const serverValueOf = new Map<string, unknown>();
|
|
66
|
+
for (const change of changes) {
|
|
67
|
+
if (change.key !== undefined)
|
|
68
|
+
serverValueOf.set(change.key, change.previous);
|
|
69
|
+
}
|
|
70
|
+
for (const [wireKey, value] of Object.entries(stated ?? {})) {
|
|
71
|
+
if (!serverValueOf.has(wireKey)) serverValueOf.set(wireKey, value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const edits = spec.fields.flatMap((field) => {
|
|
75
|
+
if (!serverValueOf.has(field.wireKey)) return [];
|
|
76
|
+
const serverValue = serverValueOf.get(field.wireKey);
|
|
77
|
+
const text =
|
|
78
|
+
serverValue === field.default ? null : JSON.stringify(serverValue);
|
|
79
|
+
return [{ key: field.key, text }];
|
|
80
|
+
});
|
|
81
|
+
// A block the config never had is seeded only when there is a value to
|
|
82
|
+
// put in it: `settings: {}` manages nothing.
|
|
83
|
+
if (edits.some((edit) => edit.text !== null)) {
|
|
84
|
+
const seeded = insertSingleton({
|
|
85
|
+
source: files.get(configPath) ?? "",
|
|
86
|
+
singleton,
|
|
87
|
+
});
|
|
88
|
+
if (seeded !== null) files.set(configPath, seeded);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const edit of edits) {
|
|
92
|
+
const located = locateBlock({ singleton, configPath, files });
|
|
93
|
+
if (located === null) {
|
|
94
|
+
// A value to write needs a literal to land in; a removal from a block
|
|
95
|
+
// that cannot be found is a flag already stated by other means.
|
|
96
|
+
if (edit.text !== null) result.unlocated.push(edit.key);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const source = files.get(located.file) ?? "";
|
|
100
|
+
const updated = patchSingletonProperty({
|
|
101
|
+
source,
|
|
102
|
+
block: located.block,
|
|
103
|
+
edit,
|
|
104
|
+
});
|
|
105
|
+
if (updated === null) {
|
|
106
|
+
result.unlocated.push(edit.key);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (updated === source) continue;
|
|
110
|
+
files.set(located.file, updated);
|
|
111
|
+
result.lines.push(`~ ${singleton}.${edit.key}`);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|