create-bascik 1.0.0-rc.6 → 1.0.0-rc.8

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.
Files changed (41) hide show
  1. package/assets/skills/README.md +27 -27
  2. package/assets/skills/bascik/SKILL.md +59 -51
  3. package/assets/skills/bascik/scripts/add-example.mjs +160 -63
  4. package/assets/skills/bascik/scripts/check-links.mjs +151 -66
  5. package/assets/skills/bascik/scripts/lib/components.mjs +92 -45
  6. package/assets/skills/bascik/scripts/lib/project.mjs +58 -32
  7. package/assets/skills/bascik/scripts/list-components.mjs +47 -20
  8. package/assets/skills/bascik/scripts/new-component.mjs +106 -57
  9. package/assets/skills/bascik/scripts/preview.mjs +82 -40
  10. package/assets/skills/bascik/scripts/read-doc.mjs +27 -14
  11. package/assets/skills/bascik/scripts/search-docs.mjs +48 -22
  12. package/assets/skills/bascik/scripts/verify.mjs +111 -37
  13. package/assets/skills/bascik-compatibility/SKILL.md +25 -24
  14. package/assets/skills/bascik-compatibility/scripts/baseline.mjs +57 -40
  15. package/assets/skills/bascik-compatibility/scripts/lookup.mjs +125 -53
  16. package/assets/skills/bascik-components/SKILL.md +19 -19
  17. package/assets/skills/bascik-components/scripts/check-tokens.mjs +67 -37
  18. package/assets/skills/bascik-config/SKILL.md +31 -23
  19. package/assets/skills/bascik-config/scripts/audit-config.mjs +239 -132
  20. package/assets/skills/bascik-config/scripts/check-workspace.mjs +181 -57
  21. package/assets/skills/bascik-migrate/SKILL.md +23 -24
  22. package/assets/skills/bascik-scripts/SKILL.md +27 -26
  23. package/assets/skills/bascik-scripts/scripts/new-post.mjs +67 -33
  24. package/assets/skills/bascik-seo/SKILL.md +16 -16
  25. package/assets/skills/bascik-seo/scripts/audit-seo.mjs +237 -89
  26. package/assets/skills/bascik-testing/SKILL.md +15 -15
  27. package/dist/index.js +7 -4
  28. package/dist/index.js.map +1 -1
  29. package/dist/npm.d.ts +26 -0
  30. package/dist/npm.js +26 -0
  31. package/dist/npm.js.map +1 -0
  32. package/dist/scaffold.js +4 -6
  33. package/dist/scaffold.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/index.ts +14 -5
  36. package/src/npm.test.ts +88 -0
  37. package/src/npm.ts +64 -0
  38. package/src/scaffold.integration.test.ts +68 -22
  39. package/src/scaffold.test.ts +95 -23
  40. package/src/scaffold.ts +183 -80
  41. package/src/skill-sync.test.ts +20 -5
@@ -22,16 +22,16 @@ New projects created with `npm create bascik@latest` include these skills alread
22
22
 
23
23
  ## Skills
24
24
 
25
- | Skill | Use it for |
26
- |---|---|
27
- | [`bascik`](bascik/SKILL.md) | Start here: project layout, components, slots, props, the CLI, verifying output, examples, and building a new site |
28
- | [`bascik-components`](bascik-components/SKILL.md) | Component CSS and scripts that work with many instances, interactivity, and theming |
29
- | [`bascik-compatibility`](bascik-compatibility/SKILL.md) | Which HTML, CSS, and JavaScript features to use: Baseline support and Bascik scoping |
30
- | [`bascik-scripts`](bascik-scripts/SKILL.md) | Build, routes, server, and stream scripts, API routes, and Markdown blogs |
31
- | [`bascik-seo`](bascik-seo/SKILL.md) | Head tags, social cards, structured data, sitemap, and `llms.txt` |
32
- | [`bascik-config`](bascik-config/SKILL.md) | `bascik.config.ts`, exec scripts, assets, deployment, and monorepos |
33
- | [`bascik-testing`](bascik-testing/SKILL.md) | Unit and E2E tests, debugging, spec validation, Lighthouse |
34
- | [`bascik-migrate`](bascik-migrate/SKILL.md) | Moving a site from React, Next.js, Vue, Svelte, Astro, Eleventy, Hugo, or WordPress |
25
+ | Skill | Use it for |
26
+ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
27
+ | [`bascik`](bascik/SKILL.md) | Start here: project layout, components, slots, props, the CLI, verifying output, examples, and building a new site |
28
+ | [`bascik-components`](bascik-components/SKILL.md) | Component CSS and scripts that work with many instances, interactivity, and theming |
29
+ | [`bascik-compatibility`](bascik-compatibility/SKILL.md) | Which HTML, CSS, and JavaScript features to use: Baseline support and Bascik scoping |
30
+ | [`bascik-scripts`](bascik-scripts/SKILL.md) | Build, routes, server, and stream scripts, API routes, and Markdown blogs |
31
+ | [`bascik-seo`](bascik-seo/SKILL.md) | Head tags, social cards, structured data, sitemap, and `llms.txt` |
32
+ | [`bascik-config`](bascik-config/SKILL.md) | `bascik.config.ts`, exec scripts, assets, deployment, and monorepos |
33
+ | [`bascik-testing`](bascik-testing/SKILL.md) | Unit and E2E tests, debugging, spec validation, Lighthouse |
34
+ | [`bascik-migrate`](bascik-migrate/SKILL.md) | Moving a site from React, Next.js, Vue, Svelte, Astro, Eleventy, Hugo, or WordPress |
35
35
 
36
36
  The skills hold the rules an assistant must follow and point to the docs at https://bascik.dev for everything else, so the guidance lives in one place. `bascik/scripts/read-doc.mjs` prints any docs page.
37
37
 
@@ -39,23 +39,23 @@ The skills hold the rules an assistant must follow and point to the docs at http
39
39
 
40
40
  The skills ship dependency-free Node.js scripts (Node 22.18+) that agents run from the project root:
41
41
 
42
- | Script | Purpose |
43
- |---|---|
44
- | `bascik/scripts/verify.mjs` | `bascik --check` plus the language server linter (including component folders outside the project), optionally a build |
45
- | `bascik/scripts/preview.mjs` | Compile a snippet or page with the project's components and print readable output |
46
- | `bascik/scripts/list-components.mjs` | Inventory components with props, slots, and descriptions |
47
- | `bascik/scripts/new-component.mjs` | Create a component that follows the conventions |
48
- | `bascik/scripts/search-docs.mjs` | Search the live docs |
49
- | `bascik/scripts/read-doc.mjs` | Print a docs page as text |
50
- | `bascik/scripts/add-example.mjs` | Copy an example from https://bascik.dev/example-components into the project |
51
- | `bascik/scripts/check-links.mjs` | Find broken internal links and `#fragments` in `dist/` |
52
- | `bascik-components/scripts/check-tokens.mjs` | Find CSS custom properties components use but nothing declares |
53
- | `bascik-compatibility/scripts/lookup.mjs` | Search Bascik's live compatibility matrix |
54
- | `bascik-compatibility/scripts/baseline.mjs` | Baseline browser support for a web feature |
55
- | `bascik-scripts/scripts/new-post.mjs` | Create a Markdown post with valid front matter |
56
- | `bascik-seo/scripts/audit-seo.mjs` | Check titles, descriptions, canonicals, social tags, and JSON-LD in `dist/` |
57
- | `bascik-config/scripts/audit-config.mjs` | Flag redundant or risky `bascik.config.ts` values |
58
- | `bascik-config/scripts/check-workspace.mjs` | Map every site in a repository and report cross-site problems |
42
+ | Script | Purpose |
43
+ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
44
+ | `bascik/scripts/verify.mjs` | `bascik --check` plus the language server linter (including component folders outside the project), optionally a build |
45
+ | `bascik/scripts/preview.mjs` | Compile a snippet or page with the project's components and print readable output |
46
+ | `bascik/scripts/list-components.mjs` | Inventory components with props, slots, and descriptions |
47
+ | `bascik/scripts/new-component.mjs` | Create a component that follows the conventions |
48
+ | `bascik/scripts/search-docs.mjs` | Search the live docs |
49
+ | `bascik/scripts/read-doc.mjs` | Print a docs page as text |
50
+ | `bascik/scripts/add-example.mjs` | Copy an example from https://bascik.dev/example-components into the project |
51
+ | `bascik/scripts/check-links.mjs` | Find broken internal links and `#fragments` in `dist/` |
52
+ | `bascik-components/scripts/check-tokens.mjs` | Find CSS custom properties components use but nothing declares |
53
+ | `bascik-compatibility/scripts/lookup.mjs` | Search Bascik's live compatibility matrix |
54
+ | `bascik-compatibility/scripts/baseline.mjs` | Baseline browser support for a web feature |
55
+ | `bascik-scripts/scripts/new-post.mjs` | Create a Markdown post with valid front matter |
56
+ | `bascik-seo/scripts/audit-seo.mjs` | Check titles, descriptions, canonicals, social tags, and JSON-LD in `dist/` |
57
+ | `bascik-config/scripts/audit-config.mjs` | Flag redundant or risky `bascik.config.ts` values |
58
+ | `bascik-config/scripts/check-workspace.mjs` | Map every site in a repository and report cross-site problems |
59
59
 
60
60
  The example components and the starter site live in the repository's [`examples/`](https://github.com/bascikdev/bascik/tree/main/examples) folder, not in the skills. The docs build publishes them at `https://bascik.dev/assets/examples.json`, which `add-example.mjs` downloads.
61
61
 
@@ -19,7 +19,7 @@ Code in this skill is illustrative. Adapt tag names, paths, and values to the pr
19
19
 
20
20
  - `package.json` depends on `@bascik/bascik`, or the root has `bascik.config.ts` / `bascik.config.js`, or there are `.html` files under `src/components/`.
21
21
  - Node.js 22.18 or newer (24 LTS recommended). Node runs `.ts` helpers natively, so no TypeScript build step exists.
22
- - Standard scripts: `npm run dev` (`bascik`), `npm run build` (`bascik --build`), `npm run lint` (`bascik-language-server --check`).
22
+ - Standard scripts: `npm run dev` (`bascik`), `npm run build` (`bascik --build`), `npm run lint` (`bascik --check`).
23
23
 
24
24
  ## Project layout
25
25
 
@@ -48,15 +48,20 @@ Default pattern: `<style>` above the markup, markup in the middle, `<script>` be
48
48
  ```html
49
49
  <!-- src/components/like-button/like-button.html -->
50
50
  <style>
51
- .like { padding: 0.5rem 1rem; border-radius: 6px; }
52
- .like.active { background: #d3ff8d; }
51
+ .like {
52
+ padding: 0.5rem 1rem;
53
+ border-radius: 6px;
54
+ }
55
+ .like.active {
56
+ background: #d3ff8d;
57
+ }
53
58
  </style>
54
59
  <button class="like" id="btn" type="button">
55
60
  <span data-bascik-prop-label>Like</span>
56
61
  </button>
57
62
  <script>
58
- const btn = document.getElementById('btn');
59
- btn.addEventListener('click', () => btn.classList.toggle('active'));
63
+ const btn = document.getElementById("btn");
64
+ btn.addEventListener("click", () => btn.classList.toggle("active"));
60
65
  </script>
61
66
  ```
62
67
 
@@ -87,26 +92,26 @@ New components are picked up by the running dev server automatically; no restart
87
92
 
88
93
  ## Slots, props, and attributes
89
94
 
90
- | Need | Component template | Usage site |
91
- |---|---|---|
92
- | Default slot (rich HTML) | `<div data-bascik-slot>fallback</div>` | `<my-card><h2>Title</h2></my-card>` |
93
- | Named slot | `<div data-bascik-slot="header"></div>` | `<div data-bascik-slot="header"><h1>Hi</h1></div>` inside the tag |
94
- | Text prop | `<p data-bascik-prop-title></p>` | `<my-card data-bascik-prop-title="Hello" />` |
95
- | Prop into an attribute | `<img data-bascik-attr-src="image" data-bascik-attr-alt="alt">` | `<my-card data-bascik-prop-image="/a.png" data-bascik-prop-alt="A" />` |
96
- | Forward attributes | (nothing) | `<my-card class="wide" aria-label="Promo" />` lands on the first root element |
95
+ | Need | Component template | Usage site |
96
+ | ------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
97
+ | Default slot (rich HTML) | `<div data-bascik-slot>fallback</div>` | `<my-card><h2>Title</h2></my-card>` |
98
+ | Named slot | `<div data-bascik-slot="header"></div>` | `<div data-bascik-slot="header"><h1>Hi</h1></div>` inside the tag |
99
+ | Text prop | `<p data-bascik-prop-title></p>` | `<my-card data-bascik-prop-title="Hello" />` |
100
+ | Prop into an attribute | `<img data-bascik-attr-src="image" data-bascik-attr-alt="alt">` | `<my-card data-bascik-prop-image="/a.png" data-bascik-prop-alt="A" />` |
101
+ | Forward attributes | (nothing) | `<my-card class="wide" aria-label="Promo" />` lands on the first root element |
97
102
 
98
103
  Pass props as `data-bascik-prop-<name>="..."` on the usage tag. A plain attribute such as `label="..."` is not a prop: it is forwarded to the root element and the prop keeps its default. The slot element itself is replaced by the content, so named-slot wrappers disappear: wrap headings and links in a `<div data-bascik-slot="x">` rather than putting the attribute on the `<h1>` or `<a>`. Details: `read-doc.mjs /slots`, `/props`, `/attribute-inheritance`.
99
104
 
100
105
  ## Script tags at a glance
101
106
 
102
- | Tag | Runs | Use for |
103
- |---|---|---|
104
- | `<script>` | Browser | Component and page behavior (scoped, IIFE-wrapped) |
105
- | `<script data-bascik-build>` | Node, at build time | Markup from files, Markdown, APIs; output replaces the tag |
106
- | `<script data-bascik-routes>` | Node, at build time | Expanding `[slug].html` templates into many pages |
107
- | `<script data-bascik-server>` | Node, per request | Personalized markup (needs `bascik --server` or a serverless target) |
108
- | `<script data-bascik-stream>` | Node, per request, streamed | Slow per-request blocks that should not delay the page shell |
109
- | `<script data-bascik-dev>` | Browser, dev only | Debug helpers removed from production builds |
107
+ | Tag | Runs | Use for |
108
+ | ----------------------------- | --------------------------- | -------------------------------------------------------------------- |
109
+ | `<script>` | Browser | Component and page behavior (scoped, IIFE-wrapped) |
110
+ | `<script data-bascik-build>` | Node, at build time | Markup from files, Markdown, APIs; output replaces the tag |
111
+ | `<script data-bascik-routes>` | Node, at build time | Expanding `[slug].html` templates into many pages |
112
+ | `<script data-bascik-server>` | Node, per request | Personalized markup (needs `bascik --server` or a serverless target) |
113
+ | `<script data-bascik-stream>` | Node, per request, streamed | Slow per-request blocks that should not delay the page shell |
114
+ | `<script data-bascik-dev>` | Browser, dev only | Debug helpers removed from production builds |
110
115
 
111
116
  Node-side scripts export a default function that returns an HTML string. Details are in the `bascik-scripts` skill.
112
117
 
@@ -117,19 +122,22 @@ bascik # dev server at http://localhost:8080 with live relo
117
122
  bascik --build # production build to dist/ (HTML, CSS, JS, and identifier minification on)
118
123
  bascik --build --only "blog/**" # rebuild matching pages without cleaning dist/
119
124
  bascik --server # serve a built dist/ (needed for server/stream scripts and API routes)
120
- bascik --check # validate pages, components, config, and API routes without building
125
+ bascik --check # project checks, plus the per-file checks when @bascik/language-server is installed
126
+ bascik --check --fix # also rewrite fixable findings (needs @bascik/language-server)
121
127
  bascik add <package> # copy components from an installed npm package into src/components/
122
128
  bascik init # add Bascik to an existing project
123
- npx @bascik/language-server --check # lint scoping patterns, unclosed tags, and directives
129
+ npx @bascik/language-server --check # the per-file checks on their own (bascik --check already runs them)
124
130
  ```
125
131
 
132
+ `bascik --check` and the linter are not two overlapping tools. `bascik --check` answers project questions (duplicate routes, unused components, config), and the linter answers per-file questions (attribute typos, CSS that cannot be scoped). `bascik --check` runs the linter too, so use it, and reach for the linter alone only on a folder outside the project.
133
+
126
134
  New project: `npm create bascik@latest my-site -y`. Production builds need a site URL for `sitemap.xml` and `robots.txt`: `BASCIK_SITE_URL=https://example.com bascik --build` (or `--site-url`, or `.env`). Full CLI and error output: `read-doc.mjs /cli`.
127
135
 
128
136
  ## Verify every change
129
137
 
130
138
  Do not stop at "the file looks right". Run these from the project root:
131
139
 
132
- 1. `node <skill-dir>/scripts/verify.mjs` runs `bascik --check` and the language server linter and summarizes both. Add `--build` to also run a production build.
140
+ 1. `node <skill-dir>/scripts/verify.mjs` runs `bascik --check` (project and per-file checks) and, for component folders outside the project, the linter, then summarizes each step. Add `--build` to also run a production build.
133
141
  2. `node <skill-dir>/scripts/preview.mjs '<my-card data-bascik-prop-title="Hi" />' --count 2` compiles a snippet with the project's real components and config and prints the unminified output, so you can confirm scoped names, slot placement, and script rewrites.
134
142
  3. Read the relevant file in `dist/` after a build. A hyphenated tag still present in `dist/` means no component matched it.
135
143
 
@@ -139,16 +147,16 @@ Do not stop at "the file looks right". Run these from the project root:
139
147
 
140
148
  All scripts are dependency-free Node.js (22.18+) and run from the project root. Each prints `--help`.
141
149
 
142
- | Script | Purpose |
143
- |---|---|
144
- | [`scripts/verify.mjs`](scripts/verify.mjs) | `bascik --check` plus `@bascik/language-server --check`, with an optional `--build` |
145
- | [`scripts/preview.mjs`](scripts/preview.mjs) | Compile a snippet, file, or existing page in a temporary build and print the result |
146
- | [`scripts/list-components.mjs`](scripts/list-components.mjs) | Inventory components with their props, slots, companion files, and `@bascik` descriptions |
147
- | [`scripts/new-component.mjs`](scripts/new-component.mjs) | Create a component folder that follows the naming and ordering rules |
148
- | [`scripts/search-docs.mjs`](scripts/search-docs.mjs) | Search the live docs at bascik.dev for a topic |
149
- | [`scripts/read-doc.mjs`](scripts/read-doc.mjs) | Print the current text of a docs page, such as `/scoped-javascript` |
150
- | [`scripts/add-example.mjs`](scripts/add-example.mjs) | Copy a tested example component or part of the starter site from bascik.dev into the project (`--list` shows them) |
151
- | [`scripts/check-links.mjs`](scripts/check-links.mjs) | Find broken internal links, missing assets, and dead `#fragments` in the built `dist/`, honoring `base` |
150
+ | Script | Purpose |
151
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
152
+ | [`scripts/verify.mjs`](scripts/verify.mjs) | `bascik --check`, plus the linter on component folders outside the project, with an optional `--build` |
153
+ | [`scripts/preview.mjs`](scripts/preview.mjs) | Compile a snippet, file, or existing page in a temporary build and print the result |
154
+ | [`scripts/list-components.mjs`](scripts/list-components.mjs) | Inventory components with their props, slots, companion files, and `@bascik` descriptions |
155
+ | [`scripts/new-component.mjs`](scripts/new-component.mjs) | Create a component folder that follows the naming and ordering rules |
156
+ | [`scripts/search-docs.mjs`](scripts/search-docs.mjs) | Search the live docs at bascik.dev for a topic |
157
+ | [`scripts/read-doc.mjs`](scripts/read-doc.mjs) | Print the current text of a docs page, such as `/scoped-javascript` |
158
+ | [`scripts/add-example.mjs`](scripts/add-example.mjs) | Copy a tested example component or part of the starter site from bascik.dev into the project (`--list` shows them) |
159
+ | [`scripts/check-links.mjs`](scripts/check-links.mjs) | Find broken internal links, missing assets, and dead `#fragments` in the built `dist/`, honoring `base` |
152
160
 
153
161
  Run `list-components.mjs` before creating a component so you reuse what exists instead of duplicating it.
154
162
 
@@ -161,15 +169,15 @@ node <skill-dir>/scripts/read-doc.mjs /dynamic-routes
161
169
  node <skill-dir>/scripts/search-docs.mjs "dynamic routes trailing slash"
162
170
  ```
163
171
 
164
- | How do I... | Read |
165
- |---|---|
166
- | Write components, slots, props, attributes | `/components`, `/slots`, `/props`, `/attribute-inheritance` |
167
- | Pass shared `<head>` tags | `/how-to/templating` |
168
- | Use a library (petite-vue, Alpine.js, HTMX, Tailwind) | `/libraries` |
169
- | Use a third-party web component | `/how-to/third-party-web-components` |
170
- | Share components between projects | `/how-to/sharing-components`, `/how-to/publishing-components` |
171
- | Find answers to edge cases | `/faq` |
172
- | Use the CLI and the dev server | `/cli`, `/development-server` |
172
+ | How do I... | Read |
173
+ | ----------------------------------------------------- | ------------------------------------------------------------- |
174
+ | Write components, slots, props, attributes | `/components`, `/slots`, `/props`, `/attribute-inheritance` |
175
+ | Pass shared `<head>` tags | `/how-to/templating` |
176
+ | Use a library (petite-vue, Alpine.js, HTMX, Tailwind) | `/libraries` |
177
+ | Use a third-party web component | `/how-to/third-party-web-components` |
178
+ | Share components between projects | `/how-to/sharing-components`, `/how-to/publishing-components` |
179
+ | Find answers to edge cases | `/faq` |
180
+ | Use the CLI and the dev server | `/cli`, `/development-server` |
173
181
 
174
182
  The full page index is at https://bascik.dev/llms.txt.
175
183
 
@@ -195,15 +203,15 @@ The script downloads from bascik.dev, maps files to the project's configured fol
195
203
 
196
204
  ## Companion skills
197
205
 
198
- | Skill | Load it when |
199
- |---|---|
200
- | `bascik-components` | Writing component CSS or `<script>` code, adding interactivity, theming, or compiled class and ID names look wrong |
201
- | `bascik-compatibility` | Deciding whether an HTML, CSS, or JavaScript feature works in browsers and inside a component |
202
- | `bascik-scripts` | Using `data-bascik-build`, `-routes`, `-server`, `-stream`, API routes, Markdown, or a blog |
203
- | `bascik-seo` | Titles, descriptions, canonical URLs, social cards, JSON-LD, sitemap, `llms.txt` |
204
- | `bascik-config` | Creating or editing `bascik.config.ts`, monorepos, deploying, or running `bascik --server` |
205
- | `bascik-testing` | Writing tests, debugging, validating output, or tuning Lighthouse scores |
206
- | `bascik-migrate` | Moving a site from React, Next.js, Vue, Svelte, Astro, Eleventy, Hugo, or WordPress |
206
+ | Skill | Load it when |
207
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
208
+ | `bascik-components` | Writing component CSS or `<script>` code, adding interactivity, theming, or compiled class and ID names look wrong |
209
+ | `bascik-compatibility` | Deciding whether an HTML, CSS, or JavaScript feature works in browsers and inside a component |
210
+ | `bascik-scripts` | Using `data-bascik-build`, `-routes`, `-server`, `-stream`, API routes, Markdown, or a blog |
211
+ | `bascik-seo` | Titles, descriptions, canonical URLs, social cards, JSON-LD, sitemap, `llms.txt` |
212
+ | `bascik-config` | Creating or editing `bascik.config.ts`, monorepos, deploying, or running `bascik --server` |
213
+ | `bascik-testing` | Writing tests, debugging, validating output, or tuning Lighthouse scores |
214
+ | `bascik-migrate` | Moving a site from React, Next.js, Vue, Svelte, Astro, Eleventy, Hugo, or WordPress |
207
215
 
208
216
  If the companions or this skill's `scripts/` folder are missing, the skill was installed as a single file. Reinstall the full set:
209
217
 
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // Copy an example component or site from https://bascik.dev/example-components into the current Bascik project.
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { parseArgs } from 'node:util';
5
- import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
6
- import { componentRoots, docsBaseUrl, fail, findProjectRoot, isMain, loadProjectConfig, pagesDirectory } from './lib/project.mjs';
7
- import { listComponents } from './lib/components.mjs';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { parseArgs } from "node:util";
5
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import {
7
+ componentRoots,
8
+ docsBaseUrl,
9
+ fail,
10
+ findProjectRoot,
11
+ isMain,
12
+ loadProjectConfig,
13
+ pagesDirectory,
14
+ } from "./lib/project.mjs";
15
+ import { listComponents } from "./lib/components.mjs";
8
16
 
9
17
  const HELP = `Usage: node add-example.mjs --list
10
18
  node add-example.mjs <example>[/<path>] [...more] [--dry-run] [--force]
@@ -36,26 +44,38 @@ Options:
36
44
  Environment:
37
45
  BASCIK_DOCS_URL Docs origin (default https://bascik.dev)`;
38
46
 
39
- const REFERENCE_ONLY = new Set(['bascik.config.ts', 'bascik.config.js', 'package.json', 'README.md']);
47
+ const REFERENCE_ONLY = new Set([
48
+ "bascik.config.ts",
49
+ "bascik.config.js",
50
+ "package.json",
51
+ "README.md",
52
+ ]);
40
53
 
41
54
  /** Load the examples bundle from a URL or a local file. Exported for tests. */
42
- export async function loadBundle(source = `${docsBaseUrl()}/assets/examples.json`) {
55
+ export async function loadBundle(
56
+ source = `${docsBaseUrl()}/assets/examples.json`,
57
+ ) {
43
58
  let text;
44
59
  if (/^https?:\/\//i.test(source)) {
45
60
  let response;
46
61
  try {
47
62
  response = await fetch(source, { signal: AbortSignal.timeout(20_000) });
48
63
  } catch (error) {
49
- throw new Error(`could not reach ${source}: ${error.message}. Check the network, or pass --source <examples.json>.`);
64
+ throw new Error(
65
+ `could not reach ${source}: ${error.message}. Check the network, or pass --source <examples.json>.`,
66
+ );
50
67
  }
51
- if (!response.ok) throw new Error(`${source} returned HTTP ${response.status}`);
68
+ if (!response.ok)
69
+ throw new Error(`${source} returned HTTP ${response.status}`);
52
70
  text = await response.text();
53
71
  } else {
54
- text = readFileSync(resolve(source), 'utf8');
72
+ text = readFileSync(resolve(source), "utf8");
55
73
  }
56
74
  const bundle = JSON.parse(text);
57
75
  if (bundle?.version !== 1 || !Array.isArray(bundle.examples)) {
58
- throw new Error(`${source} is not a supported examples bundle (version ${bundle?.version}). Update the skills.`);
76
+ throw new Error(
77
+ `${source} is not a supported examples bundle (version ${bundle?.version}). Update the skills.`,
78
+ );
59
79
  }
60
80
  return bundle;
61
81
  }
@@ -66,22 +86,32 @@ export async function loadBundle(source = `${docsBaseUrl()}/assets/examples.json
66
86
  */
67
87
  export function destinationFor(example, filePath, layout) {
68
88
  const segments = filePath.split(/[\\/]/);
69
- if (isAbsolute(filePath) || segments.some((segment) => segment === '..' || segment === '' || segment === '.')) {
89
+ if (
90
+ isAbsolute(filePath) ||
91
+ segments.some(
92
+ (segment) => segment === ".." || segment === "" || segment === ".",
93
+ )
94
+ ) {
70
95
  throw new Error(`unsafe path in the examples bundle: ${filePath}`);
71
96
  }
72
- if (example.kind === 'component') {
73
- if (typeof example.name !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(example.name)) {
74
- throw new Error(`unsafe example name in the examples bundle: ${example.name}`);
97
+ if (example.kind === "component") {
98
+ if (
99
+ typeof example.name !== "string" ||
100
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(example.name)
101
+ ) {
102
+ throw new Error(
103
+ `unsafe example name in the examples bundle: ${example.name}`,
104
+ );
75
105
  }
76
106
  return join(layout.componentRoot, example.name, filePath);
77
107
  }
78
108
  if (REFERENCE_ONLY.has(filePath)) return undefined;
79
- const [first, second, ...rest] = filePath.split('/');
80
- if (first === 'src') {
81
- const tail = rest.join('/');
82
- if (second === 'components') return join(layout.componentRoot, tail);
83
- if (second === 'pages') return join(layout.pagesDir, tail);
84
- if (second === 'api') return join(layout.apiDir, tail);
109
+ const [first, second, ...rest] = filePath.split("/");
110
+ if (first === "src") {
111
+ const tail = rest.join("/");
112
+ if (second === "components") return join(layout.componentRoot, tail);
113
+ if (second === "pages") return join(layout.pagesDir, tail);
114
+ if (second === "api") return join(layout.apiDir, tail);
85
115
  return join(layout.importRoot, second, tail);
86
116
  }
87
117
  return join(layout.root, filePath);
@@ -92,21 +122,34 @@ export function destinationFor(example, filePath, layout) {
92
122
  * [{ example, file }] with each file once. Throws on an unknown reference. Exported for tests.
93
123
  */
94
124
  export function selectFiles(bundle, references) {
95
- const byName = new Map(bundle.examples.map((example) => [example.name, example]));
125
+ const byName = new Map(
126
+ bundle.examples.map((example) => [example.name, example]),
127
+ );
96
128
  const picked = new Map();
97
- const add = (example, file) => picked.set(`${example.name}/${file.path}`, { example, file });
129
+ const add = (example, file) =>
130
+ picked.set(`${example.name}/${file.path}`, { example, file });
98
131
  // [example the using file belongs to, component name]
99
132
  const queue = [];
100
133
  for (const reference of references) {
101
- const [name, ...rest] = reference.replace(/\\/g, '/').replace(/\/+$/, '').split('/');
134
+ const [name, ...rest] = reference
135
+ .replace(/\\/g, "/")
136
+ .replace(/\/+$/, "")
137
+ .split("/");
102
138
  const example = byName.get(name);
103
139
  if (!example) {
104
- const names = bundle.examples.map((e) => e.name).join(', ');
140
+ const names = bundle.examples.map((e) => e.name).join(", ");
105
141
  throw new Error(`no example named "${name}". Available: ${names}`);
106
142
  }
107
- const path = rest.join('/');
108
- const files = path ? example.files.filter((file) => file.path === path || file.path.startsWith(`${path}/`)) : example.files;
109
- if (files.length === 0) throw new Error(`${name} has no file or folder "${path}". Run with --list, or see ${example.docs}`);
143
+ const path = rest.join("/");
144
+ const files = path
145
+ ? example.files.filter(
146
+ (file) => file.path === path || file.path.startsWith(`${path}/`),
147
+ )
148
+ : example.files;
149
+ if (files.length === 0)
150
+ throw new Error(
151
+ `${name} has no file or folder "${path}". Run with --list, or see ${example.docs}`,
152
+ );
110
153
  for (const file of files) {
111
154
  add(example, file);
112
155
  queue.push(...file.uses.map((name) => [example, name]));
@@ -116,8 +159,15 @@ export function selectFiles(bundle, references) {
116
159
  while (queue.length) {
117
160
  const [owner, name] = queue.shift();
118
161
  // A site's own src/components/<name>/ wins over the example component of the same name.
119
- const local = owner.kind === 'site' ? owner.files.filter((file) => new RegExp(`^src/components/(?:.*/)?${name}/`).test(file.path)) : [];
120
- const [example, files] = local.length ? [owner, local] : [byName.get(name), byName.get(name)?.files ?? []];
162
+ const local =
163
+ owner.kind === "site"
164
+ ? owner.files.filter((file) =>
165
+ new RegExp(`^src/components/(?:.*/)?${name}/`).test(file.path),
166
+ )
167
+ : [];
168
+ const [example, files] = local.length
169
+ ? [owner, local]
170
+ : [byName.get(name), byName.get(name)?.files ?? []];
121
171
  const key = `${example?.name}:${name}`;
122
172
  if (!example || seen.has(key)) continue;
123
173
  seen.add(key);
@@ -133,14 +183,15 @@ async function main() {
133
183
  const { values, positionals } = parseArgs({
134
184
  allowPositionals: true,
135
185
  options: {
136
- list: { type: 'boolean', default: false },
137
- 'dry-run': { type: 'boolean', default: false },
138
- force: { type: 'boolean', default: false },
139
- source: { type: 'string' },
140
- help: { type: 'boolean', short: 'h', default: false },
186
+ list: { type: "boolean", default: false },
187
+ "dry-run": { type: "boolean", default: false },
188
+ force: { type: "boolean", default: false },
189
+ source: { type: "string" },
190
+ help: { type: "boolean", short: "h", default: false },
141
191
  },
142
192
  });
143
- if (values.help || (!values.list && positionals.length === 0)) return console.log(HELP);
193
+ if (values.help || (!values.list && positionals.length === 0))
194
+ return console.log(HELP);
144
195
 
145
196
  let bundle;
146
197
  try {
@@ -152,14 +203,18 @@ async function main() {
152
203
  const docsLink = (example) => new URL(example.docs, `${docsBaseUrl()}/`).href;
153
204
 
154
205
  if (values.list) {
155
- for (const kind of ['component', 'site']) {
156
- const examples = bundle.examples.filter((example) => example.kind === kind);
206
+ for (const kind of ["component", "site"]) {
207
+ const examples = bundle.examples.filter(
208
+ (example) => example.kind === kind,
209
+ );
157
210
  if (!examples.length) continue;
158
- console.log(`\n${kind === 'component' ? 'Components' : 'Sites'}`);
211
+ console.log(`\n${kind === "component" ? "Components" : "Sites"}`);
159
212
  for (const example of examples) {
160
- const firstSentence = example.description.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? example.description;
213
+ const firstSentence =
214
+ example.description.match(/^.*?[.!?](?=\s|$)/)?.[0] ??
215
+ example.description;
161
216
  console.log(` ${example.name.padEnd(16)} ${firstSentence}`);
162
- console.log(` ${''.padEnd(16)} ${docsLink(example)}`);
217
+ console.log(` ${"".padEnd(16)} ${docsLink(example)}`);
163
218
  }
164
219
  }
165
220
  return;
@@ -167,15 +222,19 @@ async function main() {
167
222
 
168
223
  const root = findProjectRoot();
169
224
  const config = await loadProjectConfig(root);
170
- if (config?.error) fail(`add-example: could not load ${config.path}: ${config.error.message}`);
225
+ if (config?.error)
226
+ fail(`add-example: could not load ${config.path}: ${config.error.message}`);
171
227
  const directory = config?.module?.default?.directory ?? {};
172
228
  const roots = componentRoots(root, config?.module);
173
229
  const layout = {
174
230
  root,
175
231
  componentRoot: roots[0],
176
232
  pagesDir: pagesDirectory(root, config?.module),
177
- importRoot: resolve(root, config?.module?.default?.scripts?.importRoot ?? 'src'),
178
- apiDir: resolve(root, directory.api ?? 'src/api'),
233
+ importRoot: resolve(
234
+ root,
235
+ config?.module?.default?.scripts?.importRoot ?? "src",
236
+ ),
237
+ apiDir: resolve(root, directory.api ?? "src/api"),
179
238
  };
180
239
 
181
240
  let selected;
@@ -196,55 +255,93 @@ async function main() {
196
255
  fail(`add-example: ${error.message}`);
197
256
  }
198
257
  if (!destination) {
199
- if (file.path !== 'README.md') printed.push({ example, file });
258
+ if (file.path !== "README.md") printed.push({ example, file });
200
259
  continue;
201
260
  }
202
- if (destination.endsWith('.html') && example.kind === 'component' && file.path === `${example.name}.html`) {
203
- const clash = existingComponents.find((c) => c.tag === example.name && resolve(root, c.file) !== destination);
261
+ if (
262
+ destination.endsWith(".html") &&
263
+ example.kind === "component" &&
264
+ file.path === `${example.name}.html`
265
+ ) {
266
+ const clash = existingComponents.find(
267
+ (c) => c.tag === example.name && resolve(root, c.file) !== destination,
268
+ );
204
269
  if (clash) {
205
- const same = readFileSync(resolve(root, clash.file), 'utf8') === file.content;
270
+ const same =
271
+ readFileSync(resolve(root, clash.file), "utf8") === file.content;
206
272
  if (same) {
207
273
  console.log(`same ${clash.file} (already in the project)`);
208
274
  continue;
209
275
  }
210
- fail(`add-example: a component named <${example.name}> already exists at ${clash.file}. Rename one of them first.`);
276
+ fail(
277
+ `add-example: a component named <${example.name}> already exists at ${clash.file}. Rename one of them first.`,
278
+ );
211
279
  }
212
280
  }
213
281
  plan.push({ example, file, destination });
214
282
  }
215
283
 
216
- const show = (path) => relative(root, path).replace(/\\/g, '/');
217
- const conflicts = plan.filter(({ file, destination }) => existsSync(destination) && readFileSync(destination, 'utf8') !== file.content);
284
+ const show = (path) => relative(root, path).replace(/\\/g, "/");
285
+ const conflicts = plan.filter(
286
+ ({ file, destination }) =>
287
+ existsSync(destination) &&
288
+ readFileSync(destination, "utf8") !== file.content,
289
+ );
218
290
  if (conflicts.length && !values.force) {
219
- const list = conflicts.map(({ example, file, destination }) => ` ${show(destination)} (compare with ${docsLink(example)}, file ${file.path})`);
220
- fail(`add-example: these files already exist and differ; nothing was written. Merge by hand, or rerun with --force:\n${list.join('\n')}`);
291
+ const list = conflicts.map(
292
+ ({ example, file, destination }) =>
293
+ ` ${show(destination)} (compare with ${docsLink(example)}, file ${file.path})`,
294
+ );
295
+ fail(
296
+ `add-example: these files already exist and differ; nothing was written. Merge by hand, or rerun with --force:\n${list.join("\n")}`,
297
+ );
221
298
  }
222
299
 
223
300
  let written = 0;
224
301
  for (const { file, destination } of plan) {
225
- if (existsSync(destination) && readFileSync(destination, 'utf8') === file.content) {
302
+ if (
303
+ existsSync(destination) &&
304
+ readFileSync(destination, "utf8") === file.content
305
+ ) {
226
306
  console.log(`same ${show(destination)}`);
227
307
  continue;
228
308
  }
229
- console.log(`${values['dry-run'] ? 'would' : 'write'} ${show(destination)}`);
230
- if (values['dry-run']) continue;
309
+ console.log(
310
+ `${values["dry-run"] ? "would" : "write"} ${show(destination)}`,
311
+ );
312
+ if (values["dry-run"]) continue;
231
313
  mkdirSync(dirname(destination), { recursive: true });
232
314
  writeFileSync(destination, file.content);
233
315
  written++;
234
316
  }
235
317
 
236
318
  for (const { example, file } of printed) {
237
- console.log(`\n--- ${example.name}/${file.path} (not copied: merge what you need by hand) ---\n${file.content.trimEnd()}`);
319
+ console.log(
320
+ `\n--- ${example.name}/${file.path} (not copied: merge what you need by hand) ---\n${file.content.trimEnd()}`,
321
+ );
238
322
  }
239
- const packageJson = printed.find(({ file }) => file.path === 'package.json');
323
+ const packageJson = printed.find(({ file }) => file.path === "package.json");
240
324
  if (packageJson) {
241
- const wanted = Object.keys(JSON.parse(packageJson.file.content).dependencies ?? {});
242
- const projectPackage = existsSync(join(root, 'package.json')) ? JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) : {};
243
- const have = new Set([...Object.keys(projectPackage.dependencies ?? {}), ...Object.keys(projectPackage.devDependencies ?? {})]);
325
+ const wanted = Object.keys(
326
+ JSON.parse(packageJson.file.content).dependencies ?? {},
327
+ );
328
+ const projectPackage = existsSync(join(root, "package.json"))
329
+ ? JSON.parse(readFileSync(join(root, "package.json"), "utf8"))
330
+ : {};
331
+ const have = new Set([
332
+ ...Object.keys(projectPackage.dependencies ?? {}),
333
+ ...Object.keys(projectPackage.devDependencies ?? {}),
334
+ ]);
244
335
  const missing = wanted.filter((name) => !have.has(name));
245
- if (missing.length) console.log(`\nInstall the dependencies this example uses: npm install ${missing.join(' ')}`);
336
+ if (missing.length)
337
+ console.log(
338
+ `\nInstall the dependencies this example uses: npm install ${missing.join(" ")}`,
339
+ );
246
340
  }
247
- if (!values['dry-run']) console.log(`\n${written} file${written === 1 ? '' : 's'} written. Read each file's comments for setup steps.`);
341
+ if (!values["dry-run"])
342
+ console.log(
343
+ `\n${written} file${written === 1 ? "" : "s"} written. Read each file's comments for setup steps.`,
344
+ );
248
345
  }
249
346
 
250
347
  if (isMain(import.meta.url)) await main();