@rozie/cli 0.1.0

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/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@rozie/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line interface for Rozie.js — compile .rozie files to React, Vue, Svelte, Angular, Solid, and Lit from the terminal.",
5
+ "type": "module",
6
+ "private": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "bin": {
18
+ "rozie": "./dist/bin.cjs"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src",
23
+ "!src/**/__tests__/**",
24
+ "!src/**/*.test.ts",
25
+ "!src/**/*.test.tsx",
26
+ "!src/**/*.spec.ts"
27
+ ],
28
+ "dependencies": {
29
+ "@babel/code-frame": "^7.29.0",
30
+ "@babel/generator": "^7.29.1",
31
+ "@babel/parser": "^7.29.3",
32
+ "@babel/traverse": "^7.29.0",
33
+ "@babel/types": "^7.29.0",
34
+ "@vue/compiler-sfc": "^3.5.33",
35
+ "chokidar": "^4.0.3",
36
+ "commander": "^14.0.0",
37
+ "fast-glob": "^3.3.3",
38
+ "htmlparser2": "^12.0.0",
39
+ "magic-string": "^0.30.21",
40
+ "picocolors": "^1.1.0",
41
+ "postcss": "^8.5.13",
42
+ "prettier": "^3.8.3",
43
+ "sass": "^1.97.3",
44
+ "@rozie/core": "0.1.0",
45
+ "@rozie/target-lit": "0.1.0",
46
+ "@rozie/target-react": "0.1.0",
47
+ "@rozie/target-angular": "0.1.0",
48
+ "@rozie/target-solid": "0.1.0",
49
+ "@rozie/target-vue": "0.1.0",
50
+ "@rozie/unplugin": "0.1.0",
51
+ "@rozie/target-svelte": "0.1.0"
52
+ },
53
+ "peerDependencies": {
54
+ "prettier-plugin-svelte": "^3.4.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "prettier-plugin-svelte": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "prettier-plugin-svelte": "^3.4.0",
63
+ "vitest": "^4.1.5"
64
+ },
65
+ "license": "MIT",
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/One-Learning-Community/rozie.js.git",
72
+ "directory": "packages/cli"
73
+ },
74
+ "homepage": "https://github.com/One-Learning-Community/rozie.js#readme",
75
+ "bugs": {
76
+ "url": "https://github.com/One-Learning-Community/rozie.js/issues"
77
+ },
78
+ "author": "One Learning Community (https://github.com/One-Learning-Community)",
79
+ "scripts": {
80
+ "build": "tsdown",
81
+ "test": "vitest run --passWithNoTests",
82
+ "lint": "biome lint src",
83
+ "typecheck": "tsc --noEmit"
84
+ }
85
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // @rozie/cli — bin entry. Thin shebang wrapper that delegates to runCli().
3
+ // Kept tiny so the dist artefact stays grep-friendly and so commander's
4
+ // own error handling (process.exit on parse failure) is the only exit path.
5
+ import { runCli } from './index.js';
6
+
7
+ runCli(process.argv).catch((err) => {
8
+ // Defensive: runCli already prints user-facing diagnostics + exits with the
9
+ // right code. This catch only fires for genuinely unexpected errors (bugs
10
+ // in the CLI itself). Print stack to stderr and exit 1 so CI signals red.
11
+ // eslint-disable-next-line no-console
12
+ console.error(err instanceof Error ? err.stack ?? err.message : String(err));
13
+ process.exit(1);
14
+ });
@@ -0,0 +1,438 @@
1
+ // `rozie build` subcommand — D-87/D-88/D-89/D-90/D-91/D-93 multi-target build.
2
+ //
3
+ // As of Phase 6 Plan 03, the canonical entrypoint is `runBuildMatrix(inputs,
4
+ // opts)` — a single (input × target) matrix coordinator that:
5
+ // • Expands variadic positional args via `expandInputs` (D-88 file/dir/glob)
6
+ // • Validates the target list parsed by commander's `parseTargets` (D-87)
7
+ // • Routes every per-tuple compile through `@rozie/core.compile()` — the
8
+ // single source of truth shared with @rozie/unplugin and @rozie/babel-plugin
9
+ // (D-93 byte-identical contract; Plan 06-06 parity gate enforces drift)
10
+ // • Writes `dist/{target}/{source-rel}/Foo.{ext}` per D-89
11
+ // • Emits .d.ts sidecars by default (D-90); --no-types opts out
12
+ // • Suppresses .map sidecars by default (D-91); --source-map opts in
13
+ // • Errors with ROZ855 when target=react and --out is null (sidecars cannot
14
+ // stream to stdout)
15
+ //
16
+ // `runBuild` and `runBuildMany` are preserved as thin backward-compat wrappers
17
+ // — they delegate to runBuildMatrix with single-target coercion. ALL CLI
18
+ // pipelines now flow through `compile()`; the legacy parse → lowerToIR →
19
+ // emit{Target} chain has been removed from this file.
20
+ // Per the @rozie/unplugin pattern (transform.ts) and @rozie/core's own
21
+ // compile.ts: use RELATIVE imports into sibling workspace packages so the
22
+ // pipeline works whether or not dist/ has been built. tsdown inlines these
23
+ // at bundle time for the published artifact.
24
+ import { compile } from '../../../core/src/compile.js';
25
+ import { renderDiagnostic } from '../../../core/src/diagnostics/frame.js';
26
+ import type { Diagnostic } from '../../../core/src/diagnostics/Diagnostic.js';
27
+ // Phase 22 Plan 22-05 — CLI sidecar fallback (REQ-5). The `.d.rozie.ts`
28
+ // per-module declaration is rendered by the SAME `renderSidecar` the unplugin
29
+ // uses (parse → lowerToIR → per-target emit<Target>Types dispatch → do-not-edit
30
+ // + sha256 hash header), so the unplugin and CLI sidecar bytes cannot drift.
31
+ // Relative import into the sibling workspace package (the CLI's established
32
+ // pattern; tsdown inlines it at bundle time).
33
+ import { renderSidecar } from '../../../unplugin/src/emitSidecar.js';
34
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
35
+ import { dirname as pathDirname, resolve as pathResolve } from 'node:path';
36
+ import { TARGET_EXTENSIONS } from '../utils/outputPath.js';
37
+ import pc from 'picocolors';
38
+ import { expandInputs } from '../utils/expandInputs.js';
39
+ import { computeOutputPath } from '../utils/outputPath.js';
40
+ import type { Target } from '../utils/parseTargets.js';
41
+ import { prettyFormat } from '../utils/prettyFormat.js';
42
+
43
+ /**
44
+ * Legacy single-target options shape — preserved for `runBuild` /
45
+ * `runBuildMany` backward-compat. New code should use `BuildOptionsExt`.
46
+ */
47
+ export interface BuildOptions {
48
+ target?: string;
49
+ out?: string;
50
+ sourceMap?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Phase 6 multi-target options shape consumed by `runBuildMatrix`.
55
+ * Commander's `parseTargets` produces `target: Target[]`; programmatic callers
56
+ * may pass either a single `Target` (e.g., from `runBuild`) or `Target[]`.
57
+ */
58
+ export interface BuildOptionsExt {
59
+ target?: Target | Target[];
60
+ out?: string;
61
+ /** D-91 default false — emit .map sidecars only when explicitly opted in. */
62
+ sourceMap?: boolean;
63
+ /** D-90 default true — emit .d.ts sidecars; --no-types maps to false. */
64
+ types?: boolean;
65
+ /** Project root used for source-rel-path computation; defaults to process.cwd(). */
66
+ root?: string;
67
+ /**
68
+ * Opt-in: format emitted artefacts through prettier before write.
69
+ * Off by default per PROJECT.md "Out of Scope" — v1's bar is "just
70
+ * works", not "pretty output". When ON, applies prettier core to .tsx /
71
+ * .ts / .d.ts / .vue / .css sidecars, and prettier-plugin-svelte to
72
+ * .svelte. Source-map sidecars (.map) are never reformatted (spec-
73
+ * required field ordering). Prettier failures degrade gracefully:
74
+ * raw output is written and a warning prints to stderr.
75
+ */
76
+ pretty?: boolean;
77
+ /**
78
+ * Phase 23 — Angular-only opt-out for the auto `ControlValueAccessor` emit.
79
+ * Default ON (undefined/true): single-model Angular components auto-emit the
80
+ * CVA shape. `false` maps to `compile({ angular: { cva: false } })` and
81
+ * suppresses ALL CVA emit. Omitting it entirely exercises the emitter-side
82
+ * `opts.cva ?? true` default — byte-identical to the unplugin/babel-plugin
83
+ * default-ON path (dist-parity contract). No-op for non-Angular targets.
84
+ */
85
+ cva?: boolean;
86
+ /**
87
+ * Phase 26 (D-11) — the GLOBAL safe-interpolation opt-out. Default ON
88
+ * (undefined/true): non-provably-primitive interpolations are wrapped in the
89
+ * injected `rozieDisplay` helper on the five non-Vue targets. `false` maps to
90
+ * `compile({ safeInterpolation: false })` and reverts to raw per-target emit.
91
+ * Omitting it entirely exercises the lowerer-side `?? true` default —
92
+ * byte-identical to the unplugin/babel-plugin default-ON path. No-op for Vue.
93
+ */
94
+ safeInterpolation?: boolean;
95
+ }
96
+
97
+ /**
98
+ * Outcome enum for testability — runBuild itself calls process.exit when the
99
+ * caller is the bin wrapper, but tests pass `exit: 'throw'` to convert the
100
+ * exit-code into a thrown BuildExit so vitest can assert on it without the
101
+ * test runner itself exiting. The third-party `commander.exitOverride` does
102
+ * the same trick at the parser level; this is the same idea at the action
103
+ * level.
104
+ */
105
+ export class BuildExit extends Error {
106
+ constructor(
107
+ public readonly code: number,
108
+ public readonly stderr: string,
109
+ ) {
110
+ super(`rozie build exited with code ${code}`);
111
+ this.name = 'BuildExit';
112
+ }
113
+ }
114
+
115
+ export interface RunBuildContext {
116
+ /** When 'throw', exits become thrown BuildExit instead of process.exit. */
117
+ exit?: 'process' | 'throw';
118
+ /** stderr sink override — defaults to process.stderr.write. */
119
+ stderrWrite?: (chunk: string) => void;
120
+ /** stdout sink override — defaults to process.stdout.write. */
121
+ stdoutWrite?: (chunk: string) => void;
122
+ }
123
+
124
+ const VALID_TARGETS = new Set<Target>(['vue', 'react', 'svelte', 'angular', 'solid', 'lit']);
125
+
126
+ /**
127
+ * Per-target extension used to derive a synthetic filename for the stdout
128
+ * code path when --pretty is on. Mirrors TARGET_EXTENSIONS from
129
+ * outputPath.ts but kept local to avoid coupling the build command to a
130
+ * specific helper for a one-line need.
131
+ */
132
+ const TARGET_STDOUT_EXT: Record<Target, string> = {
133
+ vue: '.vue',
134
+ react: '.tsx',
135
+ svelte: '.svelte',
136
+ angular: '.ts',
137
+ solid: '.tsx',
138
+ lit: '.ts',
139
+ };
140
+
141
+ /**
142
+ * Phase 6 D-87/D-88/D-89/D-90/D-91/D-93 — the canonical build coordinator.
143
+ *
144
+ * @param inputArgs positional args (files, directories, or globs)
145
+ * @param opts parsed options (target list, out, sourceMap, types, root)
146
+ * @param ctx test-injection sinks + exit-mode toggle
147
+ */
148
+ export async function runBuildMatrix(
149
+ inputArgs: string[],
150
+ opts: BuildOptionsExt = {},
151
+ ctx: RunBuildContext = {},
152
+ ): Promise<void> {
153
+ const stderrWrite = ctx.stderrWrite ?? ((s) => void process.stderr.write(s));
154
+ const stdoutWrite = ctx.stdoutWrite ?? ((s) => void process.stdout.write(s));
155
+
156
+ const exit = (code: number, stderrBuf = ''): never => {
157
+ if (ctx.exit === 'throw') {
158
+ throw new BuildExit(code, stderrBuf);
159
+ }
160
+ process.exit(code);
161
+ };
162
+
163
+ // ----- Normalize the target list -------------------------------------
164
+ const targetsRaw: Target[] = Array.isArray(opts.target)
165
+ ? opts.target
166
+ : opts.target !== undefined
167
+ ? [opts.target as Target]
168
+ : ['vue'];
169
+
170
+ // Defensive validation — commander's parseTargets already vets these, but
171
+ // programmatic callers (runBuild/runBuildMany passthroughs, tests) might
172
+ // bypass it.
173
+ for (const t of targetsRaw) {
174
+ if (!VALID_TARGETS.has(t)) {
175
+ const msg = pc.red(
176
+ `[ROZ850] rozie build: unknown target '${t}' (expected vue|react|svelte|angular|solid|lit)\n`,
177
+ );
178
+ stderrWrite(msg);
179
+ exit(2, msg);
180
+ return;
181
+ }
182
+ }
183
+ const targets = targetsRaw;
184
+
185
+ // ----- Expand inputs --------------------------------------------------
186
+ let inputs: string[];
187
+ try {
188
+ inputs = await expandInputs(inputArgs);
189
+ } catch (err) {
190
+ const msg = pc.red(`${(err as Error).message}\n`);
191
+ stderrWrite(msg);
192
+ exit(1, msg);
193
+ return;
194
+ }
195
+
196
+ if (inputs.length === 0) {
197
+ const msg = pc.red(
198
+ `[ROZ851] rozie build: no .rozie files matched the given inputs\n`,
199
+ );
200
+ stderrWrite(msg);
201
+ exit(1, msg);
202
+ return;
203
+ }
204
+
205
+ // ----- D-89 --out requirement ----------------------------------------
206
+ // --out is required when (a) more than one input file or (b) more than one
207
+ // target — both cases produce multiple output files per invocation.
208
+ if ((inputs.length > 1 || targets.length > 1) && opts.out === undefined) {
209
+ const msg = pc.red(
210
+ `[ROZ852] rozie build: --out <dir> is required when compiling multiple files or multiple targets\n`,
211
+ );
212
+ stderrWrite(msg);
213
+ exit(2, msg);
214
+ return;
215
+ }
216
+
217
+ const outDir = opts.out !== undefined ? pathResolve(opts.out) : null;
218
+ const rootDir = opts.root ?? process.cwd();
219
+ const wantTypes = opts.types !== false; // D-90 default true
220
+ const wantSourceMap = opts.sourceMap === true; // D-91 default false
221
+ const wantPretty = opts.pretty === true; // off by default per PROJECT.md
222
+
223
+ // ----- DIST-04 React-stdout sidecar guard ----------------------------
224
+ // React emits .d.ts + .css + .global.css sidecars; these CANNOT
225
+ // be streamed to stdout (no filename to attach to). When target=react and
226
+ // --out is null, error with ROZ855 BEFORE any pipeline work runs.
227
+ if (outDir === null) {
228
+ for (const t of targets) {
229
+ if (t === 'react') {
230
+ const msg = pc.red(
231
+ `[ROZ855] rozie build: target 'react' requires --out <dir> ` +
232
+ `(cannot stream sidecar files .d.ts/.css/.global.css to stdout). ` +
233
+ `Set --out <dir> to emit React components.\n`,
234
+ );
235
+ stderrWrite(msg);
236
+ exit(2, msg);
237
+ return;
238
+ }
239
+ }
240
+ }
241
+
242
+ // ----- Build (input × target) tuples ---------------------------------
243
+ const tuples = inputs.flatMap((input) => targets.map((target) => ({ input, target })));
244
+
245
+ // ----- Parallel compile (RESEARCH recommendation: Promise.all) -------
246
+ const results = await Promise.all(
247
+ tuples.map(async ({ input, target }) => {
248
+ const source = readFileSync(input, 'utf8');
249
+ const compileOpts = {
250
+ target,
251
+ filename: input,
252
+ types: wantTypes,
253
+ sourceMap: wantSourceMap,
254
+ // Phase 23 — only attach the `angular` namespace when the user opted
255
+ // OUT (cva === false). Omitting it preserves the emitter-side
256
+ // `opts.cva ?? true` default-ON path, keeping the CLI byte-identical
257
+ // to unplugin/babel-plugin when --no-cva is absent.
258
+ ...(opts.cva === false ? { angular: { cva: false } } : {}),
259
+ // Phase 26 (D-11) — only attach `safeInterpolation` when the user opted
260
+ // OUT (--no-safe-interpolation → false). Omitting it preserves the
261
+ // lowerer-side `?? true` default-ON path (dist-parity byte-identity).
262
+ ...(opts.safeInterpolation === false ? { safeInterpolation: false } : {}),
263
+ };
264
+ const result = compile(source, compileOpts);
265
+ return { input, target, source, result };
266
+ }),
267
+ );
268
+
269
+ // Opt-in prettier pass — degrades to raw output on failure so a
270
+ // prettier hiccup never blocks an otherwise-correct compile. Captured
271
+ // in the outer scope so both the parallel matrix loop and the stdout
272
+ // path can call it without re-allocating per tuple.
273
+ const maybePretty = async (text: string, name: string): Promise<string> => {
274
+ if (!wantPretty) return text;
275
+ const r = await prettyFormat(text, name);
276
+ if (!r.ok) {
277
+ stderrWrite(
278
+ pc.yellow(`[warning] --pretty failed for ${name}: ${r.error}; emitting unformatted\n`),
279
+ );
280
+ }
281
+ return r.formatted;
282
+ };
283
+
284
+ // ----- Write phase (parallel across tuples + parallel per sidecar) ---
285
+ // The compile phase above is already in Promise.all. Without --pretty,
286
+ // the write loop is sync filesystem work and parallelism doesn't help
287
+ // (Node's libuv pool is small). WITH --pretty, every tuple has 1-4
288
+ // prettier calls that ARE worth parallelizing — at 6 targets × N inputs,
289
+ // sequential awaiting would dominate. Promise.all both layers so an
290
+ // N-file × 6-target matrix gets the full benefit.
291
+ //
292
+ // Diagnostic-stderr ordering is preserved per-tuple but interleaves
293
+ // across tuples; that matches the existing compile-phase Promise.all
294
+ // shape and isn't a regression.
295
+ const writeResults = await Promise.all(
296
+ results.map(async ({ input, target, source, result }): Promise<boolean> => {
297
+ const errors = result.diagnostics.filter((d) => d.severity === 'error');
298
+ const warnings = result.diagnostics.filter((d) => d.severity === 'warning');
299
+
300
+ if (errors.length > 0) {
301
+ stderrWrite(renderAll(result.diagnostics, source));
302
+ return true; // failed
303
+ }
304
+ if (warnings.length > 0) {
305
+ stderrWrite(renderAll(warnings, source));
306
+ }
307
+
308
+ if (outDir === null) {
309
+ // Single-target single-input non-React case routed through stdout
310
+ // (preserves runBuild backward-compat for vue/svelte/angular).
311
+ // Derive the parser hint from the target ext so --pretty still
312
+ // picks the right parser even though no file is written.
313
+ const stdoutName = `stdout${TARGET_STDOUT_EXT[target]}`;
314
+ stdoutWrite(await maybePretty(result.code, stdoutName));
315
+ return false;
316
+ }
317
+
318
+ const outPath = computeOutputPath(input, target, outDir, rootDir);
319
+ mkdirSync(pathDirname(outPath), { recursive: true });
320
+
321
+ // Phase 22 Plan 22-05 — `.d.rozie.ts` sidecar (REQ-5/REQ-7). Emitted for
322
+ // ALL six targets via the same `renderSidecar` the unplugin uses, so a
323
+ // CLI build and a bundler build produce byte-identical sidecars (ONE
324
+ // convention for consumers). React ALSO keeps its existing `result.types`
325
+ // `.d.ts` below (backward-compat) — the `.d.rozie.ts` is the cross-target
326
+ // convention that `allowArbitraryExtensions` resolves for `./Foo.rozie`.
327
+ //
328
+ // REQ-7 (LOCKED GITIGNORED): `*.d.rozie.ts` is already matched by the
329
+ // existing `.gitignore:29 *.rozie.ts` rule — these are generated artefacts,
330
+ // ignore-not-commit. NO new gitignore rule is added.
331
+ const sidecarText = wantTypes ? renderSidecar(source, target, input) : null;
332
+ const sidecarPath =
333
+ sidecarText !== null
334
+ ? outPath.slice(0, outPath.length - TARGET_EXTENSIONS[target].length) + '.d.rozie.ts'
335
+ : null;
336
+
337
+ // Pre-compute sidecar paths + content so we can fire all prettier
338
+ // calls for this tuple in parallel.
339
+ const dtsPath =
340
+ wantTypes && target === 'react' && result.types
341
+ ? outPath.replace(/\.tsx$/, '.d.ts')
342
+ : null;
343
+ // Phase 25 de-CSS-Modules: React emits a side-effect `import './X.css'`
344
+ // (was `import styles from './X.module.css'`), so the scoped-CSS sibling
345
+ // is written to a PLAIN `.css` path — `[data-rozie-s-*]` attribute
346
+ // scoping is the sole isolation layer. (Matches babel-plugin writeSibling
347
+ // + unplugin `.rozie.css` routing.)
348
+ const modPath =
349
+ target === 'react' && result.css !== undefined && result.css.length > 0
350
+ ? outPath.replace(/\.tsx$/, '.css')
351
+ : null;
352
+ const globPath =
353
+ target === 'react' && result.globalCss !== undefined && result.globalCss.length > 0
354
+ ? outPath.replace(/\.tsx$/, '.global.css')
355
+ : null;
356
+
357
+ const [mainText, dtsText, modText, globText] = await Promise.all([
358
+ maybePretty(result.code, outPath),
359
+ dtsPath ? maybePretty(result.types ?? '', dtsPath) : Promise.resolve(null),
360
+ modPath ? maybePretty(result.css ?? '', modPath) : Promise.resolve(null),
361
+ globPath ? maybePretty(result.globalCss ?? '', globPath) : Promise.resolve(null),
362
+ ]);
363
+
364
+ // writeFileSync is sync — fine, libuv-bound work would queue here
365
+ // anyway. Keeping these in source order also keeps the disk-write
366
+ // failure mode predictable (main artefact lands before sidecars).
367
+ writeFileSync(outPath, mainText, 'utf8');
368
+ // Phase 22 Plan 22-05: `.d.rozie.ts` sidecar — written RAW (never
369
+ // prettied) so the bytes stay identical to the unplugin's emitSidecar
370
+ // output (the do-not-edit hash header must match for the staleness gate).
371
+ if (sidecarPath !== null && sidecarText !== null) {
372
+ writeFileSync(sidecarPath, sidecarText, 'utf8');
373
+ }
374
+ // D-90: React .d.ts sibling (other targets emit '' per D-84).
375
+ if (dtsPath !== null && dtsText !== null) writeFileSync(dtsPath, dtsText, 'utf8');
376
+ // D-53 / D-54: React .module.css / .global.css siblings.
377
+ if (modPath !== null && modText !== null) writeFileSync(modPath, modText, 'utf8');
378
+ if (globPath !== null && globText !== null) writeFileSync(globPath, globText, 'utf8');
379
+ // D-91: .map sibling. NEVER prettied — source-map spec requires the
380
+ // `mappings` VLQ field stay in a specific order that prettier-json
381
+ // would rearrange. prettyFormat() also skips .map defensively.
382
+ if (wantSourceMap && result.map) {
383
+ writeFileSync(`${outPath}.map`, result.map.toString(), 'utf8');
384
+ }
385
+ return false;
386
+ }),
387
+ );
388
+
389
+ const failed = writeResults.filter(Boolean).length;
390
+
391
+ if (failed > 0) {
392
+ const msg = pc.red(`rozie build: ${failed} of ${tuples.length} compilation(s) failed\n`);
393
+ stderrWrite(msg);
394
+ exit(1, msg);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Single-input single-target build — backward-compat thin wrapper around
400
+ * `runBuildMatrix`. Kept stable for the existing test suite + CLI tests
401
+ * predating Phase 6.
402
+ */
403
+ export async function runBuild(
404
+ input: string,
405
+ opts: BuildOptions = {},
406
+ ctx: RunBuildContext = {},
407
+ ): Promise<void> {
408
+ const target = (opts.target ?? 'vue') as Target;
409
+ const ext: BuildOptionsExt = {
410
+ target,
411
+ ...(opts.out !== undefined ? { out: opts.out } : {}),
412
+ ...(opts.sourceMap === true ? { sourceMap: true } : {}),
413
+ };
414
+ return runBuildMatrix([input], ext, ctx);
415
+ }
416
+
417
+ /**
418
+ * Multi-input single-target build — backward-compat thin wrapper around
419
+ * `runBuildMatrix`. The original semantics required `--out <dir>` for
420
+ * multi-input invocations; runBuildMatrix preserves that via the D-89 guard.
421
+ */
422
+ export async function runBuildMany(
423
+ inputs: string[],
424
+ opts: BuildOptions = {},
425
+ ctx: RunBuildContext = {},
426
+ ): Promise<void> {
427
+ const target = (opts.target ?? 'vue') as Target;
428
+ const ext: BuildOptionsExt = {
429
+ target,
430
+ ...(opts.out !== undefined ? { out: opts.out } : {}),
431
+ ...(opts.sourceMap === true ? { sourceMap: true } : {}),
432
+ };
433
+ return runBuildMatrix(inputs, ext, ctx);
434
+ }
435
+
436
+ function renderAll(diagnostics: Diagnostic[], source: string): string {
437
+ return diagnostics.map((d) => `${renderDiagnostic(d, source)}\n`).join('');
438
+ }