Skip to content

Commit 0431b4b

Browse files
committed
Add ui sync command
1 parent bf89a1e commit 0431b4b

2 files changed

Lines changed: 146 additions & 22 deletions

File tree

packages/cli/src/index.ts

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,38 @@ function materializeCollectionRoutes(uiDir: string, schema: ThsSchema) {
11871187
}
11881188
}
11891189

1190+
function syncUiOutput(args: {
1191+
schema: ThsSchema;
1192+
outDir: string;
1193+
schemaPathForHints?: string;
1194+
withTests?: boolean;
1195+
compiledJson: string;
1196+
}) {
1197+
const resolvedOutDir = path.resolve(args.outDir);
1198+
const templateDir = resolveNextExportUiTemplateDir();
1199+
const uiDir = path.join(resolvedOutDir, 'ui');
1200+
1201+
fs.rmSync(uiDir, { recursive: true, force: true });
1202+
copyDir(templateDir, uiDir);
1203+
1204+
const thsTsPath = path.join(uiDir, 'src', 'generated', 'ths.ts');
1205+
ensureDir(path.dirname(thsTsPath));
1206+
fs.writeFileSync(thsTsPath, renderThsTs(args.schema));
1207+
materializeCollectionRoutes(uiDir, args.schema);
1208+
1209+
const compiledPublicPath = path.join(uiDir, 'public', 'compiled', 'App.json');
1210+
ensureDir(path.dirname(compiledPublicPath));
1211+
fs.writeFileSync(compiledPublicPath, args.compiledJson);
1212+
1213+
if (args.withTests) {
1214+
addGeneratedUiTestScaffold(uiDir, templateDir);
1215+
console.log(`Wrote ui/tests/ (generated app test scaffold)`);
1216+
}
1217+
1218+
applyUiExtensions(uiDir, args.schema, args.schemaPathForHints);
1219+
console.log(`Wrote ui/ (Next.js static export template)`);
1220+
}
1221+
11901222
function resolveUiExtensionsDir(schema: ThsSchema, schemaPathForHints?: string): string | null {
11911223
const declared = String(schema.app?.ui?.extensions?.directory ?? '').trim();
11921224
if (!declared) return null;
@@ -3008,34 +3040,43 @@ program
30083040
fs.writeFileSync(path.join(outDir, 'schema.json'), JSON.stringify(schema, null, 2));
30093041

30103042
if (opts.ui) {
3011-
const templateDir = resolveNextExportUiTemplateDir();
3012-
const uiDir = path.join(outDir, 'ui');
3013-
fs.rmSync(uiDir, { recursive: true, force: true });
3014-
copyDir(templateDir, uiDir);
3015-
3016-
const thsTsPath = path.join(uiDir, 'src', 'generated', 'ths.ts');
3017-
ensureDir(path.dirname(thsTsPath));
3018-
fs.writeFileSync(thsTsPath, renderThsTs(schema));
3019-
materializeCollectionRoutes(uiDir, schema);
3020-
3021-
const compiledPublicPath = path.join(uiDir, 'public', 'compiled', 'App.json');
3022-
ensureDir(path.dirname(compiledPublicPath));
3023-
fs.writeFileSync(compiledPublicPath, compiledJson);
3024-
3025-
if (opts.withTests) {
3026-
addGeneratedUiTestScaffold(uiDir, templateDir);
3027-
console.log(`Wrote ui/tests/ (generated app test scaffold)`);
3028-
}
3029-
3030-
applyUiExtensions(uiDir, schema, schemaPath);
3031-
3032-
console.log(`Wrote ui/ (Next.js static export template)`);
3043+
syncUiOutput({
3044+
schema,
3045+
outDir,
3046+
schemaPathForHints: schemaPath,
3047+
withTests: opts.withTests,
3048+
compiledJson
3049+
});
30333050
}
30343051

30353052
console.log(`Wrote compiled/App.json`);
30363053
console.log(`Wrote ${appSol.path}`);
30373054
});
30383055

3056+
program
3057+
.command('ui')
3058+
.description('UI-specific commands')
3059+
.command('sync')
3060+
.argument('<schema>', 'Path to THS schema JSON file')
3061+
.option('--out <dir>', 'Output directory', 'artifacts')
3062+
.option('--with-tests', 'Emit generated app test scaffold', false)
3063+
.action((schemaPath: string, opts: { out: string; withTests: boolean }) => {
3064+
const schema = loadThsSchemaOrThrow(schemaPath);
3065+
const outDir = path.resolve(opts.out);
3066+
const compiledPath = path.join(outDir, 'compiled', 'App.json');
3067+
if (!fs.existsSync(compiledPath)) {
3068+
throw new Error(`Missing compiled/App.json in ${outDir}. Run \`th generate\`, \`th build\`, or \`th up\` first.`);
3069+
}
3070+
const compiledJson = fs.readFileSync(compiledPath, 'utf-8');
3071+
syncUiOutput({
3072+
schema,
3073+
outDir,
3074+
schemaPathForHints: schemaPath,
3075+
withTests: opts.withTests,
3076+
compiledJson
3077+
});
3078+
});
3079+
30393080
program
30403081
.command('build')
30413082
.argument('<schema>', 'Path to THS schema JSON file')

test/testCliGenerateUi.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ function writeJson(filePath, value) {
99
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
1010
}
1111

12+
function writeCompiledArtifact(filePath) {
13+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
14+
fs.writeFileSync(
15+
filePath,
16+
JSON.stringify(
17+
{
18+
contractName: 'App',
19+
abi: [],
20+
bytecode: '0x00',
21+
deployedBytecode: '0x00',
22+
compilerProfile: 'default'
23+
},
24+
null,
25+
2
26+
)
27+
);
28+
}
29+
1230
function runTh(args, cwd) {
1331
const res = spawnSync('node', [path.resolve('packages/cli/dist/index.js'), ...args], {
1432
cwd,
@@ -242,3 +260,68 @@ describe('th generate (UI template)', function () {
242260
expect(workflow).to.include('TH_INSTALL_BROWSER_DEPS');
243261
});
244262
});
263+
264+
describe('th ui sync', function () {
265+
this.timeout(180000);
266+
267+
it('refreshes generated UI from existing compiled artifacts without regenerating contracts', function () {
268+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-sync-'));
269+
const schemaPath = path.join(dir, 'schema.json');
270+
const outDir = path.join(dir, 'out');
271+
writeJson(schemaPath, minimalSchema());
272+
writeCompiledArtifact(path.join(outDir, 'compiled', 'App.json'));
273+
274+
const res = runTh(['ui', 'sync', schemaPath, '--out', outDir], process.cwd());
275+
expect(res.status, res.stderr || res.stdout).to.equal(0);
276+
277+
expect(fs.existsSync(path.join(outDir, 'ui', 'package.json'))).to.equal(true);
278+
expect(fs.existsSync(path.join(outDir, 'ui', 'src', 'generated', 'ths.ts'))).to.equal(true);
279+
expect(fs.existsSync(path.join(outDir, 'ui', 'public', 'compiled', 'App.json'))).to.equal(true);
280+
expect(fs.existsSync(path.join(outDir, 'contracts', 'App.sol'))).to.equal(false);
281+
});
282+
283+
it('replaces stale UI output during sync and reapplies schema-declared overrides', function () {
284+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-sync-overrides-'));
285+
const schemaPath = path.join(dir, 'schema.json');
286+
const overridesDir = path.join(dir, 'ui-overrides');
287+
const outDir = path.join(dir, 'out');
288+
289+
writeJson(schemaPath, schemaWithUiOverrides());
290+
writeCompiledArtifact(path.join(outDir, 'compiled', 'App.json'));
291+
fs.mkdirSync(path.join(overridesDir, 'app', 'run'), { recursive: true });
292+
fs.writeFileSync(
293+
path.join(overridesDir, 'app', 'page.tsx'),
294+
"export default function HomePage(){return <div>custom-home-marker</div>;}\n"
295+
);
296+
fs.writeFileSync(
297+
path.join(overridesDir, 'app', 'run', 'page.tsx'),
298+
"export default function RunPage(){return <div>custom-run-page</div>;}\n"
299+
);
300+
301+
const first = runTh(['ui', 'sync', schemaPath, '--out', outDir], process.cwd());
302+
expect(first.status, first.stderr || first.stdout).to.equal(0);
303+
304+
const staleFile = path.join(outDir, 'ui', 'app', 'stale-marker.txt');
305+
fs.mkdirSync(path.dirname(staleFile), { recursive: true });
306+
fs.writeFileSync(staleFile, 'stale');
307+
308+
const second = runTh(['ui', 'sync', schemaPath, '--out', outDir], process.cwd());
309+
expect(second.status, second.stderr || second.stdout).to.equal(0);
310+
expect(fs.existsSync(staleFile)).to.equal(false);
311+
312+
const homePage = fs.readFileSync(path.join(outDir, 'ui', 'app', 'page.tsx'), 'utf-8');
313+
expect(homePage).to.include('custom-home-marker');
314+
expect(fs.existsSync(path.join(outDir, 'ui', 'app', 'run', 'page.tsx'))).to.equal(true);
315+
});
316+
317+
it('fails clearly when compiled artifacts are missing', function () {
318+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-sync-missing-compiled-'));
319+
const schemaPath = path.join(dir, 'schema.json');
320+
const outDir = path.join(dir, 'out');
321+
writeJson(schemaPath, minimalSchema());
322+
323+
const res = runTh(['ui', 'sync', schemaPath, '--out', outDir], process.cwd());
324+
expect(res.status).to.not.equal(0);
325+
expect(res.stderr || res.stdout).to.include('Missing compiled/App.json');
326+
});
327+
});

0 commit comments

Comments
 (0)