@playcraft/cli 0.1.55-beta → 0.1.56-beta
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/commands/build.js +38 -0
- package/dist/workspace-runtime/previews/finalize.js +64 -0
- package/dist/workspace-runtime/previews/publish.js +31 -8
- package/dist/workspace-runtime/server/http.js +5 -5
- package/dist/workspace-runtime/workspaces/artifact-events.js +136 -23
- package/dist/workspace-runtime/workspaces/artifact-files.js +3 -1
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +27 -4
- package/dist/workspace-runtime/workspaces/artifacts.js +115 -17
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +5 -1
- package/dist/workspace-runtime/workspaces/delivery-build.js +97 -7
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -0
- package/dist/workspace-runtime/workspaces/git.js +13 -0
- package/dist/workspace-runtime/workspaces/parameter-assets.js +38 -15
- package/dist/workspace-runtime/workspaces/parameters.js +61 -21
- package/dist/workspace-runtime/workspaces/theme-asset-index.js +123 -13
- package/package.json +3 -3
- package/project-templates/empty/AGENTS.md +1 -1
- package/templates/preview-runtime/v1/README.md +8 -0
- package/templates/preview-runtime/v1/manifest.json +7 -7
- package/templates/preview-runtime/v1/runtime.d.mts +1 -1
- package/templates/preview-runtime/v1/runtime.d.ts +1 -1
- package/templates/preview-runtime/v1/runtime.mjs +238 -47
- package/templates/preview-runtime/v1/types/adapters.d.ts +3 -1
- package/templates/preview-runtime/v1/types/preview.d.ts +1 -0
- package/templates/preview-runtime/v1/types/protocol.d.ts +37 -0
|
@@ -3,9 +3,10 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { posix, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import JSON5 from 'json5';
|
|
5
5
|
import { parameterDocumentSchema, parameterPatchPayloadSchema, parameterCatalog, validateParameterChange, workspaceRelativePathSchema, WORKSPACE_FILE_CONTENT_MAX, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
|
-
import { InvalidWorkspacePathError, writeWorkspaceFile
|
|
6
|
+
import { InvalidWorkspacePathError, writeWorkspaceFile } from './files.js';
|
|
7
7
|
import { editJson5Paths, ParameterEditError } from './json5-edit.js';
|
|
8
|
-
import { prepareParameterAssetRegistration, writeParameterAssetRegistration } from './parameter-assets.js';
|
|
8
|
+
import { prepareParameterAssetRegistration, writeParameterAssetRegistration, } from './parameter-assets.js';
|
|
9
|
+
import { themeAssetIndexFingerprint } from './theme-asset-index.js';
|
|
9
10
|
const MANIFEST_PATH = '.playcraft/workspace.json';
|
|
10
11
|
const DEFAULT_SCHEMA_PATH = 'src/theme/theme.schema.json5';
|
|
11
12
|
const THEME_INDEX_PATH = 'src/theme/index.ts';
|
|
@@ -22,7 +23,9 @@ export async function readWorkspaceParameters(root, revisions) {
|
|
|
22
23
|
throw new ParameterEditError('parameter_source_changed');
|
|
23
24
|
}
|
|
24
25
|
async function sourceFingerprint(root, located, ownIndexWrite) {
|
|
25
|
-
const selectedIndex = located.dataPath
|
|
26
|
+
const selectedIndex = located.dataPath
|
|
27
|
+
? posix.join(posix.dirname(located.dataPath), 'index.ts')
|
|
28
|
+
: undefined;
|
|
26
29
|
const sources = await Promise.all([
|
|
27
30
|
located.schemaPath ? readOptionalText(root, located.schemaPath) : undefined,
|
|
28
31
|
located.schemaPath ? readOptionalText(root, platformI18nPath(located.schemaPath)) : undefined,
|
|
@@ -30,20 +33,40 @@ async function sourceFingerprint(root, located, ownIndexWrite) {
|
|
|
30
33
|
readOptionalText(root, THEME_INDEX_PATH),
|
|
31
34
|
selectedIndex ? readOptionalText(root, selectedIndex) : undefined,
|
|
32
35
|
]);
|
|
33
|
-
const hashes = sources.map(text => text === undefined ? null : createHash('sha256').update(text).digest('hex'));
|
|
36
|
+
const hashes = sources.map((text) => text === undefined ? null : createHash('sha256').update(text).digest('hex'));
|
|
37
|
+
const rawIndexHash = hashes[4];
|
|
38
|
+
if (sources[4] !== undefined) {
|
|
39
|
+
try {
|
|
40
|
+
const data = located.dataPath ? await readOptionalText(root, located.dataPath) : undefined;
|
|
41
|
+
hashes[4] = themeAssetIndexFingerprint(sources[4], themeImageReferences(sources[0] === undefined ? {} : JSON5.parse(sources[0]), data === undefined ? {} : JSON5.parse(data)));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Invalid configuration cannot authorize a relaxed resource identity.
|
|
45
|
+
hashes[4] = rawIndexHash;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
if (ownIndexWrite) {
|
|
35
49
|
// Only our exact staged registry bytes may differ from the baseline. An external edit,
|
|
36
50
|
// including a schema/manifest change during the write callback, still fails the old guard.
|
|
37
|
-
if (selectedIndex !== ownIndexWrite.path ||
|
|
51
|
+
if (selectedIndex !== ownIndexWrite.path || rawIndexHash !== ownIndexWrite.contentHash)
|
|
38
52
|
throw new ParameterEditError('parameter_source_conflict');
|
|
39
|
-
hashes[4] = ownIndexWrite.
|
|
53
|
+
hashes[4] = ownIndexWrite.previousFingerprint;
|
|
40
54
|
}
|
|
41
|
-
return createHash('sha256')
|
|
55
|
+
return createHash('sha256')
|
|
56
|
+
.update(JSON.stringify({
|
|
42
57
|
schemaPath: located.schemaPath ?? null,
|
|
43
58
|
dataPath: located.dataPath ?? null,
|
|
44
59
|
themeId: located.themeId ?? null,
|
|
45
60
|
sources: hashes,
|
|
46
|
-
}))
|
|
61
|
+
}))
|
|
62
|
+
.digest('hex');
|
|
63
|
+
}
|
|
64
|
+
function themeImageReferences(schema, data) {
|
|
65
|
+
return parameterCatalog(schema, data).flatMap((field) => field.asset?.kind === 'image' &&
|
|
66
|
+
typeof field.value === 'string' &&
|
|
67
|
+
field.value.startsWith('./assets/')
|
|
68
|
+
? [field.value]
|
|
69
|
+
: []);
|
|
47
70
|
}
|
|
48
71
|
async function readParameterDocuments(root, revisions, located) {
|
|
49
72
|
if (located.errors.some((item) => item.reason === 'parameter_manifest_invalid')) {
|
|
@@ -79,7 +102,9 @@ async function readParameterDocuments(root, revisions, located) {
|
|
|
79
102
|
schema: schemaFile.value,
|
|
80
103
|
schemaHash: schemaFile.contentHash,
|
|
81
104
|
schemaRevision: revisions[located.schemaPath]?.revision ?? 0,
|
|
82
|
-
...(platformFile.status === 'ok'
|
|
105
|
+
...(platformFile.status === 'ok'
|
|
106
|
+
? { platformI18n: platformFile.value, platformI18nPath: i18nPath }
|
|
107
|
+
: {}),
|
|
83
108
|
};
|
|
84
109
|
if (platformFile.status === 'invalid') {
|
|
85
110
|
return parameterDocumentSchema.parse({
|
|
@@ -128,8 +153,8 @@ export async function patchWorkspaceParameters(root, revisions, payload, observe
|
|
|
128
153
|
if (document.status === 'missing' || document.status === 'schema_only' || !document.dataPath) {
|
|
129
154
|
throw new ParameterEditError('parameter_data_missing');
|
|
130
155
|
}
|
|
131
|
-
if ((parsed.sourceFingerprint && parsed.sourceFingerprint !== document.sourceFingerprint)
|
|
132
|
-
|
|
156
|
+
if ((parsed.sourceFingerprint && parsed.sourceFingerprint !== document.sourceFingerprint) ||
|
|
157
|
+
(observed && observed.dataPath !== document.dataPath)) {
|
|
133
158
|
throw new ParameterEditError('parameter_source_conflict');
|
|
134
159
|
}
|
|
135
160
|
const changes = 'changes' in parsed ? parsed.changes : [{ fieldPath: parsed.fieldPath, value: parsed.value }];
|
|
@@ -150,27 +175,33 @@ export async function patchWorkspaceParameters(root, revisions, payload, observe
|
|
|
150
175
|
throw new ParameterEditError('parameter_document_invalid');
|
|
151
176
|
}
|
|
152
177
|
const current = await locateParameterFiles(root);
|
|
153
|
-
if (await sourceFingerprint(root, current) !== document.sourceFingerprint) {
|
|
178
|
+
if ((await sourceFingerprint(root, current)) !== document.sourceFingerprint) {
|
|
154
179
|
throw new ParameterEditError('parameter_source_conflict');
|
|
155
180
|
}
|
|
156
181
|
const catalog = parameterCatalog(document.schema, document.data);
|
|
157
|
-
const bindings = changes.flatMap(change => {
|
|
182
|
+
const bindings = changes.flatMap((change) => {
|
|
158
183
|
if (change.op === 'remove' || typeof change.value !== 'string' || !change.value)
|
|
159
184
|
return [];
|
|
160
|
-
const field = catalog.find(entry => entry.key === change.fieldPath.join('.'));
|
|
185
|
+
const field = catalog.find((entry) => entry.key === change.fieldPath.join('.'));
|
|
161
186
|
return field?.asset ? [{ value: change.value, asset: field.asset }] : [];
|
|
162
187
|
});
|
|
163
|
-
const registration = await prepareParameterAssetRegistration(root, document.dataPath, bindings);
|
|
188
|
+
const registration = await prepareParameterAssetRegistration(root, document.dataPath, bindings, themeImageReferences(document.schema, document.data));
|
|
164
189
|
// Register first: a failed sparse save can leave an unused asset, never a dangling saved reference.
|
|
165
190
|
const additionalWrites = [];
|
|
166
191
|
let ownIndexWrite;
|
|
167
192
|
if (registration) {
|
|
168
193
|
const written = await writeParameterAssetRegistration(root, registration);
|
|
169
194
|
additionalWrites.push(written);
|
|
170
|
-
ownIndexWrite = {
|
|
195
|
+
ownIndexWrite = {
|
|
196
|
+
path: written.path,
|
|
197
|
+
previousHash: registration.previousHash,
|
|
198
|
+
previousFingerprint: registration.previousFingerprint,
|
|
199
|
+
contentHash: written.contentHash,
|
|
200
|
+
};
|
|
171
201
|
await options.onAdditionalWrite?.(written);
|
|
172
202
|
}
|
|
173
|
-
if (await sourceFingerprint(root, await locateParameterFiles(root), ownIndexWrite) !==
|
|
203
|
+
if ((await sourceFingerprint(root, await locateParameterFiles(root), ownIndexWrite)) !==
|
|
204
|
+
document.sourceFingerprint)
|
|
174
205
|
throw new ParameterEditError('parameter_source_conflict');
|
|
175
206
|
const latest = await readOptionalText(root, document.dataPath);
|
|
176
207
|
if (latest !== source)
|
|
@@ -232,8 +263,12 @@ function declaredPaths(value) {
|
|
|
232
263
|
if (Object.keys(record).some((key) => !['schemaPath', 'dataPath', 'themeId'].includes(key))) {
|
|
233
264
|
return undefined;
|
|
234
265
|
}
|
|
235
|
-
const schemaPath = record.schemaPath === undefined
|
|
236
|
-
|
|
266
|
+
const schemaPath = record.schemaPath === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: workspaceRelativePathSchema.safeParse(record.schemaPath);
|
|
269
|
+
const dataPath = record.dataPath === undefined
|
|
270
|
+
? undefined
|
|
271
|
+
: workspaceRelativePathSchema.safeParse(record.dataPath);
|
|
237
272
|
const themeId = record.themeId === undefined
|
|
238
273
|
? undefined
|
|
239
274
|
: typeof record.themeId === 'string' && record.themeId.trim() && record.themeId.length <= 128
|
|
@@ -258,7 +293,8 @@ async function readDeclaredTheme(root) {
|
|
|
258
293
|
const text = contents.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
259
294
|
const match = text.match(/export\s+\{\s*default\s*\}\s+from\s+['"]\.\/([^'"]+)['"]/);
|
|
260
295
|
const themeId = match?.[1]?.trim();
|
|
261
|
-
return themeId &&
|
|
296
|
+
return themeId &&
|
|
297
|
+
workspaceRelativePathSchema.safeParse(`${THEME_ROOT}/${themeId}/theme.data.json5`).success
|
|
262
298
|
? themeId
|
|
263
299
|
: undefined;
|
|
264
300
|
}
|
|
@@ -292,7 +328,11 @@ async function readOptionalJson5(root, relativePath) {
|
|
|
292
328
|
if (text === undefined)
|
|
293
329
|
return { status: 'missing' };
|
|
294
330
|
try {
|
|
295
|
-
return {
|
|
331
|
+
return {
|
|
332
|
+
status: 'ok',
|
|
333
|
+
value: JSON5.parse(text),
|
|
334
|
+
contentHash: createHash('sha256').update(text).digest('hex'),
|
|
335
|
+
};
|
|
296
336
|
}
|
|
297
337
|
catch {
|
|
298
338
|
return { status: 'invalid', reason: 'parameter_document_invalid' };
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import JSON5 from 'json5';
|
|
3
|
+
import { parameterAssetMimeType, parameterAssetValueError, } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
4
|
export class ThemeAssetIndexError extends Error {
|
|
4
5
|
code = 'parameter_asset_index_unsupported';
|
|
5
|
-
constructor() {
|
|
6
|
+
constructor() {
|
|
7
|
+
super('parameter_asset_index_unsupported');
|
|
8
|
+
}
|
|
6
9
|
}
|
|
7
10
|
/** A bounded lexer recognizes the standard literal registry without evaluating project code. */
|
|
8
11
|
function lex(source) {
|
|
@@ -84,9 +87,13 @@ function registry(source) {
|
|
|
84
87
|
for (let i = exportStart + 3; i < tokens.length && exportDepth; i++) {
|
|
85
88
|
const token = tokens[i];
|
|
86
89
|
const previous = tokens[i - 1]?.text;
|
|
87
|
-
const properKey = previous === ':' &&
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const properKey = previous === ':' &&
|
|
91
|
+
(tokens[i - 2]?.text === 'assetRecordByPath' ||
|
|
92
|
+
tokens[i - 2]?.string === 'assetRecordByPath');
|
|
93
|
+
if (exportDepth === 1 &&
|
|
94
|
+
token.text === 'assetRecordByPath' &&
|
|
95
|
+
[',', '}'].includes(tokens[i + 1]?.text ?? '') &&
|
|
96
|
+
(previous === '{' || previous === ',' || properKey))
|
|
90
97
|
exportsRegistry = true;
|
|
91
98
|
if (token.string === undefined && token.text === '{')
|
|
92
99
|
exportDepth++;
|
|
@@ -103,13 +110,17 @@ function registry(source) {
|
|
|
103
110
|
}
|
|
104
111
|
if (tokens[++cursor]?.text !== '{')
|
|
105
112
|
throw new ThemeAssetIndexError();
|
|
113
|
+
const start = tokens[cursor].start;
|
|
106
114
|
cursor++;
|
|
107
115
|
const entries = new Map();
|
|
108
116
|
let lastValueEnd, trailingComma = false;
|
|
109
117
|
while (tokens[cursor]?.text !== '}') {
|
|
110
118
|
const key = tokens[cursor], colon = tokens[cursor + 1], value = tokens[cursor + 2];
|
|
111
|
-
if (key?.string === undefined ||
|
|
112
|
-
||
|
|
119
|
+
if (key?.string === undefined ||
|
|
120
|
+
colon?.text !== ':' ||
|
|
121
|
+
!value ||
|
|
122
|
+
!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value.text) ||
|
|
123
|
+
entries.has(key.string))
|
|
113
124
|
throw new ThemeAssetIndexError();
|
|
114
125
|
entries.set(key.string, value.text);
|
|
115
126
|
lastValueEnd = value.end;
|
|
@@ -125,15 +136,110 @@ function registry(source) {
|
|
|
125
136
|
const end = tokens[cursor].start;
|
|
126
137
|
const imports = new Map();
|
|
127
138
|
for (let i = 0; i < tokens.length - 3; i++) {
|
|
128
|
-
if (tokens[i].text === 'import' &&
|
|
129
|
-
|
|
139
|
+
if (tokens[i].text === 'import' &&
|
|
140
|
+
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(tokens[i + 1].text) &&
|
|
141
|
+
tokens[i + 1].text !== 'type' &&
|
|
142
|
+
tokens[i + 2].text === 'from' &&
|
|
143
|
+
tokens[i + 3].string !== undefined)
|
|
130
144
|
imports.set(tokens[i + 3].string, tokens[i + 1].text);
|
|
131
145
|
}
|
|
132
|
-
return { end, lastValueEnd, trailingComma, entries, imports, tokens };
|
|
146
|
+
return { start, end, lastValueEnd, trailingComma, entries, imports, tokens };
|
|
147
|
+
}
|
|
148
|
+
/** Only literal, exclusively registered image imports belong to the replaceable resource domain. */
|
|
149
|
+
export function themeAssetIndexFingerprint(source, requiredImageValues = []) {
|
|
150
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
151
|
+
try {
|
|
152
|
+
const state = registry(source), tokens = state.tokens;
|
|
153
|
+
// Mutation, aliases, getters and other registry consumers cannot be separated safely.
|
|
154
|
+
if (tokens.filter((token) => token.string === undefined && token.text === 'assetRecordByPath')
|
|
155
|
+
.length !== 2)
|
|
156
|
+
return hash(source);
|
|
157
|
+
const references = new Map(), imports = new Map();
|
|
158
|
+
const values = new Map();
|
|
159
|
+
for (const token of tokens) {
|
|
160
|
+
if (token.string === undefined)
|
|
161
|
+
references.set(token.text, (references.get(token.text) ?? 0) + 1);
|
|
162
|
+
}
|
|
163
|
+
for (let i = 0; i < tokens.length - 4; i++) {
|
|
164
|
+
if (tokens[i].text !== 'import' ||
|
|
165
|
+
tokens[i + 2].text !== 'from' ||
|
|
166
|
+
tokens[i + 3].string === undefined ||
|
|
167
|
+
tokens[i + 4].text !== ';')
|
|
168
|
+
continue;
|
|
169
|
+
const variable = tokens[i + 1].text, imported = imports.get(variable) ?? [];
|
|
170
|
+
imported.push({ path: tokens[i + 3].string, start: i });
|
|
171
|
+
imports.set(variable, imported);
|
|
172
|
+
}
|
|
173
|
+
for (const [value, variable] of state.entries)
|
|
174
|
+
values.set(variable, [...(values.get(variable) ?? []), value]);
|
|
175
|
+
const imageEntries = new Set();
|
|
176
|
+
const replacements = [];
|
|
177
|
+
for (const [variable, bindings] of imports) {
|
|
178
|
+
if (bindings.length !== 1)
|
|
179
|
+
continue;
|
|
180
|
+
const binding = bindings[0], importPath = binding.path;
|
|
181
|
+
const value = importPath.startsWith('./assets/')
|
|
182
|
+
? importPath
|
|
183
|
+
: importPath.startsWith('../../assets/')
|
|
184
|
+
? 'src/' + importPath.slice(6)
|
|
185
|
+
: undefined;
|
|
186
|
+
if (!value)
|
|
187
|
+
continue;
|
|
188
|
+
const mime = parameterAssetMimeType(value);
|
|
189
|
+
if (!mime?.startsWith('image/') ||
|
|
190
|
+
parameterAssetValueError({
|
|
191
|
+
kind: 'image',
|
|
192
|
+
mimeTypes: [mime],
|
|
193
|
+
extensions: [],
|
|
194
|
+
maxBytes: 32 * 1024 * 1024,
|
|
195
|
+
}, value))
|
|
196
|
+
continue;
|
|
197
|
+
const registered = values.get(variable) ?? [];
|
|
198
|
+
if (registered.some((key) => key !== value) ||
|
|
199
|
+
references.get(variable) !== registered.length + 1)
|
|
200
|
+
continue;
|
|
201
|
+
for (const key of registered)
|
|
202
|
+
imageEntries.add(key);
|
|
203
|
+
const start = tokens[binding.start].start;
|
|
204
|
+
let end = tokens[binding.start + 4].end;
|
|
205
|
+
if (source.startsWith('\r\n', end))
|
|
206
|
+
end += 2;
|
|
207
|
+
else if (source[end] === '\n')
|
|
208
|
+
end++;
|
|
209
|
+
replacements.push({ start, end, content: '' });
|
|
210
|
+
}
|
|
211
|
+
const start = tokens.findIndex((token) => token.start === state.start);
|
|
212
|
+
const end = tokens.findIndex((token) => token.start === state.end);
|
|
213
|
+
if (start < 0 || end <= start)
|
|
214
|
+
return hash(source);
|
|
215
|
+
// Shared src/assets defaults belong to the existing build loader. Only theme-relative
|
|
216
|
+
// effective images require this selected theme's literal registry.
|
|
217
|
+
const missingThemeImages = [
|
|
218
|
+
...new Set(requiredImageValues.filter((value) => value.startsWith('./assets/') && !state.entries.has(value))),
|
|
219
|
+
].sort();
|
|
220
|
+
replacements.push({
|
|
221
|
+
start: tokens[start].end,
|
|
222
|
+
end: tokens[end].start,
|
|
223
|
+
content: JSON.stringify([...state.entries].filter(([value]) => !imageEntries.has(value))),
|
|
224
|
+
});
|
|
225
|
+
let canonical = source;
|
|
226
|
+
for (const replacement of replacements.sort((a, b) => b.start - a.start))
|
|
227
|
+
canonical =
|
|
228
|
+
canonical.slice(0, replacement.start) +
|
|
229
|
+
replacement.content +
|
|
230
|
+
canonical.slice(replacement.end);
|
|
231
|
+
return hash('theme-asset-code-v1:' + JSON.stringify({ source: canonical, missingThemeImages }));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Unknown project syntax retains the original strict byte identity.
|
|
235
|
+
return hash(source);
|
|
236
|
+
}
|
|
133
237
|
}
|
|
134
238
|
/** Add only literal imports/entries; retain user exports, comments, unrelated mappings and files. */
|
|
135
239
|
export function registerThemeAsset(source, value, importPath) {
|
|
136
|
-
if (!value ||
|
|
240
|
+
if (!value ||
|
|
241
|
+
(!importPath.startsWith('./') && !importPath.startsWith('../')) ||
|
|
242
|
+
/[\r\n\x00]/.test(importPath))
|
|
137
243
|
throw new ThemeAssetIndexError();
|
|
138
244
|
const state = registry(source);
|
|
139
245
|
const imported = state.imports.get(importPath);
|
|
@@ -143,8 +249,11 @@ export function registerThemeAsset(source, value, importPath) {
|
|
|
143
249
|
return source;
|
|
144
250
|
throw new ThemeAssetIndexError();
|
|
145
251
|
}
|
|
146
|
-
const variable = imported ??
|
|
147
|
-
|
|
252
|
+
const variable = imported ??
|
|
253
|
+
'__playcraftParameterAsset_' +
|
|
254
|
+
createHash('sha256').update(importPath).digest('hex').slice(0, 16);
|
|
255
|
+
if (!imported &&
|
|
256
|
+
state.tokens.some((token) => token.string === undefined && token.text === variable))
|
|
148
257
|
throw new ThemeAssetIndexError();
|
|
149
258
|
let result = source;
|
|
150
259
|
let end = state.end;
|
|
@@ -152,6 +261,7 @@ export function registerThemeAsset(source, value, importPath) {
|
|
|
152
261
|
result = result.slice(0, state.lastValueEnd) + ',' + result.slice(state.lastValueEnd);
|
|
153
262
|
end++;
|
|
154
263
|
}
|
|
155
|
-
result =
|
|
264
|
+
result =
|
|
265
|
+
result.slice(0, end) + `\n ${JSON.stringify(value)}: ${variable},\n` + result.slice(end);
|
|
156
266
|
return (imported ? '' : `import ${variable} from ${JSON.stringify(importPath)};\n`) + result;
|
|
157
267
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"@gltf-transform/core": "^4.3.0",
|
|
53
53
|
"@gltf-transform/extensions": "^4.3.0",
|
|
54
54
|
"@gltf-transform/functions": "^4.3.0",
|
|
55
|
-
"@playcraft/build": "0.1.
|
|
56
|
-
"@playcraft/common": "0.1.
|
|
55
|
+
"@playcraft/build": "0.1.56-beta",
|
|
56
|
+
"@playcraft/common": "0.1.56-beta",
|
|
57
57
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
58
58
|
"chokidar": "^4.0.3",
|
|
59
59
|
"commander": "^13.1.0",
|
|
@@ -46,7 +46,7 @@ The artifact panel shows final deliverables under `dist/` at the current project
|
|
|
46
46
|
|
|
47
47
|
When a generated asset is ready for delivery, save or copy it into `dist/` with a stable relative path. Keep the resources required by a model or document within `dist/` and preserve their relative links. Reuse the same delivery path for a revised artifact so its versions remain grouped; the platform retains its history. Use different paths for independent candidates or deliverables.
|
|
48
48
|
|
|
49
|
-
Run playable builds from the project root
|
|
49
|
+
Run playable builds from the project root (`playcraft build --mode full --platform playcraft --format html .`). In a workspace this command builds in an isolated copy, validates the result, and retains the playable at `dist/playable/index.html` while preserving other deliverables. A successful agent command or turn that writes a valid self-contained HTML (HyperText Markup Language, 超文本标记语言) file at this same path is collected as the same playable preview. No separate publish action is required to view it. A base build's `dist/index.html` can still require resource files and remains a document. The page's generate-preview action rebuilds the current project through the same validation. Public sharing is a separate explicit action. Temporary `.playcraft-preview-out/` and `.playcraft-temp/` files are not deliverables. Preserve other artifacts already in `dist/` when rebuilding a playable.
|
|
50
50
|
|
|
51
51
|
Keep this convention in `AGENTS.md`, the project's standard instruction filename. Do not create a separate `AGENT.md`.
|
|
52
52
|
|
|
@@ -14,6 +14,14 @@ Create your own `src/preview/runtime.ts`. Import `connectPreviewRuntime` and `re
|
|
|
14
14
|
- `createGame(context)` returns actual `apply(context)`, `restartScene(context)`, `afterRender(context)` and `dispose()` callbacks. Check `context.assertCurrent()` and honor `context.signal` before late effects. `afterRender` must wait for real rendering, not a successful placeholder.
|
|
15
15
|
- Dispose the endpoint on page exit. Advertising SDK configuration remains the production concern; do not route development updates through advertising caches.
|
|
16
16
|
|
|
17
|
+
## Runtime image parameters
|
|
18
|
+
|
|
19
|
+
Register an image consumer with `resource: 'image'` and `mode: 'live'`. Keep its logical asset path in a preview-only resolved property used by `effectivePath`; do not compare an embedded image address with the raw parameter path. `context.loadImage(field, logicalPath)` requests the current declared image through the host and returns a decoded image element. Empty paths clear the consumer without reading a file. The host validates the field against the current schema and configuration; the game receives image bytes, never platform file addresses or credentials.
|
|
20
|
+
|
|
21
|
+
Prepare all required images before changing visible objects, call `assertCurrent()` after asynchronous work and immediately before installing textures, then wait for the real engine frame. Keep old objects intact until preparation succeeds. Release old preview-owned textures and overlay references after a successful replacement and on disposal. Never remove an engine texture still used by another object.
|
|
22
|
+
|
|
23
|
+
`ResourceLoadError` is exported from `runtime.mjs`. Live adapters let this error reach the runtime, which retains the last known installation and reports a recoverable, field-level resource failure. Do not catch an arbitrary update exception and claim successful fallback. During initial `createGame`, catch only this load error to start with an embedded default image or empty background; the runtime still reports the saved image as unapplied. A later patch of that field retries it; unrelated parameter updates can continue. `resourceErrors` and file-save receipts are independent. Save logical paths and let the original production asset registry include the file when building delivery output.
|
|
24
|
+
|
|
17
25
|
## Preview build
|
|
18
26
|
|
|
19
27
|
The engineering project must install a compatible Vite as a development dependency. Set a separate package script:
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"protocolVersion": 1,
|
|
5
5
|
"files": {
|
|
6
6
|
"build-preview.mjs": "2bd512675d829cc07ceddcb58365a6f74d36ff93690f8d72f4bbaa1d45511c91",
|
|
7
|
-
"README.md": "
|
|
7
|
+
"README.md": "f104440b3acb23adb88bde0dff23840fa170a14f477d1d7338575eca75e73571",
|
|
8
8
|
"release-guard.mjs": "0c33d1559c68844f3760c3e754ee36ca70dc27151ef6f7ad62f29f95239679cc",
|
|
9
|
-
"runtime.d.mts": "
|
|
10
|
-
"runtime.d.ts": "
|
|
11
|
-
"runtime.mjs": "
|
|
12
|
-
"types/adapters.d.ts": "
|
|
13
|
-
"types/preview.d.ts": "
|
|
14
|
-
"types/protocol.d.ts": "
|
|
9
|
+
"runtime.d.mts": "a366552166a4a93103306997058951416740eaf6d6f3d0a34854386b4f1e02f5",
|
|
10
|
+
"runtime.d.ts": "a366552166a4a93103306997058951416740eaf6d6f3d0a34854386b4f1e02f5",
|
|
11
|
+
"runtime.mjs": "25b33218ff32dfeaaad7bcd69647cbf662fa4312d49b62724a2104fd27d9b001",
|
|
12
|
+
"types/adapters.d.ts": "29b5c18d4576ab00eca44875b5c4eae2fc0e0ed6ab5595dbbaf6ba9b9fd8beed",
|
|
13
|
+
"types/preview.d.ts": "19ea55ed6faf4faa4f4fc62a2d70309d7ba917fc165c2c149856798e1b4197f8",
|
|
14
|
+
"types/protocol.d.ts": "716e83802336c4e7377abc1b433c77045ed59997a45646fe2fdbe55cb82c5e62",
|
|
15
15
|
"types/runtime.d.ts": "efcd429b24d75b06afb3f05ec3f8134efc381ede0dd59cb43ec5770e2559c77b"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { connectPreviewRuntime, readWorkspacePreviewContext } from './types/preview.js';
|
|
1
|
+
export { connectPreviewRuntime, readWorkspacePreviewContext, ResourceLoadError } from './types/preview.js';
|
|
2
2
|
export type * from './types/adapters.js';
|
|
3
3
|
export type * from './types/protocol.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { connectPreviewRuntime, readWorkspacePreviewContext } from './types/preview.js';
|
|
1
|
+
export { connectPreviewRuntime, readWorkspacePreviewContext, ResourceLoadError } from './types/preview.js';
|
|
2
2
|
export type * from './types/adapters.js';
|
|
3
3
|
export type * from './types/protocol.js';
|