73 lines
5.3 KiB
TypeScript
73 lines
5.3 KiB
TypeScript
// Migration from other tools. Only reads the source; writes storebound files next to it without overwriting anything.
|
|
// storebound import fastlane [--dir <fastlane dir>[,<dir>]] [--out <project folder>] [--profile app|game|subscription-app] [--dry]
|
|
// fastlane: Appfile / Deliverfile ids and options, deliver metadata (fastlane/metadata/<locale>/*.txt, default/,
|
|
// review_information/notes.txt), supply metadata (fastlane/metadata/android/<lang>/*, changelogs, images/) and the
|
|
// screenshots become storebound.config.ts, store/appstore.md, store/listing.md and, with screenshots, a `folder` capture
|
|
// source (store/shots/frames/<lang>/<phone|tablet>/NN.png + store/shots/copy.json). Default --dir: fastlane/,
|
|
// ios/fastlane/ and android/fastlane/ under --out (default: the current folder), every one that exists.
|
|
// A file that already exists is kept: the import goes to <name>.imported.<ext> next to it (images are skipped), with a
|
|
// diff hint. Keys, team ids, the App Review contact and demo account are never copied: the report says where they go.
|
|
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
import type { Args } from './lib/args';
|
|
import { type AppProfile, PROFILES } from './lib/config';
|
|
import { findFastlaneDirs, type ImportPlan, importedPath, planImport, scanFastlane } from './lib/fastlane';
|
|
import { log, warn } from './lib/log';
|
|
|
|
export interface Applied { created: string[]; imported: { target: string; written: string }[]; kept: string[] }
|
|
|
|
/** Writes a plan under `root` (nothing with dry). Existing text files get a .imported copy; existing images are kept. */
|
|
export function applyPlan(plan: ImportPlan, root: string, dry: boolean): Applied {
|
|
const out: Applied = { created: [], imported: [], kept: [] };
|
|
for (const f of plan.files) {
|
|
const dest = join(root, f.path);
|
|
if (relative(root, dest).startsWith('..')) throw new Error(`import target outside the project: ${f.path}`);
|
|
let target = dest;
|
|
if (existsSync(dest)) {
|
|
if (f.text === undefined) { out.kept.push(f.path); log('import', `keep ${f.path} (exists)`); continue; }
|
|
target = importedPath(dest);
|
|
out.imported.push({ target: f.path, written: relative(root, target) });
|
|
log('import', `${dry ? 'would write' : 'write '} ${relative(root, target)} (${f.path} exists; diff: git diff --no-index ${f.path} ${relative(root, target)})`);
|
|
} else {
|
|
out.created.push(f.path);
|
|
log('import', `${dry ? 'would create' : 'create'} ${f.path}${f.from ? ` <- ${f.src ?? relative(root, f.from)}` : ''}`);
|
|
}
|
|
if (dry) continue;
|
|
mkdirSync(dirname(target), { recursive: true });
|
|
if (f.text !== undefined) writeFileSync(target, f.text); else copyFileSync(f.from!, target);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** The report: what went where, what did not, secrets named (never copied), placeholders, warnings. */
|
|
export function report(plan: ImportPlan): string {
|
|
const lines: string[] = [];
|
|
const sec = (title: string, rows: string[]) => { if (rows.length) lines.push('', `${title}:`, ...rows.map(r => ` - ${r}`)); };
|
|
lines.push(`fastlane import: "${plan.appName}", profile ${plan.profile}, ${plan.languages.length} language(s)` +
|
|
`${plan.bundleId ? `, iOS ${plan.bundleId}` : ''}${plan.packageName ? `, Android ${plan.packageName}` : ''}`);
|
|
sec('mapped', plan.mapped.map(m => `${m.from} -> ${m.to}`));
|
|
sec('not mapped (edit storebound.config.ts for these)', plan.notMapped.map(n => `${n.what}: ${n.where}`));
|
|
sec('keys, accounts and personal data (named only, nothing copied)', plan.secrets.map(n => `${n.what}: ${n.where}`));
|
|
sec('placeholders to replace in storebound.config.ts', plan.placeholders.map(n => `${n.what}: ${n.where}`));
|
|
sec('warnings', plan.warnings);
|
|
return lines.join('\n');
|
|
}
|
|
|
|
export async function run(args: Args): Promise<void> {
|
|
const what = args._[0];
|
|
if (what !== 'fastlane') throw new Error('usage: storebound import fastlane [--dir <fastlane dir>] [--out <project folder>] [--profile app|game|subscription-app] [--dry]');
|
|
const root = resolve(args.str('out', '.'));
|
|
const profile = args.get('profile') as AppProfile | undefined;
|
|
if (profile !== undefined && !PROFILES.includes(profile)) throw new Error(`--profile ${profile}: one of ${PROFILES.join(', ')}`);
|
|
const dirs = args.list('dir').length ? args.list('dir').map(d => resolve(d)) : findFastlaneDirs(root);
|
|
if (!dirs.length) throw new Error(`no fastlane folder in ${root} (looked for fastlane/, ios/fastlane/, android/fastlane/); pass --dir`);
|
|
for (const d of dirs) if (!existsSync(d)) throw new Error(`--dir ${d}: not found`);
|
|
const dry = args.has('dry');
|
|
log('import', `fastlane: ${dirs.map(d => relative(root, d) || '.').join(', ')} -> ${root}${dry ? ' (dry run: nothing is written)' : ''}`);
|
|
const plan = planImport(scanFastlane(dirs), { root, profile });
|
|
const applied = applyPlan(plan, root, dry);
|
|
console.log(report(plan));
|
|
if (applied.imported.length) warn('import', `${applied.imported.length} file(s) existed and were kept: compare the .imported copies (git diff --no-index <file> <file.imported>) and merge what you want`);
|
|
log('import', dry ? 'dry run: run again without --dry to write the files'
|
|
: 'next: `storebound init` (adds the remaining templates, keeps these files), replace the placeholders, then `storebound check`');
|
|
}
|