@twobirds/microcomponents 0.6.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 +36 -2
- package/dist/cli/create.d.ts +5 -5
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +237 -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
|
@@ -46,6 +46,21 @@ An MCP (Model Context Protocol) server ships with the package so AI assistants c
|
|
|
46
46
|
microcomponents-mcp # stdio server, straight from the installed package
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
`mc create` writes an `opencode.jsonc` into every scaffolded project, so opencode picks the server up as soon as you open the project — no config edit needed. opencode reads the project config from the repo root, and the server is started through `npx`, which resolves the version the project installed:
|
|
50
|
+
|
|
51
|
+
```jsonc
|
|
52
|
+
{
|
|
53
|
+
"$schema": "https://opencode.ai/config.json",
|
|
54
|
+
"mcp": {
|
|
55
|
+
"microcomponents": {
|
|
56
|
+
"type": "local",
|
|
57
|
+
"command": ["npx", "-p", "@twobirds/microcomponents", "microcomponents-mcp"],
|
|
58
|
+
"enabled": true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
49
64
|
In a published package the server runs from `dist/mcp` and reads the prebuilt `api.json` and the concept essays that the build step emits next to it. During development it generates the index live from `src/` at startup, so it cannot drift away from the library it documents:
|
|
50
65
|
|
|
51
66
|
```bash
|
|
@@ -60,6 +75,20 @@ There is no hand-written API list in the server. The whole index is extracted fr
|
|
|
60
75
|
|
|
61
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.
|
|
62
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
|
+
|
|
63
92
|
## CLI
|
|
64
93
|
|
|
65
94
|
A small command-line tool scaffolds new components so the tag-to-file convention is never hand-written:
|
|
@@ -80,20 +109,25 @@ pnpm run cli -- new my-card --target-html --target-css --dry-run
|
|
|
80
109
|
# would create src/my/card.target.css
|
|
81
110
|
```
|
|
82
111
|
|
|
83
|
-
`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/`:
|
|
84
113
|
|
|
85
114
|
```bash
|
|
86
115
|
pnpm run cli -- create --namespace mc --dir mc-app
|
|
87
116
|
# created mc-app/package.json
|
|
117
|
+
# created mc-app/opencode.jsonc
|
|
88
118
|
# created mc-app/src/index.html
|
|
119
|
+
# created mc-app/src/index-dev.html
|
|
89
120
|
# created mc-app/src/mc/app.ts
|
|
90
121
|
# created mc-app/src/mc/app.target.html
|
|
91
122
|
# created mc-app/src/mc/app.target.css
|
|
123
|
+
# created mc-app/src/mc/bundle.ts
|
|
92
124
|
# ...
|
|
93
125
|
# project "mc" created in mc-app/ — run: cd mc-app && pnpm install && pnpm dev
|
|
94
126
|
```
|
|
95
127
|
|
|
96
|
-
|
|
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.
|
|
97
131
|
|
|
98
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.
|
|
99
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
|
*/
|
|
@@ -62,11 +62,14 @@ export function scaffoldProject(namespace) {
|
|
|
62
62
|
files['tsconfig.vc.json'] = tsconfigVc();
|
|
63
63
|
files['vite.config.ts'] = viteConfig(namespace);
|
|
64
64
|
files['.gitignore'] = gitignore();
|
|
65
|
+
files['opencode.jsonc'] = opencodeConfig();
|
|
65
66
|
files['README.md'] = readme(namespace, tag);
|
|
66
|
-
files['src/index.html'] =
|
|
67
|
+
files['src/index.html'] = prodShell(tag);
|
|
68
|
+
files['src/index-dev.html'] = devShell(namespace, tag);
|
|
67
69
|
files[`src/${base}.ts`] = appSource(namespace, tag);
|
|
68
70
|
files[`src/${base}.target.html`] = appHtml(namespace);
|
|
69
71
|
files[`src/${base}.target.css`] = appCss(tag);
|
|
72
|
+
files[`src/${namespace}/bundle.ts`] = bundle();
|
|
70
73
|
return { files, namespace };
|
|
71
74
|
}
|
|
72
75
|
function packageJson(namespace) {
|
|
@@ -77,11 +80,11 @@ function packageJson(namespace) {
|
|
|
77
80
|
scripts: {
|
|
78
81
|
dev: 'vite',
|
|
79
82
|
build: 'vite build',
|
|
80
|
-
preview: 'vite preview',
|
|
83
|
+
preview: 'vite preview --outDir public --port 5174',
|
|
81
84
|
typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
|
|
82
85
|
},
|
|
83
86
|
dependencies: {
|
|
84
|
-
'@twobirds/microcomponents': '^0.
|
|
87
|
+
'@twobirds/microcomponents': '^0.8.0',
|
|
85
88
|
},
|
|
86
89
|
devDependencies: {
|
|
87
90
|
vite: '^8.1.5',
|
|
@@ -100,7 +103,9 @@ function tsconfig() {
|
|
|
100
103
|
strict: true,
|
|
101
104
|
esModuleInterop: true,
|
|
102
105
|
skipLibCheck: true,
|
|
103
|
-
|
|
106
|
+
rootDir: 'src',
|
|
107
|
+
outDir: 'build',
|
|
108
|
+
removeComments: true,
|
|
104
109
|
},
|
|
105
110
|
include: ['src/**/*.ts'],
|
|
106
111
|
}, null, '\t');
|
|
@@ -123,9 +128,22 @@ function tsconfigVc() {
|
|
|
123
128
|
}
|
|
124
129
|
function gitignore() {
|
|
125
130
|
return `node_modules/
|
|
126
|
-
|
|
131
|
+
build/
|
|
132
|
+
public/
|
|
127
133
|
`;
|
|
128
134
|
}
|
|
135
|
+
function opencodeConfig() {
|
|
136
|
+
return JSON.stringify({
|
|
137
|
+
$schema: 'https://opencode.ai/config.json',
|
|
138
|
+
mcp: {
|
|
139
|
+
microcomponents: {
|
|
140
|
+
type: 'local',
|
|
141
|
+
command: ['npx', '-p', '@twobirds/microcomponents', 'microcomponents-mcp'],
|
|
142
|
+
enabled: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
}, null, '\t');
|
|
146
|
+
}
|
|
129
147
|
function readme(namespace, tag) {
|
|
130
148
|
return `# ${namespace} App
|
|
131
149
|
|
|
@@ -143,14 +161,24 @@ pnpm install
|
|
|
143
161
|
pnpm dev
|
|
144
162
|
\`\`\`
|
|
145
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
|
+
|
|
146
169
|
## Build
|
|
147
170
|
|
|
148
171
|
\`\`\`sh
|
|
149
172
|
pnpm build
|
|
150
173
|
\`\`\`
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
\`\`\`
|
|
154
182
|
|
|
155
183
|
## Type-check
|
|
156
184
|
|
|
@@ -158,8 +186,9 @@ Components compile to \`dist/\` at their own autoload path, and companion \`.css
|
|
|
158
186
|
pnpm typecheck
|
|
159
187
|
\`\`\`
|
|
160
188
|
|
|
161
|
-
\`tsconfig.json\`
|
|
162
|
-
\`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.
|
|
163
192
|
|
|
164
193
|
## Components
|
|
165
194
|
|
|
@@ -170,31 +199,36 @@ Add a component with:
|
|
|
170
199
|
\`\`\`sh
|
|
171
200
|
npx mc new ${namespace}-name --target-html --target-css
|
|
172
201
|
\`\`\`
|
|
202
|
+
|
|
203
|
+
Then add its import to \`src/${namespace}/bundle.ts\` so the production bundle picks it up.
|
|
173
204
|
`;
|
|
174
205
|
}
|
|
175
206
|
function viteConfig(namespace) {
|
|
176
207
|
return `/**
|
|
177
208
|
* Dev and build config for a Micro Components project.
|
|
178
209
|
*
|
|
179
|
-
* \`src/\`
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* \`
|
|
185
|
-
* \`
|
|
186
|
-
*
|
|
187
|
-
* (\`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.
|
|
188
218
|
*
|
|
189
219
|
* @module ViteConfig
|
|
190
220
|
*/
|
|
191
221
|
|
|
192
222
|
import { defineConfig, type Plugin, type ResolvedConfig } from 'vite';
|
|
193
|
-
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';
|
|
194
226
|
import path from 'node:path';
|
|
195
227
|
|
|
196
228
|
const projectRoot = import.meta.dirname;
|
|
197
229
|
const sourceRoot = path.join(projectRoot, 'src');
|
|
230
|
+
const buildRoot = path.join(projectRoot, 'build');
|
|
231
|
+
const publicRoot = path.join(projectRoot, 'public');
|
|
198
232
|
const namespace = '${namespace}';
|
|
199
233
|
|
|
200
234
|
const pageShells = new Set(['index.html', 'index-dev.html']);
|
|
@@ -216,6 +250,84 @@ function isCompanionAsset(name: string): boolean {
|
|
|
216
250
|
return name.endsWith('.css') || name.endsWith('.json') || name.endsWith('.html');
|
|
217
251
|
}
|
|
218
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
|
+
|
|
219
331
|
/**
|
|
220
332
|
* Serves companion \`.css\`, \`.html\` and \`.json\` files as raw text, ahead of the
|
|
221
333
|
* Vite transform middleware. Without it a \`.html\` request returns the transformed
|
|
@@ -224,10 +336,10 @@ function isCompanionAsset(name: string): boolean {
|
|
|
224
336
|
*
|
|
225
337
|
* @example
|
|
226
338
|
* \`\`\`ts
|
|
227
|
-
* plugins: [rawAssets(
|
|
339
|
+
* plugins: [rawAssets(buildRoot)],
|
|
228
340
|
* \`\`\`
|
|
229
341
|
*
|
|
230
|
-
* @param root Source root that mirrors the
|
|
342
|
+
* @param root Source root that mirrors the served tree.
|
|
231
343
|
*/
|
|
232
344
|
function rawAssets(root: string): Plugin {
|
|
233
345
|
return {
|
|
@@ -253,109 +365,139 @@ function rawAssets(root: string): Plugin {
|
|
|
253
365
|
}
|
|
254
366
|
|
|
255
367
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* 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.
|
|
260
371
|
*
|
|
261
372
|
* @example
|
|
262
373
|
* \`\`\`ts
|
|
263
|
-
* plugins: [
|
|
374
|
+
* plugins: [devShell()],
|
|
264
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.
|
|
265
396
|
*
|
|
266
|
-
* @
|
|
397
|
+
* @example
|
|
398
|
+
* \`\`\`ts
|
|
399
|
+
* plugins: [bundleCss()],
|
|
400
|
+
* \`\`\`
|
|
267
401
|
*/
|
|
268
|
-
function
|
|
402
|
+
function bundleCss(): Plugin {
|
|
269
403
|
let outDir = '';
|
|
270
404
|
return {
|
|
271
|
-
name: '
|
|
405
|
+
name: 'bundle-css',
|
|
272
406
|
apply: 'build',
|
|
273
407
|
configResolved(config: ResolvedConfig) {
|
|
274
408
|
outDir = path.resolve(config.root, config.build.outDir);
|
|
275
409
|
},
|
|
276
410
|
async closeBundle() {
|
|
277
|
-
async function
|
|
278
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
279
|
-
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;
|
|
280
414
|
if (entry.isDirectory()) {
|
|
281
|
-
await
|
|
282
|
-
} else if (
|
|
283
|
-
|
|
284
|
-
await mkdir(path.dirname(dst), { recursive: true });
|
|
285
|
-
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));
|
|
286
418
|
}
|
|
287
419
|
}
|
|
288
420
|
}
|
|
289
|
-
|
|
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');
|
|
290
426
|
},
|
|
291
427
|
};
|
|
292
428
|
}
|
|
293
429
|
|
|
294
430
|
/**
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* last \`/\`, so \`info.name\` alone cannot carry a folder — the facade module id is
|
|
298
|
-
* the reliable source.
|
|
431
|
+
* Copies the production shell into \`public/\` next to the bundle, so the served
|
|
432
|
+
* folder is a complete static site.
|
|
299
433
|
*
|
|
300
434
|
* @example
|
|
301
435
|
* \`\`\`ts
|
|
302
|
-
*
|
|
303
|
-
* entryName({ name: 'index', facadeModuleId: '/project/src/index.html' });
|
|
436
|
+
* plugins: [copyShell()],
|
|
304
437
|
* \`\`\`
|
|
305
|
-
*
|
|
306
|
-
* @param info Rollup chunk info.
|
|
307
438
|
*/
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
};
|
|
312
451
|
}
|
|
313
452
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
* const entries: Record<string, string> = {};
|
|
322
|
-
* await collectEntries(path.join(sourceRoot, 'mc'), 'mc', entries);
|
|
323
|
-
* \`\`\`
|
|
324
|
-
*
|
|
325
|
-
* @param dir Directory to walk.
|
|
326
|
-
* @param rel Path of that directory relative to \`src/\`.
|
|
327
|
-
* @param entries Accumulated entries, mutated in place.
|
|
328
|
-
*/
|
|
329
|
-
async function collectEntries(dir: string, rel: string, entries: Record<string, string>): Promise<void> {
|
|
330
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
331
|
-
const full = path.join(dir, entry.name);
|
|
332
|
-
if (entry.isDirectory()) {
|
|
333
|
-
await collectEntries(full, path.join(rel, entry.name), entries);
|
|
334
|
-
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.js')) {
|
|
335
|
-
entries[path.join(rel, entry.name).replace(/\\.(ts|js)$/, '')] = full;
|
|
336
|
-
}
|
|
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
|
+
};
|
|
337
460
|
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export default defineConfig(async () => {
|
|
341
|
-
const entries: Record<string, string> = {};
|
|
342
|
-
await collectEntries(path.join(sourceRoot, namespace), namespace, entries);
|
|
343
461
|
return {
|
|
344
462
|
root: sourceRoot,
|
|
345
463
|
build: {
|
|
346
|
-
outDir:
|
|
464
|
+
outDir: publicRoot,
|
|
347
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
|
+
},
|
|
348
474
|
rollupOptions: {
|
|
349
|
-
|
|
350
|
-
output: {
|
|
475
|
+
treeshake: false,
|
|
476
|
+
output: { comments: false, keepNames: true },
|
|
351
477
|
},
|
|
352
478
|
},
|
|
353
|
-
plugins: [
|
|
479
|
+
plugins: [loadAssetsPlugin(sourceRoot), bundleCss(), copyShell()],
|
|
354
480
|
};
|
|
355
481
|
});
|
|
356
482
|
`;
|
|
357
483
|
}
|
|
358
|
-
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) {
|
|
359
501
|
return `<!doctype html>
|
|
360
502
|
<html lang="en">
|
|
361
503
|
<head>
|
|
@@ -374,6 +516,13 @@ function shell(namespace, tag) {
|
|
|
374
516
|
</html>
|
|
375
517
|
`;
|
|
376
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
|
+
}
|
|
377
526
|
function appSource(namespace, tag) {
|
|
378
527
|
const className = kebabToPascal(tag);
|
|
379
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": {
|