generator-white-label 8.0.0 → 9.0.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/CLI.md CHANGED
@@ -7,7 +7,7 @@ CLI ──────> createProject() ──────> project
7
7
  Node API ─┘
8
8
  ```
9
9
 
10
- `createProject()` owns the scaffold. The CLI translates command-line input into that API.
10
+ `createProject()` owns the scaffold, including destination safety. The CLI translates command-line input into that API.
11
11
 
12
12
  ## Create a project
13
13
 
@@ -25,7 +25,7 @@ yarn dlx generator-white-label create my-project
25
25
  pnpm dlx generator-white-label create my-project
26
26
  ```
27
27
 
28
- The CLI creates into a new or existing **empty** directory. It refuses a non-empty destination so a mistyped path cannot overwrite existing project files.
28
+ Project creation accepts a missing or existing **empty** directory. A non-empty destination is rejected before scaffold files are copied or written.
29
29
 
30
30
  Then install and test the generated project with npm, Yarn, or pnpm:
31
31
 
@@ -52,7 +52,7 @@ npx generator-white-label create my-project --no-jsx
52
52
 
53
53
  The same `--jsx` and `--no-jsx` flags work through `yarn dlx` and `pnpm dlx`.
54
54
 
55
- If no choice can be asked interactively and neither flag is supplied, JSX remains the default for backward compatibility.
55
+ If no choice can be asked interactively and neither flag is supplied, JSX is the default.
56
56
 
57
57
  Run `white-label --help` for usage. See [`PACKAGE_MANAGERS.md`](PACKAGE_MANAGERS.md) for package-manager compatibility details.
58
58
 
@@ -69,13 +69,13 @@ await createProject({
69
69
 
70
70
  Set `jsx: true` for JSX/TSX templates or `jsx: false` for plain TypeScript and HTML strings. Use `jsx: false` as the starting point when another template engine should own rendering. Omitting `jsx` defaults to `true`.
71
71
 
72
- `createProject()` is the lower-level programmatic API and does not apply the CLI's non-empty-directory guard. Applications using it directly own destination-policy decisions.
72
+ `createProject()` uses the same destination-safety rule as the CLI: missing and empty directories are allowed; non-empty directories are rejected before project content is written.
73
73
 
74
74
  ## Read the implementation
75
75
 
76
76
  The source is intentionally small enough to teach the design:
77
77
 
78
- - `scaffold/index.ts` — project creation
78
+ - `scaffold/index.ts` — project creation and destination-safety boundary
79
79
  - `cli/index.ts` — command-line adapter
80
80
  - `test/` — executable contracts
81
81
 
package/README.md CHANGED
@@ -60,6 +60,32 @@ DOM
60
60
 
61
61
  Both generated variants provide the same application behavior and progressive enhancement. The only difference is template syntax.
62
62
 
63
+ ## A simple View click event
64
+
65
+ Use View lifecycle hooks to add and remove browser listeners with the same callback reference:
66
+
67
+ ```ts
68
+ import View from 'white-label-view';
69
+
70
+ class ButtonView extends View {
71
+ handleClick = () => {
72
+ console.log('Clicked');
73
+ };
74
+
75
+ addListeners() {
76
+ this.element.addEventListener('click', this.handleClick);
77
+ return this;
78
+ }
79
+
80
+ removeListeners() {
81
+ this.element.removeEventListener('click', this.handleClick);
82
+ return this;
83
+ }
84
+ }
85
+ ```
86
+
87
+ `addListeners()` runs when the View mounts. `removeListeners()` runs before replacement or destruction, so the listener lifecycle stays owned by the View.
88
+
63
89
  ## Read the source
64
90
 
65
91
  For the default JSX scaffold, a useful reading order is:
@@ -140,7 +166,9 @@ npx generator-white-label create my-project --no-jsx
140
166
 
141
167
  The same `--jsx` and `--no-jsx` options work through `yarn dlx` and `pnpm dlx`.
142
168
 
143
- If no interactive answer is available and neither flag is supplied, JSX is the default for backward compatibility.
169
+ If no interactive answer is available and neither flag is supplied, JSX is the default.
170
+
171
+ The generator refuses to layer a scaffold over an existing non-empty destination. This protection is enforced by the shared `createProject()` engine, so it applies to both the CLI and programmatic use. An existing empty directory is allowed; a missing directory is created as part of generation.
144
172
 
145
173
  See [`CLI.md`](CLI.md) for the CLI contract and [`PACKAGE_MANAGERS.md`](PACKAGE_MANAGERS.md) for package-manager compatibility details.
146
174
 
@@ -160,9 +188,9 @@ await createProject({
160
188
 
161
189
  Set `jsx: true` for JSX/TSX templates or `jsx: false` for plain TypeScript and HTML strings. Use `jsx: false` as the starting point for a third-party template engine. Omitting `jsx` defaults to `true`.
162
190
 
163
- `createProject(options)` returns `Promise<void>`. A successful call resolves with `undefined`; file-system failures reject the promise instead of returning a status value.
191
+ `createProject(options)` returns `Promise<void>`. A successful call resolves with `undefined`; file-system failures reject the promise instead of returning a status value. If the destination already exists and contains files, it rejects before copying or writing project content.
164
192
 
165
- `createProject()` is the canonical creation API. The CLI and future integrations are adapters around it rather than separate generation systems.
193
+ `createProject()` is the canonical creation API. The CLI and future integrations are adapters around it rather than separate generation systems. Keeping overwrite policy here prevents adapters from accidentally bypassing the same safety boundary.
166
194
 
167
195
  This is the same design principle used throughout White Label: one responsibility, one implementation, explicit adapters at environment boundaries.
168
196
 
@@ -170,7 +198,7 @@ This is the same design principle used throughout White Label: one responsibilit
170
198
 
171
199
  | Path | Purpose |
172
200
  | --- | --- |
173
- | `scaffold/index.ts` | Canonical `createProject()` implementation |
201
+ | `scaffold/index.ts` | Canonical `createProject()` implementation and destination-safety boundary |
174
202
  | `scaffold/no-jsx/` | Plain-TypeScript template equivalents |
175
203
  | `cli/index.ts` | First-party command-line adapter |
176
204
  | `app/*.tsx` | Default JSX top-level static pages |
@@ -208,7 +236,7 @@ With `--no-jsx`, the equivalent page is ordinary TypeScript returning an HTML st
208
236
 
209
237
  For larger pages, compose focused views instead of growing one renderer indefinitely.
210
238
 
211
- JSX expressions are escaped by default. In plain-TypeScript templates or third-party engines, applications own the engine's escaping and raw-output configuration. See [Template engines and JSX options](https://whitelabeljs.org/docs/view/#template-engines) for the tested matrix and trust boundaries.
239
+ White Label JSX HTML-escapes ordinary child text and ordinary attribute values by default, rejects intrinsic `on*` event-handler attributes, and uses runtime-owned identity for trusted JSX/raw values. That escaping is not a general-purpose sanitizer for URL or CSS semantics. Plain-TypeScript templates and third-party engines likewise remain responsible for their own contextual escaping, sanitization, raw-output features, and configuration. See [Template engines and JSX options](https://whitelabeljs.org/docs/view/#template-engines) for the tested matrix and trust boundaries.
212
240
 
213
241
  ## Progressive enhancement
214
242
 
@@ -231,7 +259,7 @@ Cloud-specific adapters should stay at the boundary. Translate an AWS/Vercel/Net
231
259
 
232
260
  Generated-project tests exercise sequential warm invocations, concurrent requests, request-data escaping, execution without browser globals, and a browser/Web-target bundle smoke test. They also enforce a 100 kB minified serverless-composition bundle budget and a 750 ms fresh-process handler-import budget. These are regression guards, not universal latency guarantees.
233
261
 
234
- The Web-target bundle smoke test catches unresolved Node built-ins, but it does **not** claim blanket Cloudflare/Deno/edge-provider compatibility. The published runtime packages still document Node as their supported server runtime; verify a specific edge provider before deployment.
262
+ The Web-target bundle smoke test catches unresolved Node built-ins, but it does **not** claim blanket Cloudflare/Deno/edge-provider compatibility. The published runtime packages document Node as their supported server runtime; verify a specific edge provider before deployment.
235
263
 
236
264
  ## Build and verify
237
265
 
@@ -263,7 +291,7 @@ npm run audit
263
291
  npm pack --dry-run
264
292
  ```
265
293
 
266
- Tests are part of the documentation. They demonstrate intended contracts while protecting behavior. Executable project source is held to 100% statement, branch, function, and line coverage.
294
+ Tests are part of the documentation. They demonstrate intended contracts while protecting behavior. Executable project source is held to 100% statement, branch, function, and line coverage. CI also checks the documented Node 22.18 minimum, the primary Node 24 line, packed CLI/programmatic API installation, and generated-project compatibility across npm, Yarn, and pnpm.
267
295
 
268
296
  ## White Label ecosystem
269
297
 
@@ -272,4 +300,4 @@ Tests are part of the documentation. They demonstrate intended contracts while p
272
300
  - [`white-label-mediator`](https://github.com/bshack/white-label-mediator) — application events.
273
301
  - [`white-label-router`](https://github.com/bshack/white-label-router) — routing and URL state.
274
302
 
275
- The generated project imports the real packages rather than reproducing their behavior locally. That makes it both an example and an ecosystem integration test.
303
+ The generated project imports the real packages rather than reproducing their behavior locally. That makes it both an example and an ecosystem integration test.
package/app/README.md CHANGED
@@ -48,6 +48,32 @@ The build output is `_deploy`. The test suite checks the starter content, progre
48
48
 
49
49
  Pages live in `app/*.tsx`, page data lives in `app/assets/data/view`, browser code lives in `app/assets/script`, the provider-neutral serverless example lives in `server/handler.ts`, and shared styles live in `app/assets/style`. TypeScript is configured with `jsx: react-jsx` and `jsxImportSource: white-label-view`, so this scaffold's JSX does not require React.
50
50
 
51
+ ## Simple View click event
52
+
53
+ View lifecycle hooks are the simplest place to own a browser listener:
54
+
55
+ ```ts
56
+ import View from 'white-label-view';
57
+
58
+ class ButtonView extends View {
59
+ handleClick = () => {
60
+ console.log('Clicked');
61
+ };
62
+
63
+ addListeners() {
64
+ this.element.addEventListener('click', this.handleClick);
65
+ return this;
66
+ }
67
+
68
+ removeListeners() {
69
+ this.element.removeEventListener('click', this.handleClick);
70
+ return this;
71
+ }
72
+ }
73
+ ```
74
+
75
+ The same callback reference is used for both registration and cleanup. View calls `removeListeners()` before replacement or destruction.
76
+
51
77
  ## Serverless / function runtimes
52
78
 
53
79
  `server/handler.ts` demonstrates a cloud-agnostic server function using the Web `Request` and `Response` APIs. It composes a request-scoped Mediator, Model, Router, and server View, then destroys those mutable instances before the invocation completes.
@@ -32,25 +32,25 @@ export function initializeTaskApplication(documentRoot: Document): TaskApplicati
32
32
  const status = parentElement.querySelector<HTMLElement>('[data-task-status]')!;
33
33
  const updateStatus = (): void => {status.textContent = describeTaskStatus(model.get());};
34
34
 
35
- const addTask = (title: string): void => {model.add(title); updateStatus();};
36
- const toggleTask = (id: number): void => {model.toggle(id); updateStatus();};
37
- const setFilter = (filter: 'all' | 'active' | 'completed'): void => {model.setFilter(filter); updateStatus();};
38
- mediator.on('task:add', addTask);
39
- mediator.on('task:toggle', toggleTask);
40
- mediator.on('task:filter', setFilter);
35
+ const addTask = (event: CustomEvent<string>): void => {model.add(event.detail); updateStatus();};
36
+ const toggleTask = (event: CustomEvent<number>): void => {model.toggle(event.detail); updateStatus();};
37
+ const setFilter = (event: CustomEvent<'all' | 'active' | 'completed'>): void => {model.setFilter(event.detail); updateStatus();};
38
+ mediator.addEventListener('task:add', addTask);
39
+ mediator.addEventListener('task:toggle', toggleTask);
40
+ mediator.addEventListener('task:filter', setFilter);
41
41
 
42
42
  const delegated = view.delegate(parentElement);
43
43
  delegated.on('submit', '[data-task-form]', (event: Event) => {
44
44
  event.preventDefault();
45
45
  const form = event.target as HTMLFormElement;
46
46
  const input = form.querySelector<HTMLInputElement>('[name="task"]')!;
47
- mediator.emit('task:add', input.value);
47
+ mediator.dispatchEvent(new CustomEvent('task:add', {detail: input.value}));
48
48
  parentElement.querySelector<HTMLInputElement>('[name="task"]')!.focus();
49
49
  });
50
50
  delegated.on('change', '[data-task-toggle]', (event: Event) => {
51
51
  const input = event.target as HTMLInputElement;
52
52
  const id = Number(input.dataset.taskId);
53
- mediator.emit('task:toggle', id);
53
+ mediator.dispatchEvent(new CustomEvent('task:toggle', {detail: id}));
54
54
  const focusTarget = parentElement.querySelector<HTMLInputElement>(`[data-task-id="${id}"]`)
55
55
  ?? parentElement.querySelector<HTMLAnchorElement>('[data-task-filter][aria-current="page"]')!;
56
56
  focusTarget.focus();
@@ -66,9 +66,9 @@ export function initializeTaskApplication(documentRoot: Document): TaskApplicati
66
66
  destroy() {
67
67
  delegated.clear();
68
68
  router.destroy();
69
- mediator.removeListener('task:add', addTask);
70
- mediator.removeListener('task:toggle', toggleTask);
71
- mediator.removeListener('task:filter', setFilter);
69
+ mediator.removeEventListener('task:add', addTask);
70
+ mediator.removeEventListener('task:toggle', toggleTask);
71
+ mediator.removeEventListener('task:filter', setFilter);
72
72
  view.destroy();
73
73
  model.destroy();
74
74
  mediator.destroy();
@@ -1,20 +1,15 @@
1
1
  import Mediator from 'white-label-mediator';
2
2
  import type {TaskFilter} from '../../view/examples/tasks/task-state.js';
3
3
 
4
- /**
5
- * Document the event protocol in one place even though the currently installed
6
- * Mediator exposes the EventEmitter-compatible runtime without a generic map.
7
- * Keeping names and payloads explicit still gives learners one clear contract
8
- * to follow when tracing the example.
9
- */
4
+ /** Application event vocabulary shared by the generated task modules. */
10
5
  export type TaskEvents = {
11
- 'task:add': [title: string];
12
- 'task:filter': [filter: TaskFilter];
13
- 'task:toggle': [id: number];
6
+ 'task:add': string;
7
+ 'task:filter': TaskFilter;
8
+ 'task:toggle': number;
14
9
  };
15
10
 
16
- export type TaskMediator = Mediator;
11
+ export type TaskMediator = Mediator<TaskEvents>;
17
12
 
18
13
  export function createTaskMediator(): TaskMediator {
19
- return new Mediator().initialize();
14
+ return new Mediator<TaskEvents>().initialize();
20
15
  }
@@ -22,7 +22,7 @@ export function createTaskRouter(documentRoot: Document, mediator: TaskMediator)
22
22
  const route = (_scope: Element | null, location: {data: {query: Record<string, string>}}): void => {
23
23
  const filter = normalizeTaskFilter(location.data.query.tasks);
24
24
  const restoreFocus = documentRoot.activeElement?.hasAttribute('data-task-filter') === true;
25
- mediator.emit('task:filter', filter);
25
+ mediator.dispatchEvent(new CustomEvent('task:filter', {detail: filter}));
26
26
  if (restoreFocus) {documentRoot.querySelector<HTMLAnchorElement>(`[data-task-filter="${filter}"]`)!.focus();}
27
27
  };
28
28
  router.routes = {'/': route, defaultRoute: route};
@@ -28,7 +28,7 @@ export default function PackageDocsSection() {
28
28
  <p className="eyebrow">Model</p><h3>State is observable, not magical.</h3>
29
29
  <CodeBlock lines={[
30
30
  <><span className={syntax.keyword}>const</span> model = <span className={syntax.keyword}>new</span> <span className={syntax.type}>Model</span>({'{'}count: 0{'}'});</>,
31
- <>model.on(<span className={syntax.value}>'change'</span>, state =&gt; render(state));</>,
31
+ <>model.addEventListener(<span className={syntax.value}>'change'</span>, event =&gt; render(event.detail));</>,
32
32
  <>model.update({'{'}count: 1{'}'});</>
33
33
  ]} />
34
34
  <p><a href={repositories.model}>Model documentation</a></p>
@@ -46,8 +46,8 @@ export default function PackageDocsSection() {
46
46
  <article>
47
47
  <p className="eyebrow">Mediator</p><h3>Modules communicate through events.</h3>
48
48
  <CodeBlock lines={[
49
- <>mediator.on(<span className={syntax.value}>'counter:increment'</span>, increment);</>,
50
- <>mediator.emit(<span className={syntax.value}>'counter:increment'</span>);</>
49
+ <>mediator.addEventListener(<span className={syntax.value}>'counter:increment'</span>, increment);</>,
50
+ <>mediator.dispatchEvent(<span className={syntax.keyword}>new</span> <span className={syntax.type}>CustomEvent</span>(<span className={syntax.value}>'counter:increment'</span>));</>
51
51
  ]} />
52
52
  <p><a href={repositories.mediator}>Mediator documentation</a></p>
53
53
  </article>
@@ -56,7 +56,7 @@ export default function PackageDocsSection() {
56
56
  <CodeBlock lines={[
57
57
  <>router.routes = {'{'}</>,
58
58
  <> <span className={syntax.value}>'/'</span>: (_scope, location) =&gt; {'{'}</>,
59
- <> mediator.emit(<span className={syntax.value}>'filter:set'</span>, location.data.query.filter);</>,
59
+ <> mediator.dispatchEvent(<span className={syntax.keyword}>new</span> <span className={syntax.type}>CustomEvent</span>(<span className={syntax.value}>'filter:set'</span>, {'{'}detail: location.data.query.filter{'}'}));</>,
60
60
  <> {'}'}</>,
61
61
  <>{'}'};</>
62
62
  ]} />
package/cli/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import {realpathSync} from 'node:fs';
3
4
  import {readdir} from 'node:fs/promises';
4
5
  import path from 'node:path';
5
6
  import process from 'node:process';
@@ -106,7 +107,7 @@ export async function runCli(args: readonly string[], dependencies: CliDependenc
106
107
 
107
108
  const invokedPath = path.resolve(process.argv[1] as string);
108
109
  const modulePath = fileURLToPath(import.meta.url);
109
- if (invokedPath === modulePath) {
110
+ if (realpathSync(invokedPath) === realpathSync(modulePath)) {
110
111
  runCli(process.argv.slice(2)).then(
111
112
  (exitCode) => {process.exitCode = exitCode;},
112
113
  (error: unknown) => {
@@ -20,24 +20,24 @@ export function initializeTaskApplication(documentRoot) {
20
20
  const view = createTaskView(parentElement, model);
21
21
  const status = parentElement.querySelector('[data-task-status]');
22
22
  const updateStatus = () => { status.textContent = describeTaskStatus(model.get()); };
23
- const addTask = (title) => { model.add(title); updateStatus(); };
24
- const toggleTask = (id) => { model.toggle(id); updateStatus(); };
25
- const setFilter = (filter) => { model.setFilter(filter); updateStatus(); };
26
- mediator.on('task:add', addTask);
27
- mediator.on('task:toggle', toggleTask);
28
- mediator.on('task:filter', setFilter);
23
+ const addTask = (event) => { model.add(event.detail); updateStatus(); };
24
+ const toggleTask = (event) => { model.toggle(event.detail); updateStatus(); };
25
+ const setFilter = (event) => { model.setFilter(event.detail); updateStatus(); };
26
+ mediator.addEventListener('task:add', addTask);
27
+ mediator.addEventListener('task:toggle', toggleTask);
28
+ mediator.addEventListener('task:filter', setFilter);
29
29
  const delegated = view.delegate(parentElement);
30
30
  delegated.on('submit', '[data-task-form]', (event) => {
31
31
  event.preventDefault();
32
32
  const form = event.target;
33
33
  const input = form.querySelector('[name="task"]');
34
- mediator.emit('task:add', input.value);
34
+ mediator.dispatchEvent(new CustomEvent('task:add', { detail: input.value }));
35
35
  parentElement.querySelector('[name="task"]').focus();
36
36
  });
37
37
  delegated.on('change', '[data-task-toggle]', (event) => {
38
38
  const input = event.target;
39
39
  const id = Number(input.dataset.taskId);
40
- mediator.emit('task:toggle', id);
40
+ mediator.dispatchEvent(new CustomEvent('task:toggle', { detail: id }));
41
41
  const focusTarget = parentElement.querySelector(`[data-task-id="${id}"]`)
42
42
  ?? parentElement.querySelector('[data-task-filter][aria-current="page"]');
43
43
  focusTarget.focus();
@@ -51,9 +51,9 @@ export function initializeTaskApplication(documentRoot) {
51
51
  destroy() {
52
52
  delegated.clear();
53
53
  router.destroy();
54
- mediator.removeListener('task:add', addTask);
55
- mediator.removeListener('task:toggle', toggleTask);
56
- mediator.removeListener('task:filter', setFilter);
54
+ mediator.removeEventListener('task:add', addTask);
55
+ mediator.removeEventListener('task:toggle', toggleTask);
56
+ mediator.removeEventListener('task:filter', setFilter);
57
57
  view.destroy();
58
58
  model.destroy();
59
59
  mediator.destroy();
@@ -1 +1 @@
1
- {"version":3,"file":"TaskApplication.js","sourceRoot":"","sources":["../../../../../app/assets/script/tasks/TaskApplication.ts"],"names":[],"mappings":"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","sourcesContent":["import type Router from 'white-label-router';\nimport type View from 'white-label-view';\nimport {createInitialTaskState, describeTaskStatus} from '../../view/examples/tasks/task-state.js';\nimport {createTaskMediator, type TaskMediator} from './TaskMediator.js';\nimport TaskModel from './TaskModel.js';\nimport {createTaskRouter} from './TaskRouter.js';\nimport {createTaskView} from './TaskView.js';\n\n/** Public handles make the example straightforward to test and tear down. */\nexport interface TaskApplication {\n mediator: TaskMediator;\n model: TaskModel;\n router: Router;\n view: View;\n destroy(): void;\n}\n\n/**\n * Compose the application at one explicit boundary.\n *\n * This is the only module that knows every moving part. Model, View, Router,\n * and Mediator stay independently understandable, while this file documents\n * the small amount of wiring needed to make them cooperate in a real feature.\n */\nexport function initializeTaskApplication(documentRoot: Document): TaskApplication {\n const parentElement = documentRoot.querySelector<HTMLElement>('[data-task-example]');\n if (!parentElement) {throw new TypeError('White Label task example requires a data-task-example container');}\n\n const mediator = createTaskMediator();\n const model = new TaskModel(createInitialTaskState());\n const view = createTaskView(parentElement, model);\n const status = parentElement.querySelector<HTMLElement>('[data-task-status]')!;\n const updateStatus = (): void => {status.textContent = describeTaskStatus(model.get());};\n\n const addTask = (title: string): void => {model.add(title); updateStatus();};\n const toggleTask = (id: number): void => {model.toggle(id); updateStatus();};\n const setFilter = (filter: 'all' | 'active' | 'completed'): void => {model.setFilter(filter); updateStatus();};\n mediator.on('task:add', addTask);\n mediator.on('task:toggle', toggleTask);\n mediator.on('task:filter', setFilter);\n\n const delegated = view.delegate(parentElement);\n delegated.on('submit', '[data-task-form]', (event: Event) => {\n event.preventDefault();\n const form = event.target as HTMLFormElement;\n const input = form.querySelector<HTMLInputElement>('[name=\"task\"]')!;\n mediator.emit('task:add', input.value);\n parentElement.querySelector<HTMLInputElement>('[name=\"task\"]')!.focus();\n });\n delegated.on('change', '[data-task-toggle]', (event: Event) => {\n const input = event.target as HTMLInputElement;\n const id = Number(input.dataset.taskId);\n mediator.emit('task:toggle', id);\n const focusTarget = parentElement.querySelector<HTMLInputElement>(`[data-task-id=\"${id}\"]`)\n ?? parentElement.querySelector<HTMLAnchorElement>('[data-task-filter][aria-current=\"page\"]')!;\n focusTarget.focus();\n });\n\n const router = createTaskRouter(documentRoot, mediator);\n\n return {\n mediator,\n model,\n router,\n view,\n destroy() {\n delegated.clear();\n router.destroy();\n mediator.removeListener('task:add', addTask);\n mediator.removeListener('task:toggle', toggleTask);\n mediator.removeListener('task:filter', setFilter);\n view.destroy();\n model.destroy();\n mediator.destroy();\n }\n };\n}\n"]}
1
+ {"version":3,"file":"TaskApplication.js","sourceRoot":"","sources":["../../../../../app/assets/script/tasks/TaskApplication.ts"],"names":[],"mappings":"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","sourcesContent":["import type Router from 'white-label-router';\nimport type View from 'white-label-view';\nimport {createInitialTaskState, describeTaskStatus} from '../../view/examples/tasks/task-state.js';\nimport {createTaskMediator, type TaskMediator} from './TaskMediator.js';\nimport TaskModel from './TaskModel.js';\nimport {createTaskRouter} from './TaskRouter.js';\nimport {createTaskView} from './TaskView.js';\n\n/** Public handles make the example straightforward to test and tear down. */\nexport interface TaskApplication {\n mediator: TaskMediator;\n model: TaskModel;\n router: Router;\n view: View;\n destroy(): void;\n}\n\n/**\n * Compose the application at one explicit boundary.\n *\n * This is the only module that knows every moving part. Model, View, Router,\n * and Mediator stay independently understandable, while this file documents\n * the small amount of wiring needed to make them cooperate in a real feature.\n */\nexport function initializeTaskApplication(documentRoot: Document): TaskApplication {\n const parentElement = documentRoot.querySelector<HTMLElement>('[data-task-example]');\n if (!parentElement) {throw new TypeError('White Label task example requires a data-task-example container');}\n\n const mediator = createTaskMediator();\n const model = new TaskModel(createInitialTaskState());\n const view = createTaskView(parentElement, model);\n const status = parentElement.querySelector<HTMLElement>('[data-task-status]')!;\n const updateStatus = (): void => {status.textContent = describeTaskStatus(model.get());};\n\n const addTask = (event: CustomEvent<string>): void => {model.add(event.detail); updateStatus();};\n const toggleTask = (event: CustomEvent<number>): void => {model.toggle(event.detail); updateStatus();};\n const setFilter = (event: CustomEvent<'all' | 'active' | 'completed'>): void => {model.setFilter(event.detail); updateStatus();};\n mediator.addEventListener('task:add', addTask);\n mediator.addEventListener('task:toggle', toggleTask);\n mediator.addEventListener('task:filter', setFilter);\n\n const delegated = view.delegate(parentElement);\n delegated.on('submit', '[data-task-form]', (event: Event) => {\n event.preventDefault();\n const form = event.target as HTMLFormElement;\n const input = form.querySelector<HTMLInputElement>('[name=\"task\"]')!;\n mediator.dispatchEvent(new CustomEvent('task:add', {detail: input.value}));\n parentElement.querySelector<HTMLInputElement>('[name=\"task\"]')!.focus();\n });\n delegated.on('change', '[data-task-toggle]', (event: Event) => {\n const input = event.target as HTMLInputElement;\n const id = Number(input.dataset.taskId);\n mediator.dispatchEvent(new CustomEvent('task:toggle', {detail: id}));\n const focusTarget = parentElement.querySelector<HTMLInputElement>(`[data-task-id=\"${id}\"]`)\n ?? parentElement.querySelector<HTMLAnchorElement>('[data-task-filter][aria-current=\"page\"]')!;\n focusTarget.focus();\n });\n\n const router = createTaskRouter(documentRoot, mediator);\n\n return {\n mediator,\n model,\n router,\n view,\n destroy() {\n delegated.clear();\n router.destroy();\n mediator.removeEventListener('task:add', addTask);\n mediator.removeEventListener('task:toggle', toggleTask);\n mediator.removeEventListener('task:filter', setFilter);\n view.destroy();\n model.destroy();\n mediator.destroy();\n }\n };\n}\n"]}
@@ -1,15 +1,10 @@
1
1
  import Mediator from 'white-label-mediator';
2
2
  import type { TaskFilter } from '../../view/examples/tasks/task-state.js';
3
- /**
4
- * Document the event protocol in one place even though the currently installed
5
- * Mediator exposes the EventEmitter-compatible runtime without a generic map.
6
- * Keeping names and payloads explicit still gives learners one clear contract
7
- * to follow when tracing the example.
8
- */
3
+ /** Application event vocabulary shared by the generated task modules. */
9
4
  export type TaskEvents = {
10
- 'task:add': [title: string];
11
- 'task:filter': [filter: TaskFilter];
12
- 'task:toggle': [id: number];
5
+ 'task:add': string;
6
+ 'task:filter': TaskFilter;
7
+ 'task:toggle': number;
13
8
  };
14
- export type TaskMediator = Mediator;
9
+ export type TaskMediator = Mediator<TaskEvents>;
15
10
  export declare function createTaskMediator(): TaskMediator;
@@ -1 +1 @@
1
- {"version":3,"file":"TaskMediator.js","sourceRoot":"","sources":["../../../../../app/assets/script/tasks/TaskMediator.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,sBAAsB,CAAC;AAiB5C,MAAM,UAAU,kBAAkB;IAC9B,OAAO,IAAI,QAAQ,EAAE,CAAC,UAAU,EAAE,CAAC;AACvC,CAAC","sourcesContent":["import Mediator from 'white-label-mediator';\nimport type {TaskFilter} from '../../view/examples/tasks/task-state.js';\n\n/**\n * Document the event protocol in one place even though the currently installed\n * Mediator exposes the EventEmitter-compatible runtime without a generic map.\n * Keeping names and payloads explicit still gives learners one clear contract\n * to follow when tracing the example.\n */\nexport type TaskEvents = {\n 'task:add': [title: string];\n 'task:filter': [filter: TaskFilter];\n 'task:toggle': [id: number];\n};\n\nexport type TaskMediator = Mediator;\n\nexport function createTaskMediator(): TaskMediator {\n return new Mediator().initialize();\n}\n"]}
1
+ {"version":3,"file":"TaskMediator.js","sourceRoot":"","sources":["../../../../../app/assets/script/tasks/TaskMediator.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,sBAAsB,CAAC;AAY5C,MAAM,UAAU,kBAAkB;IAC9B,OAAO,IAAI,QAAQ,EAAc,CAAC,UAAU,EAAE,CAAC;AACnD,CAAC","sourcesContent":["import Mediator from 'white-label-mediator';\nimport type {TaskFilter} from '../../view/examples/tasks/task-state.js';\n\n/** Application event vocabulary shared by the generated task modules. */\nexport type TaskEvents = {\n 'task:add': string;\n 'task:filter': TaskFilter;\n 'task:toggle': number;\n};\n\nexport type TaskMediator = Mediator<TaskEvents>;\n\nexport function createTaskMediator(): TaskMediator {\n return new Mediator<TaskEvents>().initialize();\n}\n"]}
@@ -17,7 +17,7 @@ export function createTaskRouter(documentRoot, mediator) {
17
17
  const route = (_scope, location) => {
18
18
  const filter = normalizeTaskFilter(location.data.query.tasks);
19
19
  const restoreFocus = documentRoot.activeElement?.hasAttribute('data-task-filter') === true;
20
- mediator.emit('task:filter', filter);
20
+ mediator.dispatchEvent(new CustomEvent('task:filter', { detail: filter }));
21
21
  if (restoreFocus) {
22
22
  documentRoot.querySelector(`[data-task-filter="${filter}"]`).focus();
23
23
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TaskRouter.js","sourceRoot":"","sources":["../../../../../app/assets/script/tasks/TaskRouter.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAIxC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;AAExE,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,KAAyB;IACzD,OAAO,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,KAAmB,CAAC,CAAC,CAAC,CAAC,KAAmB,CAAC,CAAC,CAAC,KAAK,CAAC;AACvF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,YAAsB,EAAE,QAAsB;IAC3E,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,KAAK,GAAG,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,MAAsB,EAAE,QAAiD,EAAQ,EAAE;QAC9F,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9D,MAAM,YAAY,GAAG,YAAY,CAAC,aAAa,EAAE,YAAY,CAAC,kBAAkB,CAAC,KAAK,IAAI,CAAC;QAC3F,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;QACrC,IAAI,YAAY,EAAE,CAAC;YAAA,YAAY,CAAC,aAAa,CAAoB,sBAAsB,MAAM,IAAI,CAAE,CAAC,KAAK,EAAE,CAAC;QAAA,CAAC;IACjH,CAAC,CAAC;IACF,MAAM,CAAC,MAAM,GAAG,EAAC,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAC,CAAC;IAClD,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;AAC/B,CAAC","sourcesContent":["import Router from 'white-label-router';\nimport type {TaskFilter} from '../../view/examples/tasks/task-state.js';\nimport type {TaskMediator} from './TaskMediator.js';\n\nconst taskFilters = new Set<TaskFilter>(['all', 'active', 'completed']);\n\n/** Normalize URL input at the routing boundary before it reaches domain state. */\nexport function normalizeTaskFilter(value: string | undefined): TaskFilter {\n return value && taskFilters.has(value as TaskFilter) ? value as TaskFilter : 'all';\n}\n\n/**\n * Router translates URL state into an application event.\n *\n * It does not update the Model or DOM directly. That separation keeps routing\n * usable in different applications and makes browser navigation easy to test.\n */\nexport function createTaskRouter(documentRoot: Document, mediator: TaskMediator): Router {\n const router = new Router();\n router.scope = documentRoot.querySelector('main');\n router.mediator = mediator;\n const route = (_scope: Element | null, location: {data: {query: Record<string, string>}}): void => {\n const filter = normalizeTaskFilter(location.data.query.tasks);\n const restoreFocus = documentRoot.activeElement?.hasAttribute('data-task-filter') === true;\n mediator.emit('task:filter', filter);\n if (restoreFocus) {documentRoot.querySelector<HTMLAnchorElement>(`[data-task-filter=\"${filter}\"]`)!.focus();}\n };\n router.routes = {'/': route, defaultRoute: route};\n return router.initialize();\n}\n"]}
1
+ {"version":3,"file":"TaskRouter.js","sourceRoot":"","sources":["../../../../../app/assets/script/tasks/TaskRouter.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAIxC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;AAExE,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,KAAyB;IACzD,OAAO,KAAK,IAAI,WAAW,CAAC,GAAG,CAAC,KAAmB,CAAC,CAAC,CAAC,CAAC,KAAmB,CAAC,CAAC,CAAC,KAAK,CAAC;AACvF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,YAAsB,EAAE,QAAsB;IAC3E,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,KAAK,GAAG,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,MAAsB,EAAE,QAAiD,EAAQ,EAAE;QAC9F,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9D,MAAM,YAAY,GAAG,YAAY,CAAC,aAAa,EAAE,YAAY,CAAC,kBAAkB,CAAC,KAAK,IAAI,CAAC;QAC3F,QAAQ,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,aAAa,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,CAAC,CAAC,CAAC;QACzE,IAAI,YAAY,EAAE,CAAC;YAAA,YAAY,CAAC,aAAa,CAAoB,sBAAsB,MAAM,IAAI,CAAE,CAAC,KAAK,EAAE,CAAC;QAAA,CAAC;IACjH,CAAC,CAAC;IACF,MAAM,CAAC,MAAM,GAAG,EAAC,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAC,CAAC;IAClD,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;AAC/B,CAAC","sourcesContent":["import Router from 'white-label-router';\nimport type {TaskFilter} from '../../view/examples/tasks/task-state.js';\nimport type {TaskMediator} from './TaskMediator.js';\n\nconst taskFilters = new Set<TaskFilter>(['all', 'active', 'completed']);\n\n/** Normalize URL input at the routing boundary before it reaches domain state. */\nexport function normalizeTaskFilter(value: string | undefined): TaskFilter {\n return value && taskFilters.has(value as TaskFilter) ? value as TaskFilter : 'all';\n}\n\n/**\n * Router translates URL state into an application event.\n *\n * It does not update the Model or DOM directly. That separation keeps routing\n * usable in different applications and makes browser navigation easy to test.\n */\nexport function createTaskRouter(documentRoot: Document, mediator: TaskMediator): Router {\n const router = new Router();\n router.scope = documentRoot.querySelector('main');\n router.mediator = mediator;\n const route = (_scope: Element | null, location: {data: {query: Record<string, string>}}): void => {\n const filter = normalizeTaskFilter(location.data.query.tasks);\n const restoreFocus = documentRoot.activeElement?.hasAttribute('data-task-filter') === true;\n mediator.dispatchEvent(new CustomEvent('task:filter', {detail: filter}));\n if (restoreFocus) {documentRoot.querySelector<HTMLAnchorElement>(`[data-task-filter=\"${filter}\"]`)!.focus();}\n };\n router.routes = {'/': route, defaultRoute: route};\n return router.initialize();\n}\n"]}
@@ -10,7 +10,7 @@ const repositories = {
10
10
  export default function PackageDocsSection() {
11
11
  return (_jsx("section", { className: "section", id: "packages", "aria-labelledby": "packages-title", children: _jsxs("div", { className: "container", children: [_jsxs("div", { className: "section-intro", children: [_jsx("p", { className: "eyebrow", children: "Documentation" }), _jsx("h2", { id: "packages-title", children: "The core flow stays explicit." }), _jsx("p", { children: "Each package has one job, can be used independently, and stays easy to replace or test because application concerns are not hidden behind a framework." })] }), _jsx("div", { className: "architecture", children: _jsxs("ol", { children: [_jsxs("li", { children: [_jsx("strong", { children: "Router" }), _jsx("span", { children: "turns a URL into application intent" })] }), _jsxs("li", { children: [_jsx("strong", { children: "Mediator" }), _jsx("span", { children: "coordinates that intent between modules" })] }), _jsxs("li", { children: [_jsx("strong", { children: "Model" }), _jsx("span", { children: "stores and publishes application state" })] }), _jsxs("li", { children: [_jsx("strong", { children: "View" }), _jsx("span", { children: "renders the resulting interface" })] })] }) }), _jsxs("div", { className: "docs-grid", children: [_jsxs("article", { children: [_jsx("p", { className: "eyebrow", children: "Model" }), _jsx("h3", { children: "State is observable, not magical." }), _jsx(CodeBlock, { lines: [
12
12
  _jsxs(_Fragment, { children: [_jsx("span", { className: syntax.keyword, children: "const" }), " model = ", _jsx("span", { className: syntax.keyword, children: "new" }), " ", _jsx("span", { className: syntax.type, children: "Model" }), "(", '{', "count: 0", '}', ");"] }),
13
- _jsxs(_Fragment, { children: ["model.on(", _jsx("span", { className: syntax.value, children: "'change'" }), ", state => render(state));"] }),
13
+ _jsxs(_Fragment, { children: ["model.addEventListener(", _jsx("span", { className: syntax.value, children: "'change'" }), ", event => render(event.detail));"] }),
14
14
  _jsxs(_Fragment, { children: ["model.update(", '{', "count: 1", '}', ");"] })
15
15
  ] }), _jsx("p", { children: _jsx("a", { href: repositories.model, children: "Model documentation" }) })] }), _jsxs("article", { children: [_jsx("p", { className: "eyebrow", children: "View" }), _jsx("h3", { children: "JSX renders through White Label." }), _jsx(CodeBlock, { lines: [
16
16
  _jsxs(_Fragment, { children: [_jsx("span", { className: syntax.keyword, children: "const" }), " view = ", _jsx("span", { className: syntax.keyword, children: "new" }), " ", _jsx("span", { className: syntax.type, children: "View" }), "(", '{'] }),
@@ -18,12 +18,12 @@ export default function PackageDocsSection() {
18
18
  _jsxs(_Fragment, { children: [" template: state => <p>", '{', "state.count", '}', "</p>"] }),
19
19
  _jsxs(_Fragment, { children: ['}', ").initialize();"] })
20
20
  ] }), _jsx("p", { children: _jsx("a", { href: repositories.view, children: "View documentation" }) })] }), _jsxs("article", { children: [_jsx("p", { className: "eyebrow", children: "Mediator" }), _jsx("h3", { children: "Modules communicate through events." }), _jsx(CodeBlock, { lines: [
21
- _jsxs(_Fragment, { children: ["mediator.on(", _jsx("span", { className: syntax.value, children: "'counter:increment'" }), ", increment);"] }),
22
- _jsxs(_Fragment, { children: ["mediator.emit(", _jsx("span", { className: syntax.value, children: "'counter:increment'" }), ");"] })
21
+ _jsxs(_Fragment, { children: ["mediator.addEventListener(", _jsx("span", { className: syntax.value, children: "'counter:increment'" }), ", increment);"] }),
22
+ _jsxs(_Fragment, { children: ["mediator.dispatchEvent(", _jsx("span", { className: syntax.keyword, children: "new" }), " ", _jsx("span", { className: syntax.type, children: "CustomEvent" }), "(", _jsx("span", { className: syntax.value, children: "'counter:increment'" }), "));"] })
23
23
  ] }), _jsx("p", { children: _jsx("a", { href: repositories.mediator, children: "Mediator documentation" }) })] }), _jsxs("article", { children: [_jsx("p", { className: "eyebrow", children: "Router" }), _jsx("h3", { children: "Routes describe intent." }), _jsx(CodeBlock, { lines: [
24
24
  _jsxs(_Fragment, { children: ["router.routes = ", '{'] }),
25
25
  _jsxs(_Fragment, { children: [" ", _jsx("span", { className: syntax.value, children: "'/'" }), ": (_scope, location) => ", '{'] }),
26
- _jsxs(_Fragment, { children: [" mediator.emit(", _jsx("span", { className: syntax.value, children: "'filter:set'" }), ", location.data.query.filter);"] }),
26
+ _jsxs(_Fragment, { children: [" mediator.dispatchEvent(", _jsx("span", { className: syntax.keyword, children: "new" }), " ", _jsx("span", { className: syntax.type, children: "CustomEvent" }), "(", _jsx("span", { className: syntax.value, children: "'filter:set'" }), ", ", '{', "detail: location.data.query.filter", '}', "));"] }),
27
27
  _jsxs(_Fragment, { children: [" ", '}'] }),
28
28
  _jsxs(_Fragment, { children: ['}', ";"] })
29
29
  ] }), _jsx("p", { children: _jsx("a", { href: repositories.router, children: "Router documentation" }) })] })] })] }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"PackageDocsSection.js","sourceRoot":"","sources":["../../../../../app/assets/view/sections/PackageDocsSection.tsx"],"names":[],"mappings":"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","sourcesContent":["import CodeBlock, {syntax} from '../CodeBlock.js';\n\nconst repositories = {\n mediator: 'https://github.com/bshack/white-label-mediator',\n model: 'https://github.com/bshack/white-label-model',\n router: 'https://github.com/bshack/white-label-router',\n view: 'https://github.com/bshack/white-label-view'\n};\n\n/** Package documentation stays close to the example it describes. */\nexport default function PackageDocsSection() {\n return (\n <section className=\"section\" id=\"packages\" aria-labelledby=\"packages-title\">\n <div className=\"container\">\n <div className=\"section-intro\">\n <p className=\"eyebrow\">Documentation</p>\n <h2 id=\"packages-title\">The core flow stays explicit.</h2>\n <p>Each package has one job, can be used independently, and stays easy to replace or test because application concerns are not hidden behind a framework.</p>\n </div>\n <div className=\"architecture\"><ol>\n <li><strong>Router</strong><span>turns a URL into application intent</span></li>\n <li><strong>Mediator</strong><span>coordinates that intent between modules</span></li>\n <li><strong>Model</strong><span>stores and publishes application state</span></li>\n <li><strong>View</strong><span>renders the resulting interface</span></li>\n </ol></div>\n <div className=\"docs-grid\">\n <article>\n <p className=\"eyebrow\">Model</p><h3>State is observable, not magical.</h3>\n <CodeBlock lines={[\n <><span className={syntax.keyword}>const</span> model = <span className={syntax.keyword}>new</span> <span className={syntax.type}>Model</span>({'{'}count: 0{'}'});</>,\n <>model.on(<span className={syntax.value}>'change'</span>, state =&gt; render(state));</>,\n <>model.update({'{'}count: 1{'}'});</>\n ]} />\n <p><a href={repositories.model}>Model documentation</a></p>\n </article>\n <article>\n <p className=\"eyebrow\">View</p><h3>JSX renders through White Label.</h3>\n <CodeBlock lines={[\n <><span className={syntax.keyword}>const</span> view = <span className={syntax.keyword}>new</span> <span className={syntax.type}>View</span>({'{'}</>,\n <> model,</>,\n <> template: state =&gt; &lt;p&gt;{'{'}state.count{'}'}&lt;/p&gt;</>,\n <>{'}'}).initialize();</>\n ]} />\n <p><a href={repositories.view}>View documentation</a></p>\n </article>\n <article>\n <p className=\"eyebrow\">Mediator</p><h3>Modules communicate through events.</h3>\n <CodeBlock lines={[\n <>mediator.on(<span className={syntax.value}>'counter:increment'</span>, increment);</>,\n <>mediator.emit(<span className={syntax.value}>'counter:increment'</span>);</>\n ]} />\n <p><a href={repositories.mediator}>Mediator documentation</a></p>\n </article>\n <article>\n <p className=\"eyebrow\">Router</p><h3>Routes describe intent.</h3>\n <CodeBlock lines={[\n <>router.routes = {'{'}</>,\n <> <span className={syntax.value}>'/'</span>: (_scope, location) =&gt; {'{'}</>,\n <> mediator.emit(<span className={syntax.value}>'filter:set'</span>, location.data.query.filter);</>,\n <> {'}'}</>,\n <>{'}'};</>\n ]} />\n <p><a href={repositories.router}>Router documentation</a></p>\n </article>\n </div>\n </div>\n </section>\n );\n}\n"]}
1
+ {"version":3,"file":"PackageDocsSection.js","sourceRoot":"","sources":["../../../../../app/assets/view/sections/PackageDocsSection.tsx"],"names":[],"mappings":"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","sourcesContent":["import CodeBlock, {syntax} from '../CodeBlock.js';\n\nconst repositories = {\n mediator: 'https://github.com/bshack/white-label-mediator',\n model: 'https://github.com/bshack/white-label-model',\n router: 'https://github.com/bshack/white-label-router',\n view: 'https://github.com/bshack/white-label-view'\n};\n\n/** Package documentation stays close to the example it describes. */\nexport default function PackageDocsSection() {\n return (\n <section className=\"section\" id=\"packages\" aria-labelledby=\"packages-title\">\n <div className=\"container\">\n <div className=\"section-intro\">\n <p className=\"eyebrow\">Documentation</p>\n <h2 id=\"packages-title\">The core flow stays explicit.</h2>\n <p>Each package has one job, can be used independently, and stays easy to replace or test because application concerns are not hidden behind a framework.</p>\n </div>\n <div className=\"architecture\"><ol>\n <li><strong>Router</strong><span>turns a URL into application intent</span></li>\n <li><strong>Mediator</strong><span>coordinates that intent between modules</span></li>\n <li><strong>Model</strong><span>stores and publishes application state</span></li>\n <li><strong>View</strong><span>renders the resulting interface</span></li>\n </ol></div>\n <div className=\"docs-grid\">\n <article>\n <p className=\"eyebrow\">Model</p><h3>State is observable, not magical.</h3>\n <CodeBlock lines={[\n <><span className={syntax.keyword}>const</span> model = <span className={syntax.keyword}>new</span> <span className={syntax.type}>Model</span>({'{'}count: 0{'}'});</>,\n <>model.addEventListener(<span className={syntax.value}>'change'</span>, event =&gt; render(event.detail));</>,\n <>model.update({'{'}count: 1{'}'});</>\n ]} />\n <p><a href={repositories.model}>Model documentation</a></p>\n </article>\n <article>\n <p className=\"eyebrow\">View</p><h3>JSX renders through White Label.</h3>\n <CodeBlock lines={[\n <><span className={syntax.keyword}>const</span> view = <span className={syntax.keyword}>new</span> <span className={syntax.type}>View</span>({'{'}</>,\n <> model,</>,\n <> template: state =&gt; &lt;p&gt;{'{'}state.count{'}'}&lt;/p&gt;</>,\n <>{'}'}).initialize();</>\n ]} />\n <p><a href={repositories.view}>View documentation</a></p>\n </article>\n <article>\n <p className=\"eyebrow\">Mediator</p><h3>Modules communicate through events.</h3>\n <CodeBlock lines={[\n <>mediator.addEventListener(<span className={syntax.value}>'counter:increment'</span>, increment);</>,\n <>mediator.dispatchEvent(<span className={syntax.keyword}>new</span> <span className={syntax.type}>CustomEvent</span>(<span className={syntax.value}>'counter:increment'</span>));</>\n ]} />\n <p><a href={repositories.mediator}>Mediator documentation</a></p>\n </article>\n <article>\n <p className=\"eyebrow\">Router</p><h3>Routes describe intent.</h3>\n <CodeBlock lines={[\n <>router.routes = {'{'}</>,\n <> <span className={syntax.value}>'/'</span>: (_scope, location) =&gt; {'{'}</>,\n <> mediator.dispatchEvent(<span className={syntax.keyword}>new</span> <span className={syntax.type}>CustomEvent</span>(<span className={syntax.value}>'filter:set'</span>, {'{'}detail: location.data.query.filter{'}'}));</>,\n <> {'}'}</>,\n <>{'}'};</>\n ]} />\n <p><a href={repositories.router}>Router documentation</a></p>\n </article>\n </div>\n </div>\n </section>\n );\n}\n"]}
package/dist/cli/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
2
3
  import { readdir } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import process from 'node:process';
@@ -88,7 +89,7 @@ export async function runCli(args, dependencies = {}) {
88
89
  }
89
90
  const invokedPath = path.resolve(process.argv[1]);
90
91
  const modulePath = fileURLToPath(import.meta.url);
91
- if (invokedPath === modulePath) {
92
+ if (realpathSync(invokedPath) === realpathSync(modulePath)) {
92
93
  runCli(process.argv.slice(2)).then((exitCode) => { process.exitCode = exitCode; }, (error) => {
93
94
  process.stderr.write(`Unable to create White Label project: ${formatCliError(error)}\n`);
94
95
  process.exitCode = 1;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../cli/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport {readdir} from 'node:fs/promises';\nimport path from 'node:path';\nimport process from 'node:process';\nimport {createInterface} from 'node:readline/promises';\nimport {fileURLToPath} from 'node:url';\nimport {createProject} from '../scaffold/index.js';\n\nexport interface CliDependencies {\n cwd?: () => string;\n createProject?: typeof createProject;\n stdout?: Pick<NodeJS.WriteStream, 'write'>;\n stderr?: Pick<NodeJS.WriteStream, 'write'>;\n input?: NodeJS.ReadableStream;\n output?: NodeJS.WritableStream;\n isInteractive?: boolean;\n}\n\nfunction usage() {\n return [\n 'Usage:',\n ' white-label create <directory> [--jsx | --no-jsx]',\n '',\n 'Commands:',\n ' create <directory> Create a White Label project.',\n '',\n 'Options:',\n ' --jsx Generate TypeScript with JSX/TSX templates.',\n ' --no-jsx Generate plain TypeScript with HTML string templates.',\n ' -h, --help Show this help message.'\n ].join('\\n');\n}\n\nexport function formatCliError(error: unknown) {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function askForJsx(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<boolean> {\n const readline = createInterface({input, output});\n try {\n const answer = await readline.question(\n 'Use JSX/TSX for page and view templates? Choose Yes for JSX syntax like <section>...</section>, or No for plain TypeScript that returns HTML strings. (Y/n) '\n );\n return !/^n(?:o)?$/i.test(answer.trim());\n } finally {\n readline.close();\n }\n}\n\n/** Reject an existing non-empty target so a mistyped CLI destination cannot overwrite project files. */\nasync function assertDestinationAvailable(destination: string) {\n try {\n const entries = await readdir(destination);\n if (entries.length) {\n throw new Error(`Destination directory must be empty: ${destination}`);\n }\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {return;}\n throw error;\n }\n}\n\nexport async function runCli(args: readonly string[], dependencies: CliDependencies = {}) {\n const cwd = dependencies.cwd ?? process.cwd;\n const scaffold = dependencies.createProject ?? createProject;\n const stdout = dependencies.stdout ?? process.stdout;\n const stderr = dependencies.stderr ?? process.stderr;\n const input = dependencies.input ?? process.stdin;\n const output = dependencies.output ?? process.stdout;\n const [command, destination, ...options] = args;\n\n if (command === '--help' || command === '-h' || args.length === 0) {\n stdout.write(`${usage()}\\n`);\n return 0;\n }\n\n const allowedOptions = new Set(['--jsx', '--no-jsx']);\n if (\n command !== 'create' ||\n !destination ||\n options.some(option => !allowedOptions.has(option)) ||\n (options.includes('--jsx') && options.includes('--no-jsx'))\n ) {\n stderr.write(`${usage()}\\n`);\n return 1;\n }\n\n let jsx: boolean;\n if (options.includes('--jsx')) {\n jsx = true;\n } else if (options.includes('--no-jsx')) {\n jsx = false;\n } else if (dependencies.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY)) {\n jsx = await askForJsx(input, output);\n } else {\n jsx = true;\n }\n\n const resolvedDestination = path.resolve(cwd(), destination);\n await assertDestinationAvailable(resolvedDestination);\n await scaffold({destination: resolvedDestination, jsx});\n stdout.write(`Created White Label project at ${resolvedDestination}\\n`);\n return 0;\n}\n\nconst invokedPath = path.resolve(process.argv[1] as string);\nconst modulePath = fileURLToPath(import.meta.url);\nif (invokedPath === modulePath) {\n runCli(process.argv.slice(2)).then(\n (exitCode) => {process.exitCode = exitCode;},\n (error: unknown) => {\n process.stderr.write(`Unable to create White Label project: ${formatCliError(error)}\\n`);\n process.exitCode = 1;\n }\n );\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../cli/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport {realpathSync} from 'node:fs';\nimport {readdir} from 'node:fs/promises';\nimport path from 'node:path';\nimport process from 'node:process';\nimport {createInterface} from 'node:readline/promises';\nimport {fileURLToPath} from 'node:url';\nimport {createProject} from '../scaffold/index.js';\n\nexport interface CliDependencies {\n cwd?: () => string;\n createProject?: typeof createProject;\n stdout?: Pick<NodeJS.WriteStream, 'write'>;\n stderr?: Pick<NodeJS.WriteStream, 'write'>;\n input?: NodeJS.ReadableStream;\n output?: NodeJS.WritableStream;\n isInteractive?: boolean;\n}\n\nfunction usage() {\n return [\n 'Usage:',\n ' white-label create <directory> [--jsx | --no-jsx]',\n '',\n 'Commands:',\n ' create <directory> Create a White Label project.',\n '',\n 'Options:',\n ' --jsx Generate TypeScript with JSX/TSX templates.',\n ' --no-jsx Generate plain TypeScript with HTML string templates.',\n ' -h, --help Show this help message.'\n ].join('\\n');\n}\n\nexport function formatCliError(error: unknown) {\n return error instanceof Error ? error.message : String(error);\n}\n\nasync function askForJsx(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<boolean> {\n const readline = createInterface({input, output});\n try {\n const answer = await readline.question(\n 'Use JSX/TSX for page and view templates? Choose Yes for JSX syntax like <section>...</section>, or No for plain TypeScript that returns HTML strings. (Y/n) '\n );\n return !/^n(?:o)?$/i.test(answer.trim());\n } finally {\n readline.close();\n }\n}\n\n/** Reject an existing non-empty target so a mistyped CLI destination cannot overwrite project files. */\nasync function assertDestinationAvailable(destination: string) {\n try {\n const entries = await readdir(destination);\n if (entries.length) {\n throw new Error(`Destination directory must be empty: ${destination}`);\n }\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {return;}\n throw error;\n }\n}\n\nexport async function runCli(args: readonly string[], dependencies: CliDependencies = {}) {\n const cwd = dependencies.cwd ?? process.cwd;\n const scaffold = dependencies.createProject ?? createProject;\n const stdout = dependencies.stdout ?? process.stdout;\n const stderr = dependencies.stderr ?? process.stderr;\n const input = dependencies.input ?? process.stdin;\n const output = dependencies.output ?? process.stdout;\n const [command, destination, ...options] = args;\n\n if (command === '--help' || command === '-h' || args.length === 0) {\n stdout.write(`${usage()}\\n`);\n return 0;\n }\n\n const allowedOptions = new Set(['--jsx', '--no-jsx']);\n if (\n command !== 'create' ||\n !destination ||\n options.some(option => !allowedOptions.has(option)) ||\n (options.includes('--jsx') && options.includes('--no-jsx'))\n ) {\n stderr.write(`${usage()}\\n`);\n return 1;\n }\n\n let jsx: boolean;\n if (options.includes('--jsx')) {\n jsx = true;\n } else if (options.includes('--no-jsx')) {\n jsx = false;\n } else if (dependencies.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY)) {\n jsx = await askForJsx(input, output);\n } else {\n jsx = true;\n }\n\n const resolvedDestination = path.resolve(cwd(), destination);\n await assertDestinationAvailable(resolvedDestination);\n await scaffold({destination: resolvedDestination, jsx});\n stdout.write(`Created White Label project at ${resolvedDestination}\\n`);\n return 0;\n}\n\nconst invokedPath = path.resolve(process.argv[1] as string);\nconst modulePath = fileURLToPath(import.meta.url);\nif (realpathSync(invokedPath) === realpathSync(modulePath)) {\n runCli(process.argv.slice(2)).then(\n (exitCode) => {process.exitCode = exitCode;},\n (error: unknown) => {\n process.stderr.write(`Unable to create White Label project: ${formatCliError(error)}\\n`);\n process.exitCode = 1;\n }\n );\n}\n"]}
@@ -24,7 +24,7 @@ export declare function createSiteManifest(): {
24
24
  allowScripts: {
25
25
  '@parcel/watcher@2.5.1': boolean;
26
26
  'esbuild@0.28.2': boolean;
27
- 'white-label-view@5.1.0': boolean;
27
+ 'white-label-view@6.0.0': boolean;
28
28
  };
29
29
  dependenciesMeta: {
30
30
  '@parcel/watcher@2.5.1': {
@@ -33,7 +33,7 @@ export declare function createSiteManifest(): {
33
33
  'esbuild@0.28.2': {
34
34
  built: boolean;
35
35
  };
36
- 'white-label-view@5.1.0': {
36
+ 'white-label-view@6.0.0': {
37
37
  built: boolean;
38
38
  };
39
39
  };
@@ -55,10 +55,10 @@ export declare function createSiteManifest(): {
55
55
  };
56
56
  };
57
57
  /**
58
- * Create a White Label project.
58
+ * Create a White Label project without overwriting a non-empty destination.
59
59
  *
60
60
  * This is the shared implementation behind every creation interface. Adapters
61
- * translate their environment into these options instead of owning templates or
62
- * project-generation behavior themselves.
61
+ * translate their environment into these options instead of owning templates,
62
+ * overwrite policy, or project-generation behavior themselves.
63
63
  */
64
64
  export declare function createProject({ destination, fileSystem, jsx }: CreateProjectOptions): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  /** @module scaffold */
2
- import { cp, mkdir, writeFile } from 'node:fs/promises';
2
+ import { cp, mkdir, readdir, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -13,6 +13,21 @@ const nodeFileSystem = {
13
13
  await writeFile(destination, `${JSON.stringify(value, null, 2)}\n`);
14
14
  }
15
15
  };
16
+ /** Refuse to layer a generated project over existing user files. */
17
+ async function assertDestinationAvailable(destination) {
18
+ try {
19
+ const entries = await readdir(destination);
20
+ if (entries.length) {
21
+ throw new Error(`Destination directory must be empty: ${destination}`);
22
+ }
23
+ }
24
+ catch (error) {
25
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
26
+ return;
27
+ }
28
+ throw error;
29
+ }
30
+ }
16
31
  /** Return the package manifest used by generated White Label projects. */
17
32
  export function createSiteManifest() {
18
33
  return {
@@ -29,12 +44,12 @@ export function createSiteManifest() {
29
44
  allowScripts: {
30
45
  '@parcel/watcher@2.5.1': true,
31
46
  'esbuild@0.28.2': true,
32
- 'white-label-view@5.1.0': true
47
+ 'white-label-view@6.0.0': true
33
48
  },
34
49
  dependenciesMeta: {
35
50
  '@parcel/watcher@2.5.1': { built: true },
36
51
  'esbuild@0.28.2': { built: true },
37
- 'white-label-view@5.1.0': { built: true }
52
+ 'white-label-view@6.0.0': { built: true }
38
53
  },
39
54
  devDependencies: {
40
55
  '@tailwindcss/cli': '4.3.3',
@@ -47,10 +62,10 @@ export function createSiteManifest() {
47
62
  'typescript': '7.0.2'
48
63
  },
49
64
  dependencies: {
50
- 'white-label-mediator': '4.0.0',
51
- 'white-label-model': '6.0.0',
52
- 'white-label-router': '5.0.0',
53
- 'white-label-view': '5.1.0'
65
+ 'white-label-mediator': '5.0.0',
66
+ 'white-label-model': '7.0.1',
67
+ 'white-label-router': '6.0.0',
68
+ 'white-label-view': '6.0.0'
54
69
  }
55
70
  };
56
71
  }
@@ -74,13 +89,14 @@ const noJsxTemplateCopies = [
74
89
  ['tsconfig.site.no-jsx.json', 'tsconfig.json']
75
90
  ];
76
91
  /**
77
- * Create a White Label project.
92
+ * Create a White Label project without overwriting a non-empty destination.
78
93
  *
79
94
  * This is the shared implementation behind every creation interface. Adapters
80
- * translate their environment into these options instead of owning templates or
81
- * project-generation behavior themselves.
95
+ * translate their environment into these options instead of owning templates,
96
+ * overwrite policy, or project-generation behavior themselves.
82
97
  */
83
98
  export async function createProject({ destination, fileSystem = nodeFileSystem, jsx = true }) {
99
+ await assertDestinationAvailable(destination);
84
100
  const copies = jsx ? [
85
101
  ['.editorconfig', '.editorconfig'],
86
102
  ['.yarnrc.yml', '.yarnrc.yml'],
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../scaffold/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** @module scaffold */\nimport {cp, mkdir, writeFile} from 'node:fs/promises';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nconst packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');\n\nexport interface ScaffoldFileSystem {\n copy(source: string, destination: string): void | Promise<void>;\n writeJSON(destination: string, value: unknown): void | Promise<void>;\n}\n\nexport interface CreateProjectOptions {\n destination: string;\n fileSystem?: ScaffoldFileSystem;\n jsx?: boolean;\n}\n\nconst nodeFileSystem: ScaffoldFileSystem = {\n async copy(source, destination) {\n await mkdir(path.dirname(destination), {recursive: true});\n await cp(source, destination, {recursive: true});\n },\n async writeJSON(destination, value) {\n await mkdir(path.dirname(destination), {recursive: true});\n await writeFile(destination, `${JSON.stringify(value, null, 2)}\\n`);\n }\n};\n\n/** Return the package manifest used by generated White Label projects. */\nexport function createSiteManifest() {\n return {\n name: 'white-label-site',\n version: '1.0.0',\n private: true,\n type: 'module',\n engines: {node: '^22.18.0 || >=24.11.0'},\n scripts: {\n build: 'tsc -p tsconfig.json && node dist/scripts/build.js',\n typecheck: 'tsc -p tsconfig.json --noEmit',\n test: 'node --run typecheck && node --run build -- --version=test --production=true --site-url=https://example.com && node --test --experimental-test-coverage --test-coverage-include=dist/app/assets/script/index.js --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 test/*.test.js'\n },\n allowScripts: {\n '@parcel/watcher@2.5.1': true,\n 'esbuild@0.28.2': true,\n 'white-label-view@5.1.0': true\n },\n dependenciesMeta: {\n '@parcel/watcher@2.5.1': {built: true},\n 'esbuild@0.28.2': {built: true},\n 'white-label-view@5.1.0': {built: true}\n },\n devDependencies: {\n '@tailwindcss/cli': '4.3.3',\n '@types/node': '24.13.3',\n 'axe-core': '4.13.0',\n 'esbuild': '0.28.2',\n 'html-validate': '11.15.0',\n 'jsdom': '30.0.1',\n 'tailwindcss': '4.3.3',\n 'typescript': '7.0.2'\n },\n dependencies: {\n 'white-label-mediator': '4.0.0',\n 'white-label-model': '6.0.0',\n 'white-label-router': '5.0.0',\n 'white-label-view': '5.1.0'\n }\n };\n}\n\nconst commonNoJsxCopies = [\n ['app/assets/data', 'app/assets/data'],\n ['app/assets/style', 'app/assets/style'],\n ['app/assets/script/tasks/TaskApplication.ts', 'app/assets/script/tasks/TaskApplication.ts'],\n ['app/assets/script/tasks/TaskMediator.ts', 'app/assets/script/tasks/TaskMediator.ts'],\n ['app/assets/script/tasks/TaskModel.ts', 'app/assets/script/tasks/TaskModel.ts'],\n ['app/assets/script/tasks/TaskRouter.ts', 'app/assets/script/tasks/TaskRouter.ts'],\n ['app/assets/view/examples/tasks/task-state.ts', 'app/assets/view/examples/tasks/task-state.ts'],\n ['app/package.json', 'app/package.json']\n] as const;\n\nconst noJsxTemplateCopies = [\n ['scaffold/no-jsx/app/index.ts', 'app/index.ts'],\n ['scaffold/no-jsx/app/404.ts', 'app/404.ts'],\n ['scaffold/no-jsx/app/assets/script/index.ts', 'app/assets/script/index.ts'],\n ['scaffold/no-jsx/app/assets/script/tasks/TaskView.ts', 'app/assets/script/tasks/TaskView.ts'],\n ['scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts', 'app/assets/view/examples/tasks/TaskExample.ts'],\n ['scaffold/no-jsx/README.md', 'README.md'],\n ['tsconfig.site.no-jsx.json', 'tsconfig.json']\n] as const;\n\n/**\n * Create a White Label project.\n *\n * This is the shared implementation behind every creation interface. Adapters\n * translate their environment into these options instead of owning templates or\n * project-generation behavior themselves.\n */\nexport async function createProject({destination, fileSystem = nodeFileSystem, jsx = true}: CreateProjectOptions) {\n const copies = jsx ? [\n ['.editorconfig', '.editorconfig'],\n ['.yarnrc.yml', '.yarnrc.yml'],\n ['pnpm-workspace.yaml', 'pnpm-workspace.yaml'],\n ['app', 'app'],\n ['app/README.md', 'README.md'],\n ['server', 'server'],\n ['scripts', 'scripts'],\n ['template-test', 'test'],\n ['tsconfig.site.json', 'tsconfig.json']\n ] as const : [\n ['.editorconfig', '.editorconfig'],\n ['.yarnrc.yml', '.yarnrc.yml'],\n ['pnpm-workspace.yaml', 'pnpm-workspace.yaml'],\n ...commonNoJsxCopies,\n ...noJsxTemplateCopies,\n ['server', 'server'],\n ['scripts', 'scripts'],\n ['template-test', 'test']\n ] as const;\n\n for (const [source, target] of copies) {\n await fileSystem.copy(path.join(packageRoot, source), path.join(destination, target));\n }\n\n await fileSystem.writeJSON(path.join(destination, 'package.json'), createSiteManifest());\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../scaffold/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** @module scaffold */\nimport {cp, mkdir, readdir, writeFile} from 'node:fs/promises';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nconst packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');\n\nexport interface ScaffoldFileSystem {\n copy(source: string, destination: string): void | Promise<void>;\n writeJSON(destination: string, value: unknown): void | Promise<void>;\n}\n\nexport interface CreateProjectOptions {\n destination: string;\n fileSystem?: ScaffoldFileSystem;\n jsx?: boolean;\n}\n\nconst nodeFileSystem: ScaffoldFileSystem = {\n async copy(source, destination) {\n await mkdir(path.dirname(destination), {recursive: true});\n await cp(source, destination, {recursive: true});\n },\n async writeJSON(destination, value) {\n await mkdir(path.dirname(destination), {recursive: true});\n await writeFile(destination, `${JSON.stringify(value, null, 2)}\\n`);\n }\n};\n\n/** Refuse to layer a generated project over existing user files. */\nasync function assertDestinationAvailable(destination: string) {\n try {\n const entries = await readdir(destination);\n if (entries.length) {throw new Error(`Destination directory must be empty: ${destination}`);}\n } catch (error) {\n if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {return;}\n throw error;\n }\n}\n\n/** Return the package manifest used by generated White Label projects. */\nexport function createSiteManifest() {\n return {\n name: 'white-label-site',\n version: '1.0.0',\n private: true,\n type: 'module',\n engines: {node: '^22.18.0 || >=24.11.0'},\n scripts: {\n build: 'tsc -p tsconfig.json && node dist/scripts/build.js',\n typecheck: 'tsc -p tsconfig.json --noEmit',\n test: 'node --run typecheck && node --run build -- --version=test --production=true --site-url=https://example.com && node --test --experimental-test-coverage --test-coverage-include=dist/app/assets/script/index.js --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100 test/*.test.js'\n },\n allowScripts: {\n '@parcel/watcher@2.5.1': true,\n 'esbuild@0.28.2': true,\n 'white-label-view@6.0.0': true\n },\n dependenciesMeta: {\n '@parcel/watcher@2.5.1': {built: true},\n 'esbuild@0.28.2': {built: true},\n 'white-label-view@6.0.0': {built: true}\n },\n devDependencies: {\n '@tailwindcss/cli': '4.3.3',\n '@types/node': '24.13.3',\n 'axe-core': '4.13.0',\n 'esbuild': '0.28.2',\n 'html-validate': '11.15.0',\n 'jsdom': '30.0.1',\n 'tailwindcss': '4.3.3',\n 'typescript': '7.0.2'\n },\n dependencies: {\n 'white-label-mediator': '5.0.0',\n 'white-label-model': '7.0.1',\n 'white-label-router': '6.0.0',\n 'white-label-view': '6.0.0'\n }\n };\n}\n\nconst commonNoJsxCopies = [\n ['app/assets/data', 'app/assets/data'],\n ['app/assets/style', 'app/assets/style'],\n ['app/assets/script/tasks/TaskApplication.ts', 'app/assets/script/tasks/TaskApplication.ts'],\n ['app/assets/script/tasks/TaskMediator.ts', 'app/assets/script/tasks/TaskMediator.ts'],\n ['app/assets/script/tasks/TaskModel.ts', 'app/assets/script/tasks/TaskModel.ts'],\n ['app/assets/script/tasks/TaskRouter.ts', 'app/assets/script/tasks/TaskRouter.ts'],\n ['app/assets/view/examples/tasks/task-state.ts', 'app/assets/view/examples/tasks/task-state.ts'],\n ['app/package.json', 'app/package.json']\n] as const;\n\nconst noJsxTemplateCopies = [\n ['scaffold/no-jsx/app/index.ts', 'app/index.ts'],\n ['scaffold/no-jsx/app/404.ts', 'app/404.ts'],\n ['scaffold/no-jsx/app/assets/script/index.ts', 'app/assets/script/index.ts'],\n ['scaffold/no-jsx/app/assets/script/tasks/TaskView.ts', 'app/assets/script/tasks/TaskView.ts'],\n ['scaffold/no-jsx/app/assets/view/examples/tasks/TaskExample.ts', 'app/assets/view/examples/tasks/TaskExample.ts'],\n ['scaffold/no-jsx/README.md', 'README.md'],\n ['tsconfig.site.no-jsx.json', 'tsconfig.json']\n] as const;\n\n/**\n * Create a White Label project without overwriting a non-empty destination.\n *\n * This is the shared implementation behind every creation interface. Adapters\n * translate their environment into these options instead of owning templates,\n * overwrite policy, or project-generation behavior themselves.\n */\nexport async function createProject({destination, fileSystem = nodeFileSystem, jsx = true}: CreateProjectOptions) {\n await assertDestinationAvailable(destination);\n const copies = jsx ? [\n ['.editorconfig', '.editorconfig'],\n ['.yarnrc.yml', '.yarnrc.yml'],\n ['pnpm-workspace.yaml', 'pnpm-workspace.yaml'],\n ['app', 'app'],\n ['app/README.md', 'README.md'],\n ['server', 'server'],\n ['scripts', 'scripts'],\n ['template-test', 'test'],\n ['tsconfig.site.json', 'tsconfig.json']\n ] as const : [\n ['.editorconfig', '.editorconfig'],\n ['.yarnrc.yml', '.yarnrc.yml'],\n ['pnpm-workspace.yaml', 'pnpm-workspace.yaml'],\n ...commonNoJsxCopies,\n ...noJsxTemplateCopies,\n ['server', 'server'],\n ['scripts', 'scripts'],\n ['template-test', 'test']\n ] as const;\n\n for (const [source, target] of copies) {\n await fileSystem.copy(path.join(packageRoot, source), path.join(destination, target));\n }\n\n await fileSystem.writeJSON(path.join(destination, 'package.json'), createSiteManifest());\n}\n"]}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Provider-neutral serverless example using the Web Request/Response contract.
3
+ *
4
+ * Keep mutable White Label instances request-scoped. Cloud adapters should only
5
+ * translate their provider event into a Request and translate the Response back.
6
+ */
7
+ export declare function handleRequest(request: Request): Promise<Response>;
@@ -0,0 +1,67 @@
1
+ import Mediator from 'white-label-mediator';
2
+ import { Model } from 'white-label-model';
3
+ import Router from 'white-label-router';
4
+ import View from 'white-label-view/server';
5
+ function escapeHtml(value) {
6
+ return value
7
+ .replaceAll('&', '&amp;')
8
+ .replaceAll('<', '&lt;')
9
+ .replaceAll('>', '&gt;')
10
+ .replaceAll('"', '&quot;')
11
+ .replaceAll("'", '&#39;');
12
+ }
13
+ /**
14
+ * Provider-neutral serverless example using the Web Request/Response contract.
15
+ *
16
+ * Keep mutable White Label instances request-scoped. Cloud adapters should only
17
+ * translate their provider event into a Request and translate the Response back.
18
+ */
19
+ export async function handleRequest(request) {
20
+ const url = new URL(request.url);
21
+ const mediator = new Mediator();
22
+ const model = new Model({
23
+ status: 404,
24
+ title: 'Not found',
25
+ message: `No route for ${url.pathname}.`
26
+ });
27
+ const router = new Router();
28
+ const view = new View({
29
+ model,
30
+ template(data) {
31
+ const state = data;
32
+ return `<main><h1>${escapeHtml(state.title)}</h1><p>${escapeHtml(state.message)}</p></main>`;
33
+ }
34
+ });
35
+ router.mediator = mediator;
36
+ router.routes = {
37
+ '/health': () => model.update({
38
+ status: 200,
39
+ title: 'Healthy',
40
+ message: 'Serverless request handled.'
41
+ }),
42
+ '/hello': (_scope, location) => {
43
+ const name = String(location.data.query.name ?? 'world');
44
+ return model.update({
45
+ status: 200,
46
+ title: 'Hello',
47
+ message: `Hello ${name}.`
48
+ });
49
+ },
50
+ defaultRoute: () => true
51
+ };
52
+ try {
53
+ router.navigate(`${url.pathname}${url.search}`);
54
+ view.initialize();
55
+ return new Response(`<!doctype html>${view.toString()}`, {
56
+ status: model.get().status,
57
+ headers: { 'content-type': 'text/html; charset=utf-8' }
58
+ });
59
+ }
60
+ finally {
61
+ view.destroy();
62
+ router.destroy();
63
+ mediator.destroy();
64
+ model.destroy();
65
+ }
66
+ }
67
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../server/handler.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAC,KAAK,EAAC,MAAM,mBAAmB,CAAC;AACxC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,yBAAyB,CAAC;AAQ3C,SAAS,UAAU,CAAC,KAAa;IAC7B,OAAO,KAAK;SACP,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACpB,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,WAAW;QAClB,OAAO,EAAE,gBAAgB,GAAG,CAAC,QAAQ,GAAG;KAC3C,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;IAC5B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC;QAClB,KAAK;QACL,QAAQ,CAAC,IAAI;YACT,MAAM,KAAK,GAAG,IAAmB,CAAC;YAClC,OAAO,aAAa,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC;QACjG,CAAC;KACJ,CAAC,CAAC;IAEH,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,MAAM,CAAC,MAAM,GAAG;QACZ,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC;YAC1B,MAAM,EAAE,GAAG;YACX,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,6BAA6B;SACzC,CAAC;QACF,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC;YACzD,OAAO,KAAK,CAAC,MAAM,CAAC;gBAChB,MAAM,EAAE,GAAG;gBACX,KAAK,EAAE,OAAO;gBACd,OAAO,EAAE,SAAS,IAAI,GAAG;aAC5B,CAAC,CAAC;QACP,CAAC;QACD,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI;KAC3B,CAAC;IAEF,IAAI,CAAC;QACD,MAAM,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,OAAO,IAAI,QAAQ,CAAC,kBAAkB,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE;YACrD,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM;YAC1B,OAAO,EAAE,EAAC,cAAc,EAAE,0BAA0B,EAAC;SACxD,CAAC,CAAC;IACP,CAAC;YAAS,CAAC;QACP,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,MAAM,CAAC,OAAO,EAAE,CAAC;QACjB,QAAQ,CAAC,OAAO,EAAE,CAAC;QACnB,KAAK,CAAC,OAAO,EAAE,CAAC;IACpB,CAAC;AACL,CAAC","sourcesContent":["import Mediator from 'white-label-mediator';\nimport {Model} from 'white-label-model';\nimport Router from 'white-label-router';\nimport View from 'white-label-view/server';\n\ntype ServerState = {\n status: number;\n title: string;\n message: string;\n};\n\nfunction escapeHtml(value: string) {\n return value\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#39;');\n}\n\n/**\n * Provider-neutral serverless example using the Web Request/Response contract.\n *\n * Keep mutable White Label instances request-scoped. Cloud adapters should only\n * translate their provider event into a Request and translate the Response back.\n */\nexport async function handleRequest(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const mediator = new Mediator();\n const model = new Model({\n status: 404,\n title: 'Not found',\n message: `No route for ${url.pathname}.`\n });\n const router = new Router();\n const view = new View({\n model,\n template(data) {\n const state = data as ServerState;\n return `<main><h1>${escapeHtml(state.title)}</h1><p>${escapeHtml(state.message)}</p></main>`;\n }\n });\n\n router.mediator = mediator;\n router.routes = {\n '/health': () => model.update({\n status: 200,\n title: 'Healthy',\n message: 'Serverless request handled.'\n }),\n '/hello': (_scope, location) => {\n const name = String(location.data.query.name ?? 'world');\n return model.update({\n status: 200,\n title: 'Hello',\n message: `Hello ${name}.`\n });\n },\n defaultRoute: () => true\n };\n\n try {\n router.navigate(`${url.pathname}${url.search}`);\n view.initialize();\n return new Response(`<!doctype html>${view.toString()}`, {\n status: model.get().status,\n headers: {'content-type': 'text/html; charset=utf-8'}\n });\n } finally {\n view.destroy();\n router.destroy();\n mediator.destroy();\n model.destroy();\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generator-white-label",
3
- "version": "8.0.0",
3
+ "version": "9.0.0",
4
4
  "description": "Framework-independent TypeScript project generator for accessible, SEO-friendly sites with progressive enhancement, optional JSX, and a provider-neutral serverless handler example.",
5
5
  "type": "module",
6
6
  "main": "dist/scaffold/index.js",
@@ -79,10 +79,10 @@
79
79
  "jsdom": "30.0.1",
80
80
  "tailwindcss": "4.3.3",
81
81
  "typescript": "7.0.2",
82
- "white-label-mediator": "4.0.0",
83
- "white-label-model": "6.0.0",
84
- "white-label-router": "5.0.0",
85
- "white-label-view": "5.1.0",
82
+ "white-label-mediator": "5.0.0",
83
+ "white-label-model": "7.0.1",
84
+ "white-label-router": "6.0.0",
85
+ "white-label-view": "6.0.0",
86
86
  "@babel/core": "8.0.5",
87
87
  "@babel/eslint-parser": "8.0.5",
88
88
  "@babel/plugin-syntax-jsx": "8.0.1",
@@ -97,6 +97,7 @@
97
97
  "cli/**/*.ts",
98
98
  "scripts/**/*.ts",
99
99
  "scaffold/**/*.ts",
100
+ "server/**/*.ts",
100
101
  "app/**/*.ts",
101
102
  "app/**/*.tsx"
102
103
  ],
@@ -0,0 +1,18 @@
1
+ # generator-white-label scaffold instructions
2
+
3
+ These instructions are more specific than the repository-root agent guide for files under `scaffold/`.
4
+
5
+ ## Generator 9 creation contract
6
+
7
+ - `createProject()` is the canonical project-creation engine. CLI and future adapters must call it rather than reimplement scaffold copying or policy.
8
+ - Destination safety belongs in `createProject()`: missing and empty destinations are allowed; an existing non-empty destination must be rejected before scaffold files are copied or package metadata is written.
9
+ - Do not add a force/overwrite path unless the user explicitly requests and approves that product change.
10
+ - JSX and no-JSX scaffolds must preserve equivalent White Label architecture and core behavior. JSX is a rendering-syntax choice, not a capability tier.
11
+ - Keep third-party template engines application-owned. Start those integrations from the no-JSX scaffold; do not add renderer-specific adapters or runtime dependencies to the generator without an explicit requirement.
12
+ - Generated JSX uses the `white-label-view` trust contract: ordinary text/attributes are HTML-escaped, intrinsic `on*` handlers are not supported, and URL/CSS semantics remain application validation concerns.
13
+ - Keep generated serverless examples provider-neutral and request-scoped. Do not make Express, a cloud provider SDK, or mutable module-level request state a requirement.
14
+ - Do not hand-edit dependency lockfiles. Regenerate them with the package manager only after the coordinated runtime versions actually exist in the registry.
15
+
16
+ ## Coordinated release dependency
17
+
18
+ Generator 9 targets `white-label-mediator@5`, `white-label-model@7`, `white-label-router@6`, and `white-label-view@6`. Registry installation failures for unpublished target versions are a real release gate; do not bypass them with Git references in the release lockfile.
package/scaffold/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** @module scaffold */
2
- import {cp, mkdir, writeFile} from 'node:fs/promises';
2
+ import {cp, mkdir, readdir, writeFile} from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import {fileURLToPath} from 'node:url';
5
5
 
@@ -27,6 +27,17 @@ const nodeFileSystem: ScaffoldFileSystem = {
27
27
  }
28
28
  };
29
29
 
30
+ /** Refuse to layer a generated project over existing user files. */
31
+ async function assertDestinationAvailable(destination: string) {
32
+ try {
33
+ const entries = await readdir(destination);
34
+ if (entries.length) {throw new Error(`Destination directory must be empty: ${destination}`);}
35
+ } catch (error) {
36
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {return;}
37
+ throw error;
38
+ }
39
+ }
40
+
30
41
  /** Return the package manifest used by generated White Label projects. */
31
42
  export function createSiteManifest() {
32
43
  return {
@@ -43,12 +54,12 @@ export function createSiteManifest() {
43
54
  allowScripts: {
44
55
  '@parcel/watcher@2.5.1': true,
45
56
  'esbuild@0.28.2': true,
46
- 'white-label-view@5.1.0': true
57
+ 'white-label-view@6.0.0': true
47
58
  },
48
59
  dependenciesMeta: {
49
60
  '@parcel/watcher@2.5.1': {built: true},
50
61
  'esbuild@0.28.2': {built: true},
51
- 'white-label-view@5.1.0': {built: true}
62
+ 'white-label-view@6.0.0': {built: true}
52
63
  },
53
64
  devDependencies: {
54
65
  '@tailwindcss/cli': '4.3.3',
@@ -61,10 +72,10 @@ export function createSiteManifest() {
61
72
  'typescript': '7.0.2'
62
73
  },
63
74
  dependencies: {
64
- 'white-label-mediator': '4.0.0',
65
- 'white-label-model': '6.0.0',
66
- 'white-label-router': '5.0.0',
67
- 'white-label-view': '5.1.0'
75
+ 'white-label-mediator': '5.0.0',
76
+ 'white-label-model': '7.0.1',
77
+ 'white-label-router': '6.0.0',
78
+ 'white-label-view': '6.0.0'
68
79
  }
69
80
  };
70
81
  }
@@ -91,13 +102,14 @@ const noJsxTemplateCopies = [
91
102
  ] as const;
92
103
 
93
104
  /**
94
- * Create a White Label project.
105
+ * Create a White Label project without overwriting a non-empty destination.
95
106
  *
96
107
  * This is the shared implementation behind every creation interface. Adapters
97
- * translate their environment into these options instead of owning templates or
98
- * project-generation behavior themselves.
108
+ * translate their environment into these options instead of owning templates,
109
+ * overwrite policy, or project-generation behavior themselves.
99
110
  */
100
111
  export async function createProject({destination, fileSystem = nodeFileSystem, jsx = true}: CreateProjectOptions) {
112
+ await assertDestinationAvailable(destination);
101
113
  const copies = jsx ? [
102
114
  ['.editorconfig', '.editorconfig'],
103
115
  ['.yarnrc.yml', '.yarnrc.yml'],
@@ -34,6 +34,32 @@ pnpm build
34
34
  pnpm test
35
35
  ```
36
36
 
37
+ ## Simple View click event
38
+
39
+ View lifecycle hooks can own a browser listener directly:
40
+
41
+ ```ts
42
+ import View from 'white-label-view';
43
+
44
+ class ButtonView extends View {
45
+ handleClick = () => {
46
+ console.log('Clicked');
47
+ };
48
+
49
+ addListeners() {
50
+ this.element.addEventListener('click', this.handleClick);
51
+ return this;
52
+ }
53
+
54
+ removeListeners() {
55
+ this.element.removeEventListener('click', this.handleClick);
56
+ return this;
57
+ }
58
+ }
59
+ ```
60
+
61
+ The same callback reference is used for both registration and cleanup. View calls `removeListeners()` before replacement or destruction.
62
+
37
63
  ## Serverless / function runtimes
38
64
 
39
65
  `server/handler.ts` is the same provider-neutral serverless example included in the JSX scaffold. It uses Web `Request` and `Response`, request-scoped Model/View/Router/Mediator instances, and the server View entrypoint; it does not require JSX or a provider SDK.
@@ -141,7 +141,9 @@ test('all client packages cooperate through the task application without losing
141
141
 
142
142
  assert.equal(normalizeTaskFilter('nope'), 'all');
143
143
  application.destroy();
144
- assert.equal(application.mediator.listenerCount('task:add'), 0);
144
+ assert.deepEqual(application.model.get(), {});
145
+ application.mediator.dispatchEvent(new CustomEvent('task:add', {detail: 'Ignored after destroy'}));
146
+ assert.deepEqual(application.model.get(), {});
145
147
  });
146
148
 
147
149
  test('serverless handler keeps warm and concurrent requests isolated', async () => {