console: Play App content card Saves are the agent's (declaration-save; Send changes for review, Submit, Publish, Restart review stay forbidden), one owner batch per plan with the IARC Terms tick before the run, console plan play-app-content --all, New App fields typed not set, play.appId from the Create app redirect (console verify --url --write) and storebound play app-id across app list pages
This commit is contained in:
parent
1366e7e231
commit
f741c15829
29 changed files with 1029 additions and 165 deletions
|
|
@ -1,6 +1,7 @@
|
|||
// Writes one top-level block (dataSafety, appPrivacy) into storebound.config.ts / .js / .json text: replaces the
|
||||
// existing `key: { ... }` property, or inserts it after the `app: { ... }` block. Pure text in, text out; the rest of
|
||||
// the file (comments, order, formatting) stays as it is. Used by `storebound scan --write` only.
|
||||
// the file (comments, order, formatting) stays as it is. Used by `storebound scan --write`; upsertNestedString sets one
|
||||
// nested string (play.appId: `console verify play-app-record --write`, `play app-id --write`).
|
||||
|
||||
/** Index of the brace that closes the one at `open`, skipping strings, template literals and comments. -1 if none. */
|
||||
export function matchBrace(text: string, open: number): number {
|
||||
|
|
@ -69,3 +70,63 @@ export function upsertBlock(text: string, file: string, key: string, value: unkn
|
|||
const block = `\n${comment ? `${ind}// ${comment}\n` : ''}${ind}${key}: ${toTs(value, ind)},`;
|
||||
return text.slice(0, at) + block + text.slice(at);
|
||||
}
|
||||
|
||||
/**
|
||||
* The config text with `parent.key` set to a string `value` (an upsert: `play.appId`, used by `console verify
|
||||
* play-app-record --write` and `play app-id --write`): JSON is parsed and written back; in .ts / .js the `key: '…'`
|
||||
* inside the top-level `parent: { … }` block is replaced, or `key: '<value>',` is added right after its opening brace.
|
||||
* Throws when the block is missing or the key holds something other than a string literal. The rest of the file
|
||||
* (comments, order, formatting) stays as it is.
|
||||
*/
|
||||
export function upsertNestedString(text: string, file: string, parent: string, key: string, value: string): string {
|
||||
if (file.endsWith('.json')) {
|
||||
const j = JSON.parse(text) as Record<string, unknown>;
|
||||
const p = j[parent];
|
||||
if (!p || typeof p !== 'object' || Array.isArray(p)) throw new Error(`${file}: no ${parent} block to put ${parent}.${key} into`);
|
||||
(p as Record<string, unknown>)[key] = value;
|
||||
return JSON.stringify(j, null, 2) + '\n';
|
||||
}
|
||||
// the top-level block: the `parent: {` with the smallest indent
|
||||
const re = new RegExp(`^([ \\t]*)${parent}\\s*:\\s*\\{`, 'gm');
|
||||
let best: RegExpExecArray | null = null;
|
||||
for (let m = re.exec(text); m; m = re.exec(text)) if (!best || m[1]!.length < best[1]!.length) best = m;
|
||||
// a one-line config (`export default { play: { … } }`): the first `parent: {` anywhere
|
||||
best ??= new RegExp(`()\\b${parent}\\s*:\\s*\\{`).exec(text);
|
||||
if (!best) throw new Error(`${file}: no ${parent} block to put ${parent}.${key} into; add ${key}: '${value}' to ${parent} in the config file`);
|
||||
const open = best.index + best[0].length - 1;
|
||||
const close = matchBrace(text, open);
|
||||
if (close < 0) throw new Error(`${file}: could not find the end of the ${parent} block`);
|
||||
const body = text.slice(open + 1, close);
|
||||
const lit = toTs(value);
|
||||
const keyRe = new RegExp(`(^|[{,\\s])${key}\\s*:\\s*`, 'g');
|
||||
for (let k = keyRe.exec(body); k; k = keyRe.exec(body)) {
|
||||
if (depthAt(body, k.index + k[1]!.length) !== 0) continue; // a key of a nested object
|
||||
const valStart = k.index + k[0].length;
|
||||
const q = body[valStart];
|
||||
if (q !== '\'' && q !== '"' && q !== '`') throw new Error(`${file}: ${parent}.${key} is not a plain string; set it to '${value}' in the config file`);
|
||||
let end = valStart + 1;
|
||||
for (; end < body.length && body[end] !== q; end++) if (body[end] === '\\') end++;
|
||||
const abs = open + 1;
|
||||
return text.slice(0, abs + valStart) + lit + text.slice(abs + end + 1);
|
||||
}
|
||||
if (!body.includes('\n')) {
|
||||
const inner = body.trim().replace(/,$/, '');
|
||||
return `${text.slice(0, open + 1)} ${inner ? `${inner}, ` : ''}${key}: ${lit} ${text.slice(close)}`;
|
||||
}
|
||||
const ind = /\n([ \t]*)\S/.exec(body)?.[1] ?? `${best[1]} `;
|
||||
return `${text.slice(0, open + 1)}\n${ind}${key}: ${lit},${body}${text.slice(close)}`;
|
||||
}
|
||||
|
||||
/** Bracket depth at `at` in `s` (strings and comments skipped). */
|
||||
function depthAt(s: string, at: number): number {
|
||||
let d = 0;
|
||||
for (let i = 0; i < at; i++) {
|
||||
const ch = s[i]!;
|
||||
if (ch === '/' && s[i + 1] === '/') { const n = s.indexOf('\n', i); if (n < 0 || n >= at) return d; i = n; continue; }
|
||||
if (ch === '/' && s[i + 1] === '*') { const n = s.indexOf('*/', i + 2); if (n < 0 || n >= at) return d; i = n + 1; continue; }
|
||||
if (ch === '"' || ch === '\'' || ch === '`') { for (i++; i < at && s[i] !== ch; i++) if (s[i] === '\\') i++; continue; }
|
||||
if (ch === '{' || ch === '[' || ch === '(') d++;
|
||||
else if (ch === '}' || ch === ']' || ch === ')') d--;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue