@twobirds/microcomponents 0.7.0 → 0.9.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 +309 -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,CA2BhE"}
|
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
|
*/
|
|
@@ -63,11 +63,14 @@ export function scaffoldProject(namespace) {
|
|
|
63
63
|
files['vite.config.ts'] = viteConfig(namespace);
|
|
64
64
|
files['.gitignore'] = gitignore();
|
|
65
65
|
files['opencode.jsonc'] = opencodeConfig();
|
|
66
|
+
files['.opencode/skills/components/SKILL.md'] = componentSkill(namespace, tag);
|
|
66
67
|
files['README.md'] = readme(namespace, tag);
|
|
67
|
-
files['src/index.html'] =
|
|
68
|
+
files['src/index.html'] = prodShell(tag);
|
|
69
|
+
files['src/index-dev.html'] = devShell(namespace, tag);
|
|
68
70
|
files[`src/${base}.ts`] = appSource(namespace, tag);
|
|
69
71
|
files[`src/${base}.target.html`] = appHtml(namespace);
|
|
70
72
|
files[`src/${base}.target.css`] = appCss(tag);
|
|
73
|
+
files[`src/${namespace}/bundle.ts`] = bundle();
|
|
71
74
|
return { files, namespace };
|
|
72
75
|
}
|
|
73
76
|
function packageJson(namespace) {
|
|
@@ -78,11 +81,11 @@ function packageJson(namespace) {
|
|
|
78
81
|
scripts: {
|
|
79
82
|
dev: 'vite',
|
|
80
83
|
build: 'vite build',
|
|
81
|
-
preview: 'vite preview',
|
|
84
|
+
preview: 'vite preview --outDir public --port 5174',
|
|
82
85
|
typecheck: 'tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.vc.json',
|
|
83
86
|
},
|
|
84
87
|
dependencies: {
|
|
85
|
-
'@twobirds/microcomponents': '^0.
|
|
88
|
+
'@twobirds/microcomponents': '^0.9.0',
|
|
86
89
|
},
|
|
87
90
|
devDependencies: {
|
|
88
91
|
vite: '^8.1.5',
|
|
@@ -101,7 +104,9 @@ function tsconfig() {
|
|
|
101
104
|
strict: true,
|
|
102
105
|
esModuleInterop: true,
|
|
103
106
|
skipLibCheck: true,
|
|
104
|
-
|
|
107
|
+
rootDir: 'src',
|
|
108
|
+
outDir: 'build',
|
|
109
|
+
removeComments: true,
|
|
105
110
|
},
|
|
106
111
|
include: ['src/**/*.ts'],
|
|
107
112
|
}, null, '\t');
|
|
@@ -124,7 +129,8 @@ function tsconfigVc() {
|
|
|
124
129
|
}
|
|
125
130
|
function gitignore() {
|
|
126
131
|
return `node_modules/
|
|
127
|
-
|
|
132
|
+
build/
|
|
133
|
+
public/
|
|
128
134
|
`;
|
|
129
135
|
}
|
|
130
136
|
function opencodeConfig() {
|
|
@@ -139,6 +145,86 @@ function opencodeConfig() {
|
|
|
139
145
|
},
|
|
140
146
|
}, null, '\t');
|
|
141
147
|
}
|
|
148
|
+
function componentSkill(namespace, tag) {
|
|
149
|
+
return `---
|
|
150
|
+
name: components
|
|
151
|
+
description: Use when adding, editing, naming, or wiring a micro component in this project, registering a component in the bundle, choosing an event dispatch target, running dev or build, or touching build/ or public/.
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
# Micro Components project
|
|
155
|
+
|
|
156
|
+
Namespace \`${namespace}\`. Every tag starts with \`${namespace}-\`. Components live in \`src/${namespace}/\` and the starter is \`<${tag}>\`.
|
|
157
|
+
|
|
158
|
+
## The chain
|
|
159
|
+
|
|
160
|
+
\`src/\` → \`build/\` → \`public/\`
|
|
161
|
+
|
|
162
|
+
- \`src/\` is the only folder to edit.
|
|
163
|
+
- \`build/\` is the \`tsc\` output plus mirrored companion assets, served by the dev server. Generated.
|
|
164
|
+
- \`public/\` is the production site: \`bundle.js\` with companion assets inlined, \`bundle.css\`, \`index.html\`. Generated.
|
|
165
|
+
|
|
166
|
+
Never edit, rename, or delete anything under \`build/\` or \`public/\`. Change \`src/\` and rebuild.
|
|
167
|
+
|
|
168
|
+
## Add a component
|
|
169
|
+
|
|
170
|
+
\`\`\`sh
|
|
171
|
+
npx mc new ${namespace}-name --target-html --target-css
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
The tag must be kebab-case with at least one hyphen. \`ns-name\` becomes \`src/ns/name.ts\`: the first hyphen becomes the folder, which is the same rule the autoloader uses.
|
|
175
|
+
|
|
176
|
+
Register the component so the production bundle picks it up:
|
|
177
|
+
|
|
178
|
+
\`\`\`ts
|
|
179
|
+
// src/${namespace}/bundle.ts
|
|
180
|
+
import './name.js';
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
A component missing from the barrel loads in dev through \`autoload()\` but is absent from \`public/bundle.js\`.
|
|
184
|
+
|
|
185
|
+
## Companion assets
|
|
186
|
+
|
|
187
|
+
A camelCase descriptor maps to a dotted filename and is listed in \`static assets\`:
|
|
188
|
+
|
|
189
|
+
| descriptor | file | role |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| \`css\` | \`name.css\` | document-level tokens under \`:root\` |
|
|
192
|
+
| \`targetCss\` | \`name.target.css\` | styles scoped to the host tag |
|
|
193
|
+
| \`shadowCss\` | \`name.shadow.css\` | styles inside the shadow root |
|
|
194
|
+
| \`html\` | \`name.html\` | light-DOM template |
|
|
195
|
+
| \`targetHtml\` | \`name.target.html\` | template for \`target.innerHTML\`, \`{key}\` tokens filled by \`parse()\` |
|
|
196
|
+
| \`shadowHtml\` | \`name.shadow.html\` | template for \`shadowRoot.innerHTML\` |
|
|
197
|
+
|
|
198
|
+
The loader fetches exactly the listed descriptors, so every entry in \`static assets\` needs its file.
|
|
199
|
+
|
|
200
|
+
## Events
|
|
201
|
+
|
|
202
|
+
Prefer a direct target with the default local dispatch and a typed payload:
|
|
203
|
+
|
|
204
|
+
\`\`\`ts
|
|
205
|
+
target.dispatchEvent(
|
|
206
|
+
new CustomEvent('${namespace}:action', { detail: { data: { id: 'x' } } })
|
|
207
|
+
);
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
Bubble up or down the tree only when the receiver cannot be named.
|
|
211
|
+
|
|
212
|
+
## Verify
|
|
213
|
+
|
|
214
|
+
\`\`\`sh
|
|
215
|
+
pnpm typecheck
|
|
216
|
+
pnpm dev
|
|
217
|
+
pnpm build
|
|
218
|
+
pnpm preview
|
|
219
|
+
\`\`\`
|
|
220
|
+
|
|
221
|
+
\`mc clean\` removes \`build/\`, \`public/\` and \`coverage/\`.
|
|
222
|
+
|
|
223
|
+
## API reference
|
|
224
|
+
|
|
225
|
+
The \`microcomponents\` MCP server registered in \`opencode.jsonc\` documents every export, the event system, and the concept essays. Query it instead of guessing an API.
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
142
228
|
function readme(namespace, tag) {
|
|
143
229
|
return `# ${namespace} App
|
|
144
230
|
|
|
@@ -156,14 +242,24 @@ pnpm install
|
|
|
156
242
|
pnpm dev
|
|
157
243
|
\`\`\`
|
|
158
244
|
|
|
245
|
+
The dev server runs \`tsc -w\` and serves \`build/\`, so a saved component reaches the
|
|
246
|
+
browser through \`autoload()\` with no bundling. Companion \`.css\`, \`.html\` and \`.json\`
|
|
247
|
+
files are mirrored into \`build/\` next to the compiled component, which is where the
|
|
248
|
+
autoloader looks for them.
|
|
249
|
+
|
|
159
250
|
## Build
|
|
160
251
|
|
|
161
252
|
\`\`\`sh
|
|
162
253
|
pnpm build
|
|
163
254
|
\`\`\`
|
|
164
255
|
|
|
165
|
-
|
|
166
|
-
|
|
256
|
+
\`public/\` is the production site: one minified \`bundle.js\` with every companion asset
|
|
257
|
+
inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the page shell. A
|
|
258
|
+
single script tag loads it, no autoloader needed.
|
|
259
|
+
|
|
260
|
+
\`\`\`sh
|
|
261
|
+
pnpm preview
|
|
262
|
+
\`\`\`
|
|
167
263
|
|
|
168
264
|
## Type-check
|
|
169
265
|
|
|
@@ -171,8 +267,9 @@ Components compile to \`dist/\` at their own autoload path, and companion \`.css
|
|
|
171
267
|
pnpm typecheck
|
|
172
268
|
\`\`\`
|
|
173
269
|
|
|
174
|
-
\`tsconfig.json\`
|
|
175
|
-
\`src/\` and needs node types for its \`node:\`
|
|
270
|
+
\`tsconfig.json\` compiles \`src/\` into \`build/\`; \`tsconfig.vc.json\` checks
|
|
271
|
+
\`vite.config.ts\`, which lives outside \`src/\` and needs node types for its \`node:\`
|
|
272
|
+
imports.
|
|
176
273
|
|
|
177
274
|
## Components
|
|
178
275
|
|
|
@@ -183,31 +280,40 @@ Add a component with:
|
|
|
183
280
|
\`\`\`sh
|
|
184
281
|
npx mc new ${namespace}-name --target-html --target-css
|
|
185
282
|
\`\`\`
|
|
283
|
+
|
|
284
|
+
Then add its import to \`src/${namespace}/bundle.ts\` so the production bundle picks it up.
|
|
285
|
+
|
|
286
|
+
\`.opencode/skills/components/SKILL.md\` carries the same rules for opencode: the
|
|
287
|
+
\`src/\` → \`build/\` → \`public/\` chain, the asset descriptors, the bundle registration,
|
|
288
|
+
and the event dispatch preference.
|
|
186
289
|
`;
|
|
187
290
|
}
|
|
188
291
|
function viteConfig(namespace) {
|
|
189
292
|
return `/**
|
|
190
293
|
* Dev and build config for a Micro Components project.
|
|
191
294
|
*
|
|
192
|
-
* \`src/\`
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* \`
|
|
198
|
-
* \`
|
|
199
|
-
*
|
|
200
|
-
* (\`mc/app.ts\` → \`dist/mc/app.js\`).
|
|
295
|
+
* The chain is \`src/\` → \`build/\` → \`public/\`:
|
|
296
|
+
*
|
|
297
|
+
* - \`build/\` is the \`tsc\` output: one \`.js\` per component at its own autoload path,
|
|
298
|
+
* plus the copied companion assets and the dev shell. The dev server serves it, so
|
|
299
|
+
* the page loads components on demand through \`autoload()\` and nothing is bundled.
|
|
300
|
+
* - \`public/\` is the production output: one minified \`bundle.js\` with every companion
|
|
301
|
+
* asset inlined by \`loadAssetsPlugin\`, the concatenated \`bundle.css\`, and the
|
|
302
|
+
* production shell. A single script tag loads it, no autoloader needed.
|
|
201
303
|
*
|
|
202
304
|
* @module ViteConfig
|
|
203
305
|
*/
|
|
204
306
|
|
|
205
307
|
import { defineConfig, type Plugin, type ResolvedConfig } from 'vite';
|
|
206
|
-
import {
|
|
308
|
+
import { loadAssetsPlugin } from '@twobirds/microcomponents/vite';
|
|
309
|
+
import { spawn } from 'node:child_process';
|
|
310
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
207
311
|
import path from 'node:path';
|
|
208
312
|
|
|
209
313
|
const projectRoot = import.meta.dirname;
|
|
210
314
|
const sourceRoot = path.join(projectRoot, 'src');
|
|
315
|
+
const buildRoot = path.join(projectRoot, 'build');
|
|
316
|
+
const publicRoot = path.join(projectRoot, 'public');
|
|
211
317
|
const namespace = '${namespace}';
|
|
212
318
|
|
|
213
319
|
const pageShells = new Set(['index.html', 'index-dev.html']);
|
|
@@ -229,6 +335,84 @@ function isCompanionAsset(name: string): boolean {
|
|
|
229
335
|
return name.endsWith('.css') || name.endsWith('.json') || name.endsWith('.html');
|
|
230
336
|
}
|
|
231
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Mirrors companion assets and the dev shell from \`src/\` into the served folder,
|
|
340
|
+
* preserving the tree. Autoload resolves \`<ns-app>\` to \`<ns>/app.js\` relative to the
|
|
341
|
+
* page shell, so the served folder needs the compiled component *and* its companion
|
|
342
|
+
* files at the same relative path.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* \`\`\`ts
|
|
346
|
+
* await mirrorAssets(sourceRoot, buildRoot);
|
|
347
|
+
* \`\`\`
|
|
348
|
+
*
|
|
349
|
+
* @param root Source root to walk.
|
|
350
|
+
* @param out Folder to write into.
|
|
351
|
+
* @param rel Path of the walked directory relative to \`root\`.
|
|
352
|
+
*/
|
|
353
|
+
async function mirrorAssets(root: string, out: string, rel = ''): Promise<void> {
|
|
354
|
+
for (const entry of await readdir(path.join(root, rel), { withFileTypes: true })) {
|
|
355
|
+
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
356
|
+
if (entry.isDirectory()) {
|
|
357
|
+
await mirrorAssets(root, out, next);
|
|
358
|
+
} else if (isCompanionAsset(entry.name) || entry.name === 'index-dev.html') {
|
|
359
|
+
const dst = path.join(out, next);
|
|
360
|
+
await mkdir(path.dirname(dst), { recursive: true });
|
|
361
|
+
await cp(path.join(root, next), dst);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Runs \`tsc -w\` for the lifetime of the dev server, so a saved component reaches
|
|
368
|
+
* \`build/\` without a second command.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* \`\`\`ts
|
|
372
|
+
* plugins: [tscWatch()],
|
|
373
|
+
* \`\`\`
|
|
374
|
+
*/
|
|
375
|
+
function tscWatch(): Plugin {
|
|
376
|
+
let child: ReturnType<typeof spawn> | undefined;
|
|
377
|
+
return {
|
|
378
|
+
name: 'tsc-watch',
|
|
379
|
+
configResolved() {
|
|
380
|
+
child = spawn('npx', ['tsc', '-w', '-p', 'tsconfig.json'], { cwd: projectRoot, stdio: 'inherit' });
|
|
381
|
+
},
|
|
382
|
+
closeServer() {
|
|
383
|
+
child?.kill();
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Mirrors companion assets into \`build/\` before the dev server starts, and again on
|
|
390
|
+
* every change, so the served folder stays autoload-ready.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* \`\`\`ts
|
|
394
|
+
* plugins: [syncBuild()],
|
|
395
|
+
* \`\`\`
|
|
396
|
+
*/
|
|
397
|
+
function syncBuild(): Plugin {
|
|
398
|
+
return {
|
|
399
|
+
name: 'sync-build',
|
|
400
|
+
enforce: 'pre',
|
|
401
|
+
async configureServer(server) {
|
|
402
|
+
await mirrorAssets(sourceRoot, buildRoot);
|
|
403
|
+
server.watcher.on('all', async (event, file) => {
|
|
404
|
+
if (!file.startsWith(sourceRoot) || file.endsWith('.ts')) return;
|
|
405
|
+
const rel = path.relative(sourceRoot, file);
|
|
406
|
+
if (isCompanionAsset(path.basename(file)) || file.endsWith('index-dev.html')) {
|
|
407
|
+
const dst = path.join(buildRoot, rel);
|
|
408
|
+
await mkdir(path.dirname(dst), { recursive: true });
|
|
409
|
+
await cp(file, dst);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
232
416
|
/**
|
|
233
417
|
* Serves companion \`.css\`, \`.html\` and \`.json\` files as raw text, ahead of the
|
|
234
418
|
* Vite transform middleware. Without it a \`.html\` request returns the transformed
|
|
@@ -237,10 +421,10 @@ function isCompanionAsset(name: string): boolean {
|
|
|
237
421
|
*
|
|
238
422
|
* @example
|
|
239
423
|
* \`\`\`ts
|
|
240
|
-
* plugins: [rawAssets(
|
|
424
|
+
* plugins: [rawAssets(buildRoot)],
|
|
241
425
|
* \`\`\`
|
|
242
426
|
*
|
|
243
|
-
* @param root Source root that mirrors the
|
|
427
|
+
* @param root Source root that mirrors the served tree.
|
|
244
428
|
*/
|
|
245
429
|
function rawAssets(root: string): Plugin {
|
|
246
430
|
return {
|
|
@@ -266,109 +450,139 @@ function rawAssets(root: string): Plugin {
|
|
|
266
450
|
}
|
|
267
451
|
|
|
268
452
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* at the same relative path.
|
|
453
|
+
* Serves the autoload shell (\`index-dev.html\`) at \`/\`. Vite answers \`/\` with
|
|
454
|
+
* \`index.html\`, which is the production shell referencing \`bundle.js\` — that file
|
|
455
|
+
* only exists in \`public/\`, so the dev server has to redirect to the dev shell.
|
|
273
456
|
*
|
|
274
457
|
* @example
|
|
275
458
|
* \`\`\`ts
|
|
276
|
-
* plugins: [
|
|
459
|
+
* plugins: [devShell()],
|
|
277
460
|
* \`\`\`
|
|
461
|
+
*/
|
|
462
|
+
function devShell(): Plugin {
|
|
463
|
+
return {
|
|
464
|
+
name: 'dev-shell',
|
|
465
|
+
enforce: 'pre',
|
|
466
|
+
configureServer(server) {
|
|
467
|
+
server.middlewares.use((req, res, next) => {
|
|
468
|
+
const url = req.url?.split('?')[0];
|
|
469
|
+
if (url !== '/' && url !== '/index.html') return next();
|
|
470
|
+
req.url = '/index-dev.html';
|
|
471
|
+
next();
|
|
472
|
+
});
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Concatenates the plain \`.css\` token presets under \`src/\` into \`bundle.css\`,
|
|
479
|
+
* skipping the companion \`.target.css\` and \`.shadow.css\` files, which
|
|
480
|
+
* \`loadAssetsPlugin\` inlines into the JS bundle.
|
|
278
481
|
*
|
|
279
|
-
* @
|
|
482
|
+
* @example
|
|
483
|
+
* \`\`\`ts
|
|
484
|
+
* plugins: [bundleCss()],
|
|
485
|
+
* \`\`\`
|
|
280
486
|
*/
|
|
281
|
-
function
|
|
487
|
+
function bundleCss(): Plugin {
|
|
282
488
|
let outDir = '';
|
|
283
489
|
return {
|
|
284
|
-
name: '
|
|
490
|
+
name: 'bundle-css',
|
|
285
491
|
apply: 'build',
|
|
286
492
|
configResolved(config: ResolvedConfig) {
|
|
287
493
|
outDir = path.resolve(config.root, config.build.outDir);
|
|
288
494
|
},
|
|
289
495
|
async closeBundle() {
|
|
290
|
-
async function
|
|
291
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
292
|
-
const
|
|
496
|
+
async function collect(dir: string, rel: string, found: string[]): Promise<void> {
|
|
497
|
+
for (const entry of await readdir(path.join(dir, rel), { withFileTypes: true })) {
|
|
498
|
+
const next = rel ? path.join(rel, entry.name) : entry.name;
|
|
293
499
|
if (entry.isDirectory()) {
|
|
294
|
-
await
|
|
295
|
-
} else if (
|
|
296
|
-
|
|
297
|
-
await mkdir(path.dirname(dst), { recursive: true });
|
|
298
|
-
await cp(src, dst);
|
|
500
|
+
await collect(path.join(dir, rel), next, found);
|
|
501
|
+
} else if (entry.name.endsWith('.css') && !/\\.(target|shadow)\\.css$/.test(entry.name)) {
|
|
502
|
+
found.push(path.join(dir, next));
|
|
299
503
|
}
|
|
300
504
|
}
|
|
301
505
|
}
|
|
302
|
-
|
|
506
|
+
const files: string[] = [];
|
|
507
|
+
await collect(sourceRoot, '', files);
|
|
508
|
+
const parts = await Promise.all(files.map((f) => readFile(f, 'utf-8')));
|
|
509
|
+
await mkdir(outDir, { recursive: true });
|
|
510
|
+
await writeFile(path.join(outDir, 'bundle.css'), parts.join('\\n'), 'utf-8');
|
|
303
511
|
},
|
|
304
512
|
};
|
|
305
513
|
}
|
|
306
514
|
|
|
307
515
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* last \`/\`, so \`info.name\` alone cannot carry a folder — the facade module id is
|
|
311
|
-
* the reliable source.
|
|
516
|
+
* Copies the production shell into \`public/\` next to the bundle, so the served
|
|
517
|
+
* folder is a complete static site.
|
|
312
518
|
*
|
|
313
519
|
* @example
|
|
314
520
|
* \`\`\`ts
|
|
315
|
-
*
|
|
316
|
-
* entryName({ name: 'index', facadeModuleId: '/project/src/index.html' });
|
|
521
|
+
* plugins: [copyShell()],
|
|
317
522
|
* \`\`\`
|
|
318
|
-
*
|
|
319
|
-
* @param info Rollup chunk info.
|
|
320
523
|
*/
|
|
321
|
-
function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
524
|
+
function copyShell(): Plugin {
|
|
525
|
+
let outDir = '';
|
|
526
|
+
return {
|
|
527
|
+
name: 'copy-shell',
|
|
528
|
+
apply: 'build',
|
|
529
|
+
configResolved(config: ResolvedConfig) {
|
|
530
|
+
outDir = path.resolve(config.root, config.build.outDir);
|
|
531
|
+
},
|
|
532
|
+
async closeBundle() {
|
|
533
|
+
await cp(path.join(sourceRoot, 'index.html'), path.join(outDir, 'index.html'));
|
|
534
|
+
},
|
|
535
|
+
};
|
|
325
536
|
}
|
|
326
537
|
|
|
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
|
-
}
|
|
538
|
+
export default defineConfig(({ command }) => {
|
|
539
|
+
if (command === 'serve') {
|
|
540
|
+
return {
|
|
541
|
+
root: buildRoot,
|
|
542
|
+
server: { port: 5173, open: false },
|
|
543
|
+
plugins: [syncBuild(), tscWatch(), rawAssets(buildRoot), devShell()],
|
|
544
|
+
};
|
|
350
545
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export default defineConfig(async () => {
|
|
354
|
-
const entries: Record<string, string> = {};
|
|
355
|
-
await collectEntries(path.join(sourceRoot, namespace), namespace, entries);
|
|
356
546
|
return {
|
|
357
547
|
root: sourceRoot,
|
|
358
548
|
build: {
|
|
359
|
-
outDir:
|
|
549
|
+
outDir: publicRoot,
|
|
360
550
|
emptyOutDir: true,
|
|
551
|
+
minify: true,
|
|
552
|
+
target: 'esnext',
|
|
553
|
+
sourcemap: false,
|
|
554
|
+
lib: {
|
|
555
|
+
entry: path.join(sourceRoot, namespace, 'bundle.ts'),
|
|
556
|
+
formats: ['es'],
|
|
557
|
+
fileName: () => 'bundle.js',
|
|
558
|
+
},
|
|
361
559
|
rollupOptions: {
|
|
362
|
-
|
|
363
|
-
output: {
|
|
560
|
+
treeshake: false,
|
|
561
|
+
output: { comments: false, keepNames: true },
|
|
364
562
|
},
|
|
365
563
|
},
|
|
366
|
-
plugins: [
|
|
564
|
+
plugins: [loadAssetsPlugin(sourceRoot), bundleCss(), copyShell()],
|
|
367
565
|
};
|
|
368
566
|
});
|
|
369
567
|
`;
|
|
370
568
|
}
|
|
371
|
-
function
|
|
569
|
+
function prodShell(tag) {
|
|
570
|
+
return `<!doctype html>
|
|
571
|
+
<html lang="en">
|
|
572
|
+
<head>
|
|
573
|
+
<meta charset="UTF-8" />
|
|
574
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
575
|
+
<title>${tag.replace('-app', ' App')}</title>
|
|
576
|
+
<script type="module" src="bundle.js"></script>
|
|
577
|
+
<link rel="stylesheet" href="./bundle.css" />
|
|
578
|
+
</head>
|
|
579
|
+
<body>
|
|
580
|
+
<${tag}></${tag}>
|
|
581
|
+
</body>
|
|
582
|
+
</html>
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
function devShell(namespace, tag) {
|
|
372
586
|
return `<!doctype html>
|
|
373
587
|
<html lang="en">
|
|
374
588
|
<head>
|
|
@@ -387,6 +601,13 @@ function shell(namespace, tag) {
|
|
|
387
601
|
</html>
|
|
388
602
|
`;
|
|
389
603
|
}
|
|
604
|
+
function bundle() {
|
|
605
|
+
return `// Barrel entry for the production bundle: imports every component so the bundler
|
|
606
|
+
// registers them all in one file. Add a line here when a component is created.
|
|
607
|
+
|
|
608
|
+
import './app.js';
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
390
611
|
function appSource(namespace, tag) {
|
|
391
612
|
const className = kebabToPascal(tag);
|
|
392
613
|
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.9.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.9.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": {
|