@prisma/orm-mongo 8.0.0-rc.18-dev.46 → 8.0.0-rc.19
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/{mongo-static-Be-tE2Ra-a5WLdGKy.mjs → mongo-static-DNXbLhsJ-C9Jn5l2_.mjs} +3 -3
- package/dist/mongo-static-DNXbLhsJ-C9Jn5l2_.mjs.map +1 -0
- package/dist/runtime.mjs +1 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/static.mjs +1 -1
- package/package.json +8 -8
- package/skills/prisma-8/SKILL.md +1 -1
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/instructions.md +1826 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/client-takes-contract-value/contract-from-json.ts +382 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/data-type-values/data-type-values.ts +1117 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/default-generators-to-on-create/rewrite-default-generators.mjs +86 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/mongo-field-storage-names/mongo-field-storage-names.ts +250 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/remove-nocheck/remove-nocheck.ts +813 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/instructions.md +2000 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/client-takes-contract-value/contract-from-json.ts +382 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.18-to-8.0.0-rc.19/scripts/remove-nocheck/remove-nocheck.ts +813 -0
- package/dist/mongo-static-Be-tE2Ra-a5WLdGKy.mjs.map +0 -1
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Removes the `noCheck` key from every column of every SQL contract in a project. Where the key waived the element check of a list, the column and the field that maps it become lists whose elements may be null, in `contract.json` and `contract.d.ts`, because that is what the database enforces. The checks each contract declares are left as they are. The script then renames the snapshot directories and rewrites every migration, ref, `migration.ts` and `contract.d.ts` that names an old storage hash.
|
|
4
|
+
*
|
|
5
|
+
* Usage: node remove-nocheck.ts [project-root]
|
|
6
|
+
*
|
|
7
|
+
* The project root defaults to the working directory. The script reads and writes files only. It needs no database, network or configured stack, and a project with no `noCheck` key is left unchanged. When it stops (a file it must read is not valid JSON, a contract whose stored hash does not match its content, or a snapshot directory that already exists with different content) it changes no file, prints one line per case and exits 1. When it finishes it prints how many files and snapshot directories it changed and each storage hash it replaced.
|
|
8
|
+
*
|
|
9
|
+
* The storage hash and migration hash rules are copied from `@internal/contract` and `@internal/migration-tools` because a project's strict `node_modules` does not expose them.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import {
|
|
13
|
+
chmodSync,
|
|
14
|
+
type Dirent,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
realpathSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
26
|
+
|
|
27
|
+
type Json = null | boolean | number | string | Json[] | JsonRecord;
|
|
28
|
+
interface JsonRecord {
|
|
29
|
+
[key: string]: Json;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
33
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
34
|
+
const TEMPORARY_SUFFIX = '.remove-nocheck-tmp';
|
|
35
|
+
|
|
36
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
37
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
38
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
39
|
+
return prototype === Object.prototype || prototype === null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseJson(text: string): Json | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const value: Json = JSON.parse(text);
|
|
45
|
+
return value;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sha256(text: string): string {
|
|
52
|
+
return createHash('sha256').update(text).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compareCodeUnits(a: string, b: string): number {
|
|
56
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sortKeys(value: Json): Json {
|
|
60
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
61
|
+
if (!isRecord(value)) return value;
|
|
62
|
+
const sorted: JsonRecord = {};
|
|
63
|
+
for (const key of Object.keys(value).sort(compareCodeUnits)) {
|
|
64
|
+
const child = value[key];
|
|
65
|
+
if (child !== undefined) sorted[key] = sortKeys(child);
|
|
66
|
+
}
|
|
67
|
+
return sorted;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function canonicalizeJson(value: Json): string {
|
|
71
|
+
return JSON.stringify(sortKeys(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type PathSegment = string | readonly string[];
|
|
75
|
+
|
|
76
|
+
function matchesPath(path: readonly string[], pattern: readonly PathSegment[]): boolean {
|
|
77
|
+
if (path.length !== pattern.length) return false;
|
|
78
|
+
return pattern.every((segment, index) => {
|
|
79
|
+
const value = path[index];
|
|
80
|
+
if (value === undefined) return false;
|
|
81
|
+
if (segment === '*') return true;
|
|
82
|
+
return typeof segment === 'string' ? value === segment : segment.includes(value);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const SQL_PRESERVED_EMPTY: readonly (readonly PathSegment[])[] = [
|
|
87
|
+
['storage', 'namespaces', '*', 'entries', 'table'],
|
|
88
|
+
['storage', 'namespaces', '*', 'entries', 'table', '*'],
|
|
89
|
+
['storage', 'namespaces', '*', 'entries', 'table', '*', ['uniques', 'indexes', 'foreignKeys']],
|
|
90
|
+
['storage', 'namespaces', '*', 'entries', 'table', '*', 'indexes', 'unique'],
|
|
91
|
+
['storage', 'namespaces', '*', 'entries', 'table', '*', 'columns', '*', 'default', 'value'],
|
|
92
|
+
];
|
|
93
|
+
const COLUMN_DEFAULT_VALUE: readonly PathSegment[] = [
|
|
94
|
+
'storage',
|
|
95
|
+
'namespaces',
|
|
96
|
+
'*',
|
|
97
|
+
'entries',
|
|
98
|
+
'table',
|
|
99
|
+
'*',
|
|
100
|
+
'columns',
|
|
101
|
+
'*',
|
|
102
|
+
'default',
|
|
103
|
+
'value',
|
|
104
|
+
];
|
|
105
|
+
const FRAMEWORK_PRESERVED_EMPTY: readonly (readonly PathSegment[])[] = [
|
|
106
|
+
['domain', 'namespaces'],
|
|
107
|
+
['domain', 'namespaces', '*'],
|
|
108
|
+
['domain', 'namespaces', '*', 'models'],
|
|
109
|
+
['domain', 'namespaces', '*', 'models', '*', 'relations'],
|
|
110
|
+
['domain', 'namespaces', '*', 'models', '*', 'storage'],
|
|
111
|
+
['storage', 'namespaces'],
|
|
112
|
+
['storage', 'namespaces', '*', 'entries'],
|
|
113
|
+
['roots'],
|
|
114
|
+
['extensions'],
|
|
115
|
+
['extensions', '*'],
|
|
116
|
+
['capabilities'],
|
|
117
|
+
['meta'],
|
|
118
|
+
['execution', 'mutations', 'defaults'],
|
|
119
|
+
];
|
|
120
|
+
const TOP_LEVEL_ORDER = [
|
|
121
|
+
'schemaVersion',
|
|
122
|
+
'canonicalVersion',
|
|
123
|
+
'targetFamily',
|
|
124
|
+
'target',
|
|
125
|
+
'profileHash',
|
|
126
|
+
'roots',
|
|
127
|
+
'domain',
|
|
128
|
+
'storage',
|
|
129
|
+
'execution',
|
|
130
|
+
'capabilities',
|
|
131
|
+
'extensions',
|
|
132
|
+
'defaultControlPolicy',
|
|
133
|
+
'meta',
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
function isDefaultValue(value: Json): boolean {
|
|
137
|
+
if (value === false) return true;
|
|
138
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
139
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isPreservedEmpty(path: readonly string[], key: string): boolean {
|
|
143
|
+
if (key === 'nullable') return true;
|
|
144
|
+
if (key === 'elementNullable' && path.at(-2) === 'many') return true;
|
|
145
|
+
if (FRAMEWORK_PRESERVED_EMPTY.some((pattern) => matchesPath(path, pattern))) return true;
|
|
146
|
+
if (
|
|
147
|
+
path.length >= COLUMN_DEFAULT_VALUE.length &&
|
|
148
|
+
matchesPath(path.slice(0, COLUMN_DEFAULT_VALUE.length), COLUMN_DEFAULT_VALUE)
|
|
149
|
+
)
|
|
150
|
+
return true;
|
|
151
|
+
return SQL_PRESERVED_EMPTY.some((pattern) => matchesPath(path, pattern));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function omitDefaults(value: Json, path: readonly string[]): Json {
|
|
155
|
+
if (Array.isArray(value)) return value.map((item) => omitDefaults(item, path));
|
|
156
|
+
if (!isRecord(value)) return value;
|
|
157
|
+
const result: JsonRecord = {};
|
|
158
|
+
for (const [key, child] of Object.entries(value)) {
|
|
159
|
+
const childPath = [...path, key];
|
|
160
|
+
if (key === '_generated') continue;
|
|
161
|
+
if (key === 'generated' && child === false) continue;
|
|
162
|
+
if ((key === 'onDelete' || key === 'onUpdate') && child === 'noAction') continue;
|
|
163
|
+
if (isDefaultValue(child) && !isPreservedEmpty(childPath, key)) continue;
|
|
164
|
+
result[key] = omitDefaults(child, childPath);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function compareByName(a: Json, b: Json): number {
|
|
170
|
+
const nameA = isRecord(a) && typeof a['name'] === 'string' ? a['name'] : '';
|
|
171
|
+
const nameB = isRecord(b) && typeof b['name'] === 'string' ? b['name'] : '';
|
|
172
|
+
return compareCodeUnits(nameA, nameB);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sortTableArrays(storage: Json): Json {
|
|
176
|
+
if (!isRecord(storage)) return storage;
|
|
177
|
+
const namespaces = storage['namespaces'];
|
|
178
|
+
if (!isRecord(namespaces)) return storage;
|
|
179
|
+
const sortedNamespaces: JsonRecord = {};
|
|
180
|
+
for (const [namespaceId, namespace] of Object.entries(namespaces)) {
|
|
181
|
+
const entries = isRecord(namespace) ? namespace['entries'] : undefined;
|
|
182
|
+
const tables = isRecord(entries) ? entries['table'] : undefined;
|
|
183
|
+
if (!isRecord(namespace) || !isRecord(entries) || !isRecord(tables)) {
|
|
184
|
+
sortedNamespaces[namespaceId] = namespace;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const sortedTables: JsonRecord = {};
|
|
188
|
+
for (const [tableName, table] of Object.entries(tables)) {
|
|
189
|
+
if (!isRecord(table)) {
|
|
190
|
+
sortedTables[tableName] = table;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const sortedTable: JsonRecord = { ...table };
|
|
194
|
+
for (const key of ['checks', 'indexes', 'uniques']) {
|
|
195
|
+
const list = table[key];
|
|
196
|
+
if (Array.isArray(list)) sortedTable[key] = [...list].sort(compareByName);
|
|
197
|
+
}
|
|
198
|
+
sortedTables[tableName] = sortedTable;
|
|
199
|
+
}
|
|
200
|
+
sortedNamespaces[namespaceId] = { ...namespace, entries: { ...entries, table: sortedTables } };
|
|
201
|
+
}
|
|
202
|
+
return { ...storage, namespaces: sortedNamespaces };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function orderTopLevel(value: JsonRecord): JsonRecord {
|
|
206
|
+
const ordered: JsonRecord = {};
|
|
207
|
+
const remaining = new Set(Object.keys(value));
|
|
208
|
+
for (const key of TOP_LEVEL_ORDER) {
|
|
209
|
+
const child = value[key];
|
|
210
|
+
if (child !== undefined) {
|
|
211
|
+
ordered[key] = child;
|
|
212
|
+
remaining.delete(key);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const key of [...remaining].sort(compareCodeUnits)) {
|
|
216
|
+
const child = value[key];
|
|
217
|
+
if (child !== undefined) ordered[key] = child;
|
|
218
|
+
}
|
|
219
|
+
return ordered;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function omitNamespaceKinds(storage: JsonRecord): JsonRecord {
|
|
223
|
+
const namespaces = storage['namespaces'];
|
|
224
|
+
if (!isRecord(namespaces)) return storage;
|
|
225
|
+
const stripped: JsonRecord = {};
|
|
226
|
+
for (const [namespaceId, namespace] of Object.entries(namespaces)) {
|
|
227
|
+
if (isRecord(namespace)) {
|
|
228
|
+
const { kind: _kind, ...rest } = namespace;
|
|
229
|
+
stripped[namespaceId] = rest;
|
|
230
|
+
} else {
|
|
231
|
+
stripped[namespaceId] = namespace;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ...storage, namespaces: stripped };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function computeStorageHash(contract: JsonRecord): string {
|
|
238
|
+
const storage = isRecord(contract['storage']) ? contract['storage'] : {};
|
|
239
|
+
const { storageHash: _published, ...withoutHash } = storage;
|
|
240
|
+
const hashed: JsonRecord = {
|
|
241
|
+
schemaVersion: '1',
|
|
242
|
+
targetFamily: typeof contract['targetFamily'] === 'string' ? contract['targetFamily'] : '',
|
|
243
|
+
target: typeof contract['target'] === 'string' ? contract['target'] : '',
|
|
244
|
+
profileHash: '',
|
|
245
|
+
roots: {},
|
|
246
|
+
domain: { namespaces: {} },
|
|
247
|
+
storage: omitNamespaceKinds(withoutHash),
|
|
248
|
+
extensions: {},
|
|
249
|
+
capabilities: {},
|
|
250
|
+
meta: {},
|
|
251
|
+
};
|
|
252
|
+
const withoutDefaults = omitDefaults(hashed, []);
|
|
253
|
+
if (!isRecord(withoutDefaults)) return '';
|
|
254
|
+
const withSortedStorage = {
|
|
255
|
+
...withoutDefaults,
|
|
256
|
+
storage: sortTableArrays(withoutDefaults['storage'] ?? {}),
|
|
257
|
+
};
|
|
258
|
+
const sorted = sortKeys(withSortedStorage);
|
|
259
|
+
return isRecord(sorted) ? sha256(JSON.stringify(orderTopLevel(sorted), null, 2)) : '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function computeMigrationHash(metadata: JsonRecord, ops: Json): string {
|
|
263
|
+
const { migrationHash: _migrationHash, ...withoutHash } = metadata;
|
|
264
|
+
const parts = [canonicalizeJson(withoutHash), canonicalizeJson(ops)].map(sha256);
|
|
265
|
+
return sha256(canonicalizeJson(parts));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface ProjectFiles {
|
|
269
|
+
readonly json: readonly string[];
|
|
270
|
+
readonly migrationJson: readonly string[];
|
|
271
|
+
readonly refs: readonly string[];
|
|
272
|
+
readonly migrationTs: readonly string[];
|
|
273
|
+
readonly leftovers: readonly string[];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isDirectory(path: string): boolean {
|
|
277
|
+
try {
|
|
278
|
+
return statSync(path).isDirectory();
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isFile(path: string): boolean {
|
|
285
|
+
try {
|
|
286
|
+
return statSync(path).isFile();
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A file, or a link to one: the script reads and writes through links to files. */
|
|
293
|
+
function isFileEntry(dir: string, entry: Dirent): boolean {
|
|
294
|
+
return entry.isFile() || (entry.isSymbolicLink() && isFile(join(dir, entry.name)));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** A file the script reads, by the path it was reached through: its own path rather than a link's. */
|
|
298
|
+
interface FoundFile {
|
|
299
|
+
readonly path: string;
|
|
300
|
+
readonly linked: boolean;
|
|
301
|
+
readonly inMigrationPackage: boolean;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function listFiles(root: string): ProjectFiles {
|
|
305
|
+
const leftovers: string[] = [];
|
|
306
|
+
const visited = new Set<string>();
|
|
307
|
+
const found = new Map<string, FoundFile>();
|
|
308
|
+
const visit = (dir: string): void => {
|
|
309
|
+
const realDir = realpathSync(dir);
|
|
310
|
+
if (visited.has(realDir)) return;
|
|
311
|
+
visited.add(realDir);
|
|
312
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
|
|
313
|
+
compareCodeUnits(a.name, b.name),
|
|
314
|
+
);
|
|
315
|
+
const inMigrationPackage = entries.some(
|
|
316
|
+
(entry) => entry.name === 'migration.json' && isFileEntry(dir, entry),
|
|
317
|
+
);
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
const path = join(dir, entry.name);
|
|
320
|
+
if (entry.name.endsWith(TEMPORARY_SUFFIX)) {
|
|
321
|
+
leftovers.push(path);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (entry.isDirectory() || (entry.isSymbolicLink() && isDirectory(path))) {
|
|
325
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) visit(path);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!isFileEntry(dir, entry)) continue;
|
|
329
|
+
const realFile = realpathSync(path);
|
|
330
|
+
const linked = entry.isSymbolicLink();
|
|
331
|
+
const known = found.get(realFile);
|
|
332
|
+
if (known === undefined || (known.linked && !linked))
|
|
333
|
+
found.set(realFile, { path, linked, inMigrationPackage });
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
visit(root);
|
|
337
|
+
const files = [...found.values()];
|
|
338
|
+
const named = (test: (file: FoundFile, name: string) => boolean): string[] =>
|
|
339
|
+
files.filter((file) => test(file, basename(file.path))).map((file) => file.path);
|
|
340
|
+
return {
|
|
341
|
+
json: named((_file, name) => name.endsWith('.json')),
|
|
342
|
+
migrationJson: named((_file, name) => name === 'migration.json'),
|
|
343
|
+
refs: named((file, name) => name.endsWith('.json') && basename(dirname(file.path)) === 'refs'),
|
|
344
|
+
migrationTs: named((file, name) => name === 'migration.ts' && file.inMigrationPackage),
|
|
345
|
+
leftovers,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isSnapshotContract(path: string): boolean {
|
|
350
|
+
const snapshotDir = dirname(path);
|
|
351
|
+
return (
|
|
352
|
+
basename(path) === 'contract.json' &&
|
|
353
|
+
HASH.test(basename(snapshotDir)) &&
|
|
354
|
+
basename(dirname(snapshotDir)) === 'snapshots'
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function replaceQuotedHashes(text: string, hashes: ReadonlyMap<string, string>): string {
|
|
359
|
+
return text.replace(/"([0-9a-f]{64})"/g, (match: string, hash: string) => {
|
|
360
|
+
const next = hashes.get(hash);
|
|
361
|
+
return next === undefined ? match : `"${next}"`;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readDirectory(dir: string): Map<string, string> {
|
|
366
|
+
const files = new Map<string, string>();
|
|
367
|
+
if (!existsSync(dir)) return files;
|
|
368
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }))
|
|
369
|
+
if (isFileEntry(dir, entry) && !entry.name.endsWith(TEMPORARY_SUFFIX))
|
|
370
|
+
files.set(entry.name, readFileSync(join(dir, entry.name), 'utf8'));
|
|
371
|
+
return files;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function sameFiles(a: ReadonlyMap<string, string>, b: ReadonlyMap<string, string>): boolean {
|
|
375
|
+
return a.size === b.size && [...a].every(([name, content]) => b.get(name) === content);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function temporaryPath(path: string): string {
|
|
379
|
+
return join(dirname(path), `.${basename(path)}${TEMPORARY_SUFFIX}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function writeWithModeOf(path: string, text: string, modeSource: string): void {
|
|
383
|
+
writeFileSync(path, text);
|
|
384
|
+
if (existsSync(modeSource)) chmodSync(path, statSync(modeSource).mode & 0o7777);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function writeFile(path: string, text: string): void {
|
|
388
|
+
const target = realpathSync(path);
|
|
389
|
+
const temporary = temporaryPath(target);
|
|
390
|
+
writeWithModeOf(temporary, text, target);
|
|
391
|
+
renameSync(temporary, target);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function writeDirectory(dir: string, files: ReadonlyMap<string, string>, sourceDir: string): void {
|
|
395
|
+
const temporary = temporaryPath(dir);
|
|
396
|
+
mkdirSync(temporary);
|
|
397
|
+
for (const [name, text] of files)
|
|
398
|
+
writeWithModeOf(join(temporary, name), text, join(sourceDir, name));
|
|
399
|
+
renameSync(temporary, dir);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function removeDirectory(dir: string): void {
|
|
403
|
+
const temporary = temporaryPath(dir);
|
|
404
|
+
renameSync(dir, temporary);
|
|
405
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function count(amount: number, singular: string, plural: string): string {
|
|
409
|
+
return `${amount} ${amount === 1 ? singular : plural}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const REMOVED_KEY = 'noCheck';
|
|
413
|
+
const ELEMENT_KIND = 'elementNotNull';
|
|
414
|
+
|
|
415
|
+
function listKey(namespace: string, owner: string, member: string): string {
|
|
416
|
+
return `${namespace}\u0000${owner}\u0000${member}`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface Rewrite {
|
|
420
|
+
readonly contract: JsonRecord;
|
|
421
|
+
readonly changed: boolean;
|
|
422
|
+
readonly columns: ReadonlySet<string>;
|
|
423
|
+
readonly fields: ReadonlySet<string>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function allowNullElements(holder: JsonRecord): void {
|
|
427
|
+
const many = holder['many'];
|
|
428
|
+
if (isRecord(many)) holder['many'] = { ...many, elementNullable: true };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Mutates `copy`: the domain fields that map a column whose element check was waived. */
|
|
432
|
+
function fieldsMappingColumns(copy: JsonRecord, columns: ReadonlySet<string>): Set<string> {
|
|
433
|
+
const fields = new Set<string>();
|
|
434
|
+
const domain = copy['domain'];
|
|
435
|
+
const namespaces = isRecord(domain) ? domain['namespaces'] : undefined;
|
|
436
|
+
for (const [namespaceId, namespace] of isRecord(namespaces) ? Object.entries(namespaces) : []) {
|
|
437
|
+
const models = isRecord(namespace) ? namespace['models'] : undefined;
|
|
438
|
+
for (const [modelName, model] of isRecord(models) ? Object.entries(models) : []) {
|
|
439
|
+
if (!isRecord(model)) continue;
|
|
440
|
+
const storage = model['storage'];
|
|
441
|
+
const modelFields = model['fields'];
|
|
442
|
+
if (!isRecord(storage) || !isRecord(modelFields)) continue;
|
|
443
|
+
const table = storage['table'];
|
|
444
|
+
const tableNamespace =
|
|
445
|
+
typeof storage['namespaceId'] === 'string' ? storage['namespaceId'] : namespaceId;
|
|
446
|
+
const mapped = storage['fields'];
|
|
447
|
+
for (const [fieldName, mapping] of isRecord(mapped) ? Object.entries(mapped) : []) {
|
|
448
|
+
const column = isRecord(mapping) ? mapping['column'] : undefined;
|
|
449
|
+
const field = modelFields[fieldName];
|
|
450
|
+
if (typeof table !== 'string' || typeof column !== 'string' || !isRecord(field)) continue;
|
|
451
|
+
if (!columns.has(listKey(tableNamespace, table, column))) continue;
|
|
452
|
+
allowNullElements(field);
|
|
453
|
+
fields.add(listKey(namespaceId, modelName, fieldName));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return fields;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The contract with the key removed from each column, and the lists whose element check it waived made element-nullable. */
|
|
461
|
+
function rewriteContract(contract: JsonRecord): Rewrite {
|
|
462
|
+
const copy = parseJson(JSON.stringify(contract));
|
|
463
|
+
if (!isRecord(copy)) return { contract, changed: false, columns: new Set(), fields: new Set() };
|
|
464
|
+
let changed = false;
|
|
465
|
+
const columns = new Set<string>();
|
|
466
|
+
const storage = copy['storage'];
|
|
467
|
+
const namespaces = isRecord(storage) ? storage['namespaces'] : undefined;
|
|
468
|
+
for (const [namespaceId, namespace] of isRecord(namespaces) ? Object.entries(namespaces) : []) {
|
|
469
|
+
const entries = isRecord(namespace) ? namespace['entries'] : undefined;
|
|
470
|
+
const tables = isRecord(entries) ? entries['table'] : undefined;
|
|
471
|
+
for (const [tableName, table] of isRecord(tables) ? Object.entries(tables) : []) {
|
|
472
|
+
const tableColumns = isRecord(table) ? table['columns'] : undefined;
|
|
473
|
+
for (const [columnName, column] of isRecord(tableColumns)
|
|
474
|
+
? Object.entries(tableColumns)
|
|
475
|
+
: []) {
|
|
476
|
+
if (!isRecord(column) || !Object.hasOwn(column, REMOVED_KEY)) continue;
|
|
477
|
+
const kinds = column[REMOVED_KEY];
|
|
478
|
+
delete column[REMOVED_KEY];
|
|
479
|
+
changed = true;
|
|
480
|
+
if (Array.isArray(kinds) && kinds.includes(ELEMENT_KIND) && isRecord(column['many'])) {
|
|
481
|
+
allowNullElements(column);
|
|
482
|
+
columns.add(listKey(namespaceId, tableName, columnName));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return { contract: copy, changed, columns, fields: fieldsMappingColumns(copy, columns) };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function withStorageHash(contract: JsonRecord, hash: string): JsonRecord {
|
|
491
|
+
const storage = contract['storage'];
|
|
492
|
+
if (!isRecord(storage) || typeof storage['storageHash'] !== 'string') return contract;
|
|
493
|
+
return { ...contract, storage: { ...storage, storageHash: hash } };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The contract written the way the file held it: a snapshot's canonical line, or indented JSON. */
|
|
497
|
+
function writtenForm(contract: JsonRecord, original: JsonRecord, text: string): string {
|
|
498
|
+
const newline = text.endsWith('\n') ? '\n' : '';
|
|
499
|
+
if (text === `${canonicalizeJson(original)}${newline}`)
|
|
500
|
+
return `${canonicalizeJson(contract)}${newline}`;
|
|
501
|
+
return `${JSON.stringify(contract, null, 2)}${newline}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
interface Block {
|
|
505
|
+
readonly start: number;
|
|
506
|
+
end: number;
|
|
507
|
+
readonly label: string;
|
|
508
|
+
readonly parent: Block | undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function scanBlocks(text: string): Block[] {
|
|
512
|
+
const blocks: Block[] = [];
|
|
513
|
+
const open: Block[] = [];
|
|
514
|
+
let quote: string | undefined;
|
|
515
|
+
for (let index = 0; index < text.length; index++) {
|
|
516
|
+
const char = text[index];
|
|
517
|
+
if (quote !== undefined) {
|
|
518
|
+
if (char === '\\') index++;
|
|
519
|
+
else if (char === quote) quote = undefined;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
523
|
+
quote = char;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (char === '{') {
|
|
527
|
+
const key =
|
|
528
|
+
/(?:readonly\s+)?([\p{ID_Start}_$][\p{ID_Continue}$]*|'[^']*'|"[^"]*")\s*\??\s*:\s*$/u.exec(
|
|
529
|
+
text.slice(Math.max(0, index - 200), index),
|
|
530
|
+
);
|
|
531
|
+
const block: Block = {
|
|
532
|
+
start: index,
|
|
533
|
+
end: text.length,
|
|
534
|
+
label: key?.[1]?.replace(/^['"]|['"]$/g, '') ?? '',
|
|
535
|
+
parent: open.at(-1),
|
|
536
|
+
};
|
|
537
|
+
open.push(block);
|
|
538
|
+
blocks.push(block);
|
|
539
|
+
} else if (char === '}') {
|
|
540
|
+
const block = open.pop();
|
|
541
|
+
if (block !== undefined) block.end = index;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return blocks;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function innermostBlock(blocks: readonly Block[], position: number): Block | undefined {
|
|
548
|
+
let found: Block | undefined;
|
|
549
|
+
for (const block of blocks)
|
|
550
|
+
if (
|
|
551
|
+
block.start < position &&
|
|
552
|
+
position < block.end &&
|
|
553
|
+
(found === undefined || block.start > found.start)
|
|
554
|
+
)
|
|
555
|
+
found = block;
|
|
556
|
+
return found;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The key of a storage column block (`namespaces.<ns>.entries.table.<table>.columns.<column>`). */
|
|
560
|
+
function columnBlockKey(block: Block): string | undefined {
|
|
561
|
+
const table = block.parent?.parent;
|
|
562
|
+
const namespace = table?.parent?.parent?.parent;
|
|
563
|
+
if (block.parent?.label !== 'columns' || table?.parent?.label !== 'table') return undefined;
|
|
564
|
+
if (table.parent.parent?.label !== 'entries' || namespace === undefined) return undefined;
|
|
565
|
+
return listKey(namespace.label, table.label, block.label);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** The key of a domain field block (`namespaces.<ns>.models.<model>.fields.<field>`). */
|
|
569
|
+
function fieldBlockKey(block: Block): string | undefined {
|
|
570
|
+
const model = block.parent?.parent;
|
|
571
|
+
const namespace = model?.parent?.parent;
|
|
572
|
+
if (block.parent?.label !== 'fields' || model?.parent?.label !== 'models') return undefined;
|
|
573
|
+
if (namespace === undefined) return undefined;
|
|
574
|
+
return listKey(namespace.label, model.label, block.label);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function rewriteDts(
|
|
578
|
+
text: string,
|
|
579
|
+
columns: ReadonlySet<string>,
|
|
580
|
+
fields: ReadonlySet<string>,
|
|
581
|
+
hashes: ReadonlyMap<string, string>,
|
|
582
|
+
): string {
|
|
583
|
+
const blocks = scanBlocks(text);
|
|
584
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
585
|
+
for (const match of text.matchAll(/readonly elementNullable:\s*false/g)) {
|
|
586
|
+
const many = innermostBlock(blocks, match.index);
|
|
587
|
+
const holder = many?.label === 'many' ? many.parent : undefined;
|
|
588
|
+
if (holder === undefined) continue;
|
|
589
|
+
const column = columnBlockKey(holder);
|
|
590
|
+
const field = fieldBlockKey(holder);
|
|
591
|
+
if (
|
|
592
|
+
!(column !== undefined && columns.has(column)) &&
|
|
593
|
+
!(field !== undefined && fields.has(field))
|
|
594
|
+
)
|
|
595
|
+
continue;
|
|
596
|
+
edits.push({
|
|
597
|
+
start: match.index,
|
|
598
|
+
end: match.index + match[0].length,
|
|
599
|
+
text: 'readonly elementNullable: true',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
let result = text;
|
|
603
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
604
|
+
result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
|
|
605
|
+
return replaceHashTokens(result, hashes);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function replaceHashTokens(text: string, hashes: ReadonlyMap<string, string>): string {
|
|
609
|
+
return text.replace(
|
|
610
|
+
/(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])/g,
|
|
611
|
+
(hash: string) => hashes.get(hash) ?? hash,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
interface ContractPlan {
|
|
616
|
+
readonly path: string;
|
|
617
|
+
readonly snapshot: boolean;
|
|
618
|
+
readonly oldHash: string;
|
|
619
|
+
readonly newHash: string;
|
|
620
|
+
readonly content: string;
|
|
621
|
+
readonly columns: ReadonlySet<string>;
|
|
622
|
+
readonly fields: ReadonlySet<string>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function summary(
|
|
626
|
+
root: string,
|
|
627
|
+
contractsFound: number,
|
|
628
|
+
files: number,
|
|
629
|
+
directories: number,
|
|
630
|
+
hashes: ReadonlyMap<string, string>,
|
|
631
|
+
): readonly string[] {
|
|
632
|
+
if (contractsFound === 0) return [`No SQL contract was found under ${root}; nothing changed.`];
|
|
633
|
+
if (files === 0 && directories === 0)
|
|
634
|
+
return ['No contract stores a noCheck key; nothing changed.'];
|
|
635
|
+
const renamed = count(directories, 'snapshot directory', 'snapshot directories');
|
|
636
|
+
const changes = `Rewrote ${count(files, 'file', 'files')} and renamed ${renamed}.`;
|
|
637
|
+
if (hashes.size === 0) return [changes];
|
|
638
|
+
return [
|
|
639
|
+
`${changes} Storage hashes changed (old -> new):`,
|
|
640
|
+
...[...hashes]
|
|
641
|
+
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
642
|
+
.map(([oldHash, newHash]) => ` ${oldHash} -> ${newHash}`),
|
|
643
|
+
];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const JSON_FILES_THAT_MUST_PARSE = new Set(['contract.json', 'migration.json']);
|
|
647
|
+
|
|
648
|
+
function main(root: string): number {
|
|
649
|
+
const display = (path: string): string => relative(root, path).split(sep).join('/');
|
|
650
|
+
const files = listFiles(root);
|
|
651
|
+
const plans: ContractPlan[] = [];
|
|
652
|
+
const stops: string[] = [];
|
|
653
|
+
let contractsFound = 0;
|
|
654
|
+
|
|
655
|
+
for (const path of files.json) {
|
|
656
|
+
const text = readFileSync(path, 'utf8');
|
|
657
|
+
const contract = parseJson(text);
|
|
658
|
+
if (
|
|
659
|
+
contract === undefined &&
|
|
660
|
+
(JSON_FILES_THAT_MUST_PARSE.has(basename(path)) || files.refs.includes(path))
|
|
661
|
+
) {
|
|
662
|
+
stops.push(`${display(path)}: not valid JSON`);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (
|
|
666
|
+
!isRecord(contract) ||
|
|
667
|
+
contract['targetFamily'] !== 'sql' ||
|
|
668
|
+
typeof contract['target'] !== 'string' ||
|
|
669
|
+
!isRecord(contract['storage'])
|
|
670
|
+
)
|
|
671
|
+
continue;
|
|
672
|
+
contractsFound++;
|
|
673
|
+
const rewrite = rewriteContract(contract);
|
|
674
|
+
if (!rewrite.changed) continue;
|
|
675
|
+
const snapshot = isSnapshotContract(path);
|
|
676
|
+
const stored = contract['storage']['storageHash'];
|
|
677
|
+
const oldHash = snapshot ? basename(dirname(path)) : typeof stored === 'string' ? stored : '';
|
|
678
|
+
const computed = computeStorageHash(contract);
|
|
679
|
+
if (computed !== oldHash || (typeof stored === 'string' && stored !== oldHash)) {
|
|
680
|
+
stops.push(
|
|
681
|
+
`${display(path)}: the stored storage hash ${oldHash} does not match the contract's content, which hashes to ${computed}; restore the file from version control, or re-emit it, and run the script again`,
|
|
682
|
+
);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const newHash = computeStorageHash(rewrite.contract);
|
|
686
|
+
plans.push({
|
|
687
|
+
path,
|
|
688
|
+
snapshot,
|
|
689
|
+
oldHash,
|
|
690
|
+
newHash,
|
|
691
|
+
content: writtenForm(withStorageHash(rewrite.contract, newHash), contract, text),
|
|
692
|
+
columns: rewrite.columns,
|
|
693
|
+
fields: rewrite.fields,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const hashes = new Map<string, string>();
|
|
698
|
+
for (const plan of [...plans].sort((a, b) => Number(b.snapshot) - Number(a.snapshot)))
|
|
699
|
+
if (plan.oldHash !== plan.newHash && !hashes.has(plan.oldHash))
|
|
700
|
+
hashes.set(plan.oldHash, plan.newHash);
|
|
701
|
+
|
|
702
|
+
const newDirectories = new Map<string, Map<string, string>>();
|
|
703
|
+
const directorySources = new Map<string, string>();
|
|
704
|
+
const sourceDirectories = new Set<string>();
|
|
705
|
+
const removals = new Set<string>();
|
|
706
|
+
const snapshotRewrites: [string, string][] = [];
|
|
707
|
+
const referenceWrites = new Map<string, string>();
|
|
708
|
+
const contractWrites = new Map<string, string>();
|
|
709
|
+
for (const plan of plans) {
|
|
710
|
+
const dtsPath = plan.snapshot
|
|
711
|
+
? join(dirname(plan.path), 'contract.d.ts')
|
|
712
|
+
: `${plan.path.slice(0, -'.json'.length)}.d.ts`;
|
|
713
|
+
const dtsHashes = new Map([...hashes, [plan.oldHash, plan.newHash]]);
|
|
714
|
+
const dts = existsSync(dtsPath)
|
|
715
|
+
? rewriteDts(readFileSync(dtsPath, 'utf8'), plan.columns, plan.fields, dtsHashes)
|
|
716
|
+
: undefined;
|
|
717
|
+
if (!plan.snapshot) {
|
|
718
|
+
if (dts !== undefined) referenceWrites.set(dtsPath, dts);
|
|
719
|
+
contractWrites.set(plan.path, plan.content);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
const oldDir = dirname(plan.path);
|
|
723
|
+
const newDir = join(dirname(oldDir), plan.newHash);
|
|
724
|
+
sourceDirectories.add(oldDir);
|
|
725
|
+
if (newDir === oldDir) {
|
|
726
|
+
if (dts !== undefined) snapshotRewrites.push([dtsPath, dts]);
|
|
727
|
+
snapshotRewrites.push([plan.path, plan.content]);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
removals.add(oldDir);
|
|
731
|
+
const content = readDirectory(oldDir);
|
|
732
|
+
content.set('contract.json', plan.content);
|
|
733
|
+
if (dts !== undefined) content.set('contract.d.ts', dts);
|
|
734
|
+
const planned = newDirectories.get(newDir);
|
|
735
|
+
if (planned !== undefined && !sameFiles(planned, content)) {
|
|
736
|
+
stops.push(`${display(newDir)}: snapshot directory already exists with different content`);
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
newDirectories.set(newDir, content);
|
|
740
|
+
directorySources.set(newDir, oldDir);
|
|
741
|
+
}
|
|
742
|
+
for (const [dir, content] of newDirectories) {
|
|
743
|
+
if (sourceDirectories.has(dir) || !existsSync(dir)) continue;
|
|
744
|
+
if (sameFiles(readDirectory(dir), content)) newDirectories.delete(dir);
|
|
745
|
+
else stops.push(`${display(dir)}: snapshot directory already exists with different content`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for (const path of files.migrationJson) {
|
|
749
|
+
const text = readFileSync(path, 'utf8');
|
|
750
|
+
const metadata = parseJson(text);
|
|
751
|
+
if (!isRecord(metadata)) continue;
|
|
752
|
+
const from = metadata['from'];
|
|
753
|
+
const to = metadata['to'];
|
|
754
|
+
const nextFrom = typeof from === 'string' ? (hashes.get(from) ?? from) : from;
|
|
755
|
+
const nextTo = typeof to === 'string' ? (hashes.get(to) ?? to) : to;
|
|
756
|
+
if (nextFrom === from && nextTo === to) continue;
|
|
757
|
+
const opsPath = join(dirname(path), 'ops.json');
|
|
758
|
+
const ops = existsSync(opsPath) ? parseJson(readFileSync(opsPath, 'utf8')) : undefined;
|
|
759
|
+
if (existsSync(opsPath) && ops === undefined) {
|
|
760
|
+
stops.push(`${display(opsPath)}: not valid JSON`);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const oldMigrationHash = metadata['migrationHash'];
|
|
764
|
+
let next = replaceQuotedHashes(text, hashes);
|
|
765
|
+
if (
|
|
766
|
+
ops !== undefined &&
|
|
767
|
+
typeof oldMigrationHash === 'string' &&
|
|
768
|
+
nextFrom !== undefined &&
|
|
769
|
+
nextTo !== undefined
|
|
770
|
+
) {
|
|
771
|
+
const migrationHash = computeMigrationHash({ ...metadata, from: nextFrom, to: nextTo }, ops);
|
|
772
|
+
next = next.replace(`"${oldMigrationHash}"`, `"${migrationHash}"`);
|
|
773
|
+
}
|
|
774
|
+
referenceWrites.set(path, next);
|
|
775
|
+
}
|
|
776
|
+
for (const path of files.refs) {
|
|
777
|
+
const text = readFileSync(path, 'utf8');
|
|
778
|
+
const next = replaceQuotedHashes(text, hashes);
|
|
779
|
+
if (next !== text) referenceWrites.set(path, next);
|
|
780
|
+
}
|
|
781
|
+
for (const path of files.migrationTs) {
|
|
782
|
+
const text = readFileSync(path, 'utf8');
|
|
783
|
+
const next = replaceHashTokens(text, hashes);
|
|
784
|
+
if (next !== text) referenceWrites.set(path, next);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (stops.length > 0) {
|
|
788
|
+
process.stderr.write(`${[...new Set(stops)].join('\n')}\n`);
|
|
789
|
+
return 1;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
try {
|
|
793
|
+
for (const path of files.leftovers) rmSync(path, { recursive: true, force: true });
|
|
794
|
+
for (const [dir, content] of newDirectories)
|
|
795
|
+
writeDirectory(dir, content, directorySources.get(dir) ?? dir);
|
|
796
|
+
for (const [path, text] of snapshotRewrites) writeFile(path, text);
|
|
797
|
+
for (const [path, text] of referenceWrites) writeFile(path, text);
|
|
798
|
+
for (const [path, text] of contractWrites) writeFile(path, text);
|
|
799
|
+
for (const dir of removals) removeDirectory(dir);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
802
|
+
process.stderr.write(
|
|
803
|
+
`${message}; the upgrade stopped partway, run the script again to finish it\n`,
|
|
804
|
+
);
|
|
805
|
+
return 1;
|
|
806
|
+
}
|
|
807
|
+
const rewrittenFiles = snapshotRewrites.length + referenceWrites.size + contractWrites.size;
|
|
808
|
+
const lines = summary(root, contractsFound, rewrittenFiles, removals.size, hashes);
|
|
809
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
810
|
+
return 0;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
process.exitCode = main(resolve(process.argv[2] ?? '.'));
|