package: ship declarations (dist/types, the "types" condition) and a Node JS entry for 'storebound'; Bun keeps the .ts source. 0.8.5 exported only cli/lib/config.ts, so tsc (next build, vue-tsc, tsc --noEmit) failed on a scaffolded storebound.config.ts and broke a Next app's build; a test type-checks the template against the packed entry under bundler / node16 / nodenext without @types/node. check fails on the template's own placeholders (Example Studio, bare example.com URLs and addresses)

This commit is contained in:
CRTkafa 2026-10-15 05:49:05 +08:00
parent 77f141e223
commit d717ceb4cd
10 changed files with 138 additions and 7 deletions

View file

@ -5,7 +5,7 @@
// storebound check --aso keyword audit of both listing files (App Store keyword field, Play stuffing); exit 1 on errors
// --json structured result ({ ok, items, warnings, hints, problems } or { findings, errors }), exit 1 on problems
import { templateBlockers } from './lib/template-answers';
import { COPYRIGHT_MISSING } from './lib/validate-rules';
import { COPYRIGHT_MISSING, templatePlaceholders } from './lib/validate-rules';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { appPrivacyMismatches, type PrivacyMismatch, trackingDeclaredIssues, type TrackingSources, typeIdOf } from './lib/app-privacy';
@ -145,6 +145,9 @@ export async function run(ctx: Ctx): Promise<void> {
try { for (const n of (await runScan(ctx.loaded.root, { config: ctx.loaded.path })).report.notes) if (n.startsWith('scan.ignore[')) note(n); } catch (e) { note(`scan.ignore not checked: ${(e as Error).message}`); }
}
if (config.apple && !config.app.copyright?.trim()) problems.push(COPYRIGHT_MISSING);
// the scaffolder's own placeholders would reach the stores as the app's copyright, contact and legal pages
const tph = templatePlaceholders(config as never);
if (tph.length) problems.push(`template placeholders still in the config: ${tph.join(', ')} ("Example Studio", example.com): the stores would show them as your copyright, contact address and legal pages; put the real values in`);
if (config.dataSafety) tryDo('data safety', () => {
const rows = dataSafetyRows(config.dataSafety!, config.accounts);
// conditional required rows (e.g. the delete-account URL once accounts exist): Play answers 400 without them

View file

@ -144,6 +144,22 @@ const ageOpen = (r: Record<string, unknown>): { open: string[]; soft: string[] }
const urlOk = (u: UrlCheck) => u.status !== null && u.status >= 200 && u.status < 300;
const urlMsg = (u: UrlCheck) => (u.status === null ? `unreachable (${u.error ?? 'no answer'})` : `HTTP ${u.status}`);
/** The scaffolder's own placeholder values (templates/*storebound.config.ts) that would reach a store as the app's
* legal and contact details: "2026 Example Studio", example.com / example.org / example.net URLs and addresses
* (the bare domain; subdomains such as notes.example.com are a project's own test fixtures).
* Returns the dotted paths that still hold one. Pure. */
export function templatePlaceholders(config: { app?: object; accounts?: object; reviewAccess?: object } & Record<string, unknown>): string[] {
const out: string[] = [];
const bad = (v: string) => /\bexample studio\b/i.test(v) || /(^|@|\/\/)(www\.)?example\.(com|org|net)(?![\w.-])/i.test(v);
const walk = (v: unknown, path: string) => {
if (typeof v === 'string') { if (bad(v)) out.push(path); }
else if (Array.isArray(v)) v.forEach((x, i) => walk(x, `${path}[${i}]`));
else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) walk(x, `${path}.${k}`);
};
for (const k of ['app', 'accounts', 'reviewAccess'] as const) walk(config[k], k);
return out;
}
export const COPYRIGHT_MISSING = "app.copyright is missing: App Store Connect requires it. Use your public brand (e.g. '2026 <brand>'); never a company name you keep private.";
/** Copyright: App Store Connect refuses to submit a version without one; storebound never fills it in. */