@twobirds/microcomponents 0.9.0 → 0.10.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 +2 -16
- package/dist/cli/bundle.d.ts +106 -0
- package/dist/cli/bundle.d.ts.map +1 -0
- package/dist/cli/bundler.d.ts +44 -0
- package/dist/cli/bundler.d.ts.map +1 -0
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/dev.d.ts +87 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dispatch.d.ts +6 -2
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/index.js +1807 -1
- package/dist/cli/inliner.d.ts +42 -0
- package/dist/cli/inliner.d.ts.map +1 -0
- package/dist/cli/preview.d.ts +48 -0
- package/dist/cli/preview.d.ts.map +1 -0
- package/dist/core/elements.js +1294 -75
- package/dist/core/mcGuards.js +5 -8
- package/dist/mcp/api.json +2 -17
- package/dist/mcp/concepts/project-layout.md +9 -8
- package/dist/mcp/index.js +450 -230
- package/dist/microcomponents.js +2882 -9
- package/dist/state/formValues.js +2236 -281
- package/dist/state/observables.js +1794 -342
- package/dist/utils/events.js +171 -197
- package/package.json +3 -7
- package/dist/cli/create.js +0 -653
- package/dist/cli/dispatch.js +0 -316
- package/dist/cli/scaffold.js +0 -109
- package/dist/cli/writeScaffold.js +0 -114
- package/dist/core/MC.js +0 -837
- package/dist/core/index.js +0 -72
- package/dist/state/index.js +0 -5
- package/dist/state/placeholders.js +0 -187
- package/dist/types/index.js +0 -1
- package/dist/types/mcTypes.js +0 -9
- package/dist/utils/autoload.js +0 -160
- package/dist/utils/helpers.js +0 -532
- package/dist/utils/index.js +0 -12
- package/dist/utils/resolveComponentPath.js +0 -39
- package/dist/vite/index.d.ts +0 -7
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/index.js +0 -6
- package/dist/vite/loadAssets.d.ts +0 -36
- package/dist/vite/loadAssets.d.ts.map +0 -1
- package/dist/vite/loadAssets.js +0 -221
package/dist/cli/create.js
DELETED
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
Project scaffolding for `mc create --namespace xxx`.
|
|
3
|
-
|
|
4
|
-
Pure, filesystem-free logic: given a namespace it returns the files of a complete,
|
|
5
|
-
working Micro Components dev environment and their contents. The starter component's
|
|
6
|
-
path is not re-derived here — it comes from {@link tagPath}, the same primitive the
|
|
7
|
-
runtime autoload loader uses, so the scaffold and the runtime can never disagree about
|
|
8
|
-
where a component lives.
|
|
9
|
-
|
|
10
|
-
The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
|
|
11
|
-
the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
|
|
12
|
-
environment follows the three-path chain the library's own demos use: `src/` holds the
|
|
13
|
-
authored sources, `build/` receives the per-file `tsc` output that the dev server
|
|
14
|
-
serves through `autoload()`, and `public/` receives the bundled production site. Because
|
|
15
|
-
the dev shell is the autoload base, `import.meta.url` + `tagPath('xxx-app')` resolves to
|
|
16
|
-
`build/xxx/app.js` — the compiled starter.
|
|
17
|
-
|
|
18
|
-
@module CLI/Project
|
|
19
|
-
*/
|
|
20
|
-
import { tagPath } from '../utils/resolveComponentPath.js';
|
|
21
|
-
import { kebabToPascal } from '../utils/helpers.js';
|
|
22
|
-
/** The starter component tag for a namespace: `xxx` → `xxx-app`. */
|
|
23
|
-
function starterTag(namespace) {
|
|
24
|
-
return `${namespace}-app`;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
Whether a string is a valid project namespace: 1 to 4 lowercase letters.
|
|
28
|
-
|
|
29
|
-
@example
|
|
30
|
-
```ts
|
|
31
|
-
console.log(isValidNamespace('mc'));
|
|
32
|
-
console.log(isValidNamespace('mcxyz'));
|
|
33
|
-
console.log(isValidNamespace('toolong'));
|
|
34
|
-
```
|
|
35
|
-
@param ns - Candidate namespace.
|
|
36
|
-
*/
|
|
37
|
-
export function isValidNamespace(ns) {
|
|
38
|
-
return /^[a-z]{1,4}$/.test(ns);
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
Builds the files of a complete dev environment for a namespace.
|
|
42
|
-
|
|
43
|
-
@example
|
|
44
|
-
```ts
|
|
45
|
-
const { namespace, files } = scaffoldProject('mc');
|
|
46
|
-
console.log(namespace);
|
|
47
|
-
console.log(Object.keys(files));
|
|
48
|
-
```
|
|
49
|
-
@param namespace - 1 to 4 lowercase letters; becomes the `src/<namespace>/` folder and
|
|
50
|
-
the first segment of every component tag.
|
|
51
|
-
@throws If `namespace` is not 1 to 4 lowercase letters.
|
|
52
|
-
*/
|
|
53
|
-
export function scaffoldProject(namespace) {
|
|
54
|
-
if (!isValidNamespace(namespace)) {
|
|
55
|
-
throw new Error(`invalid namespace "${namespace}": must be 1 to 4 lowercase letters (e.g. "mc")`);
|
|
56
|
-
}
|
|
57
|
-
const tag = starterTag(namespace);
|
|
58
|
-
const base = tagPath(tag);
|
|
59
|
-
const files = {};
|
|
60
|
-
files['package.json'] = packageJson(namespace);
|
|
61
|
-
files['tsconfig.json'] = tsconfig();
|
|
62
|
-
files['tsconfig.vc.json'] = tsconfigVc();
|
|
63
|
-
files['vite.config.ts'] = viteConfig(namespace);
|
|
64
|
-
files['.gitignore'] = gitignore();
|
|
65
|
-
files['opencode.jsonc'] = opencodeConfig();
|
|
66
|
-
files['.opencode/skills/components/SKILL.md'] = componentSkill(namespace, tag);
|
|
67
|
-
files['README.md'] = readme(namespace, tag);
|
|
68
|
-
files['src/index.html'] = prodShell(tag);
|
|
69
|
-
files['src/index-dev.html'] = devShell(namespace, tag);
|
|
70
|
-
files[`src/${base}.ts`] = appSource(namespace, tag);
|
|
71
|
-
files[`src/${base}.target.html`] = appHtml(namespace);
|
|
72
|
-
files[`src/${base}.target.css`] = appCss(tag);
|
|
73
|
-
files[`src/${namespace}/bundle.ts`] = bundle();
|
|
74
|
-
return { files, namespace };
|
|
75
|
-
}
|
|
76
|
-
function packageJson(namespace) {
|
|
77
|
-
return JSON.stringify({
|
|
78
|
-
name: `${namespace}-app`,
|
|
79
|
-
private: true,
|
|
80
|
-
type: 'module',
|
|
81
|
-
scripts: {
|
|
82
|
-
dev: 'vite',
|
|
83
|
-
build: 'vite build',
|
|
84
|
-
preview: 'vite preview --outDir public --port 5174',
|
|
85
|
-
typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
|
|
86
|
-
},
|
|
87
|
-
dependencies: {
|
|
88
|
-
'@twobirds/microcomponents': '^0.9.0',
|
|
89
|
-
},
|
|
90
|
-
devDependencies: {
|
|
91
|
-
vite: '^8.1.5',
|
|
92
|
-
typescript: '^6.0.3',
|
|
93
|
-
'@types/node': '^26.6.5',
|
|
94
|
-
},
|
|
95
|
-
}, null, '\t');
|
|
96
|
-
}
|
|
97
|
-
function tsconfig() {
|
|
98
|
-
return JSON.stringify({
|
|
99
|
-
compilerOptions: {
|
|
100
|
-
target: 'ESNext',
|
|
101
|
-
module: 'ESNext',
|
|
102
|
-
moduleResolution: 'Bundler',
|
|
103
|
-
lib: ['ESNext', 'DOM', 'DOM.Iterable'],
|
|
104
|
-
strict: true,
|
|
105
|
-
esModuleInterop: true,
|
|
106
|
-
skipLibCheck: true,
|
|
107
|
-
rootDir: 'src',
|
|
108
|
-
outDir: 'build',
|
|
109
|
-
removeComments: true,
|
|
110
|
-
},
|
|
111
|
-
include: ['src/**/*.ts'],
|
|
112
|
-
}, null, '\t');
|
|
113
|
-
}
|
|
114
|
-
function tsconfigVc() {
|
|
115
|
-
return JSON.stringify({
|
|
116
|
-
compilerOptions: {
|
|
117
|
-
target: 'ESNext',
|
|
118
|
-
module: 'ESNext',
|
|
119
|
-
moduleResolution: 'Bundler',
|
|
120
|
-
lib: ['ESNext', 'DOM'],
|
|
121
|
-
types: ['node'],
|
|
122
|
-
strict: true,
|
|
123
|
-
esModuleInterop: true,
|
|
124
|
-
skipLibCheck: true,
|
|
125
|
-
noEmit: true,
|
|
126
|
-
},
|
|
127
|
-
files: ['vite.config.ts'],
|
|
128
|
-
}, null, '\t');
|
|
129
|
-
}
|
|
130
|
-
function gitignore() {
|
|
131
|
-
return `node_modules/
|
|
132
|
-
build/
|
|
133
|
-
public/
|
|
134
|
-
`;
|
|
135
|
-
}
|
|
136
|
-
function opencodeConfig() {
|
|
137
|
-
return JSON.stringify({
|
|
138
|
-
$schema: 'https://opencode.ai/config.json',
|
|
139
|
-
mcp: {
|
|
140
|
-
microcomponents: {
|
|
141
|
-
type: 'local',
|
|
142
|
-
command: ['npx', '-p', '@twobirds/microcomponents', 'microcomponents-mcp'],
|
|
143
|
-
enabled: true,
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
}, null, '\t');
|
|
147
|
-
}
|
|
148
|
-
function componentSkill(namespace, tag) {
|
|
149
|
-
return `---
|
|
150
|
-
name: components
|
|
151
|
-
description: Use when adding, editing, naming, or wiring a micro component in this project, registering a component in the bundle, choosing an event dispatch target, running dev or build, or touching build/ or public/.
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
# Micro Components project
|
|
155
|
-
|
|
156
|
-
Namespace \`${namespace}\`. Every tag starts with \`${namespace}-\`. Components live in \`src/${namespace}/\` and the starter is \`<${tag}>\`.
|
|
157
|
-
|
|
158
|
-
## The chain
|
|
159
|
-
|
|
160
|
-
\`src/\` → \`build/\` → \`public/\`
|
|
161
|
-
|
|
162
|
-
- \`src/\` is the only folder to edit.
|
|
163
|
-
- \`build/\` is the \`tsc\` output plus mirrored companion assets, served by the dev server. Generated.
|
|
164
|
-
- \`public/\` is the production site: \`bundle.js\` with companion assets inlined, \`bundle.css\`, \`index.html\`. Generated.
|
|
165
|
-
|
|
166
|
-
Never edit, rename, or delete anything under \`build/\` or \`public/\`. Change \`src/\` and rebuild.
|
|
167
|
-
|
|
168
|
-
## Add a component
|
|
169
|
-
|
|
170
|
-
\`\`\`sh
|
|
171
|
-
npx mc new ${namespace}-name --target-html --target-css
|
|
172
|
-
\`\`\`
|
|
173
|
-
|
|
174
|
-
The tag must be kebab-case with at least one hyphen. \`ns-name\` becomes \`src/ns/name.ts\`: the first hyphen becomes the folder, which is the same rule the autoloader uses.
|
|
175
|
-
|
|
176
|
-
Register the component so the production bundle picks it up:
|
|
177
|
-
|
|
178
|
-
\`\`\`ts
|
|
179
|
-
// src/${namespace}/bundle.ts
|
|
180
|
-
import './name.js';
|
|
181
|
-
\`\`\`
|
|
182
|
-
|
|
183
|
-
A component missing from the barrel loads in dev through \`autoload()\` but is absent from \`public/bundle.js\`.
|
|
184
|
-
|
|
185
|
-
## Companion assets
|
|
186
|
-
|
|
187
|
-
A camelCase descriptor maps to a dotted filename and is listed in \`static assets\`:
|
|
188
|
-
|
|
189
|
-
| descriptor | file | role |
|
|
190
|
-
| --- | --- | --- |
|
|
191
|
-
| \`css\` | \`name.css\` | document-level tokens under \`:root\` |
|
|
192
|
-
| \`targetCss\` | \`name.target.css\` | styles scoped to the host tag |
|
|
193
|
-
| \`shadowCss\` | \`name.shadow.css\` | styles inside the shadow root |
|
|
194
|
-
| \`html\` | \`name.html\` | light-DOM template |
|
|
195
|
-
| \`targetHtml\` | \`name.target.html\` | template for \`target.innerHTML\`, \`{key}\` tokens filled by \`parse()\` |
|
|
196
|
-
| \`shadowHtml\` | \`name.shadow.html\` | template for \`shadowRoot.innerHTML\` |
|
|
197
|
-
|
|
198
|
-
The loader fetches exactly the listed descriptors, so every entry in \`static assets\` needs its file.
|
|
199
|
-
|
|
200
|
-
## Events
|
|
201
|
-
|
|
202
|
-
Prefer a direct target with the default local dispatch and a typed payload:
|
|
203
|
-
|
|
204
|
-
\`\`\`ts
|
|
205
|
-
target.dispatchEvent(
|
|
206
|
-
new CustomEvent('${namespace}:action', { detail: { data: { id: 'x' } } })
|
|
207
|
-
);
|
|
208
|
-
\`\`\`
|
|
209
|
-
|
|
210
|
-
Bubble up or down the tree only when the receiver cannot be named.
|
|
211
|
-
|
|
212
|
-
## Verify
|
|
213
|
-
|
|
214
|
-
\`\`\`sh
|
|
215
|
-
pnpm typecheck
|
|
216
|
-
pnpm dev
|
|
217
|
-
pnpm build
|
|
218
|
-
pnpm preview
|
|
219
|
-
\`\`\`
|
|
220
|
-
|
|
221
|
-
\`mc clean\` removes \`build/\`, \`public/\` and \`coverage/\`.
|
|
222
|
-
|
|
223
|
-
## API reference
|
|
224
|
-
|
|
225
|
-
The \`microcomponents\` MCP server registered in \`opencode.jsonc\` documents every export, the event system, and the concept essays. Query it instead of guessing an API.
|
|
226
|
-
`;
|
|
227
|
-
}
|
|
228
|
-
function readme(namespace, tag) {
|
|
229
|
-
return `# ${namespace} App
|
|
230
|
-
|
|
231
|
-
A Micro Components project scaffolded by \`mc create --namespace ${namespace}\`.
|
|
232
|
-
|
|
233
|
-
## Install
|
|
234
|
-
|
|
235
|
-
\`\`\`sh
|
|
236
|
-
pnpm install
|
|
237
|
-
\`\`\`
|
|
238
|
-
|
|
239
|
-
## Dev
|
|
240
|
-
|
|
241
|
-
\`\`\`sh
|
|
242
|
-
pnpm dev
|
|
243
|
-
\`\`\`
|
|
244
|
-
|
|
245
|
-
The dev server runs \`tsc -w\` and serves \`build/\`, so a saved component reaches the
|
|
246
|
-
browser through \`autoload()\` with no bundling. Companion \`.css\`, \`.html\` and \`.json\`
|
|
247
|
-
files are mirrored into \`build/\` next to the compiled component, which is where the
|
|
248
|
-
autoloader looks for them.
|
|
249
|
-
|
|
250
|
-
## Build
|
|
251
|
-
|
|
252
|
-
\`\`\`sh
|
|
253
|
-
pnpm build
|
|
254
|
-
\`\`\`
|
|
255
|
-
|
|
256
|
-
\`public/\` is the production site: one minified \`bundle.js\` with every companion asset
|
|
257
|
-
inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the page shell. A
|
|
258
|
-
single script tag loads it, no autoloader needed.
|
|
259
|
-
|
|
260
|
-
\`\`\`sh
|
|
261
|
-
pnpm preview
|
|
262
|
-
\`\`\`
|
|
263
|
-
|
|
264
|
-
## Type-check
|
|
265
|
-
|
|
266
|
-
\`\`\`sh
|
|
267
|
-
pnpm typecheck
|
|
268
|
-
\`\`\`
|
|
269
|
-
|
|
270
|
-
\`tsconfig.json\` compiles \`src/\` into \`build/\`; \`tsconfig.vc.json\` checks
|
|
271
|
-
\`vite.config.ts\`, which lives outside \`src/\` and needs node types for its \`node:\`
|
|
272
|
-
imports.
|
|
273
|
-
|
|
274
|
-
## Components
|
|
275
|
-
|
|
276
|
-
Components live in \`src/${namespace}/\`. The starter is \`<${tag}>\` (\`src/${namespace}/app.ts\`).
|
|
277
|
-
|
|
278
|
-
Add a component with:
|
|
279
|
-
|
|
280
|
-
\`\`\`sh
|
|
281
|
-
npx mc new ${namespace}-name --target-html --target-css
|
|
282
|
-
\`\`\`
|
|
283
|
-
|
|
284
|
-
Then add its import to \`src/${namespace}/bundle.ts\` so the production bundle picks it up.
|
|
285
|
-
|
|
286
|
-
\`.opencode/skills/components/SKILL.md\` carries the same rules for opencode: the
|
|
287
|
-
\`src/\` → \`build/\` → \`public/\` chain, the asset descriptors, the bundle registration,
|
|
288
|
-
and the event dispatch preference.
|
|
289
|
-
`;
|
|
290
|
-
}
|
|
291
|
-
function viteConfig(namespace) {
|
|
292
|
-
return `/**
|
|
293
|
-
* Dev and build config for a Micro Components project.
|
|
294
|
-
*
|
|
295
|
-
* The chain is \`src/\` → \`build/\` → \`public/\`:
|
|
296
|
-
*
|
|
297
|
-
* - \`build/\` is the \`tsc\` output: one \`.js\` per component at its own autoload path,
|
|
298
|
-
* plus the copied companion assets and the dev shell. The dev server serves it, so
|
|
299
|
-
* the page loads components on demand through \`autoload()\` and nothing is bundled.
|
|
300
|
-
* - \`public/\` is the production output: one minified \`bundle.js\` with every companion
|
|
301
|
-
* asset inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the
|
|
302
|
-
* production shell. A single script tag loads it, no autoloader needed.
|
|
303
|
-
*
|
|
304
|
-
* @module ViteConfig
|
|
305
|
-
*/
|
|
306
|
-
|
|
307
|
-
import { defineConfig, type Plugin, type ResolvedConfig } from 'vite';
|
|
308
|
-
import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
|
|
309
|
-
import { spawn } from 'node:child_process';
|
|
310
|
-
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
311
|
-
import path from 'node:path';
|
|
312
|
-
|
|
313
|
-
const projectRoot = import.meta.dirname;
|
|
314
|
-
const sourceRoot = path.join(projectRoot, 'src');
|
|
315
|
-
const buildRoot = path.join(projectRoot, 'build');
|
|
316
|
-
const publicRoot = path.join(projectRoot, 'public');
|
|
317
|
-
const namespace = '${namespace}';
|
|
318
|
-
|
|
319
|
-
const pageShells = new Set(['index.html', 'index-dev.html']);
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Whether a served file is a companion asset that must stay raw text, rather
|
|
323
|
-
* than one of the page shells Vite is allowed to transform.
|
|
324
|
-
*
|
|
325
|
-
* @example
|
|
326
|
-
* \`\`\`ts
|
|
327
|
-
* isCompanionAsset('app.target.html');
|
|
328
|
-
* isCompanionAsset('index.html');
|
|
329
|
-
* \`\`\`
|
|
330
|
-
*
|
|
331
|
-
* @param name Basename of the requested file.
|
|
332
|
-
*/
|
|
333
|
-
function isCompanionAsset(name: string): boolean {
|
|
334
|
-
if (pageShells.has(name)) return false;
|
|
335
|
-
return name.endsWith('.css') || name.endsWith('.json') || name.endsWith('.html');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Mirrors companion assets and the dev shell from \`src/\` into the served folder,
|
|
340
|
-
* preserving the tree. Autoload resolves \`<ns-app>\` to \`<ns>/app.js\` relative to the
|
|
341
|
-
* page shell, so the served folder needs the compiled component *and* its companion
|
|
342
|
-
* files at the same relative path.
|
|
343
|
-
*
|
|
344
|
-
* @example
|
|
345
|
-
* \`\`\`ts
|
|
346
|
-
* await mirrorAssets(sourceRoot, buildRoot);
|
|
347
|
-
* \`\`\`
|
|
348
|
-
*
|
|
349
|
-
* @param root Source root to walk.
|
|
350
|
-
* @param out Folder to write into.
|
|
351
|
-
* @param rel Path of the walked directory relative to \`root\`.
|
|
352
|
-
*/
|
|
353
|
-
async function mirrorAssets(root: string, out: string, rel = ''): Promise<void> {
|
|
354
|
-
for (const entry of await readdir(path.join(root, rel), { withFileTypes: true })) {
|
|
355
|
-
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
356
|
-
if (entry.isDirectory()) {
|
|
357
|
-
await mirrorAssets(root, out, next);
|
|
358
|
-
} else if (isCompanionAsset(entry.name) || entry.name === 'index-dev.html') {
|
|
359
|
-
const dst = path.join(out, next);
|
|
360
|
-
await mkdir(path.dirname(dst), { recursive: true });
|
|
361
|
-
await cp(path.join(root, next), dst);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Runs \`tsc -w\` for the lifetime of the dev server, so a saved component reaches
|
|
368
|
-
* \`build/\` without a second command.
|
|
369
|
-
*
|
|
370
|
-
* @example
|
|
371
|
-
* \`\`\`ts
|
|
372
|
-
* plugins: [tscWatch()],
|
|
373
|
-
* \`\`\`
|
|
374
|
-
*/
|
|
375
|
-
function tscWatch(): Plugin {
|
|
376
|
-
let child: ReturnType<typeof spawn> | undefined;
|
|
377
|
-
return {
|
|
378
|
-
name: 'tsc-watch',
|
|
379
|
-
configResolved() {
|
|
380
|
-
child = spawn('npx', ['tsc', '-w', '-p', 'tsconfig.json'], { cwd: projectRoot, stdio: 'inherit' });
|
|
381
|
-
},
|
|
382
|
-
closeServer() {
|
|
383
|
-
child?.kill();
|
|
384
|
-
},
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Mirrors companion assets into \`build/\` before the dev server starts, and again on
|
|
390
|
-
* every change, so the served folder stays autoload-ready.
|
|
391
|
-
*
|
|
392
|
-
* @example
|
|
393
|
-
* \`\`\`ts
|
|
394
|
-
* plugins: [syncBuild()],
|
|
395
|
-
* \`\`\`
|
|
396
|
-
*/
|
|
397
|
-
function syncBuild(): Plugin {
|
|
398
|
-
return {
|
|
399
|
-
name: 'sync-build',
|
|
400
|
-
enforce: 'pre',
|
|
401
|
-
async configureServer(server) {
|
|
402
|
-
await mirrorAssets(sourceRoot, buildRoot);
|
|
403
|
-
server.watcher.on('all', async (event, file) => {
|
|
404
|
-
if (!file.startsWith(sourceRoot) || file.endsWith('.ts')) return;
|
|
405
|
-
const rel = path.relative(sourceRoot, file);
|
|
406
|
-
if (isCompanionAsset(path.basename(file)) || file.endsWith('index-dev.html')) {
|
|
407
|
-
const dst = path.join(buildRoot, rel);
|
|
408
|
-
await mkdir(path.dirname(dst), { recursive: true });
|
|
409
|
-
await cp(file, dst);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
},
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Serves companion \`.css\`, \`.html\` and \`.json\` files as raw text, ahead of the
|
|
418
|
-
* Vite transform middleware. Without it a \`.html\` request returns the transformed
|
|
419
|
-
* page shell and a \`.css\` request returns a JS module, so \`loadAssets()\` receives
|
|
420
|
-
* markup it cannot use.
|
|
421
|
-
*
|
|
422
|
-
* @example
|
|
423
|
-
* \`\`\`ts
|
|
424
|
-
* plugins: [rawAssets(buildRoot)],
|
|
425
|
-
* \`\`\`
|
|
426
|
-
*
|
|
427
|
-
* @param root Source root that mirrors the served tree.
|
|
428
|
-
*/
|
|
429
|
-
function rawAssets(root: string): Plugin {
|
|
430
|
-
return {
|
|
431
|
-
name: 'raw-assets',
|
|
432
|
-
enforce: 'pre',
|
|
433
|
-
configureServer(server) {
|
|
434
|
-
server.middlewares.use(async (req, res, next) => {
|
|
435
|
-
const url = req.url?.split('?')[0];
|
|
436
|
-
if (!url) return next();
|
|
437
|
-
const file = decodeURIComponent(url);
|
|
438
|
-
const name = file.split('/').pop() ?? '';
|
|
439
|
-
if (!isCompanionAsset(name)) return next();
|
|
440
|
-
try {
|
|
441
|
-
const text = await readFile(path.join(root, file), 'utf-8');
|
|
442
|
-
res.setHeader('Content-Type', name.endsWith('.css') ? 'text/css' : 'text/plain');
|
|
443
|
-
res.end(text);
|
|
444
|
-
} catch {
|
|
445
|
-
next();
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Serves the autoload shell (\`index-dev.html\`) at \`/\`. Vite answers \`/\` with
|
|
454
|
-
* \`index.html\`, which is the production shell referencing \`bundle.js\` — that file
|
|
455
|
-
* only exists in \`public/\`, so the dev server has to redirect to the dev shell.
|
|
456
|
-
*
|
|
457
|
-
* @example
|
|
458
|
-
* \`\`\`ts
|
|
459
|
-
* plugins: [devShell()],
|
|
460
|
-
* \`\`\`
|
|
461
|
-
*/
|
|
462
|
-
function devShell(): Plugin {
|
|
463
|
-
return {
|
|
464
|
-
name: 'dev-shell',
|
|
465
|
-
enforce: 'pre',
|
|
466
|
-
configureServer(server) {
|
|
467
|
-
server.middlewares.use((req, res, next) => {
|
|
468
|
-
const url = req.url?.split('?')[0];
|
|
469
|
-
if (url !== '/' && url !== '/index.html') return next();
|
|
470
|
-
req.url = '/index-dev.html';
|
|
471
|
-
next();
|
|
472
|
-
});
|
|
473
|
-
},
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Concatenates the plain \`.css\` token presets under \`src/\` into \`bundle.css\`,
|
|
479
|
-
* skipping the companion \`.target.css\` and \`.shadow.css\` files, which
|
|
480
|
-
* \`loadAssetsPlugin\` inlines into the JS bundle.
|
|
481
|
-
*
|
|
482
|
-
* @example
|
|
483
|
-
* \`\`\`ts
|
|
484
|
-
* plugins: [bundleCss()],
|
|
485
|
-
* \`\`\`
|
|
486
|
-
*/
|
|
487
|
-
function bundleCss(): Plugin {
|
|
488
|
-
let outDir = '';
|
|
489
|
-
return {
|
|
490
|
-
name: 'bundle-css',
|
|
491
|
-
apply: 'build',
|
|
492
|
-
configResolved(config: ResolvedConfig) {
|
|
493
|
-
outDir = path.resolve(config.root, config.build.outDir);
|
|
494
|
-
},
|
|
495
|
-
async closeBundle() {
|
|
496
|
-
async function collect(dir: string, rel: string, found: string[]): Promise<void> {
|
|
497
|
-
for (const entry of await readdir(path.join(dir, rel), { withFileTypes: true })) {
|
|
498
|
-
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
499
|
-
if (entry.isDirectory()) {
|
|
500
|
-
await collect(path.join(dir, rel), next, found);
|
|
501
|
-
} else if (entry.name.endsWith('.css') && !/\\.(target|shadow)\\.css$/.test(entry.name)) {
|
|
502
|
-
found.push(path.join(dir, next));
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
const files: string[] = [];
|
|
507
|
-
await collect(sourceRoot, '', files);
|
|
508
|
-
const parts = await Promise.all(files.map((f) => readFile(f, 'utf-8')));
|
|
509
|
-
await mkdir(outDir, { recursive: true });
|
|
510
|
-
await writeFile(path.join(outDir, 'bundle.css'), parts.join('\\n'), 'utf-8');
|
|
511
|
-
},
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* Copies the production shell into \`public/\` next to the bundle, so the served
|
|
517
|
-
* folder is a complete static site.
|
|
518
|
-
*
|
|
519
|
-
* @example
|
|
520
|
-
* \`\`\`ts
|
|
521
|
-
* plugins: [copyShell()],
|
|
522
|
-
* \`\`\`
|
|
523
|
-
*/
|
|
524
|
-
function copyShell(): Plugin {
|
|
525
|
-
let outDir = '';
|
|
526
|
-
return {
|
|
527
|
-
name: 'copy-shell',
|
|
528
|
-
apply: 'build',
|
|
529
|
-
configResolved(config: ResolvedConfig) {
|
|
530
|
-
outDir = path.resolve(config.root, config.build.outDir);
|
|
531
|
-
},
|
|
532
|
-
async closeBundle() {
|
|
533
|
-
await cp(path.join(sourceRoot, 'index.html'), path.join(outDir, 'index.html'));
|
|
534
|
-
},
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
export default defineConfig(({ command }) => {
|
|
539
|
-
if (command === 'serve') {
|
|
540
|
-
return {
|
|
541
|
-
root: buildRoot,
|
|
542
|
-
server: { port: 5173, open: false },
|
|
543
|
-
plugins: [syncBuild(), tscWatch(), rawAssets(buildRoot), devShell()],
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
return {
|
|
547
|
-
root: sourceRoot,
|
|
548
|
-
build: {
|
|
549
|
-
outDir: publicRoot,
|
|
550
|
-
emptyOutDir: true,
|
|
551
|
-
minify: true,
|
|
552
|
-
target: 'esnext',
|
|
553
|
-
sourcemap: false,
|
|
554
|
-
lib: {
|
|
555
|
-
entry: path.join(sourceRoot, namespace, 'bundle.ts'),
|
|
556
|
-
formats: ['es'],
|
|
557
|
-
fileName: () => 'bundle.js',
|
|
558
|
-
},
|
|
559
|
-
rollupOptions: {
|
|
560
|
-
treeshake: false,
|
|
561
|
-
output: { comments: false, keepNames: true },
|
|
562
|
-
},
|
|
563
|
-
},
|
|
564
|
-
plugins: [loadAssetsPlugin(sourceRoot), bundleCss(), copyShell()],
|
|
565
|
-
};
|
|
566
|
-
});
|
|
567
|
-
`;
|
|
568
|
-
}
|
|
569
|
-
function prodShell(tag) {
|
|
570
|
-
return `<!doctype html>
|
|
571
|
-
<html lang="en">
|
|
572
|
-
<head>
|
|
573
|
-
<meta charset="UTF-8" />
|
|
574
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
575
|
-
<title>${tag.replace('-app', ' App')}</title>
|
|
576
|
-
<script type="module" src="bundle.js"></script>
|
|
577
|
-
<link rel="stylesheet" href="./bundle.css" />
|
|
578
|
-
</head>
|
|
579
|
-
<body>
|
|
580
|
-
<${tag}></${tag}>
|
|
581
|
-
</body>
|
|
582
|
-
</html>
|
|
583
|
-
`;
|
|
584
|
-
}
|
|
585
|
-
function devShell(namespace, tag) {
|
|
586
|
-
return `<!doctype html>
|
|
587
|
-
<html lang="en">
|
|
588
|
-
<head>
|
|
589
|
-
<meta charset="UTF-8" />
|
|
590
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
591
|
-
<title>${namespace} App</title>
|
|
592
|
-
<script type="module">
|
|
593
|
-
import * as m from '@twobirds/microcomponents';
|
|
594
|
-
m.autoload(true);
|
|
595
|
-
Object.assign(window, m);
|
|
596
|
-
</script>
|
|
597
|
-
</head>
|
|
598
|
-
<body>
|
|
599
|
-
<${tag}></${tag}>
|
|
600
|
-
</body>
|
|
601
|
-
</html>
|
|
602
|
-
`;
|
|
603
|
-
}
|
|
604
|
-
function bundle() {
|
|
605
|
-
return `// Barrel entry for the production bundle: imports every component so the bundler
|
|
606
|
-
// registers them all in one file. Add a line here when a component is created.
|
|
607
|
-
|
|
608
|
-
import './app.js';
|
|
609
|
-
`;
|
|
610
|
-
}
|
|
611
|
-
function appSource(namespace, tag) {
|
|
612
|
-
const className = kebabToPascal(tag);
|
|
613
|
-
return `import { DC, defineCE, parse, processAssets, type McEvent } from '@twobirds/microcomponents';
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
\`${tag}\` — the starter micro component.
|
|
617
|
-
|
|
618
|
-
@example
|
|
619
|
-
\`\`\`html
|
|
620
|
-
<${tag}></${tag}>
|
|
621
|
-
\`\`\`
|
|
622
|
-
*/
|
|
623
|
-
class ${className} extends DC {
|
|
624
|
-
static assets = ['targetHtml', 'targetCss'];
|
|
625
|
-
|
|
626
|
-
data = { title: 'Micro Components' };
|
|
627
|
-
|
|
628
|
-
// Fill the template \`{key}\` tokens once, instead of binding reactive state.
|
|
629
|
-
oneAssets(ev: McEvent<Record<string, string>>): void {
|
|
630
|
-
processAssets(this, {
|
|
631
|
-
targetHtml: parse(ev.detail.data.targetHtml, this.data),
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
defineCE('${tag}', ${className});
|
|
637
|
-
`;
|
|
638
|
-
}
|
|
639
|
-
function appHtml(namespace) {
|
|
640
|
-
return `<div class="app">
|
|
641
|
-
<h1>{title}</h1>
|
|
642
|
-
<p>Namespace scaffold ready. Edit <code>src/${namespace}/app.ts</code>.</p>
|
|
643
|
-
</div>
|
|
644
|
-
`;
|
|
645
|
-
}
|
|
646
|
-
function appCss(tag) {
|
|
647
|
-
return `${tag} {
|
|
648
|
-
display: block;
|
|
649
|
-
padding: 2rem;
|
|
650
|
-
font-family: system-ui, sans-serif;
|
|
651
|
-
}
|
|
652
|
-
`;
|
|
653
|
-
}
|