@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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bin.cjs +7 -0
- package/dist/bin.mjs +9 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +141 -0
- package/dist/index.d.mts +141 -0
- package/dist/index.mjs +2 -0
- package/dist/src-CIv3UOaa.cjs +55302 -0
- package/dist/src-WZKv4m5y.mjs +55192 -0
- package/package.json +85 -0
- package/src/bin.ts +14 -0
- package/src/commands/build.ts +438 -0
- package/src/commands/watch.ts +407 -0
- package/src/index.ts +187 -0
- package/src/utils/expandInputs.ts +98 -0
- package/src/utils/outputPath.ts +67 -0
- package/src/utils/parseTargets.ts +31 -0
- package/src/utils/prettyFormat.ts +138 -0
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
|
+
}
|