@domino-sdk/relay-cli 0.6.0 → 0.8.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/cli/commands/hosting.mjs +19 -2
- package/cli/commands/project.mjs +25 -3
- package/cli/dev.mjs +57 -35
- package/cli/migrate.mjs +399 -0
- package/cli/output.mjs +11 -0
- package/cli/project.mjs +39 -15
- package/cli/skills/domino/references/authoring.md +50 -4
- package/cli/skills/domino/references/existing-app.md +4 -0
- package/cli/skills/domino/references/participant-api.md +4 -0
- package/cli/skills/domino/references/referrals.md +1 -1
- package/dist/index.d.ts +319 -16
- package/dist/index.js +62 -4
- package/package.json +4 -2
package/cli/commands/hosting.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Option } from "commander";
|
|
2
3
|
import { connectedDev } from "../connected-dev.mjs";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
@@ -20,6 +21,12 @@ export function registerHostingCommands(program) {
|
|
|
20
21
|
.action(
|
|
21
22
|
action(async ({ project }) => {
|
|
22
23
|
if (!project) throw new Error("No relay.json found.");
|
|
24
|
+
const { migrationPlan } = await import("../migrate.mjs");
|
|
25
|
+
const { diagnostics } = await migrationPlan(project.path);
|
|
26
|
+
if (diagnostics.some((item) => item.severity === "error")) {
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return { checked: false, diagnostics };
|
|
29
|
+
}
|
|
23
30
|
if (project.config.app) {
|
|
24
31
|
try {
|
|
25
32
|
await promisify(execFile)(
|
|
@@ -34,10 +41,12 @@ export function registerHostingCommands(program) {
|
|
|
34
41
|
const bundle = await bundleProject(project, { allowEmpty: true });
|
|
35
42
|
return {
|
|
36
43
|
checked: true,
|
|
44
|
+
diagnostics,
|
|
37
45
|
quests: bundle.releases.length,
|
|
38
46
|
leaderboards: bundle.leaderboards.length,
|
|
39
47
|
referrals: bundle.referrals.length,
|
|
40
48
|
types: bundle.types.length,
|
|
49
|
+
pointLedgers: bundle.pointLedgers.length,
|
|
41
50
|
collections: bundle.collections.length,
|
|
42
51
|
};
|
|
43
52
|
}),
|
|
@@ -119,13 +128,21 @@ export function registerHostingCommands(program) {
|
|
|
119
128
|
}
|
|
120
129
|
if (!organization)
|
|
121
130
|
throw new Error("Select an organization with --organization ID.");
|
|
131
|
+
const created = await request(
|
|
132
|
+
{ ...connection, organization },
|
|
133
|
+
"/projects",
|
|
134
|
+
"POST",
|
|
135
|
+
{
|
|
136
|
+
actionId: randomUUID(),
|
|
137
|
+
name,
|
|
138
|
+
},
|
|
139
|
+
);
|
|
122
140
|
const scoped = {
|
|
123
141
|
...connection,
|
|
124
142
|
organization,
|
|
125
|
-
project:
|
|
143
|
+
project: created.project,
|
|
126
144
|
environment: "test",
|
|
127
145
|
};
|
|
128
|
-
await request(scoped, "/projects", "POST", { project: name, name });
|
|
129
146
|
const repository = await request(scoped, "/repository", "POST", {
|
|
130
147
|
starter: "campaign",
|
|
131
148
|
});
|
package/cli/commands/project.mjs
CHANGED
|
@@ -4,16 +4,19 @@ import { action } from "../runtime.mjs";
|
|
|
4
4
|
import { confirmAction } from "../prompts.mjs";
|
|
5
5
|
import { selectProject } from "../select-project.mjs";
|
|
6
6
|
import { request } from "../connection.mjs";
|
|
7
|
-
import { initialize, bundleProject } from "../project.mjs";
|
|
7
|
+
import { initialize, bundleProject, findProjectFile } from "../project.mjs";
|
|
8
|
+
|
|
9
|
+
import { formatResult } from "../output.mjs";
|
|
8
10
|
|
|
9
11
|
function summary(value) {
|
|
10
12
|
return {
|
|
11
13
|
deployment: value.id,
|
|
12
14
|
revision: value.revision ?? value.baseRevision,
|
|
13
15
|
releases: value.releases.map(
|
|
14
|
-
({ id, quest, title, createdAt, configuration, values }) => ({
|
|
16
|
+
({ id, quest, key, title, createdAt, configuration, values }) => ({
|
|
15
17
|
id,
|
|
16
18
|
quest,
|
|
19
|
+
key,
|
|
17
20
|
title,
|
|
18
21
|
createdAt,
|
|
19
22
|
deploymentDefaults: configuration.defaults,
|
|
@@ -24,10 +27,12 @@ function summary(value) {
|
|
|
24
27
|
),
|
|
25
28
|
types: value.types.map((t) => ({
|
|
26
29
|
id: t.id,
|
|
30
|
+
key: t.key,
|
|
27
31
|
version: t.version,
|
|
28
32
|
title: t.definition.title,
|
|
29
33
|
})),
|
|
30
34
|
collections: value.collections,
|
|
35
|
+
pointLedgers: value.pointLedgers,
|
|
31
36
|
supportedInteractions: value.supportedInteractions,
|
|
32
37
|
leaderboards: value.leaderboards,
|
|
33
38
|
referrals: value.referrals,
|
|
@@ -87,7 +92,7 @@ ${detail}`,
|
|
|
87
92
|
);
|
|
88
93
|
if (options.preview) return { status: "preview", ...summary(preview) };
|
|
89
94
|
await confirmPublication(
|
|
90
|
-
`Deployment ${preview.id}: ${preview.releases.length} quests, ${preview.types.length} types, ${preview.collections.length} collections, ${preview.leaderboards.length} leaderboards, ${preview.referrals.length} referral programs.`,
|
|
95
|
+
`Deployment ${preview.id}: ${preview.releases.length} quests, ${preview.types.length} types, ${preview.collections.length} collections, ${preview.pointLedgers?.length ?? 0} point ledgers, ${preview.leaderboards.length} leaderboards, ${preview.referrals.length} referral programs.`,
|
|
91
96
|
);
|
|
92
97
|
return publish(connection, preview.id);
|
|
93
98
|
}
|
|
@@ -98,6 +103,7 @@ ${detail}`,
|
|
|
98
103
|
file: options.out,
|
|
99
104
|
quests: payload.releases.length,
|
|
100
105
|
types: payload.types.length,
|
|
106
|
+
pointLedgers: payload.pointLedgers.length,
|
|
101
107
|
collections: payload.collections.length,
|
|
102
108
|
leaderboards: payload.leaderboards.length,
|
|
103
109
|
referrals: payload.referrals.length,
|
|
@@ -107,6 +113,22 @@ ${detail}`,
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
export function registerProjectCommands(program) {
|
|
116
|
+
program
|
|
117
|
+
.command("migrate")
|
|
118
|
+
.description(
|
|
119
|
+
"Preview an offline authoring migration without changing resource names",
|
|
120
|
+
)
|
|
121
|
+
.option("--write", "Apply the displayed source and manifest changes")
|
|
122
|
+
.action(async (_, command) => {
|
|
123
|
+
const options = command.optsWithGlobals();
|
|
124
|
+
const path = await findProjectFile(options.config);
|
|
125
|
+
if (!path) throw new Error("No relay.json found. Pass --config FILE.");
|
|
126
|
+
const { migrate } = await import("../migrate.mjs");
|
|
127
|
+
const result = await migrate(path, options.write);
|
|
128
|
+
if (result.blocked) process.exitCode = 1;
|
|
129
|
+
console.log(formatResult("migrate", result, options));
|
|
130
|
+
});
|
|
131
|
+
|
|
110
132
|
program
|
|
111
133
|
.command("init")
|
|
112
134
|
.description("Link an existing project without generating starter data")
|
package/cli/dev.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { createServer } from "node:http";
|
|
@@ -53,6 +54,7 @@ export async function dev(project, options) {
|
|
|
53
54
|
token: randomBytes(32).toString("hex"),
|
|
54
55
|
};
|
|
55
56
|
const children = [];
|
|
57
|
+
const expectedStops = new WeakSet();
|
|
56
58
|
let stopping = false;
|
|
57
59
|
let failure;
|
|
58
60
|
let watcher;
|
|
@@ -122,7 +124,7 @@ export async function dev(project, options) {
|
|
|
122
124
|
stop();
|
|
123
125
|
});
|
|
124
126
|
child.on("exit", (code) => {
|
|
125
|
-
if (!stopping) {
|
|
127
|
+
if (!stopping && !expectedStops.has(child)) {
|
|
126
128
|
failure = new Error(`Development process exited (${code}).`);
|
|
127
129
|
stop();
|
|
128
130
|
}
|
|
@@ -142,42 +144,62 @@ export async function dev(project, options) {
|
|
|
142
144
|
AUTH_ENVIRONMENT: "test",
|
|
143
145
|
AUTH_ORIGINS: origin,
|
|
144
146
|
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
147
|
+
const startApi = () =>
|
|
148
|
+
start(
|
|
149
|
+
process.execPath,
|
|
150
|
+
[
|
|
151
|
+
wrangler,
|
|
152
|
+
"dev",
|
|
153
|
+
"--local",
|
|
154
|
+
"--ip",
|
|
155
|
+
"127.0.0.1",
|
|
156
|
+
"--port",
|
|
157
|
+
String(apiPort),
|
|
158
|
+
"--config",
|
|
159
|
+
join(runtime, "wrangler.jsonc"),
|
|
160
|
+
"--persist-to",
|
|
161
|
+
join(root, ".wrangler/domino"),
|
|
162
|
+
...Object.entries(vars).flatMap(([key, value]) => [
|
|
163
|
+
"--var",
|
|
164
|
+
`${key}:${value}`,
|
|
165
|
+
]),
|
|
166
|
+
],
|
|
167
|
+
{ WRANGLER_SEND_METRICS: "false" },
|
|
168
|
+
);
|
|
169
|
+
let api = startApi();
|
|
170
|
+
const waitReady = async () => {
|
|
171
|
+
let ready = false;
|
|
172
|
+
for (let attempt = 0; attempt < 150 && !stopping; attempt++) {
|
|
173
|
+
try {
|
|
174
|
+
ready = (await fetch(connection.apiUrl + "/v1/auth/session")).ok;
|
|
175
|
+
} catch {
|
|
176
|
+
/* Runtime is starting. */
|
|
177
|
+
}
|
|
178
|
+
if (ready) break;
|
|
179
|
+
await delay(200);
|
|
172
180
|
}
|
|
173
|
-
if (ready)
|
|
174
|
-
|
|
181
|
+
if (!ready) throw failure ?? new Error("Local Relay did not start.");
|
|
182
|
+
};
|
|
183
|
+
await waitReady();
|
|
184
|
+
const access = await request(connection, "/access");
|
|
185
|
+
const legacy = access.projects.find((p) => p.project === "campaign");
|
|
186
|
+
const local =
|
|
187
|
+
legacy ??
|
|
188
|
+
(await request(connection, "/projects", "POST", {
|
|
189
|
+
actionId: "offline-campaign-v1",
|
|
190
|
+
name: "Local campaign",
|
|
191
|
+
}));
|
|
192
|
+
if (local.project !== connection.project) {
|
|
193
|
+
connection.project = local.project;
|
|
194
|
+
vars.AUTH_PROJECT = local.project;
|
|
195
|
+
expectedStops.add(api);
|
|
196
|
+
const exited = once(api, "exit");
|
|
197
|
+
if (process.platform === "win32") api.kill("SIGTERM");
|
|
198
|
+
else process.kill(-api.pid, "SIGTERM");
|
|
199
|
+
await exited;
|
|
200
|
+
api = startApi();
|
|
201
|
+
await waitReady();
|
|
175
202
|
}
|
|
176
|
-
if (!ready) throw failure ?? new Error("Local Relay did not start.");
|
|
177
|
-
await request(connection, "/projects", "POST", {
|
|
178
|
-
project: connection.project,
|
|
179
|
-
name: "Local campaign",
|
|
180
|
-
});
|
|
181
203
|
const sync = developmentSync(connection, project.path, options.reviewMode);
|
|
182
204
|
await sync();
|
|
183
205
|
let dirty = false;
|
package/cli/migrate.mjs
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { createTwoFilesPatch } from "diff";
|
|
5
|
+
import { discoverEntries } from "./discovery.mjs";
|
|
6
|
+
|
|
7
|
+
const helpers = new Set([
|
|
8
|
+
"defineQuest",
|
|
9
|
+
"defineQuestType",
|
|
10
|
+
"defineTieredReward",
|
|
11
|
+
"defineLeaderboard",
|
|
12
|
+
"defineReferral",
|
|
13
|
+
]);
|
|
14
|
+
const excluded =
|
|
15
|
+
/(^|[/\\])(node_modules|dist|build|out|coverage|\.git|\.next|\.domino|\.domino-build|\.wrangler|\.artifacts)([/\\]|$)/;
|
|
16
|
+
const fieldName = (node) =>
|
|
17
|
+
node.name && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))
|
|
18
|
+
? node.name.text
|
|
19
|
+
: undefined;
|
|
20
|
+
const property = (node, name) =>
|
|
21
|
+
node.properties.find((p) => fieldName(p) === name);
|
|
22
|
+
const valueOf = (node) =>
|
|
23
|
+
ts.isPropertyAssignment(node)
|
|
24
|
+
? node.initializer
|
|
25
|
+
: ts.isShorthandPropertyAssignment(node)
|
|
26
|
+
? node.name
|
|
27
|
+
: undefined;
|
|
28
|
+
|
|
29
|
+
/** Inspect syntax only. Never import or execute a project's modules. */
|
|
30
|
+
export async function migrationPlan(path) {
|
|
31
|
+
const root = dirname(path);
|
|
32
|
+
const manifestText = await readFile(path, "utf8");
|
|
33
|
+
const manifest = JSON.parse(manifestText);
|
|
34
|
+
const diagnostics = [];
|
|
35
|
+
const changes = [];
|
|
36
|
+
const references = new Map();
|
|
37
|
+
const project = {
|
|
38
|
+
path,
|
|
39
|
+
config: {
|
|
40
|
+
...manifest,
|
|
41
|
+
quests: manifest.quests ?? [],
|
|
42
|
+
questTypes: manifest.questTypes ?? [],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const entries = [
|
|
46
|
+
...(await discoverEntries(project, "quests")),
|
|
47
|
+
...(await discoverEntries(project, "questTypes")),
|
|
48
|
+
...(manifest.leaderboards ?? []),
|
|
49
|
+
...(manifest.referrals ?? []),
|
|
50
|
+
];
|
|
51
|
+
const configPath = ts.findConfigFile(root, ts.sys.fileExists);
|
|
52
|
+
const config = configPath
|
|
53
|
+
? ts.readConfigFile(configPath, ts.sys.readFile)
|
|
54
|
+
: null;
|
|
55
|
+
const options =
|
|
56
|
+
config && !config.error
|
|
57
|
+
? ts.parseJsonConfigFileContent(
|
|
58
|
+
config.config,
|
|
59
|
+
ts.sys,
|
|
60
|
+
dirname(configPath),
|
|
61
|
+
).options
|
|
62
|
+
: {};
|
|
63
|
+
const program = ts.createProgram(
|
|
64
|
+
entries.map((entry) => resolve(root, entry.entry)),
|
|
65
|
+
{ ...options, allowJs: true, noEmit: true },
|
|
66
|
+
);
|
|
67
|
+
const files = program
|
|
68
|
+
.getSourceFiles()
|
|
69
|
+
.filter((source) => {
|
|
70
|
+
const local = relative(root, source.fileName);
|
|
71
|
+
return (
|
|
72
|
+
!source.isDeclarationFile &&
|
|
73
|
+
!local.startsWith("..") &&
|
|
74
|
+
!excluded.test(local)
|
|
75
|
+
);
|
|
76
|
+
})
|
|
77
|
+
.map((source) => source.fileName)
|
|
78
|
+
.sort();
|
|
79
|
+
const checker = program.getTypeChecker();
|
|
80
|
+
function diagnostic(source, node, code, severity, message, suggestion) {
|
|
81
|
+
const position = source.getLineAndCharacterOfPosition(
|
|
82
|
+
node.getStart(source),
|
|
83
|
+
);
|
|
84
|
+
const item = {
|
|
85
|
+
code,
|
|
86
|
+
severity,
|
|
87
|
+
file: relative(root, source.fileName),
|
|
88
|
+
line: position.line + 1,
|
|
89
|
+
column: position.character + 1,
|
|
90
|
+
message,
|
|
91
|
+
suggestion,
|
|
92
|
+
};
|
|
93
|
+
diagnostics.push(item);
|
|
94
|
+
return item;
|
|
95
|
+
}
|
|
96
|
+
function importedName(expression) {
|
|
97
|
+
if (ts.isIdentifier(expression)) {
|
|
98
|
+
const declaration =
|
|
99
|
+
checker.getSymbolAtLocation(expression)?.declarations?.[0];
|
|
100
|
+
if (!declaration || !ts.isImportSpecifier(declaration)) return;
|
|
101
|
+
const imported = declaration.parent.parent.parent;
|
|
102
|
+
if (
|
|
103
|
+
ts.isImportDeclaration(imported) &&
|
|
104
|
+
/^@domino-sdk\/relay(?:\/authoring)?$/.test(
|
|
105
|
+
imported.moduleSpecifier.text,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return (declaration.propertyName ?? declaration.name).text;
|
|
109
|
+
}
|
|
110
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
111
|
+
const declaration = checker.getSymbolAtLocation(expression.expression)
|
|
112
|
+
?.declarations?.[0];
|
|
113
|
+
if (!declaration || !ts.isNamespaceImport(declaration)) return;
|
|
114
|
+
const imported = declaration.parent.parent;
|
|
115
|
+
if (
|
|
116
|
+
ts.isImportDeclaration(imported) &&
|
|
117
|
+
/^@domino-sdk\/relay(?:\/authoring)?$/.test(
|
|
118
|
+
imported.moduleSpecifier.text,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return expression.name.text;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function literal(node, depth = 0) {
|
|
125
|
+
if (!node || depth > 8) return;
|
|
126
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
127
|
+
return node.text;
|
|
128
|
+
if (
|
|
129
|
+
ts.isParenthesizedExpression(node) ||
|
|
130
|
+
ts.isAsExpression(node) ||
|
|
131
|
+
ts.isSatisfiesExpression(node)
|
|
132
|
+
)
|
|
133
|
+
return literal(node.expression, depth + 1);
|
|
134
|
+
if (ts.isIdentifier(node)) {
|
|
135
|
+
const declaration = checker.getSymbolAtLocation(node)?.valueDeclaration;
|
|
136
|
+
if (
|
|
137
|
+
declaration &&
|
|
138
|
+
ts.isVariableDeclaration(declaration) &&
|
|
139
|
+
declaration.parent.flags & ts.NodeFlags.Const
|
|
140
|
+
)
|
|
141
|
+
return literal(declaration.initializer, depth + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function ledger(source, node) {
|
|
145
|
+
const name = literal(node);
|
|
146
|
+
if (name === undefined) {
|
|
147
|
+
diagnostic(
|
|
148
|
+
source,
|
|
149
|
+
node,
|
|
150
|
+
"LEDGER_REFERENCE_DYNAMIC",
|
|
151
|
+
"warning",
|
|
152
|
+
"This ledger reference cannot be determined without running code.",
|
|
153
|
+
"Verify that the selected ledger already exists, or declare its stable key in relay.json pointLedgers.",
|
|
154
|
+
);
|
|
155
|
+
} else if (
|
|
156
|
+
/^[\w-]+$/.test(name) &&
|
|
157
|
+
!/^ledger_[A-Za-z0-9]{22}$/.test(name)
|
|
158
|
+
) {
|
|
159
|
+
if (!references.has(name)) references.set(name, { source, node });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const source = program.getSourceFile(file);
|
|
164
|
+
if (!source) continue;
|
|
165
|
+
const edits = [];
|
|
166
|
+
function visit(node) {
|
|
167
|
+
if (ts.isCallExpression(node)) {
|
|
168
|
+
const name = importedName(node.expression);
|
|
169
|
+
const argument = node.arguments[0];
|
|
170
|
+
if (name === "points" && argument) ledger(source, argument);
|
|
171
|
+
if (helpers.has(name)) {
|
|
172
|
+
if (
|
|
173
|
+
!argument ||
|
|
174
|
+
!ts.isObjectLiteralExpression(argument) ||
|
|
175
|
+
argument.properties.some(ts.isSpreadAssignment)
|
|
176
|
+
) {
|
|
177
|
+
diagnostic(
|
|
178
|
+
source,
|
|
179
|
+
node,
|
|
180
|
+
"AUTHORING_IDENTITY_DYNAMIC",
|
|
181
|
+
"warning",
|
|
182
|
+
"This declaration uses an indirect object or spread; it was left unchanged.",
|
|
183
|
+
"Keep the existing authoring name and replace id with key in the defining object. Legacy id remains supported.",
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
const id = property(argument, "id");
|
|
187
|
+
const key = property(argument, "key");
|
|
188
|
+
if (id && key) {
|
|
189
|
+
const oldValue = literal(valueOf(id));
|
|
190
|
+
const newValue = literal(valueOf(key));
|
|
191
|
+
if (
|
|
192
|
+
oldValue === undefined ||
|
|
193
|
+
newValue === undefined ||
|
|
194
|
+
oldValue !== newValue
|
|
195
|
+
) {
|
|
196
|
+
diagnostic(
|
|
197
|
+
source,
|
|
198
|
+
key,
|
|
199
|
+
"AUTHORING_IDENTITY_CONFLICT",
|
|
200
|
+
"error",
|
|
201
|
+
"Both id and key are present and cannot be proven equal.",
|
|
202
|
+
"Keep the original id value as key and remove id. Do not choose a new name for an existing resource.",
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
const index = argument.properties.indexOf(id);
|
|
206
|
+
const next = argument.properties[index + 1];
|
|
207
|
+
const previous = argument.properties[index - 1];
|
|
208
|
+
edits.push({
|
|
209
|
+
start: id.getStart(source),
|
|
210
|
+
end: id.end,
|
|
211
|
+
text: "",
|
|
212
|
+
});
|
|
213
|
+
const scanner = ts.createScanner(
|
|
214
|
+
ts.ScriptTarget.Latest,
|
|
215
|
+
true,
|
|
216
|
+
ts.LanguageVariant.Standard,
|
|
217
|
+
source.text,
|
|
218
|
+
);
|
|
219
|
+
scanner.setTextPos(next ? id.end : (previous?.end ?? id.end));
|
|
220
|
+
if (scanner.scan() === ts.SyntaxKind.CommaToken)
|
|
221
|
+
edits.push({
|
|
222
|
+
start: scanner.getTokenPos(),
|
|
223
|
+
end: scanner.getTextPos(),
|
|
224
|
+
text: "",
|
|
225
|
+
});
|
|
226
|
+
diagnostic(
|
|
227
|
+
source,
|
|
228
|
+
id,
|
|
229
|
+
"AUTHORING_ID_DEPRECATED",
|
|
230
|
+
"warning",
|
|
231
|
+
"id repeats the authoring key.",
|
|
232
|
+
"Run domino migrate --write to remove the redundant alias.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
} else if (id) {
|
|
236
|
+
edits.push({
|
|
237
|
+
start: id.name.getStart(source),
|
|
238
|
+
end: id.name.end,
|
|
239
|
+
text: ts.isShorthandPropertyAssignment(id) ? "key: id" : "key",
|
|
240
|
+
});
|
|
241
|
+
diagnostic(
|
|
242
|
+
source,
|
|
243
|
+
id,
|
|
244
|
+
"AUTHORING_ID_DEPRECATED",
|
|
245
|
+
"warning",
|
|
246
|
+
"id is supported as an authoring key alias.",
|
|
247
|
+
"Run domino migrate --write to rename it to key without changing its value.",
|
|
248
|
+
);
|
|
249
|
+
} else if (!key) {
|
|
250
|
+
diagnostic(
|
|
251
|
+
source,
|
|
252
|
+
argument,
|
|
253
|
+
"AUTHORING_KEY_MISSING",
|
|
254
|
+
"error",
|
|
255
|
+
"This declaration has no authoring key.",
|
|
256
|
+
"Add a stable key. For an existing resource, use its original authoring id value.",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function balances(child) {
|
|
260
|
+
if (
|
|
261
|
+
ts.isPropertyAssignment(child) &&
|
|
262
|
+
fieldName(child) === "balance"
|
|
263
|
+
) {
|
|
264
|
+
const object = child.parent;
|
|
265
|
+
const parent = object.parent;
|
|
266
|
+
const kind = ts.isObjectLiteralExpression(object)
|
|
267
|
+
? property(object, "kind")
|
|
268
|
+
: undefined;
|
|
269
|
+
const isPointReward =
|
|
270
|
+
kind && literal(valueOf(kind)) === "points";
|
|
271
|
+
const isBudgetCost =
|
|
272
|
+
ts.isPropertyAssignment(parent) &&
|
|
273
|
+
fieldName(parent) === "cost";
|
|
274
|
+
const isBonus =
|
|
275
|
+
name === "defineReferral" &&
|
|
276
|
+
ts.isArrayLiteralExpression(parent) &&
|
|
277
|
+
ts.isPropertyAssignment(parent.parent) &&
|
|
278
|
+
fieldName(parent.parent) === "bonuses";
|
|
279
|
+
if (
|
|
280
|
+
(name === "defineLeaderboard" && object === argument) ||
|
|
281
|
+
isPointReward ||
|
|
282
|
+
isBudgetCost ||
|
|
283
|
+
isBonus
|
|
284
|
+
)
|
|
285
|
+
ledger(source, child.initializer);
|
|
286
|
+
}
|
|
287
|
+
ts.forEachChild(child, balances);
|
|
288
|
+
}
|
|
289
|
+
balances(argument);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
ts.forEachChild(node, visit);
|
|
294
|
+
}
|
|
295
|
+
visit(source);
|
|
296
|
+
let after = source.text;
|
|
297
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
298
|
+
after = after.slice(0, edit.start) + edit.text + after.slice(edit.end);
|
|
299
|
+
if (after !== source.text)
|
|
300
|
+
changes.push({ path: file, before: source.text, after });
|
|
301
|
+
}
|
|
302
|
+
const jsonSource = ts.parseJsonText(path, manifestText);
|
|
303
|
+
const jsonObject = jsonSource.statements[0]?.expression;
|
|
304
|
+
const collectionNodes =
|
|
305
|
+
jsonObject && property(jsonObject, "collections")?.initializer?.elements;
|
|
306
|
+
let manifestChanged = false;
|
|
307
|
+
for (const [index, collection] of (manifest.collections ?? []).entries()) {
|
|
308
|
+
if (
|
|
309
|
+
!collection ||
|
|
310
|
+
typeof collection !== "object" ||
|
|
311
|
+
collection.id === undefined
|
|
312
|
+
)
|
|
313
|
+
continue;
|
|
314
|
+
const node = collectionNodes?.[index] ?? jsonObject;
|
|
315
|
+
if (collection.key !== undefined && collection.key !== collection.id) {
|
|
316
|
+
diagnostic(
|
|
317
|
+
jsonSource,
|
|
318
|
+
node,
|
|
319
|
+
"AUTHORING_IDENTITY_CONFLICT",
|
|
320
|
+
"error",
|
|
321
|
+
"Collection id and key differ.",
|
|
322
|
+
"Keep the original id value as key and remove id.",
|
|
323
|
+
);
|
|
324
|
+
} else {
|
|
325
|
+
diagnostic(
|
|
326
|
+
jsonSource,
|
|
327
|
+
node,
|
|
328
|
+
"AUTHORING_ID_DEPRECATED",
|
|
329
|
+
"warning",
|
|
330
|
+
"Collection id is supported as an authoring key alias.",
|
|
331
|
+
"Run domino migrate --write to rename it to key.",
|
|
332
|
+
);
|
|
333
|
+
collection.key = collection.id;
|
|
334
|
+
delete collection.id;
|
|
335
|
+
manifestChanged = true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const declared = new Set(
|
|
339
|
+
(manifest.pointLedgers ?? []).map((ledger) => ledger.key),
|
|
340
|
+
);
|
|
341
|
+
for (const [key, { source, node }] of references) {
|
|
342
|
+
if (declared.has(key)) continue;
|
|
343
|
+
diagnostic(
|
|
344
|
+
source,
|
|
345
|
+
node,
|
|
346
|
+
"LEDGER_DECLARATION_MISSING",
|
|
347
|
+
"warning",
|
|
348
|
+
`Ledger ${JSON.stringify(key)} has no local declaration. It must already exist remotely or be declared before publication.`,
|
|
349
|
+
"Run domino migrate to review a pointLedgers declaration, or keep using the existing remote ledger.",
|
|
350
|
+
);
|
|
351
|
+
manifest.pointLedgers ??= [];
|
|
352
|
+
manifest.pointLedgers.push({ key, name: key });
|
|
353
|
+
manifestChanged = true;
|
|
354
|
+
}
|
|
355
|
+
if (manifestChanged)
|
|
356
|
+
changes.push({
|
|
357
|
+
path,
|
|
358
|
+
before: manifestText,
|
|
359
|
+
after: JSON.stringify(manifest, null, 2) + "\n",
|
|
360
|
+
});
|
|
361
|
+
return { diagnostics, changes };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function migrate(path, write = false) {
|
|
365
|
+
const plan = await migrationPlan(path);
|
|
366
|
+
const blocked = plan.diagnostics.some((item) => item.severity === "error");
|
|
367
|
+
if (write && !blocked) {
|
|
368
|
+
// Refuse stale plans before touching any file.
|
|
369
|
+
for (const change of plan.changes)
|
|
370
|
+
if ((await readFile(change.path, "utf8")) !== change.before)
|
|
371
|
+
throw new Error(
|
|
372
|
+
`File changed during migration: ${change.path}. Run domino migrate again.`,
|
|
373
|
+
);
|
|
374
|
+
for (const change of plan.changes)
|
|
375
|
+
await writeFile(change.path, change.after);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
applied: write && !blocked,
|
|
379
|
+
blocked,
|
|
380
|
+
files: plan.changes.map((change) => relative(dirname(path), change.path)),
|
|
381
|
+
diff: plan.changes
|
|
382
|
+
.map((change) => {
|
|
383
|
+
const name = relative(dirname(path), change.path);
|
|
384
|
+
return createTwoFilesPatch(
|
|
385
|
+
`a/${name}`,
|
|
386
|
+
`b/${name}`,
|
|
387
|
+
change.before,
|
|
388
|
+
change.after,
|
|
389
|
+
);
|
|
390
|
+
})
|
|
391
|
+
.join("\n"),
|
|
392
|
+
diagnostics: plan.diagnostics,
|
|
393
|
+
next: blocked
|
|
394
|
+
? "Resolve the reported conflicts, then run domino migrate again."
|
|
395
|
+
: write
|
|
396
|
+
? "Review the diff, then run domino check --json."
|
|
397
|
+
: "Review the diff. Run domino migrate --write to apply it.",
|
|
398
|
+
};
|
|
399
|
+
}
|
package/cli/output.mjs
CHANGED
|
@@ -44,6 +44,16 @@ export function formatDetails(value, indent = "") {
|
|
|
44
44
|
export function formatResult(command, result, options = {}) {
|
|
45
45
|
if (options.json || command === "api")
|
|
46
46
|
return JSON.stringify(result, null, options.json ? undefined : 2);
|
|
47
|
+
if (command === "migrate")
|
|
48
|
+
return [
|
|
49
|
+
result.diff || "No automatic changes needed.",
|
|
50
|
+
formatDetails({
|
|
51
|
+
applied: result.applied,
|
|
52
|
+
blocked: result.blocked,
|
|
53
|
+
diagnostics: result.diagnostics,
|
|
54
|
+
next: result.next,
|
|
55
|
+
}),
|
|
56
|
+
].join("\n");
|
|
47
57
|
if (command === "logs") return result.logs.trimEnd();
|
|
48
58
|
if (command === "reference" && result.markdown)
|
|
49
59
|
return result.markdown.trimEnd();
|
|
@@ -66,6 +76,7 @@ export function formatResult(command, result, options = {}) {
|
|
|
66
76
|
result: "Bundle ready",
|
|
67
77
|
quests: result.releases.length,
|
|
68
78
|
types: result.types.length,
|
|
79
|
+
pointLedgers: result.pointLedgers.length,
|
|
69
80
|
collections: result.collections.length,
|
|
70
81
|
leaderboards: result.leaderboards.length,
|
|
71
82
|
referrals: result.referrals.length,
|