@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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// outputPath — D-89 output layout for `rozie build`.
|
|
2
|
+
//
|
|
3
|
+
// `dist/{target}/{source-rel-path}/Foo.{ext}` — per-target subdir + source-tree
|
|
4
|
+
// preservation. The source-rel-path is computed from `rootDir` (project root,
|
|
5
|
+
// default process.cwd()) so a flat per-target tree mirrors the source tree.
|
|
6
|
+
//
|
|
7
|
+
// Examples (rootDir='/repo', outDir='/repo/dist'):
|
|
8
|
+
// /repo/Counter.rozie → /repo/dist/{target}/Counter.{ext}
|
|
9
|
+
// /repo/src/components/forms/Input.rozie → /repo/dist/{target}/src/components/forms/Input.{ext}
|
|
10
|
+
//
|
|
11
|
+
// Phase 6 OQ4 RESOLVED: source-rel-path preservation is the canonical
|
|
12
|
+
// per-package npm convention for cross-framework component libraries.
|
|
13
|
+
import {
|
|
14
|
+
basename as pathBasename,
|
|
15
|
+
dirname as pathDirname,
|
|
16
|
+
join as pathJoin,
|
|
17
|
+
relative as pathRelative,
|
|
18
|
+
} from 'node:path';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Per-target file extension for the primary emitted artefact.
|
|
22
|
+
* Sidecars (`.d.ts`, `.module.css`, `.global.css`, `.map`) are derived
|
|
23
|
+
* from this in `runBuildMatrix`.
|
|
24
|
+
*/
|
|
25
|
+
export const TARGET_EXTENSIONS: Record<'vue' | 'react' | 'svelte' | 'angular' | 'solid' | 'lit', string> = {
|
|
26
|
+
vue: '.vue',
|
|
27
|
+
react: '.tsx',
|
|
28
|
+
svelte: '.svelte',
|
|
29
|
+
angular: '.ts',
|
|
30
|
+
solid: '.tsx',
|
|
31
|
+
lit: '.ts',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* D-89: compute the absolute output path for a given (input, target) tuple.
|
|
36
|
+
*
|
|
37
|
+
* Layout: `<outDir>/<target>/<source-rel-from-rootDir>/<basename>.<ext>`.
|
|
38
|
+
*
|
|
39
|
+
* Path-traversal safety: when the source file lives OUTSIDE `rootDir`
|
|
40
|
+
* (`pathRelative(rootDir, sourceDir)` starts with `..`), we strip the rel
|
|
41
|
+
* component and emit `<outDir>/<target>/<basename>.<ext>` instead. This
|
|
42
|
+
* prevents `--out` from being escaped via cleverly-located inputs.
|
|
43
|
+
*
|
|
44
|
+
* @param inputAbs absolute path to the source `.rozie` file
|
|
45
|
+
* @param target one of the four supported targets
|
|
46
|
+
* @param outDir absolute path to the output root
|
|
47
|
+
* @param rootDir absolute path to the project root (controls the rel-path)
|
|
48
|
+
* @returns absolute output path with target subdir + source-rel-path preserved
|
|
49
|
+
*/
|
|
50
|
+
export function computeOutputPath(
|
|
51
|
+
inputAbs: string,
|
|
52
|
+
target: 'vue' | 'react' | 'svelte' | 'angular' | 'solid' | 'lit',
|
|
53
|
+
outDir: string,
|
|
54
|
+
rootDir: string,
|
|
55
|
+
): string {
|
|
56
|
+
const sourceDir = pathDirname(inputAbs);
|
|
57
|
+
let sourceRel = pathRelative(rootDir, sourceDir);
|
|
58
|
+
// Defense-in-depth: if the source lives outside rootDir, refuse to thread
|
|
59
|
+
// the `..` traversal through pathJoin (it would escape outDir). Flatten to
|
|
60
|
+
// basename-only when the rel-path starts with `..`.
|
|
61
|
+
if (sourceRel.startsWith('..')) {
|
|
62
|
+
sourceRel = '';
|
|
63
|
+
}
|
|
64
|
+
const baseName = pathBasename(inputAbs, '.rozie');
|
|
65
|
+
const ext = TARGET_EXTENSIONS[target];
|
|
66
|
+
return pathJoin(outDir, target, sourceRel, baseName + ext);
|
|
67
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// parseTargets — D-87 commander parser for comma-separated `--target`.
|
|
2
|
+
//
|
|
3
|
+
// `--target react,vue` → ['react', 'vue']. Unknown tokens raise a commander
|
|
4
|
+
// `InvalidArgumentError` which becomes a clean exit 1 with a stderr message
|
|
5
|
+
// rather than a stack trace. The ROZ850 prefix lets the consumer grep CI logs
|
|
6
|
+
// for the stable diagnostic code.
|
|
7
|
+
import { InvalidArgumentError } from 'commander';
|
|
8
|
+
|
|
9
|
+
export type Target = 'vue' | 'react' | 'svelte' | 'angular' | 'solid' | 'lit';
|
|
10
|
+
|
|
11
|
+
export const VALID_TARGETS = new Set<Target>(['vue', 'react', 'svelte', 'angular', 'solid', 'lit']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse the `--target` flag value as a comma-separated list of valid targets.
|
|
15
|
+
* Whitespace around each token is trimmed.
|
|
16
|
+
*
|
|
17
|
+
* @throws InvalidArgumentError on any unknown token (commander surfaces this
|
|
18
|
+
* as exit 1 with stderr `error: option '-t, --target ...' argument
|
|
19
|
+
* '...' is invalid. [ROZ850] unknown target ...`)
|
|
20
|
+
*/
|
|
21
|
+
export function parseTargets(value: string): Target[] {
|
|
22
|
+
const tokens = value.split(',').map((t) => t.trim());
|
|
23
|
+
for (const t of tokens) {
|
|
24
|
+
if (!VALID_TARGETS.has(t as Target)) {
|
|
25
|
+
throw new InvalidArgumentError(
|
|
26
|
+
`[ROZ850] unknown target '${t}' (expected vue|react|svelte|angular|solid|lit)`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return tokens as Target[];
|
|
31
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// prettyFormat — opt-in `--pretty` formatting for emitted CLI artefacts.
|
|
2
|
+
//
|
|
3
|
+
// Used by `rozie build --pretty` and `rozie watch --pretty`. Off by
|
|
4
|
+
// default per PROJECT.md "Out of Scope" carve-out: "Output prettiness
|
|
5
|
+
// is a v2 concern … Available behind --pretty for CLI codegen."
|
|
6
|
+
//
|
|
7
|
+
// Parser-by-filename strategy: derive the right prettier parser from the
|
|
8
|
+
// output extension, NOT from the target name, so React .module.css /
|
|
9
|
+
// .global.css / .d.ts sidecars get formatted too.
|
|
10
|
+
//
|
|
11
|
+
// Plugin scope:
|
|
12
|
+
// • prettier core (built-in parsers): typescript (.tsx, .ts, .d.ts),
|
|
13
|
+
// vue (.vue), css (.css / .module.css / .global.css)
|
|
14
|
+
// • prettier-plugin-svelte (separate dep): svelte (.svelte)
|
|
15
|
+
// • Lit / Angular / Solid all emit .ts or .tsx — they ride built-in.
|
|
16
|
+
// • Source-map sidecars (.map) are skipped — they're JSON-shaped but
|
|
17
|
+
// must stay byte-stable for source-map consumers; reformatting would
|
|
18
|
+
// break the spec-required ordering of the `mappings` field.
|
|
19
|
+
//
|
|
20
|
+
// Failure mode: prettier errors are surfaced as warnings, not hard
|
|
21
|
+
// failures. The compile output is already correct; --pretty is purely
|
|
22
|
+
// cosmetic. Returning ok=false lets the caller emit the raw output and
|
|
23
|
+
// log a degradation warning, mirroring what tsc does on a comment-formatter
|
|
24
|
+
// crash — the build keeps going.
|
|
25
|
+
import { format as prettierFormat } from 'prettier';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Result of a single pretty-format attempt. `ok: false` carries the
|
|
29
|
+
* original (unformatted) source in `formatted` so callers can fall
|
|
30
|
+
* through to writing it unmodified.
|
|
31
|
+
*/
|
|
32
|
+
export interface PrettyResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
formatted: string;
|
|
35
|
+
/** Present only when ok=false — short diagnostic for the caller to log. */
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Map of file-extension → prettier parser name. Longer extensions take
|
|
41
|
+
* precedence (`.d.ts` before `.ts`, `.module.css` before `.css`) so the
|
|
42
|
+
* sidecar shapes route correctly. `null` marks an extension we
|
|
43
|
+
* intentionally never format.
|
|
44
|
+
*/
|
|
45
|
+
const PARSER_BY_EXT: ReadonlyArray<readonly [string, string | null]> = [
|
|
46
|
+
['.d.ts', 'typescript'],
|
|
47
|
+
['.module.css', 'css'],
|
|
48
|
+
['.global.css', 'css'],
|
|
49
|
+
['.tsx', 'typescript'],
|
|
50
|
+
['.svelte', 'svelte'],
|
|
51
|
+
['.vue', 'vue'],
|
|
52
|
+
['.css', 'css'],
|
|
53
|
+
['.ts', 'typescript'],
|
|
54
|
+
// Never format — source maps must preserve spec-exact field ordering.
|
|
55
|
+
['.map', null],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Cached prettier-plugin-svelte handle. Loaded lazily on first .svelte
|
|
60
|
+
* format request so a user who only builds React/Vue doesn't pay the
|
|
61
|
+
* import cost. Set to `false` if loading fails so subsequent attempts
|
|
62
|
+
* short-circuit instead of re-throwing.
|
|
63
|
+
*/
|
|
64
|
+
let sveltePluginCache: unknown | false | undefined;
|
|
65
|
+
|
|
66
|
+
async function loadSveltePlugin(): Promise<unknown | false> {
|
|
67
|
+
if (sveltePluginCache !== undefined) return sveltePluginCache;
|
|
68
|
+
try {
|
|
69
|
+
const mod = await import('prettier-plugin-svelte');
|
|
70
|
+
// Some plugin builds put the export on .default, some on the module
|
|
71
|
+
// namespace itself. Try both.
|
|
72
|
+
sveltePluginCache = (mod as { default?: unknown }).default ?? mod;
|
|
73
|
+
} catch {
|
|
74
|
+
sveltePluginCache = false;
|
|
75
|
+
}
|
|
76
|
+
return sveltePluginCache;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format `source` using the prettier parser matching `filename`'s
|
|
81
|
+
* extension. Never throws — failures return `{ ok: false, formatted:
|
|
82
|
+
* source, error }` so the caller can fall through to writing the raw
|
|
83
|
+
* (unformatted) emit and log a degradation warning.
|
|
84
|
+
*
|
|
85
|
+
* @param source the raw emit text
|
|
86
|
+
* @param filename the output filename (used only for parser detection)
|
|
87
|
+
*/
|
|
88
|
+
export async function prettyFormat(
|
|
89
|
+
source: string,
|
|
90
|
+
filename: string,
|
|
91
|
+
): Promise<PrettyResult> {
|
|
92
|
+
let parser: string | null = null;
|
|
93
|
+
for (const [ext, p] of PARSER_BY_EXT) {
|
|
94
|
+
if (filename.endsWith(ext)) {
|
|
95
|
+
parser = p;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (parser === null) {
|
|
101
|
+
// Either a never-format extension (.map) or an unknown one. Either
|
|
102
|
+
// way, return the source untouched without flagging an error — the
|
|
103
|
+
// caller wrote a real file, just not a pretty one.
|
|
104
|
+
return { ok: true, formatted: source };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const plugins: unknown[] = [];
|
|
108
|
+
if (parser === 'svelte') {
|
|
109
|
+
const plugin = await loadSveltePlugin();
|
|
110
|
+
if (plugin === false) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
formatted: source,
|
|
114
|
+
error:
|
|
115
|
+
'prettier-plugin-svelte is not installed (required for --pretty + .svelte output)',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
plugins.push(plugin);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// `as` cast: prettier's plugin type is intentionally loose (`Plugin`),
|
|
123
|
+
// and `unknown[]` here is the safest shape we can hand it without
|
|
124
|
+
// pulling in @types/prettier-plugin-svelte (which doesn't exist).
|
|
125
|
+
const formatted = await prettierFormat(source, {
|
|
126
|
+
parser,
|
|
127
|
+
// biome-ignore lint/suspicious/noExplicitAny: prettier plugin types are loose
|
|
128
|
+
plugins: plugins as any,
|
|
129
|
+
});
|
|
130
|
+
return { ok: true, formatted };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
formatted: source,
|
|
135
|
+
error: (err as Error).message,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|