create-kerf-component 0.15.2

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 ADDED
@@ -0,0 +1,51 @@
1
+ # create-kerf-component
2
+
3
+ Scaffold a publishable [kerf](https://github.com/brianwestphal/kerf) component
4
+ package that already follows kerf's hard packaging rules — so you don't have to
5
+ reverse-engineer them.
6
+
7
+ ```bash
8
+ npm create kerf-component@latest my-widgets
9
+ # or: npm init kerf-component my-widgets
10
+ # or: npx create-kerf-component my-widgets
11
+ cd my-widgets
12
+ npm install
13
+ npm run build
14
+ ```
15
+
16
+ Pass `.` to scaffold into the current directory. The target directory's basename
17
+ is used as the package name.
18
+
19
+ ## What you get
20
+
21
+ A ready-to-publish component package that encodes the rules from the kerf docs
22
+ (*Building reusable component packages*):
23
+
24
+ - **`kerfjs` as a `peerDependency`, `external` in the tsup build** — never
25
+ bundled, so `isSafeHtml` brand checks and signal identity stay intact across
26
+ the package boundary.
27
+ - **ESM + `.d.ts` output** via `tsup`, with **subpath exports** (`.` and
28
+ `./counter`).
29
+ - **`tsconfig` with `jsxImportSource: "kerfjs"`** so the author's `.tsx` compiles
30
+ against kerf's JSX runtime; consumers need no extra setup.
31
+ - **An example `Counter` component** demonstrating the two patterns every kerf
32
+ component needs:
33
+ - **per-instance state via a factory + props** (`createCounter` → `<Counter store={…} />`), and
34
+ - **a `wire(root)` delegation disposer** (`wireCounter`) instead of inline event handlers.
35
+
36
+ ## Layout produced
37
+
38
+ ```
39
+ my-widgets/
40
+ ├── package.json # peerDependencies.kerfjs, exports map, files: [dist]
41
+ ├── tsconfig.json # jsxImportSource: "kerfjs"
42
+ ├── tsup.config.ts # external: ['kerfjs'], format esm, dts
43
+ ├── .gitignore
44
+ ├── README.md
45
+ └── src/
46
+ ├── index.ts # public barrel
47
+ └── counter.tsx # factory + component + wire() disposer
48
+ ```
49
+
50
+ This package is part of the kerf repository and releases in lockstep with
51
+ `kerfjs`.
package/index.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // create-kerf-component — scaffold a publishable kerf component package that
3
+ // already follows kerf's hard packaging rules (docs/13-component-packages.md):
4
+ // kerfjs as a peerDependency + external in the build (never bundled), ESM +
5
+ // .d.ts output, `jsxImportSource: "kerfjs"`, subpath exports, and an example
6
+ // component showing per-instance state via a factory + props and a `wire(root)`
7
+ // delegation disposer.
8
+ //
9
+ // Usage:
10
+ // npm create kerf-component@latest <dir>
11
+ // npm init kerf-component <dir>
12
+ // npx create-kerf-component <dir>
13
+ //
14
+ // <dir> is the target directory; its basename is the default package name (pass
15
+ // `.` to scaffold into the current directory). No third-party dependencies — the
16
+ // initializer is plain Node so `npm create` runs it with zero install latency.
17
+
18
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
19
+ import { basename, dirname, join, resolve } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'template');
23
+ const TOKEN = /__PKG_NAME__/g;
24
+
25
+ function fail(msg) {
26
+ process.stderr.write(`create-kerf-component: ${msg}\n`);
27
+ process.exit(1);
28
+ }
29
+
30
+ // npm package name rules, trimmed to what we actually need to guard: no spaces,
31
+ // no uppercase, no leading dot/underscore, URL-safe. (Scoped names like
32
+ // `@scope/name` are allowed.)
33
+ function isValidPackageName(name) {
34
+ return /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
35
+ }
36
+
37
+ // Recursively walk a directory, returning absolute file paths.
38
+ function walk(dir) {
39
+ const out = [];
40
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
41
+ const abs = join(dir, entry.name);
42
+ if (entry.isDirectory()) out.push(...walk(abs));
43
+ else out.push(abs);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function main(argv) {
49
+ const target = argv[2];
50
+ if (target == null || target === '' || target === '--help' || target === '-h') {
51
+ process.stdout.write(
52
+ 'Usage: npm create kerf-component@latest <dir>\n' +
53
+ ' (or: npx create-kerf-component <dir>)\n\n' +
54
+ 'Scaffolds a kerf component package into <dir>. Pass `.` for the current directory.\n',
55
+ );
56
+ process.exit(target == null || target === '' ? 1 : 0);
57
+ }
58
+
59
+ const targetDir = resolve(process.cwd(), target);
60
+ const pkgName = target === '.' ? basename(targetDir) : basename(target);
61
+
62
+ if (!isValidPackageName(pkgName)) {
63
+ fail(`"${pkgName}" is not a valid npm package name.`);
64
+ }
65
+
66
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
67
+ fail(`target directory "${target}" already exists and is not empty.`);
68
+ }
69
+
70
+ // Copy the whole template tree, then post-process each file in place: replace
71
+ // the package-name token and rename the dotfile placeholders npm can't ship
72
+ // verbatim (`_gitignore` → `.gitignore`).
73
+ mkdirSync(targetDir, { recursive: true });
74
+ cpSync(TEMPLATE_DIR, targetDir, { recursive: true });
75
+
76
+ for (const file of walk(targetDir)) {
77
+ const text = readFileSync(file, 'utf8');
78
+ if (TOKEN.test(text)) writeFileSync(file, text.replace(TOKEN, pkgName));
79
+ }
80
+
81
+ const dotfiles = [['_gitignore', '.gitignore']];
82
+ for (const [from, to] of dotfiles) {
83
+ const src = join(targetDir, from);
84
+ if (existsSync(src)) renameSync(src, join(targetDir, to));
85
+ }
86
+
87
+ const cdHint = target === '.' ? '' : ` cd ${target}\n`;
88
+ process.stdout.write(
89
+ `\nScaffolded kerf component package "${pkgName}" in ${target}\n\n` +
90
+ 'Next steps:\n' +
91
+ cdHint +
92
+ ' npm install\n' +
93
+ ' npm run build # tsup → ESM + .d.ts (kerfjs stays external)\n' +
94
+ ' npm run typecheck\n\n' +
95
+ 'Edit src/counter.tsx, then publish with `npm publish --access public`.\n',
96
+ );
97
+ }
98
+
99
+ main(process.argv);
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "create-kerf-component",
3
+ "version": "0.15.2",
4
+ "description": "Scaffold a publishable kerf component package that already follows kerf's hard packaging rules.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Brian Westphal <brian.westphal@bleugris.com>",
8
+ "homepage": "https://brianwestphal.github.io/kerf/docs/13-component-packages/",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/brianwestphal/kerf",
12
+ "directory": "create-kerf-component"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/brianwestphal/kerf/issues"
16
+ },
17
+ "keywords": [
18
+ "kerf",
19
+ "kerfjs",
20
+ "create",
21
+ "scaffold",
22
+ "initializer",
23
+ "component"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "bin": {
29
+ "create-kerf-component": "index.js"
30
+ },
31
+ "files": [
32
+ "index.js",
33
+ "template",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "test": "node --test tests/*.test.js"
39
+ }
40
+ }
@@ -0,0 +1,54 @@
1
+ # __PKG_NAME__
2
+
3
+ A reusable [kerf](https://github.com/brianwestphal/kerf) component package,
4
+ scaffolded with `create-kerf-component`.
5
+
6
+ ## Develop
7
+
8
+ ```bash
9
+ npm install
10
+ npm run build # tsup → dist/ (ESM + .d.ts); kerfjs stays external
11
+ npm run typecheck
12
+ ```
13
+
14
+ ## Use it
15
+
16
+ A kerf app that already has `jsxImportSource: "kerfjs"` configured can import and
17
+ render the component like any local function — there's no extra toolchain:
18
+
19
+ ```tsx
20
+ import { mount } from 'kerfjs';
21
+ import { Counter, createCounter, wireCounter } from '__PKG_NAME__';
22
+
23
+ const counter = createCounter(0); // per-instance state (a factory)
24
+ const root = document.getElementById('app')!;
25
+
26
+ mount(root, () => <Counter store={counter} label="Clicks" />);
27
+
28
+ const dispose = wireCounter(root, counter); // delegation disposer (call on teardown)
29
+ ```
30
+
31
+ ## The rules this package follows
32
+
33
+ These are kerf's hard packaging rules (see the kerf docs,
34
+ *Building reusable component packages*). The scaffold encodes them so you don't
35
+ have to:
36
+
37
+ - **`kerfjs` is a `peerDependency` and is `external` in the build — never
38
+ bundled.** A bundled second copy of kerfjs would break `isSafeHtml` brand
39
+ checks and signal identity across the package boundary.
40
+ - **No per-instance state in module scope.** A module-level `signal`/`store` is a
41
+ singleton shared by every instance and every app. Hand the consumer a factory
42
+ (`createCounter`) or accept a signal/store via props.
43
+ - **No inline JSX event handlers.** Components are pure `(props) => SafeHtml`
44
+ string-builders; emit `data-action` hooks and let the host wire events with
45
+ `delegate()` (see `wireCounter`), which returns a disposer.
46
+ - **Build emits ESM + `.d.ts`; `tsconfig` sets `jsxImportSource: "kerfjs"`.**
47
+
48
+ ## Publish
49
+
50
+ ```bash
51
+ npm publish --access public
52
+ ```
53
+
54
+ `prepublishOnly` runs the build first; `files` ships only `dist/` + docs.
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ *.tsbuildinfo
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "__PKG_NAME__",
3
+ "version": "0.1.0",
4
+ "description": "A reusable kerf component package.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./counter": {
13
+ "types": "./dist/counter.d.ts",
14
+ "import": "./dist/counter.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "kerfjs": ">=0.14.0"
29
+ },
30
+ "devDependencies": {
31
+ "kerfjs": "^0.15.0",
32
+ "tsup": "^8",
33
+ "typescript": "^5"
34
+ }
35
+ }
@@ -0,0 +1,70 @@
1
+ import { defineStore, delegate, type SafeHtml } from 'kerfjs';
2
+
3
+ export interface CounterState {
4
+ count: number;
5
+ }
6
+
7
+ /**
8
+ * (a) Per-instance state via a FACTORY.
9
+ *
10
+ * Each call returns an independent store, so two `<Counter>` on the same page —
11
+ * or two apps importing this package — never share state. The trap to avoid is a
12
+ * module-scope `signal`/`store`: that would be a singleton shared by every
13
+ * instance. A reusable component must never own per-instance module state; hand
14
+ * the consumer a factory (here) or accept a signal/store via props.
15
+ */
16
+ export function createCounter(start = 0) {
17
+ return defineStore({
18
+ initial: (): CounterState => ({ count: start }),
19
+ actions: (set, get) => ({
20
+ inc: () => set({ count: get().count + 1 }),
21
+ dec: () => set({ count: get().count - 1 }),
22
+ }),
23
+ });
24
+ }
25
+
26
+ export type CounterStore = ReturnType<typeof createCounter>;
27
+
28
+ export interface CounterProps {
29
+ store: CounterStore;
30
+ label?: string;
31
+ }
32
+
33
+ /**
34
+ * The component is a pure function `(props) => SafeHtml`. It emits stable
35
+ * `data-action` hooks instead of inline event handlers — inline `onClick={...}`
36
+ * handlers don't survive kerf's morph (and the `no-inline-jsx-event-handlers`
37
+ * lint rule flags them). The host wires the events; see `wireCounter` below.
38
+ */
39
+ export function Counter({ store, label = 'Count' }: CounterProps): SafeHtml {
40
+ return (
41
+ <div class="kerf-counter">
42
+ <button type="button" data-action="counter:dec" aria-label="Decrement">
43
+
44
+ </button>
45
+ <output>
46
+ {label}: {store.state.value.count}
47
+ </output>
48
+ <button type="button" data-action="counter:inc" aria-label="Increment">
49
+ +
50
+ </button>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ /**
56
+ * (b) A `wire(root)` delegation disposer.
57
+ *
58
+ * Components can't attach their own listeners (no lifecycle, re-rendered every
59
+ * paint), so the host calls this ONCE at its `mount()` root and disposes on
60
+ * teardown. `delegate()` lives on the root, not on the component's nodes, so the
61
+ * single listener survives every re-render. Returns the disposer `delegate()`
62
+ * hands back — call it when the host unmounts.
63
+ */
64
+ export function wireCounter(root: HTMLElement, store: CounterStore): () => void {
65
+ return delegate(root, 'click', '[data-action^="counter:"]', (_event, el) => {
66
+ const action = el.getAttribute('data-action');
67
+ if (action === 'counter:inc') store.actions.inc();
68
+ else if (action === 'counter:dec') store.actions.dec();
69
+ });
70
+ }
@@ -0,0 +1,11 @@
1
+ // Public barrel. Re-export everything consumers import from the package root.
2
+ // (The `./counter` subpath export in package.json lets consumers also import the
3
+ // component directly: `import { Counter } from '__PKG_NAME__/counter'`.)
4
+ export {
5
+ Counter,
6
+ createCounter,
7
+ wireCounter,
8
+ type CounterProps,
9
+ type CounterState,
10
+ type CounterStore,
11
+ } from './counter.js';
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "skipLibCheck": true,
12
+ "isolatedModules": true,
13
+ "verbatimModuleSyntax": true,
14
+ "declaration": true,
15
+ "noEmit": true,
16
+
17
+ // The one piece of kerf-specific wiring: author .tsx compiles against kerf's
18
+ // JSX runtime. Consumers need no extra setup — your package ships compiled JS
19
+ // whose JSX is already lowered to `kerfjs/jsx-runtime` calls.
20
+ "jsx": "react-jsx",
21
+ "jsxImportSource": "kerfjs"
22
+ },
23
+ "include": ["src"]
24
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ // One entry per `exports` subpath. tsup emits ESM + `.d.ts` for each.
4
+ export default defineConfig({
5
+ entry: ['src/index.ts', 'src/counter.tsx'],
6
+ format: ['esm'],
7
+ dts: true,
8
+ clean: true,
9
+ // THE hard rule: kerfjs must NEVER be bundled. A component returns `SafeHtml`
10
+ // and reads signals; both rely on the consumer and this package sharing ONE
11
+ // SafeHtml class and ONE signals instance. Bundling a second copy of kerfjs
12
+ // would silently break `isSafeHtml` brand checks and signal identity across the
13
+ // boundary. Keep it external (it's a peerDependency).
14
+ external: ['kerfjs'],
15
+ });