@twobirds/microcomponents 0.2.5 → 0.4.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/README.md +91 -34
- package/dist/MC.min.js +1 -1
- package/dist/autoload.min.js +1 -1
- package/dist/cli/create.d.ts +53 -0
- package/dist/cli/create.d.ts.map +1 -0
- package/dist/cli/create.js +419 -0
- package/dist/cli/dispatch.d.ts +43 -0
- package/dist/cli/dispatch.d.ts.map +1 -0
- package/dist/cli/dispatch.js +251 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/scaffold.d.ts +68 -0
- package/dist/cli/scaffold.d.ts.map +1 -0
- package/dist/cli/scaffold.js +109 -0
- package/dist/cli/writeScaffold.d.ts +37 -0
- package/dist/cli/writeScaffold.d.ts.map +1 -0
- package/dist/cli/writeScaffold.js +60 -0
- package/dist/core/MC.d.ts +50 -9
- package/dist/core/MC.d.ts.map +1 -1
- package/dist/core/MC.js +78 -33
- package/dist/core/elements.d.ts +11 -8
- package/dist/core/elements.d.ts.map +1 -1
- package/dist/core/elements.js +12 -7
- package/dist/core/mcGuards.d.ts +2 -2
- package/dist/elements.min.js +1 -1
- package/dist/formValues.min.js +1 -1
- package/dist/microcomponents.min.js +1 -1
- package/dist/observables.min.js +1 -1
- package/dist/state/formValues.d.ts +11 -5
- package/dist/state/formValues.d.ts.map +1 -1
- package/dist/state/formValues.js +18 -11
- package/dist/state/observables.d.ts +30 -13
- package/dist/state/observables.d.ts.map +1 -1
- package/dist/state/observables.js +64 -35
- package/dist/state/placeholders.d.ts.map +1 -1
- package/dist/state/placeholders.js +16 -11
- package/dist/types/mcTypes.d.ts +16 -7
- package/dist/types/mcTypes.d.ts.map +1 -1
- package/dist/utils/autoload.d.ts +6 -3
- package/dist/utils/autoload.d.ts.map +1 -1
- package/dist/utils/autoload.js +2 -2
- package/dist/utils/events.d.ts +21 -28
- package/dist/utils/events.d.ts.map +1 -1
- package/dist/utils/events.js +23 -29
- package/dist/utils/helpers.d.ts +19 -19
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +19 -19
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -2
- package/dist/utils/resolveComponentPath.d.ts +36 -0
- package/dist/utils/resolveComponentPath.d.ts.map +1 -0
- package/dist/utils/resolveComponentPath.js +39 -0
- package/package.json +20 -13
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Command dispatcher for the `mc` CLI.
|
|
3
|
+
|
|
4
|
+
Kept free of any top-level side effects so the routing can be unit-tested:
|
|
5
|
+
`index.ts` is the thin executable entry that calls {@link main}, while this
|
|
6
|
+
module only exports the logic. Commands that wrap a package.json script
|
|
7
|
+
(`clean`, `check`) are deliberately thin — they delegate to the script so the
|
|
8
|
+
script stays the single source of truth. When the current project defines no
|
|
9
|
+
such script, the CLI runs its own implementation against the current project,
|
|
10
|
+
so the command works in any consumer project.
|
|
11
|
+
|
|
12
|
+
@module CLI/Dispatch
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { parseArgs } from 'node:util';
|
|
16
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { writeScaffold, writeProject } from './writeScaffold.js';
|
|
20
|
+
import { scaffoldComponent } from './scaffold.js';
|
|
21
|
+
import { scaffoldProject } from './create.js';
|
|
22
|
+
const USAGE = `mc — Micro Components build tools
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
mc new <tag> [--css] [--html] [--target-html] [--shadow-html] [--target-css] [--shadow-css] [--js] [--dir <path>] [--dry-run]
|
|
26
|
+
mc create --namespace <ns> [--dir <path>] [--dry-run]
|
|
27
|
+
mc clean
|
|
28
|
+
mc check
|
|
29
|
+
|
|
30
|
+
Commands:
|
|
31
|
+
new <tag> Scaffold a micro component (e.g. mc new my-card --target-html --target-css)
|
|
32
|
+
create Create a dev environment (e.g. mc create --namespace mc --dir mc-app)
|
|
33
|
+
clean Remove build artifacts (dist, public, build, coverage)
|
|
34
|
+
check Type-check every project (lib, cli, test, demo, node)
|
|
35
|
+
|
|
36
|
+
Companion assets:
|
|
37
|
+
--css document-level stylesheet (CSS custom properties / design tokens)
|
|
38
|
+
--html unvariant light-DOM template
|
|
39
|
+
--target-html template applied to target.innerHTML
|
|
40
|
+
--shadow-html template applied to shadowRoot.innerHTML
|
|
41
|
+
--target-css document-level styles scoped to the host tag
|
|
42
|
+
--shadow-css styles injected into the shadow root
|
|
43
|
+
|
|
44
|
+
Other options:
|
|
45
|
+
--js emit plain JavaScript instead of TypeScript
|
|
46
|
+
--dir <path> scaffold root (default: src)
|
|
47
|
+
--dry-run print what would be written; touch nothing on disk
|
|
48
|
+
`;
|
|
49
|
+
// Map CLI flags to the runtime's camelCase asset descriptors.
|
|
50
|
+
const ASSET_FLAGS = {
|
|
51
|
+
css: 'css',
|
|
52
|
+
html: 'html',
|
|
53
|
+
'target-html': 'targetHtml',
|
|
54
|
+
'shadow-html': 'shadowHtml',
|
|
55
|
+
'target-css': 'targetCss',
|
|
56
|
+
'shadow-css': 'shadowCss',
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
Spawns a command and resolves with its exit code.
|
|
60
|
+
|
|
61
|
+
@example
|
|
62
|
+
```ts
|
|
63
|
+
const code = await spawnProcess('pnpm', ['run', 'clean']);
|
|
64
|
+
```
|
|
65
|
+
@param command - Executable to spawn.
|
|
66
|
+
@param args - Arguments to pass.
|
|
67
|
+
@param cwd - Working directory (defaults to the current one).
|
|
68
|
+
*/
|
|
69
|
+
function spawnProcess(command, args, cwd) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const child = spawn(command, args, { stdio: 'inherit', cwd });
|
|
72
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
Whether the package.json in the current directory declares `script` in its
|
|
77
|
+
"scripts". Consumers get this check so `mc clean`/`mc check` can fall back to
|
|
78
|
+
the CLI's own implementation when the project defines no such script.
|
|
79
|
+
|
|
80
|
+
@example
|
|
81
|
+
```ts
|
|
82
|
+
const ok = await hasScript('clean');
|
|
83
|
+
```
|
|
84
|
+
@param script - Script name to look up.
|
|
85
|
+
*/
|
|
86
|
+
async function hasScript(script) {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(await readFile(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
89
|
+
return typeof pkg?.scripts?.[script] === 'string';
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Build artifacts to remove, relative to the project root. */
|
|
96
|
+
const CLEAN_DIRS = ['dist', 'public', 'build', 'coverage'];
|
|
97
|
+
/**
|
|
98
|
+
Removes build artifacts in the current project.
|
|
99
|
+
|
|
100
|
+
@example
|
|
101
|
+
```ts
|
|
102
|
+
const code = await cleanSelf();
|
|
103
|
+
```
|
|
104
|
+
@param cwd - Project root (defaults to the current directory).
|
|
105
|
+
*/
|
|
106
|
+
async function cleanSelf(cwd = process.cwd()) {
|
|
107
|
+
await Promise.all(CLEAN_DIRS.map(async (dir) => {
|
|
108
|
+
const abs = path.join(cwd, dir);
|
|
109
|
+
if (existsSync(abs)) {
|
|
110
|
+
await rm(abs, { recursive: true, force: true });
|
|
111
|
+
console.log(`removed ${dir}/`);
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
Type-checks the current project with its own `tsconfig.json`.
|
|
118
|
+
|
|
119
|
+
@example
|
|
120
|
+
```ts
|
|
121
|
+
const code = await checkSelf();
|
|
122
|
+
```
|
|
123
|
+
@param cwd - Project root (defaults to the current directory).
|
|
124
|
+
*/
|
|
125
|
+
async function checkSelf(cwd = process.cwd()) {
|
|
126
|
+
const config = path.join(cwd, 'tsconfig.json');
|
|
127
|
+
if (!existsSync(config)) {
|
|
128
|
+
console.error('mc check: no tsconfig.json in the current project');
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
return spawnProcess('pnpm', ['exec', 'tsc', '--noEmit', '-p', 'tsconfig.json'], cwd);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
Runs a package.json script through pnpm and resolves with its exit code.
|
|
135
|
+
|
|
136
|
+
Thin wrappers like `clean` and `check` delegate here so the script — not the
|
|
137
|
+
CLI — remains the single source of truth for what each command does. If the
|
|
138
|
+
current project defines no such script, the CLI runs its own implementation
|
|
139
|
+
against the current project, so the command works in any consumer project.
|
|
140
|
+
|
|
141
|
+
@example
|
|
142
|
+
```ts
|
|
143
|
+
const code = await runScript('check');
|
|
144
|
+
```
|
|
145
|
+
*/
|
|
146
|
+
export async function runScript(script) {
|
|
147
|
+
if (await hasScript(script)) {
|
|
148
|
+
return spawnProcess('pnpm', ['run', script]);
|
|
149
|
+
}
|
|
150
|
+
if (script === 'clean')
|
|
151
|
+
return cleanSelf();
|
|
152
|
+
if (script === 'check')
|
|
153
|
+
return checkSelf();
|
|
154
|
+
console.error(`mc: no "${script}" script in the current project`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
Parses `argv` and routes it to the matching command, returning a process exit
|
|
159
|
+
code. `run` is injectable so the script-wrapping commands can be tested without
|
|
160
|
+
spawning anything.
|
|
161
|
+
|
|
162
|
+
@example
|
|
163
|
+
```ts
|
|
164
|
+
const code = await main(['clean'], async (script) => {
|
|
165
|
+
console.log(`would run ${script}`);
|
|
166
|
+
return 0;
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
*/
|
|
170
|
+
export async function main(argv, run = runScript) {
|
|
171
|
+
const { values, positionals } = parseArgs({
|
|
172
|
+
args: argv,
|
|
173
|
+
allowPositionals: true,
|
|
174
|
+
options: {
|
|
175
|
+
css: { type: 'boolean' },
|
|
176
|
+
html: { type: 'boolean' },
|
|
177
|
+
'target-html': { type: 'boolean' },
|
|
178
|
+
'shadow-html': { type: 'boolean' },
|
|
179
|
+
'target-css': { type: 'boolean' },
|
|
180
|
+
'shadow-css': { type: 'boolean' },
|
|
181
|
+
js: { type: 'boolean' },
|
|
182
|
+
dir: { type: 'string' },
|
|
183
|
+
namespace: { type: 'string' },
|
|
184
|
+
'dry-run': { type: 'boolean' },
|
|
185
|
+
help: { type: 'boolean', short: 'h' },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const command = positionals[0];
|
|
189
|
+
if (values.help || !command) {
|
|
190
|
+
console.log(USAGE);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
if (command === 'new') {
|
|
194
|
+
const tag = positionals[1];
|
|
195
|
+
if (!tag) {
|
|
196
|
+
console.error('mc new: missing component tag (e.g. mc new my-card)');
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
const assets = [];
|
|
200
|
+
const flags = values;
|
|
201
|
+
for (const [flag, kind] of Object.entries(ASSET_FLAGS)) {
|
|
202
|
+
if (flags[flag])
|
|
203
|
+
assets.push(kind);
|
|
204
|
+
}
|
|
205
|
+
if (values['dry-run']) {
|
|
206
|
+
const { files } = scaffoldComponent(tag, { assets, js: values.js });
|
|
207
|
+
for (const file of Object.keys(files).sort())
|
|
208
|
+
console.log(`would create ${file}`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const written = await writeScaffold(tag, values.dir ?? 'src', {
|
|
212
|
+
assets,
|
|
213
|
+
js: values.js,
|
|
214
|
+
});
|
|
215
|
+
for (const file of written)
|
|
216
|
+
console.log(`created ${file}`);
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
if (command === 'create') {
|
|
220
|
+
const namespace = values.namespace;
|
|
221
|
+
if (!namespace) {
|
|
222
|
+
console.error('mc create: missing --namespace (e.g. mc create --namespace mc)');
|
|
223
|
+
return 1;
|
|
224
|
+
}
|
|
225
|
+
const root = values.dir ?? namespace;
|
|
226
|
+
let files;
|
|
227
|
+
try {
|
|
228
|
+
({ files } = scaffoldProject(namespace));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
console.error(e.message);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
if (values['dry-run']) {
|
|
235
|
+
for (const file of Object.keys(files).sort())
|
|
236
|
+
console.log(`would create ${file}`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
const written = await writeProject(namespace, root);
|
|
240
|
+
for (const file of written)
|
|
241
|
+
console.log(`created ${file}`);
|
|
242
|
+
console.log(`\nproject "${namespace}" created in ${root}/ — run: cd ${root} && pnpm install && pnpm dev`);
|
|
243
|
+
return 0;
|
|
244
|
+
}
|
|
245
|
+
if (command === 'clean')
|
|
246
|
+
return run('clean');
|
|
247
|
+
if (command === 'check')
|
|
248
|
+
return run('check');
|
|
249
|
+
console.error(`mc: unknown command "${command}"\n\n${USAGE}`);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
Executable entry point of the `mc` CLI (package.json "bin").
|
|
4
|
+
|
|
5
|
+
Deliberately thin: all parsing and routing lives in {@link main} so it can be
|
|
6
|
+
unit-tested without spawning a process. This module only feeds `process.argv`
|
|
7
|
+
in and pushes the resulting exit code out.
|
|
8
|
+
|
|
9
|
+
@module CLI/Index
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;EAQE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
Executable entry point of the `mc` CLI (package.json "bin").
|
|
4
|
+
|
|
5
|
+
Deliberately thin: all parsing and routing lives in {@link main} so it can be
|
|
6
|
+
unit-tested without spawning a process. This module only feeds `process.argv`
|
|
7
|
+
in and pushes the resulting exit code out.
|
|
8
|
+
|
|
9
|
+
@module CLI/Index
|
|
10
|
+
*/
|
|
11
|
+
import { main } from './dispatch.js';
|
|
12
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Scaffolding for new micro components.
|
|
3
|
+
|
|
4
|
+
Pure, filesystem-free logic: given a tag name it returns the files to write and
|
|
5
|
+
their contents. The tag-to-path rule is not re-derived here — it comes from
|
|
6
|
+
{@link tagPath}, the same primitive the runtime autoload loader uses, so the
|
|
7
|
+
scaffold and the runtime can never disagree about where a component lives.
|
|
8
|
+
|
|
9
|
+
Companion assets follow the runtime's variant convention (see `loadAssets` /
|
|
10
|
+
`processAssets`): a camelCase descriptor maps to a dotted filename, e.g.
|
|
11
|
+
`'targetHtml'` → `{path}.target.html`, `'shadowCss'` → `{path}.shadow.css`, and
|
|
12
|
+
the unvariant `'css'` → `{path}.css` (the document-level stylesheet where CSS
|
|
13
|
+
custom properties / design tokens live).
|
|
14
|
+
|
|
15
|
+
@module CLI/Scaffold
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
Companion asset kinds, mirroring the camelCase descriptors the runtime loads.
|
|
19
|
+
|
|
20
|
+
- `css` — document-level global stylesheet (CSS custom properties / design tokens).
|
|
21
|
+
- `targetCss` — document-level styles scoped to the host tag.
|
|
22
|
+
- `shadowCss` — styles injected into the shadow root.
|
|
23
|
+
- `html` — unvariant light-DOM template.
|
|
24
|
+
- `targetHtml` — light-DOM template applied to `target.innerHTML`.
|
|
25
|
+
- `shadowHtml` — template applied to `shadowRoot.innerHTML`.
|
|
26
|
+
*/
|
|
27
|
+
export type AssetKind = 'css' | 'targetCss' | 'shadowCss' | 'html' | 'targetHtml' | 'shadowHtml';
|
|
28
|
+
/** Options controlling which files a scaffold emits. */
|
|
29
|
+
export interface ScaffoldOptions {
|
|
30
|
+
/** Companion assets to emit and wire into `static assets`. */
|
|
31
|
+
assets?: AssetKind[];
|
|
32
|
+
/** Emit a plain `.js` file instead of `.ts` (rapid-prototyping path). */
|
|
33
|
+
js?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Result of {@link scaffoldComponent}. */
|
|
36
|
+
export interface ScaffoldResult {
|
|
37
|
+
/** Files to write, keyed by path relative to the scaffold root (e.g. `my/card.ts`). */
|
|
38
|
+
files: Record<string, string>;
|
|
39
|
+
/** The PascalCase class name derived from the tag. */
|
|
40
|
+
className: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
Whether a string is a valid micro component tag name.
|
|
44
|
+
|
|
45
|
+
@example
|
|
46
|
+
```ts
|
|
47
|
+
console.log(isValidComponentTag('my-card'));
|
|
48
|
+
console.log(isValidComponentTag('mycard'));
|
|
49
|
+
```
|
|
50
|
+
*/
|
|
51
|
+
export declare function isValidComponentTag(tag: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
Builds the files for a new micro component from its tag name.
|
|
54
|
+
|
|
55
|
+
@example
|
|
56
|
+
```ts
|
|
57
|
+
const { className, files } = scaffoldComponent('my-card', {
|
|
58
|
+
assets: ['targetHtml', 'targetCss'],
|
|
59
|
+
});
|
|
60
|
+
console.log(className);
|
|
61
|
+
console.log(Object.keys(files));
|
|
62
|
+
```
|
|
63
|
+
@param tag - Kebab-case component tag, e.g. `my-card`.
|
|
64
|
+
@param options - Which companion assets to emit and whether to use `.js`.
|
|
65
|
+
@throws If `tag` is not a valid custom element name.
|
|
66
|
+
*/
|
|
67
|
+
export declare function scaffoldComponent(tag: string, options?: ScaffoldOptions): ScaffoldResult;
|
|
68
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;EAeE;AAIF;;;;;;;;;EASE;AACF,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,WAAW,GAAG,WAAW,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEjG,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC/B,8DAA8D;IAC9D,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,yEAAyE;IACzE,EAAE,CAAC,EAAE,OAAO,CAAC;CACb;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC9B,uFAAuF;IACvF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CAClB;AAMD;;;;;;;;EAQE;AACF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;;EAcE;AACF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,cAAc,CAuB5F"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Scaffolding for new micro components.
|
|
3
|
+
|
|
4
|
+
Pure, filesystem-free logic: given a tag name it returns the files to write and
|
|
5
|
+
their contents. The tag-to-path rule is not re-derived here — it comes from
|
|
6
|
+
{@link tagPath}, the same primitive the runtime autoload loader uses, so the
|
|
7
|
+
scaffold and the runtime can never disagree about where a component lives.
|
|
8
|
+
|
|
9
|
+
Companion assets follow the runtime's variant convention (see `loadAssets` /
|
|
10
|
+
`processAssets`): a camelCase descriptor maps to a dotted filename, e.g.
|
|
11
|
+
`'targetHtml'` → `{path}.target.html`, `'shadowCss'` → `{path}.shadow.css`, and
|
|
12
|
+
the unvariant `'css'` → `{path}.css` (the document-level stylesheet where CSS
|
|
13
|
+
custom properties / design tokens live).
|
|
14
|
+
|
|
15
|
+
@module CLI/Scaffold
|
|
16
|
+
*/
|
|
17
|
+
import { tagPath } from '../utils/resolveComponentPath.js';
|
|
18
|
+
// Custom element names must be lowercase kebab-case and contain at least one
|
|
19
|
+
// hyphen (defineCE rejects hyphenless tags), with no empty segments.
|
|
20
|
+
const TAG_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/;
|
|
21
|
+
/**
|
|
22
|
+
Whether a string is a valid micro component tag name.
|
|
23
|
+
|
|
24
|
+
@example
|
|
25
|
+
```ts
|
|
26
|
+
console.log(isValidComponentTag('my-card'));
|
|
27
|
+
console.log(isValidComponentTag('mycard'));
|
|
28
|
+
```
|
|
29
|
+
*/
|
|
30
|
+
export function isValidComponentTag(tag) {
|
|
31
|
+
return TAG_RE.test(tag);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
Builds the files for a new micro component from its tag name.
|
|
35
|
+
|
|
36
|
+
@example
|
|
37
|
+
```ts
|
|
38
|
+
const { className, files } = scaffoldComponent('my-card', {
|
|
39
|
+
assets: ['targetHtml', 'targetCss'],
|
|
40
|
+
});
|
|
41
|
+
console.log(className);
|
|
42
|
+
console.log(Object.keys(files));
|
|
43
|
+
```
|
|
44
|
+
@param tag - Kebab-case component tag, e.g. `my-card`.
|
|
45
|
+
@param options - Which companion assets to emit and whether to use `.js`.
|
|
46
|
+
@throws If `tag` is not a valid custom element name.
|
|
47
|
+
*/
|
|
48
|
+
export function scaffoldComponent(tag, options = {}) {
|
|
49
|
+
if (!isValidComponentTag(tag)) {
|
|
50
|
+
throw new Error(`invalid component tag "${tag}": must be kebab-case with at least one hyphen (e.g. "my-card")`);
|
|
51
|
+
}
|
|
52
|
+
const className = tag
|
|
53
|
+
.split('-')
|
|
54
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
55
|
+
.join('');
|
|
56
|
+
const ext = options.js ? 'js' : 'ts';
|
|
57
|
+
const ctorParam = options.js ? 'target' : 'target: HTMLElement';
|
|
58
|
+
const base = tagPath(tag);
|
|
59
|
+
const assets = [...new Set(options.assets ?? [])];
|
|
60
|
+
const files = {};
|
|
61
|
+
files[`${base}.${ext}`] = componentSource(tag, ctorParam, assets);
|
|
62
|
+
for (const kind of assets) {
|
|
63
|
+
files[assetFile(tag, kind)] = assetSource(kind, tag);
|
|
64
|
+
}
|
|
65
|
+
return { files, className };
|
|
66
|
+
}
|
|
67
|
+
// Mirrors loadAssets' parseAsset: camelCase variant+ext → dotted filename,
|
|
68
|
+
// plain ext → single extension. Reuses tagPath so scaffold and runtime agree.
|
|
69
|
+
function assetFile(tag, kind) {
|
|
70
|
+
const m = kind.match(/^([a-z]+)([A-Z][a-z]*)$/);
|
|
71
|
+
const suffix = m ? `.${m[1]}.${m[2].toLowerCase()}` : `.${kind.toLowerCase()}`;
|
|
72
|
+
return `${tagPath(tag)}${suffix}`;
|
|
73
|
+
}
|
|
74
|
+
function componentSource(tag, ctorParam, assets) {
|
|
75
|
+
const lines = [
|
|
76
|
+
`import { DC, defineCE } from '@twobirds/microcomponents';`,
|
|
77
|
+
``,
|
|
78
|
+
`/**`,
|
|
79
|
+
`\`${tag}\` — describe what this micro component does.`,
|
|
80
|
+
``,
|
|
81
|
+
`@example`,
|
|
82
|
+
'```html',
|
|
83
|
+
`<${tag}></${tag}>`,
|
|
84
|
+
'```',
|
|
85
|
+
`*/`,
|
|
86
|
+
`defineCE(`,
|
|
87
|
+
`\t'${tag}',`,
|
|
88
|
+
`\tclass extends DC {`,
|
|
89
|
+
];
|
|
90
|
+
if (assets.length) {
|
|
91
|
+
lines.push(`\t\tstatic assets = [${assets.map((a) => `'${a}'`).join(', ')}];`);
|
|
92
|
+
}
|
|
93
|
+
lines.push(`\t\tconstructor(${ctorParam}) {`, `\t\t\tsuper(target);`, `\t\t}`, `\t}`, `);`, ``);
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
function assetSource(kind, tag) {
|
|
97
|
+
switch (kind) {
|
|
98
|
+
case 'css':
|
|
99
|
+
// Document-level global stylesheet — the home of CSS custom properties.
|
|
100
|
+
return [':root {', '\t/* design tokens (CSS custom properties) */', '}', ''].join('\n');
|
|
101
|
+
case 'targetCss':
|
|
102
|
+
return [`${tag} {`, '\tdisplay: block;', '}', ''].join('\n');
|
|
103
|
+
case 'shadowCss':
|
|
104
|
+
return [':host {', '\tdisplay: block;', '}', ''].join('\n');
|
|
105
|
+
default:
|
|
106
|
+
// html / targetHtml / shadowHtml
|
|
107
|
+
return '<slot></slot>\n';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Filesystem side of {@link scaffoldComponent}: writes the generated files under a
|
|
3
|
+
root directory, creating nested folders as needed.
|
|
4
|
+
|
|
5
|
+
@module CLI/Scaffold
|
|
6
|
+
*/
|
|
7
|
+
import { type ScaffoldOptions } from './scaffold.js';
|
|
8
|
+
/**
|
|
9
|
+
Scaffolds a component onto disk under `root`.
|
|
10
|
+
|
|
11
|
+
@example
|
|
12
|
+
```ts
|
|
13
|
+
const written = await writeScaffold('my-card', 'src', { assets: ['css'] });
|
|
14
|
+
console.log(written);
|
|
15
|
+
```
|
|
16
|
+
@param tag - Kebab-case component tag.
|
|
17
|
+
@param root - Directory to scaffold under (e.g. `src`).
|
|
18
|
+
@param options - Which companion files to emit.
|
|
19
|
+
@returns The written file paths, relative to `root`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeScaffold(tag: string, root: string, options?: ScaffoldOptions): Promise<string[]>;
|
|
22
|
+
/**
|
|
23
|
+
Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
|
|
24
|
+
namespace under a project root, creating nested folders as needed.
|
|
25
|
+
|
|
26
|
+
@example
|
|
27
|
+
```ts
|
|
28
|
+
const written = await writeProject('mc', 'mc-app');
|
|
29
|
+
console.log(written);
|
|
30
|
+
```
|
|
31
|
+
@param namespace - 1 to 4 lowercase letters.
|
|
32
|
+
@param root - Directory to create the project in (e.g. `mc-app`).
|
|
33
|
+
@returns The written file paths, relative to `root`.
|
|
34
|
+
@throws If `namespace` is not 1 to 4 lowercase letters.
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeProject(namespace: string, root: string): Promise<string[]>;
|
|
37
|
+
//# sourceMappingURL=writeScaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeScaffold.d.ts","sourceRoot":"","sources":["../../src/cli/writeScaffold.ts"],"names":[],"mappings":"AACA;;;;;EAKE;AAIF,OAAO,EAAqB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGxE;;;;;;;;;;;;EAYE;AACF,wBAAsB,aAAa,CAClC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAAC,MAAM,EAAE,CAAC,CAYnB;AAED;;;;;;;;;;;;;EAaE;AACF,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAYrF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
/**
|
|
3
|
+
Filesystem side of {@link scaffoldComponent}: writes the generated files under a
|
|
4
|
+
root directory, creating nested folders as needed.
|
|
5
|
+
|
|
6
|
+
@module CLI/Scaffold
|
|
7
|
+
*/
|
|
8
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { scaffoldComponent } from './scaffold.js';
|
|
11
|
+
import { scaffoldProject } from './create.js';
|
|
12
|
+
/**
|
|
13
|
+
Scaffolds a component onto disk under `root`.
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
```ts
|
|
17
|
+
const written = await writeScaffold('my-card', 'src', { assets: ['css'] });
|
|
18
|
+
console.log(written);
|
|
19
|
+
```
|
|
20
|
+
@param tag - Kebab-case component tag.
|
|
21
|
+
@param root - Directory to scaffold under (e.g. `src`).
|
|
22
|
+
@param options - Which companion files to emit.
|
|
23
|
+
@returns The written file paths, relative to `root`.
|
|
24
|
+
*/
|
|
25
|
+
export async function writeScaffold(tag, root, options = {}) {
|
|
26
|
+
const { files } = scaffoldComponent(tag, options);
|
|
27
|
+
const written = [];
|
|
28
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
29
|
+
const abs = path.join(root, rel);
|
|
30
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
31
|
+
await writeFile(abs, contents, 'utf8');
|
|
32
|
+
written.push(rel);
|
|
33
|
+
}
|
|
34
|
+
return written;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
Filesystem side of {@link scaffoldProject}: writes a complete dev environment for a
|
|
38
|
+
namespace under a project root, creating nested folders as needed.
|
|
39
|
+
|
|
40
|
+
@example
|
|
41
|
+
```ts
|
|
42
|
+
const written = await writeProject('mc', 'mc-app');
|
|
43
|
+
console.log(written);
|
|
44
|
+
```
|
|
45
|
+
@param namespace - 1 to 4 lowercase letters.
|
|
46
|
+
@param root - Directory to create the project in (e.g. `mc-app`).
|
|
47
|
+
@returns The written file paths, relative to `root`.
|
|
48
|
+
@throws If `namespace` is not 1 to 4 lowercase letters.
|
|
49
|
+
*/
|
|
50
|
+
export async function writeProject(namespace, root) {
|
|
51
|
+
const { files } = scaffoldProject(namespace);
|
|
52
|
+
const written = [];
|
|
53
|
+
for (const [rel, contents] of Object.entries(files)) {
|
|
54
|
+
const abs = path.join(root, rel);
|
|
55
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
56
|
+
await writeFile(abs, contents, 'utf8');
|
|
57
|
+
written.push(rel);
|
|
58
|
+
}
|
|
59
|
+
return written;
|
|
60
|
+
}
|