@twobirds/microcomponents 0.7.0 → 0.8.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 +20 -2
- package/dist/cli/create.d.ts +5 -5
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +224 -88
- package/dist/mcp/api.json +18 -3
- package/dist/mcp/concepts/event-system.md +23 -0
- package/dist/mcp/concepts/project-layout.md +37 -0
- package/dist/utils/events.d.ts +6 -0
- package/dist/utils/events.d.ts.map +1 -1
- package/dist/utils/events.js +6 -0
- package/dist/vite/index.d.ts +7 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +6 -0
- package/dist/vite/loadAssets.d.ts +36 -0
- package/dist/vite/loadAssets.d.ts.map +1 -0
- package/dist/vite/loadAssets.js +221 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -75,6 +75,20 @@ There is no hand-written API list in the server. The whole index is extracted fr
|
|
|
75
75
|
|
|
76
76
|
`get-source-file` reads package-root relative paths (`dist/core/MC.d.ts`, `dist/mcp/concepts/event-system.md`) and refuses any path outside the package sources.
|
|
77
77
|
|
|
78
|
+
## Vite Plugin
|
|
79
|
+
|
|
80
|
+
The package ships a Vite plugin as its own entry, `@twobirds/microcomponents/vite`, for the bundled production build:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [loadAssetsPlugin(sourceRoot)],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`loadAssetsPlugin` reads the companion `.css` / `.html` files of every DC class that declares `static assets = [...]` and embeds them as a `static _assets` property inside the class, so `loadAssets()` never fetches them at runtime. `mc create` wires it into the generated `vite.config.ts`, which bundles a single `bundle.js` into `public/`.
|
|
91
|
+
|
|
78
92
|
## CLI
|
|
79
93
|
|
|
80
94
|
A small command-line tool scaffolds new components so the tag-to-file convention is never hand-written:
|
|
@@ -95,21 +109,25 @@ pnpm run cli -- new my-card --target-html --target-css --dry-run
|
|
|
95
109
|
# would create src/my/card.target.css
|
|
96
110
|
```
|
|
97
111
|
|
|
98
|
-
`mc create` scaffolds a complete dev environment for a namespace
|
|
112
|
+
`mc create` scaffolds a complete dev environment for a namespace, following the three-path chain the library's own demos use — `src/` → (tsc) → `build/` → (bundle) → `public/`:
|
|
99
113
|
|
|
100
114
|
```bash
|
|
101
115
|
pnpm run cli -- create --namespace mc --dir mc-app
|
|
102
116
|
# created mc-app/package.json
|
|
103
117
|
# created mc-app/opencode.jsonc
|
|
104
118
|
# created mc-app/src/index.html
|
|
119
|
+
# created mc-app/src/index-dev.html
|
|
105
120
|
# created mc-app/src/mc/app.ts
|
|
106
121
|
# created mc-app/src/mc/app.target.html
|
|
107
122
|
# created mc-app/src/mc/app.target.css
|
|
123
|
+
# created mc-app/src/mc/bundle.ts
|
|
108
124
|
# ...
|
|
109
125
|
# project "mc" created in mc-app/ — run: cd mc-app && pnpm install && pnpm dev
|
|
110
126
|
```
|
|
111
127
|
|
|
112
|
-
|
|
128
|
+
`pnpm dev` runs `tsc -w` and serves `build/`, so a saved component reaches the browser through `autoload()` with nothing bundled; companion assets are mirrored into `build/` next to the compiled component, which is where the autoloader looks for them. `pnpm build` emits one minified `bundle.js` into `public/` with every companion asset inlined by `loadAssetsPlugin`, the concatenated `bundle.css`, and the production shell — a single script tag, no autoloader. `opencode.jsonc` registers the MCP server, so opencode picks it up when you open the project.
|
|
129
|
+
|
|
130
|
+
The namespace (1 to 4 lowercase letters) becomes the `src/<namespace>/` folder and the first segment of every component tag, so `mc` is the home of tags like `<mc-app>`. Because the dev shell is the autoload base, `import.meta.url` + the tag-to-path rule resolves `<mc-app>` to `build/mc/app.js` — the compiled starter. New components are served automatically by the autoloader, but the bundle walks one entry, so add each one to `src/<namespace>/bundle.ts`. `--dir` defaults to the namespace, `--dry-run` prints the plan without touching disk.
|
|
113
131
|
|
|
114
132
|
The project is written into a folder of its own, so its files are reported under that folder (`mc-app/src/mc/app.ts`) — the path you can find from the directory you ran the command in, not relative to the project root.
|
|
115
133
|
|
package/dist/cli/create.d.ts
CHANGED
|
@@ -9,11 +9,11 @@ where a component lives.
|
|
|
9
9
|
|
|
10
10
|
The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
|
|
11
11
|
the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
|
|
12
|
-
environment
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
base, `import.meta.url` + `tagPath('xxx-app')` resolves to
|
|
16
|
-
compiled starter.
|
|
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
17
|
|
|
18
18
|
@module CLI/Project
|
|
19
19
|
*/
|
package/dist/cli/create.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/cli/create.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;EAkBE;AAUF;;;;;;;;;;EAUE;AACF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC7B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;EAYE;AACF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/cli/create.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;EAkBE;AAUF;;;;;;;;;;EAUE;AACF,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC7B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;EAYE;AACF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CA0BhE"}
|
package/dist/cli/create.js
CHANGED
|
@@ -9,11 +9,11 @@ where a component lives.
|
|
|
9
9
|
|
|
10
10
|
The namespace is the first segment of every component tag, so `xxx` makes `src/xxx/`
|
|
11
11
|
the home of tags like `<xxx-app>` (tagPath `xxx-app` → `xxx/app`). The generated
|
|
12
|
-
environment
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
base, `import.meta.url` + `tagPath('xxx-app')` resolves to
|
|
16
|
-
compiled starter.
|
|
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
17
|
|
|
18
18
|
@module CLI/Project
|
|
19
19
|
*/
|
|
@@ -64,10 +64,12 @@ export function scaffoldProject(namespace) {
|
|
|
64
64
|
files['.gitignore'] = gitignore();
|
|
65
65
|
files['opencode.jsonc'] = opencodeConfig();
|
|
66
66
|
files['README.md'] = readme(namespace, tag);
|
|
67
|
-
files['src/index.html'] =
|
|
67
|
+
files['src/index.html'] = prodShell(tag);
|
|
68
|
+
files['src/index-dev.html'] = devShell(namespace, tag);
|
|
68
69
|
files[`src/${base}.ts`] = appSource(namespace, tag);
|
|
69
70
|
files[`src/${base}.target.html`] = appHtml(namespace);
|
|
70
71
|
files[`src/${base}.target.css`] = appCss(tag);
|
|
72
|
+
files[`src/${namespace}/bundle.ts`] = bundle();
|
|
71
73
|
return { files, namespace };
|
|
72
74
|
}
|
|
73
75
|
function packageJson(namespace) {
|
|
@@ -78,11 +80,11 @@ function packageJson(namespace) {
|
|
|
78
80
|
scripts: {
|
|
79
81
|
dev: 'vite',
|
|
80
82
|
build: 'vite build',
|
|
81
|
-
preview: 'vite preview',
|
|
83
|
+
preview: 'vite preview --outDir public --port 5174',
|
|
82
84
|
typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
|
|
83
85
|
},
|
|
84
86
|
dependencies: {
|
|
85
|
-
'@twobirds/microcomponents': '^0.
|
|
87
|
+
'@twobirds/microcomponents': '^0.8.0',
|
|
86
88
|
},
|
|
87
89
|
devDependencies: {
|
|
88
90
|
vite: '^8.1.5',
|
|
@@ -101,7 +103,9 @@ function tsconfig() {
|
|
|
101
103
|
strict: true,
|
|
102
104
|
esModuleInterop: true,
|
|
103
105
|
skipLibCheck: true,
|
|
104
|
-
|
|
106
|
+
rootDir: 'src',
|
|
107
|
+
outDir: 'build',
|
|
108
|
+
removeComments: true,
|
|
105
109
|
},
|
|
106
110
|
include: ['src/**/*.ts'],
|
|
107
111
|
}, null, '\t');
|
|
@@ -124,7 +128,8 @@ function tsconfigVc() {
|
|
|
124
128
|
}
|
|
125
129
|
function gitignore() {
|
|
126
130
|
return `node_modules/
|
|
127
|
-
|
|
131
|
+
build/
|
|
132
|
+
public/
|
|
128
133
|
`;
|
|
129
134
|
}
|
|
130
135
|
function opencodeConfig() {
|
|
@@ -156,14 +161,24 @@ pnpm install
|
|
|
156
161
|
pnpm dev
|
|
157
162
|
\`\`\`
|
|
158
163
|
|
|
164
|
+
The dev server runs \`tsc -w\` and serves \`build/\`, so a saved component reaches the
|
|
165
|
+
browser through \`autoload()\` with no bundling. Companion \`.css\`, \`.html\` and \`.json\`
|
|
166
|
+
files are mirrored into \`build/\` next to the compiled component, which is where the
|
|
167
|
+
autoloader looks for them.
|
|
168
|
+
|
|
159
169
|
## Build
|
|
160
170
|
|
|
161
171
|
\`\`\`sh
|
|
162
172
|
pnpm build
|
|
163
173
|
\`\`\`
|
|
164
174
|
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
\`public/\` is the production site: one minified \`bundle.js\` with every companion asset
|
|
176
|
+
inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the page shell. A
|
|
177
|
+
single script tag loads it, no autoloader needed.
|
|
178
|
+
|
|
179
|
+
\`\`\`sh
|
|
180
|
+
pnpm preview
|
|
181
|
+
\`\`\`
|
|
167
182
|
|
|
168
183
|
## Type-check
|
|
169
184
|
|
|
@@ -171,8 +186,9 @@ Components compile to \`dist/\` at their own autoload path, and companion \`.css
|
|
|
171
186
|
pnpm typecheck
|
|
172
187
|
\`\`\`
|
|
173
188
|
|
|
174
|
-
\`tsconfig.json\`
|
|
175
|
-
\`src/\` and needs node types for its \`node:\`
|
|
189
|
+
\`tsconfig.json\` compiles \`src/\` into \`build/\`; \`tsconfig.vc.json\` checks
|
|
190
|
+
\`vite.config.ts\`, which lives outside \`src/\` and needs node types for its \`node:\`
|
|
191
|
+
imports.
|
|
176
192
|
|
|
177
193
|
## Components
|
|
178
194
|
|
|
@@ -183,31 +199,36 @@ Add a component with:
|
|
|
183
199
|
\`\`\`sh
|
|
184
200
|
npx mc new ${namespace}-name --target-html --target-css
|
|
185
201
|
\`\`\`
|
|
202
|
+
|
|
203
|
+
Then add its import to \`src/${namespace}/bundle.ts\` so the production bundle picks it up.
|
|
186
204
|
`;
|
|
187
205
|
}
|
|
188
206
|
function viteConfig(namespace) {
|
|
189
207
|
return `/**
|
|
190
208
|
* Dev and build config for a Micro Components project.
|
|
191
209
|
*
|
|
192
|
-
* \`src/\`
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* \`
|
|
198
|
-
* \`
|
|
199
|
-
*
|
|
200
|
-
* (\`mc/app.ts\` → \`dist/mc/app.js\`).
|
|
210
|
+
* The chain is \`src/\` → \`build/\` → \`public/\`:
|
|
211
|
+
*
|
|
212
|
+
* - \`build/\` is the \`tsc\` output: one \`.js\` per component at its own autoload path,
|
|
213
|
+
* plus the copied companion assets and the dev shell. The dev server serves it, so
|
|
214
|
+
* the page loads components on demand through \`autoload()\` and nothing is bundled.
|
|
215
|
+
* - \`public/\` is the production output: one minified \`bundle.js\` with every companion
|
|
216
|
+
* asset inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the
|
|
217
|
+
* production shell. A single script tag loads it, no autoloader needed.
|
|
201
218
|
*
|
|
202
219
|
* @module ViteConfig
|
|
203
220
|
*/
|
|
204
221
|
|
|
205
222
|
import { defineConfig, type Plugin, type ResolvedConfig } from 'vite';
|
|
206
|
-
import {
|
|
223
|
+
import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
|
|
224
|
+
import { spawn } from 'node:child_process';
|
|
225
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
207
226
|
import path from 'node:path';
|
|
208
227
|
|
|
209
228
|
const projectRoot = import.meta.dirname;
|
|
210
229
|
const sourceRoot = path.join(projectRoot, 'src');
|
|
230
|
+
const buildRoot = path.join(projectRoot, 'build');
|
|
231
|
+
const publicRoot = path.join(projectRoot, 'public');
|
|
211
232
|
const namespace = '${namespace}';
|
|
212
233
|
|
|
213
234
|
const pageShells = new Set(['index.html', 'index-dev.html']);
|
|
@@ -229,6 +250,84 @@ function isCompanionAsset(name: string): boolean {
|
|
|
229
250
|
return name.endsWith('.css') || name.endsWith('.json') || name.endsWith('.html');
|
|
230
251
|
}
|
|
231
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Mirrors companion assets and the dev shell from \`src/\` into the served folder,
|
|
255
|
+
* preserving the tree. Autoload resolves \`<ns-app>\` to \`<ns>/app.js\` relative to the
|
|
256
|
+
* page shell, so the served folder needs the compiled component *and* its companion
|
|
257
|
+
* files at the same relative path.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* \`\`\`ts
|
|
261
|
+
* await mirrorAssets(sourceRoot, buildRoot);
|
|
262
|
+
* \`\`\`
|
|
263
|
+
*
|
|
264
|
+
* @param root Source root to walk.
|
|
265
|
+
* @param out Folder to write into.
|
|
266
|
+
* @param rel Path of the walked directory relative to \`root\`.
|
|
267
|
+
*/
|
|
268
|
+
async function mirrorAssets(root: string, out: string, rel = ''): Promise<void> {
|
|
269
|
+
for (const entry of await readdir(path.join(root, rel), { withFileTypes: true })) {
|
|
270
|
+
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
271
|
+
if (entry.isDirectory()) {
|
|
272
|
+
await mirrorAssets(root, out, next);
|
|
273
|
+
} else if (isCompanionAsset(entry.name) || entry.name === 'index-dev.html') {
|
|
274
|
+
const dst = path.join(out, next);
|
|
275
|
+
await mkdir(path.dirname(dst), { recursive: true });
|
|
276
|
+
await cp(path.join(root, next), dst);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Runs \`tsc -w\` for the lifetime of the dev server, so a saved component reaches
|
|
283
|
+
* \`build/\` without a second command.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* \`\`\`ts
|
|
287
|
+
* plugins: [tscWatch()],
|
|
288
|
+
* \`\`\`
|
|
289
|
+
*/
|
|
290
|
+
function tscWatch(): Plugin {
|
|
291
|
+
let child: ReturnType<typeof spawn> | undefined;
|
|
292
|
+
return {
|
|
293
|
+
name: 'tsc-watch',
|
|
294
|
+
configResolved() {
|
|
295
|
+
child = spawn('npx', ['tsc', '-w', '-p', 'tsconfig.json'], { cwd: projectRoot, stdio: 'inherit' });
|
|
296
|
+
},
|
|
297
|
+
closeServer() {
|
|
298
|
+
child?.kill();
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Mirrors companion assets into \`build/\` before the dev server starts, and again on
|
|
305
|
+
* every change, so the served folder stays autoload-ready.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* \`\`\`ts
|
|
309
|
+
* plugins: [syncBuild()],
|
|
310
|
+
* \`\`\`
|
|
311
|
+
*/
|
|
312
|
+
function syncBuild(): Plugin {
|
|
313
|
+
return {
|
|
314
|
+
name: 'sync-build',
|
|
315
|
+
enforce: 'pre',
|
|
316
|
+
async configureServer(server) {
|
|
317
|
+
await mirrorAssets(sourceRoot, buildRoot);
|
|
318
|
+
server.watcher.on('all', async (event, file) => {
|
|
319
|
+
if (!file.startsWith(sourceRoot) || file.endsWith('.ts')) return;
|
|
320
|
+
const rel = path.relative(sourceRoot, file);
|
|
321
|
+
if (isCompanionAsset(path.basename(file)) || file.endsWith('index-dev.html')) {
|
|
322
|
+
const dst = path.join(buildRoot, rel);
|
|
323
|
+
await mkdir(path.dirname(dst), { recursive: true });
|
|
324
|
+
await cp(file, dst);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
232
331
|
/**
|
|
233
332
|
* Serves companion \`.css\`, \`.html\` and \`.json\` files as raw text, ahead of the
|
|
234
333
|
* Vite transform middleware. Without it a \`.html\` request returns the transformed
|
|
@@ -237,10 +336,10 @@ function isCompanionAsset(name: string): boolean {
|
|
|
237
336
|
*
|
|
238
337
|
* @example
|
|
239
338
|
* \`\`\`ts
|
|
240
|
-
* plugins: [rawAssets(
|
|
339
|
+
* plugins: [rawAssets(buildRoot)],
|
|
241
340
|
* \`\`\`
|
|
242
341
|
*
|
|
243
|
-
* @param root Source root that mirrors the
|
|
342
|
+
* @param root Source root that mirrors the served tree.
|
|
244
343
|
*/
|
|
245
344
|
function rawAssets(root: string): Plugin {
|
|
246
345
|
return {
|
|
@@ -266,109 +365,139 @@ function rawAssets(root: string): Plugin {
|
|
|
266
365
|
}
|
|
267
366
|
|
|
268
367
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* at the same relative path.
|
|
368
|
+
* Serves the autoload shell (\`index-dev.html\`) at \`/\`. Vite answers \`/\` with
|
|
369
|
+
* \`index.html\`, which is the production shell referencing \`bundle.js\` — that file
|
|
370
|
+
* only exists in \`public/\`, so the dev server has to redirect to the dev shell.
|
|
273
371
|
*
|
|
274
372
|
* @example
|
|
275
373
|
* \`\`\`ts
|
|
276
|
-
* plugins: [
|
|
374
|
+
* plugins: [devShell()],
|
|
277
375
|
* \`\`\`
|
|
376
|
+
*/
|
|
377
|
+
function devShell(): Plugin {
|
|
378
|
+
return {
|
|
379
|
+
name: 'dev-shell',
|
|
380
|
+
enforce: 'pre',
|
|
381
|
+
configureServer(server) {
|
|
382
|
+
server.middlewares.use((req, res, next) => {
|
|
383
|
+
const url = req.url?.split('?')[0];
|
|
384
|
+
if (url !== '/' && url !== '/index.html') return next();
|
|
385
|
+
req.url = '/index-dev.html';
|
|
386
|
+
next();
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Concatenates the plain \`.css\` token presets under \`src/\` into \`bundle.css\`,
|
|
394
|
+
* skipping the companion \`.target.css\` and \`.shadow.css\` files, which
|
|
395
|
+
* \`loadAssetsPlugin\` inlines into the JS bundle.
|
|
278
396
|
*
|
|
279
|
-
* @
|
|
397
|
+
* @example
|
|
398
|
+
* \`\`\`ts
|
|
399
|
+
* plugins: [bundleCss()],
|
|
400
|
+
* \`\`\`
|
|
280
401
|
*/
|
|
281
|
-
function
|
|
402
|
+
function bundleCss(): Plugin {
|
|
282
403
|
let outDir = '';
|
|
283
404
|
return {
|
|
284
|
-
name: '
|
|
405
|
+
name: 'bundle-css',
|
|
285
406
|
apply: 'build',
|
|
286
407
|
configResolved(config: ResolvedConfig) {
|
|
287
408
|
outDir = path.resolve(config.root, config.build.outDir);
|
|
288
409
|
},
|
|
289
410
|
async closeBundle() {
|
|
290
|
-
async function
|
|
291
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
292
|
-
const
|
|
411
|
+
async function collect(dir: string, rel: string, found: string[]): Promise<void> {
|
|
412
|
+
for (const entry of await readdir(path.join(dir, rel), { withFileTypes: true })) {
|
|
413
|
+
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
293
414
|
if (entry.isDirectory()) {
|
|
294
|
-
await
|
|
295
|
-
} else if (
|
|
296
|
-
|
|
297
|
-
await mkdir(path.dirname(dst), { recursive: true });
|
|
298
|
-
await cp(src, dst);
|
|
415
|
+
await collect(path.join(dir, rel), next, found);
|
|
416
|
+
} else if (entry.name.endsWith('.css') && !/\\.(target|shadow)\\.css$/.test(entry.name)) {
|
|
417
|
+
found.push(path.join(dir, next));
|
|
299
418
|
}
|
|
300
419
|
}
|
|
301
420
|
}
|
|
302
|
-
|
|
421
|
+
const files: string[] = [];
|
|
422
|
+
await collect(sourceRoot, '', files);
|
|
423
|
+
const parts = await Promise.all(files.map((f) => readFile(f, 'utf-8')));
|
|
424
|
+
await mkdir(outDir, { recursive: true });
|
|
425
|
+
await writeFile(path.join(outDir, 'bundle.css'), parts.join('\\n'), 'utf-8');
|
|
303
426
|
},
|
|
304
427
|
};
|
|
305
428
|
}
|
|
306
429
|
|
|
307
430
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* last \`/\`, so \`info.name\` alone cannot carry a folder — the facade module id is
|
|
311
|
-
* the reliable source.
|
|
431
|
+
* Copies the production shell into \`public/\` next to the bundle, so the served
|
|
432
|
+
* folder is a complete static site.
|
|
312
433
|
*
|
|
313
434
|
* @example
|
|
314
435
|
* \`\`\`ts
|
|
315
|
-
*
|
|
316
|
-
* entryName({ name: 'index', facadeModuleId: '/project/src/index.html' });
|
|
436
|
+
* plugins: [copyShell()],
|
|
317
437
|
* \`\`\`
|
|
318
|
-
*
|
|
319
|
-
* @param info Rollup chunk info.
|
|
320
438
|
*/
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
439
|
+
function copyShell(): Plugin {
|
|
440
|
+
let outDir = '';
|
|
441
|
+
return {
|
|
442
|
+
name: 'copy-shell',
|
|
443
|
+
apply: 'build',
|
|
444
|
+
configResolved(config: ResolvedConfig) {
|
|
445
|
+
outDir = path.resolve(config.root, config.build.outDir);
|
|
446
|
+
},
|
|
447
|
+
async closeBundle() {
|
|
448
|
+
await cp(path.join(sourceRoot, 'index.html'), path.join(outDir, 'index.html'));
|
|
449
|
+
},
|
|
450
|
+
};
|
|
325
451
|
}
|
|
326
452
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
* const entries: Record<string, string> = {};
|
|
335
|
-
* await collectEntries(path.join(sourceRoot, 'mc'), 'mc', entries);
|
|
336
|
-
* \`\`\`
|
|
337
|
-
*
|
|
338
|
-
* @param dir Directory to walk.
|
|
339
|
-
* @param rel Path of that directory relative to \`src/\`.
|
|
340
|
-
* @param entries Accumulated entries, mutated in place.
|
|
341
|
-
*/
|
|
342
|
-
async function collectEntries(dir: string, rel: string, entries: Record<string, string>): Promise<void> {
|
|
343
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
344
|
-
const full = path.join(dir, entry.name);
|
|
345
|
-
if (entry.isDirectory()) {
|
|
346
|
-
await collectEntries(full, path.join(rel, entry.name), entries);
|
|
347
|
-
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
|
|
348
|
-
entries[path.join(rel, entry.name).replace(/\\.(ts|js)$/, '')] = full;
|
|
349
|
-
}
|
|
453
|
+
export default defineConfig(({ command }) => {
|
|
454
|
+
if (command === 'serve') {
|
|
455
|
+
return {
|
|
456
|
+
root: buildRoot,
|
|
457
|
+
server: { port: 5173, open: false },
|
|
458
|
+
plugins: [syncBuild(), tscWatch(), rawAssets(buildRoot), devShell()],
|
|
459
|
+
};
|
|
350
460
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export default defineConfig(async () => {
|
|
354
|
-
const entries: Record<string, string> = {};
|
|
355
|
-
await collectEntries(path.join(sourceRoot, namespace), namespace, entries);
|
|
356
461
|
return {
|
|
357
462
|
root: sourceRoot,
|
|
358
463
|
build: {
|
|
359
|
-
outDir:
|
|
464
|
+
outDir: publicRoot,
|
|
360
465
|
emptyOutDir: true,
|
|
466
|
+
minify: true,
|
|
467
|
+
target: 'esnext',
|
|
468
|
+
sourcemap: false,
|
|
469
|
+
lib: {
|
|
470
|
+
entry: path.join(sourceRoot, namespace, 'bundle.ts'),
|
|
471
|
+
formats: ['es'],
|
|
472
|
+
fileName: () => 'bundle.js',
|
|
473
|
+
},
|
|
361
474
|
rollupOptions: {
|
|
362
|
-
|
|
363
|
-
output: {
|
|
475
|
+
treeshake: false,
|
|
476
|
+
output: { comments: false, keepNames: true },
|
|
364
477
|
},
|
|
365
478
|
},
|
|
366
|
-
plugins: [
|
|
479
|
+
plugins: [loadAssetsPlugin(sourceRoot), bundleCss(), copyShell()],
|
|
367
480
|
};
|
|
368
481
|
});
|
|
369
482
|
`;
|
|
370
483
|
}
|
|
371
|
-
function
|
|
484
|
+
function prodShell(tag) {
|
|
485
|
+
return `<!doctype html>
|
|
486
|
+
<html lang="en">
|
|
487
|
+
<head>
|
|
488
|
+
<meta charset="UTF-8" />
|
|
489
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
490
|
+
<title>${tag.replace('-app', ' App')}</title>
|
|
491
|
+
<script type="module" src="bundle.js"></script>
|
|
492
|
+
<link rel="stylesheet" href="./bundle.css" />
|
|
493
|
+
</head>
|
|
494
|
+
<body>
|
|
495
|
+
<${tag}></${tag}>
|
|
496
|
+
</body>
|
|
497
|
+
</html>
|
|
498
|
+
`;
|
|
499
|
+
}
|
|
500
|
+
function devShell(namespace, tag) {
|
|
372
501
|
return `<!doctype html>
|
|
373
502
|
<html lang="en">
|
|
374
503
|
<head>
|
|
@@ -387,6 +516,13 @@ function shell(namespace, tag) {
|
|
|
387
516
|
</html>
|
|
388
517
|
`;
|
|
389
518
|
}
|
|
519
|
+
function bundle() {
|
|
520
|
+
return `// Barrel entry for the production bundle: imports every component so the bundler
|
|
521
|
+
// registers them all in one file. Add a line here when a component is created.
|
|
522
|
+
|
|
523
|
+
import './app.js';
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
390
526
|
function appSource(namespace, tag) {
|
|
391
527
|
const className = kebabToPascal(tag);
|
|
392
528
|
return `import { DC, defineCE, parse, processAssets, type McEvent } from '@twobirds/microcomponents';
|
package/dist/mcp/api.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"libraryVersion": "0.
|
|
3
|
-
"exportedCount":
|
|
2
|
+
"libraryVersion": "0.8.0",
|
|
3
|
+
"exportedCount": 42,
|
|
4
4
|
"entries": {
|
|
5
5
|
"MC": {
|
|
6
6
|
"name": "MC",
|
|
@@ -429,7 +429,7 @@
|
|
|
429
429
|
"signatures": [
|
|
430
430
|
"export function trigger<D = unknown>( target: HTMLElement | string | undefined | null, event: string, data: D = {} as D, bubble: BubbleDirections = 'l', // direction of bubbling controller?: BubbleController ) {"
|
|
431
431
|
],
|
|
432
|
-
"description": "Creates and dispatches a custom event on the specified DOM node.\n\nThe event is namespaced with an `_mc` prefix by default, unless already prefixed.\nSupports bidirectional bubbling (up/down the DOM tree) based on the `bubble` parameter:\n- 'l': local dispatch only\n- 'u': bubble up towards ancestors\n- 'd': bubble down to descendants\n\nThe dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload\nunder `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.\n\nThe first dispatch fires locally only; the second bubbles upward to ancestor components.",
|
|
432
|
+
"description": "Creates and dispatches a custom event on the specified DOM node.\n\nThe event is namespaced with an `_mc` prefix by default, unless already prefixed.\nSupports bidirectional bubbling (up/down the DOM tree) based on the `bubble` parameter:\n- 'l': local dispatch only\n- 'u': bubble up towards ancestors\n- 'd': bubble down to descendants\n\nThe dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload\nunder `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.\n\nPrefer a direct target over bubbling: name the receiving element in `target` and keep the\ndefault `'l'`, so one `querySelectorAll` reaches it in a single step. Reserve `'u'`/`'d'`\nfor receivers that cannot be named in advance, since they walk the tree and re-dispatch the\nevent at every level. Pass a typed object as `data` rather than a bare string, so the\nhandler reads named fields from `ev.detail.data` and the flow stays readable as it grows.\n\nThe first dispatch fires locally only; the second bubbles upward to ancestor components.",
|
|
433
433
|
"examples": [
|
|
434
434
|
{
|
|
435
435
|
"title": "",
|
|
@@ -595,6 +595,21 @@
|
|
|
595
595
|
"export type McRemoved<T extends HTMLElement, K extends string> = T extends { _mc: McController } ? { _mc: Record<K, never> } : unknown;"
|
|
596
596
|
],
|
|
597
597
|
"description": "The type `remove()` asserts onto the caller's reference: on an enhanced target the\nremoved key is poisoned to `never` (member access becomes a compile error); a\nplain target is left untouched. A later `add()` re-registers the key cleanly."
|
|
598
|
+
},
|
|
599
|
+
"loadAssetsPlugin": {
|
|
600
|
+
"name": "loadAssetsPlugin",
|
|
601
|
+
"kind": "function",
|
|
602
|
+
"file": "src/vite/loadAssets.ts",
|
|
603
|
+
"signatures": [
|
|
604
|
+
"export function loadAssetsPlugin(rootDir: string): PluginOption {"
|
|
605
|
+
],
|
|
606
|
+
"description": "Vite plugin factory that inlines companion `.css` / `.html` assets into the\nDC classes declaring them through `static assets = [...]`.",
|
|
607
|
+
"examples": [
|
|
608
|
+
{
|
|
609
|
+
"title": "Register the plugin in a Vite config, pointing at the source root.",
|
|
610
|
+
"code": "import { loadAssetsPlugin } from './vite-plugin-load-assets';\nexport default defineConfig({ plugins: [loadAssetsPlugin(__dirname)] });"
|
|
611
|
+
}
|
|
612
|
+
]
|
|
598
613
|
}
|
|
599
614
|
},
|
|
600
615
|
"undocumented": []
|
|
@@ -22,6 +22,29 @@ Bubble directions in trigger():
|
|
|
22
22
|
|
|
23
23
|
Only use bubbling when there is a specific reason for it. Usually directly addressing the target is better, because of the speed advantage using querySelectorAll().
|
|
24
24
|
|
|
25
|
+
## Choosing the target
|
|
26
|
+
|
|
27
|
+
Name the receiving element in the trigger call and keep the default `'l'`, so a single
|
|
28
|
+
`querySelectorAll` reaches it in one step. `'u'` and `'d'` walk the tree and re-dispatch the
|
|
29
|
+
event at every level, so reserve them for receivers you cannot name in advance — a plugin
|
|
30
|
+
listening to unknown descendants, or a widget whose host markup is not yours.
|
|
31
|
+
|
|
32
|
+
The template declares the handler, the handler builds the payload, the trigger carries it:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<button class="remove" click="|remove">✕</button>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
onRemove(): void {
|
|
40
|
+
trigger('todo-list', 'remove', { id: this.data.id });
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Pass an object, not a bare string, so the receiver reads named fields (`ev.detail.data.id`)
|
|
45
|
+
and the flow stays readable as the payload grows. Declare the events on the component's event
|
|
46
|
+
map (`EM` on `DC`) so the payload type is inferred at both ends.
|
|
47
|
+
|
|
25
48
|
## Payload & propagation
|
|
26
49
|
|
|
27
50
|
MC-triggered custom events and lifecycle events are `McEvent<D>` — a `CustomEvent` whose `detail` is `{ data, control }`. The payload is always read from `ev.detail.data`, never `ev.data`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
A Micro Components project keeps one folder per stage of the chain:
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
src/ → (tsc) → build/ → (bundle) → public/
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
- `src/` holds the authored sources: component `.ts` files, the companion
|
|
8
|
+
`.target.html` / `.target.css` / `.shadow.css` assets, the dev shell
|
|
9
|
+
(`index-dev.html`) and the production shell (`index.html`).
|
|
10
|
+
- `build/` is the `tsc` output: one `.js` per component at its own autoload path,
|
|
11
|
+
plus the mirrored companion assets and the dev shell. The dev server serves it, so
|
|
12
|
+
the page loads components on demand through `autoload()` and nothing is bundled.
|
|
13
|
+
- `public/` is the production site: one minified `bundle.js` with every companion
|
|
14
|
+
asset inlined by `loadAssetsPlugin`, the concatenated `bundle.css`, and the
|
|
15
|
+
production shell. A single script tag loads it; the autoloader is not needed.
|
|
16
|
+
|
|
17
|
+
`build/` and `public/` are generated, so they stay out of git.
|
|
18
|
+
|
|
19
|
+
A website that ships with the project — a playground, a component browser, a docs page —
|
|
20
|
+
lives in `src/` like any other page, is served from `build/` while developing, and is
|
|
21
|
+
served from `public/` in production. Do not add a fourth tree for it.
|
|
22
|
+
|
|
23
|
+
A new component is picked up by the dev server automatically, because `autoload()`
|
|
24
|
+
resolves its tag to a file under `build/`. The bundle is not: the bundler walks a single
|
|
25
|
+
entry, so every component must be imported by the barrel `src/<namespace>/bundle.ts`.
|
|
26
|
+
|
|
27
|
+
## Shells
|
|
28
|
+
|
|
29
|
+
The two shells are deliberately different:
|
|
30
|
+
|
|
31
|
+
- `index-dev.html` imports the library and calls `autoload(true)`, so a tag in the markup
|
|
32
|
+
pulls its own `.js` from `build/`.
|
|
33
|
+
- `index.html` loads `bundle.js` and `bundle.css`, which only exist in `public/`.
|
|
34
|
+
|
|
35
|
+
The dev server redirects `/` to `index-dev.html`; the production build copies
|
|
36
|
+
`index.html` next to the bundle. Serving the production shell in dev would request a
|
|
37
|
+
`bundle.js` that was never built.
|
package/dist/utils/events.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export declare class BubbleController implements McBubbleController {
|
|
|
170
170
|
* The dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload
|
|
171
171
|
* under `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.
|
|
172
172
|
*
|
|
173
|
+
* Prefer a direct target over bubbling: name the receiving element in `target` and keep the
|
|
174
|
+
* default `'l'`, so one `querySelectorAll` reaches it in a single step. Reserve `'u'`/`'d'`
|
|
175
|
+
* for receivers that cannot be named in advance, since they walk the tree and re-dispatch the
|
|
176
|
+
* event at every level. Pass a typed object as `data` rather than a bare string, so the
|
|
177
|
+
* handler reads named fields from `ev.detail.data` and the flow stays readable as it grows.
|
|
178
|
+
*
|
|
173
179
|
* @param target - The DOM element on which to trigger the event, or a querySelectorAll(selector) selector string.
|
|
174
180
|
* @param event - The name of the event (will be prefixed if needed).
|
|
175
181
|
* @param data - Additional data payload attached to `event.detail.data`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/utils/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDE;AAIF,OAAO,KAAK,EACX,gBAAgB,EAChB,kBAAkB,EAElB,cAAc,EACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAK5E;AAED;;;;;;;;EAQE;AACF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEzC;;;;;;;;;;;;;EAaE;AACF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,mBAAmB,GAAG,SAAS,GAAG,IAAI,CAAC;AAEhF,wBAAgB,GAAG,CAAC,CAAC,GAAG,OAAO,EAC9B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,IAAI,CAAC;AACR,wBAAgB,GAAG,CAClB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kCAAkC,GACzC,IAAI,CAAC;AA4CR,wBAAgB,EAAE,CAAC,CAAC,GAAG,OAAO,EAC7B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAC1B,IAAI,CAAC,EAAE,OAAO,GACZ,cAAc,CAAC,CAAC,CAAC,CAAC;AACrB,wBAAgB,EAAE,CACjB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kCAAkC,EAC3C,IAAI,CAAC,EAAE,OAAO,GACZ,kCAAkC,CAAC;AAsDtC,wBAAgB,GAAG,CAAC,CAAC,GAAG,OAAO,EAC9B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,cAAc,CAAC,CAAC,CAAC,CAAC;AACrB,wBAAgB,GAAG,CAClB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kCAAkC,GACzC,kCAAkC,CAAC;AA2CtC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gBAAiB,YAAW,kBAAkB;IAC1D,OAAO,UAAS;;IAEhB;;;;;;;;;;;;;;OAcG;IACH,IAAI;CAGJ;AAED
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/utils/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDE;AAIF,OAAO,KAAK,EACX,gBAAgB,EAChB,kBAAkB,EAElB,cAAc,EACd,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAK5E;AAED;;;;;;;;EAQE;AACF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEzC;;;;;;;;;;;;;EAaE;AACF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,mBAAmB,GAAG,SAAS,GAAG,IAAI,CAAC;AAEhF,wBAAgB,GAAG,CAAC,CAAC,GAAG,OAAO,EAC9B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,IAAI,CAAC;AACR,wBAAgB,GAAG,CAClB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kCAAkC,GACzC,IAAI,CAAC;AA4CR,wBAAgB,EAAE,CAAC,CAAC,GAAG,OAAO,EAC7B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAC1B,IAAI,CAAC,EAAE,OAAO,GACZ,cAAc,CAAC,CAAC,CAAC,CAAC;AACrB,wBAAgB,EAAE,CACjB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kCAAkC,EAC3C,IAAI,CAAC,EAAE,OAAO,GACZ,kCAAkC,CAAC;AAsDtC,wBAAgB,GAAG,CAAC,CAAC,GAAG,OAAO,EAC9B,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACxB,cAAc,CAAC,CAAC,CAAC,CAAC;AACrB,wBAAgB,GAAG,CAClB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,kCAAkC,GACzC,kCAAkC,CAAC;AA2CtC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gBAAiB,YAAW,kBAAkB;IAC1D,OAAO,UAAS;;IAEhB;;;;;;;;;;;;;;OAcG;IACH,IAAI;CAGJ;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,OAAO,CAAC,CAAC,GAAG,OAAO,EAClC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,IAAI,EAC/C,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,CAAW,EACjB,MAAM,GAAE,gBAAsB,EAAE,wBAAwB;AACxD,UAAU,CAAC,EAAE,gBAAgB,QA+C7B"}
|
package/dist/utils/events.js
CHANGED
|
@@ -253,6 +253,12 @@ export class BubbleController {
|
|
|
253
253
|
* The dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload
|
|
254
254
|
* under `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.
|
|
255
255
|
*
|
|
256
|
+
* Prefer a direct target over bubbling: name the receiving element in `target` and keep the
|
|
257
|
+
* default `'l'`, so one `querySelectorAll` reaches it in a single step. Reserve `'u'`/`'d'`
|
|
258
|
+
* for receivers that cannot be named in advance, since they walk the tree and re-dispatch the
|
|
259
|
+
* event at every level. Pass a typed object as `data` rather than a bare string, so the
|
|
260
|
+
* handler reads named fields from `ev.detail.data` and the flow stays readable as it grows.
|
|
261
|
+
*
|
|
256
262
|
* @param target - The DOM element on which to trigger the event, or a querySelectorAll(selector) selector string.
|
|
257
263
|
* @param event - The name of the event (will be prefixed if needed).
|
|
258
264
|
* @param data - Additional data payload attached to `event.detail.data`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AAEF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Inlines `static assets = [...]` declarations on DC classes for a bundled build: companion
|
|
3
|
+
`.css` / `.html` files are read at build time and embedded as a `static _assets = { ... }`
|
|
4
|
+
property inserted as the first line inside the class body, so `loadAssets()` never fetches
|
|
5
|
+
them at runtime. The original `static assets` line is preserved for debugging purposes.
|
|
6
|
+
|
|
7
|
+
The plugin ships as its own entry, `@twobirds/microcomponents/vite`, so a project that
|
|
8
|
+
bundles to `public/` imports it without copying the source.
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
```ts
|
|
12
|
+
import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
plugins: [loadAssetsPlugin(sourceRoot)],
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
@module Vite
|
|
20
|
+
*/
|
|
21
|
+
import type { PluginOption } from 'vite';
|
|
22
|
+
/**
|
|
23
|
+
* Vite plugin factory that inlines companion `.css` / `.html` assets into the
|
|
24
|
+
* DC classes declaring them through `static assets = [...]`.
|
|
25
|
+
*
|
|
26
|
+
* @example Register the plugin in a Vite config, pointing at the source root.
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { loadAssetsPlugin } from './vite-plugin-load-assets';
|
|
29
|
+
* export default defineConfig({ plugins: [loadAssetsPlugin(__dirname)] });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @param rootDir Directory holding the component source files.
|
|
33
|
+
* @returns A Vite plugin option.
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadAssetsPlugin(rootDir: string): PluginOption;
|
|
36
|
+
//# sourceMappingURL=loadAssets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadAssets.d.ts","sourceRoot":"","sources":["../../src/vite/loadAssets.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;EAmBE;AAIF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAgJzC;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAiE9D"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// oxlint-disable no-await-in-loop
|
|
2
|
+
/**
|
|
3
|
+
Inlines `static assets = [...]` declarations on DC classes for a bundled build: companion
|
|
4
|
+
`.css` / `.html` files are read at build time and embedded as a `static _assets = { ... }`
|
|
5
|
+
property inserted as the first line inside the class body, so `loadAssets()` never fetches
|
|
6
|
+
them at runtime. The original `static assets` line is preserved for debugging purposes.
|
|
7
|
+
|
|
8
|
+
The plugin ships as its own entry, `@twobirds/microcomponents/vite`, so a project that
|
|
9
|
+
bundles to `public/` imports it without copying the source.
|
|
10
|
+
|
|
11
|
+
@example
|
|
12
|
+
```ts
|
|
13
|
+
import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [loadAssetsPlugin(sourceRoot)],
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
@module Vite
|
|
21
|
+
*/
|
|
22
|
+
import { readFile } from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
/**
|
|
25
|
+
* Escape a raw string so it can be safely embedded inside a JS template literal.
|
|
26
|
+
* Backticks and `${` sequences are escaped to prevent breaking the enclosing literal.
|
|
27
|
+
*
|
|
28
|
+
* @example Returns the input with backticks and `${` sequences escaped.
|
|
29
|
+
* ```ts
|
|
30
|
+
* escapeTemplateLiteral('price is ${cost}`done`');
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function escapeTemplateLiteral(raw) {
|
|
34
|
+
return raw.replace(/\\/g, '\\\\').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the relative file path for a given asset descriptor and component tag.
|
|
38
|
+
* Only the last segment of the tag is used as the basename since the .ts source
|
|
39
|
+
* already lives in the correct directory (e.g. `cart-container-checkout.ts` is
|
|
40
|
+
* inside `cart/container/`, so we just need `checkout.target.html`).
|
|
41
|
+
*
|
|
42
|
+
* @example Returns `./header.shadow.css`, `./card.target.html` and `./summary.css` for the calls below.
|
|
43
|
+
* ```ts
|
|
44
|
+
* resolveAssetPath('shop-header', 'shadowCss');
|
|
45
|
+
* resolveAssetPath('product-card', 'targetHtml');
|
|
46
|
+
* resolveAssetPath('cart-summary', 'css');
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
function resolveAssetPath(tagName, asset) {
|
|
50
|
+
const basename = tagName.split('-').pop() ?? tagName;
|
|
51
|
+
const m = asset.match(/^([a-z]+)([A-Z][a-z]*)$/);
|
|
52
|
+
if (m) {
|
|
53
|
+
return `./${basename}.${m[1]}.${m[2].toLowerCase()}`;
|
|
54
|
+
}
|
|
55
|
+
return `./${basename}.${asset.toLowerCase()}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract the tag name from a `defineCE('tag-name', ...)` call in source code.
|
|
59
|
+
* Returns the first found tag or undefined.
|
|
60
|
+
*
|
|
61
|
+
* @example Returns `'shop-header'` for the code below.
|
|
62
|
+
* ```ts
|
|
63
|
+
* extractDefineCETag("defineCE('shop-header', ShopHeader);");
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
function extractDefineCETag(code) {
|
|
67
|
+
const m = code.match(/defineCE\s*\(\s*['"]([^'"]+)['"]/);
|
|
68
|
+
return m ? m[1] : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Strip all line and block comments from source code, replacing
|
|
72
|
+
* their content with equivalent whitespace so character positions stay valid for
|
|
73
|
+
* later string slicing.
|
|
74
|
+
*
|
|
75
|
+
* @example Blanks out the `// note` segment while keeping every character
|
|
76
|
+
* position and line break, so later string slicing stays aligned.
|
|
77
|
+
* ```ts
|
|
78
|
+
* stripComments('const a = 1; // note\nconst b = 2;');
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
function stripComments(code) {
|
|
82
|
+
let result = '';
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < code.length) {
|
|
85
|
+
// Line comment
|
|
86
|
+
if (code[i] === '/' && code[i + 1] === '/') {
|
|
87
|
+
const end = code.indexOf('\n', i);
|
|
88
|
+
if (end === -1) {
|
|
89
|
+
result += ' '.repeat(code.length - i);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
result += ' '.repeat(end - i) + '\n';
|
|
93
|
+
i = end + 1;
|
|
94
|
+
}
|
|
95
|
+
else if (code[i] === '/' && code[i + 1] === '*') {
|
|
96
|
+
const end = code.indexOf('*/', i + 2);
|
|
97
|
+
if (end === -1) {
|
|
98
|
+
result += ' '.repeat(code.length - i);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
// Preserve newlines inside block comments to keep line positions valid
|
|
102
|
+
let j = i;
|
|
103
|
+
while (j <= end + 1) {
|
|
104
|
+
result += code[j] === '\n' ? '\n' : ' ';
|
|
105
|
+
j++;
|
|
106
|
+
}
|
|
107
|
+
i = end + 2;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
result += code[i];
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read and inline all assets for a given tag, returning an array of
|
|
118
|
+
* `{ key, literal }` entries suitable for building object literals.
|
|
119
|
+
* Plain `.css` files hold CSS custom-property presets and are inlined with
|
|
120
|
+
* their real content so the component injects the default variables at
|
|
121
|
+
* runtime; the same files are also merged into bundle.css as the override
|
|
122
|
+
* placeholder. They are never left empty — an empty string would make the
|
|
123
|
+
* runtime treat the asset as not-yet-loaded and re-fetch it.
|
|
124
|
+
*
|
|
125
|
+
* @example Returns one escaped template literal per descriptor, ready to be joined into an object.
|
|
126
|
+
* ```ts
|
|
127
|
+
* readAssetsInline('/src/shop/header', 'shop-header', ['shadowCss', 'targetHtml']);
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @throws If a companion file for a descriptor does not exist.
|
|
131
|
+
*/
|
|
132
|
+
async function readAssetsInline(dir, tagName, assets) {
|
|
133
|
+
const entries = [];
|
|
134
|
+
for (const asset of assets) {
|
|
135
|
+
const relPath = resolveAssetPath(tagName, asset);
|
|
136
|
+
const filePath = path.resolve(dir, relPath);
|
|
137
|
+
try {
|
|
138
|
+
const content = await readFile(filePath, 'utf8');
|
|
139
|
+
entries.push({
|
|
140
|
+
key: asset,
|
|
141
|
+
literal: '`' +
|
|
142
|
+
escapeTemplateLiteral(content)
|
|
143
|
+
.replace(/[\n\t\r]/g, ' ')
|
|
144
|
+
.replace(/\s\s/g, ' ') +
|
|
145
|
+
'`',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (_err) {
|
|
149
|
+
throw new Error(`[load-assets] Cannot resolve asset "${asset}" for component "${tagName}": file not found at ${filePath}`, { cause: _err });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return entries;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Vite plugin factory that inlines companion `.css` / `.html` assets into the
|
|
156
|
+
* DC classes declaring them through `static assets = [...]`.
|
|
157
|
+
*
|
|
158
|
+
* @example Register the plugin in a Vite config, pointing at the source root.
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { loadAssetsPlugin } from './vite-plugin-load-assets';
|
|
161
|
+
* export default defineConfig({ plugins: [loadAssetsPlugin(__dirname)] });
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @param rootDir Directory holding the component source files.
|
|
165
|
+
* @returns A Vite plugin option.
|
|
166
|
+
*/
|
|
167
|
+
export function loadAssetsPlugin(rootDir) {
|
|
168
|
+
return {
|
|
169
|
+
name: 'load-assets-inline',
|
|
170
|
+
async transform(code, id) {
|
|
171
|
+
if (!code.includes('static assets'))
|
|
172
|
+
return null;
|
|
173
|
+
const dir = path.dirname(id);
|
|
174
|
+
// Strip comments so JSDoc examples don't trigger false matches.
|
|
175
|
+
// Whitespace-preserving replacement keeps character indices valid.
|
|
176
|
+
const stripped = stripComments(code);
|
|
177
|
+
// Resolve the component tag name from defineCE(...) call
|
|
178
|
+
const tagName = extractDefineCETag(stripped);
|
|
179
|
+
if (!tagName)
|
|
180
|
+
return null;
|
|
181
|
+
// Match: static assets = [ 'asset1' , 'asset2' ];
|
|
182
|
+
const saRegex = /static\s+assets\s*=\s*\[(.*?)\]\s*;?/gs;
|
|
183
|
+
const saMatches = [...stripped.matchAll(saRegex)];
|
|
184
|
+
if (saMatches.length === 0)
|
|
185
|
+
return null;
|
|
186
|
+
let transformed = code;
|
|
187
|
+
// There should be exactly one per file, but handle all in reverse
|
|
188
|
+
for (let i = saMatches.length - 1; i >= 0; i--) {
|
|
189
|
+
const match = saMatches[i];
|
|
190
|
+
const assetListRaw = match[1];
|
|
191
|
+
const assets = assetListRaw
|
|
192
|
+
.split(',')
|
|
193
|
+
.map((a) => a.trim().replace(/^['"]|['"]$/g, ''))
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
if (assets.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
// Read and inline all asset contents
|
|
198
|
+
const entries = await readAssetsInline(dir, tagName, assets);
|
|
199
|
+
// Build the static _assets property to inject as first line in class body
|
|
200
|
+
const objEntries = entries.map((e) => `${e.key}: ${e.literal}`);
|
|
201
|
+
const assetsObj = `{ ${objEntries.join(', ')} }`;
|
|
202
|
+
// Find the opening brace of the class that owns this `static assets` declaration.
|
|
203
|
+
// Search backwards from the match position to locate "class ... {" — we need the
|
|
204
|
+
// index of the opening brace so we can insert right after it.
|
|
205
|
+
const beforeMatch = stripped.substring(0, match.index);
|
|
206
|
+
const classOpenMatch = /class\s+\w+[^{]*\{/.exec(beforeMatch);
|
|
207
|
+
if (!classOpenMatch)
|
|
208
|
+
continue;
|
|
209
|
+
// Absolute position of the opening brace in `transformed` (= same as in `stripped`)
|
|
210
|
+
const classBraceIndex = classOpenMatch.index + classOpenMatch[0].length - 1;
|
|
211
|
+
// Build injection code — static _assets as first line inside the class body
|
|
212
|
+
const injectCode = `\n static _assets = ${assetsObj};\n`;
|
|
213
|
+
transformed =
|
|
214
|
+
transformed.substring(0, classBraceIndex + 1) +
|
|
215
|
+
injectCode +
|
|
216
|
+
transformed.substring(classBraceIndex + 1);
|
|
217
|
+
}
|
|
218
|
+
return transformed;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twobirds/microcomponents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Micro Components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"PWA",
|
|
@@ -73,6 +73,10 @@
|
|
|
73
73
|
"types": "./dist/mcp/index.d.ts",
|
|
74
74
|
"import": "./dist/mcp/index.js"
|
|
75
75
|
},
|
|
76
|
+
"./vite": {
|
|
77
|
+
"types": "./dist/vite/index.d.ts",
|
|
78
|
+
"import": "./dist/vite/index.js"
|
|
79
|
+
},
|
|
76
80
|
"./package.json": "./package.json"
|
|
77
81
|
},
|
|
78
82
|
"scripts": {
|