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.
- package/assets/skills/README.md +27 -27
- package/assets/skills/bascik/SKILL.md +59 -51
- package/assets/skills/bascik/scripts/add-example.mjs +160 -63
- package/assets/skills/bascik/scripts/check-links.mjs +151 -66
- package/assets/skills/bascik/scripts/lib/components.mjs +92 -45
- package/assets/skills/bascik/scripts/lib/project.mjs +58 -32
- package/assets/skills/bascik/scripts/list-components.mjs +47 -20
- package/assets/skills/bascik/scripts/new-component.mjs +106 -57
- package/assets/skills/bascik/scripts/preview.mjs +82 -40
- package/assets/skills/bascik/scripts/read-doc.mjs +27 -14
- package/assets/skills/bascik/scripts/search-docs.mjs +48 -22
- package/assets/skills/bascik/scripts/verify.mjs +111 -37
- package/assets/skills/bascik-compatibility/SKILL.md +25 -24
- package/assets/skills/bascik-compatibility/scripts/baseline.mjs +57 -40
- package/assets/skills/bascik-compatibility/scripts/lookup.mjs +125 -53
- package/assets/skills/bascik-components/SKILL.md +19 -19
- package/assets/skills/bascik-components/scripts/check-tokens.mjs +67 -37
- package/assets/skills/bascik-config/SKILL.md +31 -23
- package/assets/skills/bascik-config/scripts/audit-config.mjs +239 -132
- package/assets/skills/bascik-config/scripts/check-workspace.mjs +181 -57
- package/assets/skills/bascik-migrate/SKILL.md +23 -24
- package/assets/skills/bascik-scripts/SKILL.md +27 -26
- package/assets/skills/bascik-scripts/scripts/new-post.mjs +67 -33
- package/assets/skills/bascik-seo/SKILL.md +16 -16
- package/assets/skills/bascik-seo/scripts/audit-seo.mjs +237 -89
- package/assets/skills/bascik-testing/SKILL.md +15 -15
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/npm.d.ts +26 -0
- package/dist/npm.js +26 -0
- package/dist/npm.js.map +1 -0
- package/dist/scaffold.js +4 -6
- package/dist/scaffold.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +14 -5
- package/src/npm.test.ts +88 -0
- package/src/npm.ts +64 -0
- package/src/scaffold.integration.test.ts +68 -22
- package/src/scaffold.test.ts +95 -23
- package/src/scaffold.ts +183 -80
- package/src/skill-sync.test.ts +20 -5
package/assets/skills/README.md
CHANGED
|
@@ -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
|
|
26
|
-
|
|
27
|
-
| [`bascik`](bascik/SKILL.md)
|
|
28
|
-
| [`bascik-components`](bascik-components/SKILL.md)
|
|
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)
|
|
31
|
-
| [`bascik-seo`](bascik-seo/SKILL.md)
|
|
32
|
-
| [`bascik-config`](bascik-config/SKILL.md)
|
|
33
|
-
| [`bascik-testing`](bascik-testing/SKILL.md)
|
|
34
|
-
| [`bascik-migrate`](bascik-migrate/SKILL.md)
|
|
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
|
|
43
|
-
|
|
44
|
-
| `bascik/scripts/verify.mjs`
|
|
45
|
-
| `bascik/scripts/preview.mjs`
|
|
46
|
-
| `bascik/scripts/list-components.mjs`
|
|
47
|
-
| `bascik/scripts/new-component.mjs`
|
|
48
|
-
| `bascik/scripts/search-docs.mjs`
|
|
49
|
-
| `bascik/scripts/read-doc.mjs`
|
|
50
|
-
| `bascik/scripts/add-example.mjs`
|
|
51
|
-
| `bascik/scripts/check-links.mjs`
|
|
52
|
-
| `bascik-components/scripts/check-tokens.mjs` | Find CSS custom properties components use but nothing declares
|
|
53
|
-
| `bascik-compatibility/scripts/lookup.mjs`
|
|
54
|
-
| `bascik-compatibility/scripts/baseline.mjs`
|
|
55
|
-
| `bascik-scripts/scripts/new-post.mjs`
|
|
56
|
-
| `bascik-seo/scripts/audit-seo.mjs`
|
|
57
|
-
| `bascik-config/scripts/audit-config.mjs`
|
|
58
|
-
| `bascik-config/scripts/check-workspace.mjs`
|
|
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
|
|
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 {
|
|
52
|
-
|
|
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(
|
|
59
|
-
btn.addEventListener(
|
|
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
|
|
91
|
-
|
|
92
|
-
| Default slot (rich HTML) | `<div data-bascik-slot>fallback</div>`
|
|
93
|
-
| Named slot
|
|
94
|
-
| Text prop
|
|
95
|
-
| Prop into an attribute
|
|
96
|
-
| Forward attributes
|
|
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
|
|
103
|
-
|
|
104
|
-
| `<script>`
|
|
105
|
-
| `<script data-bascik-build>`
|
|
106
|
-
| `<script data-bascik-routes>` | Node, at build time
|
|
107
|
-
| `<script data-bascik-server>` | Node, per request
|
|
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>`
|
|
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 #
|
|
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 #
|
|
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
|
|
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
|
|
143
|
-
|
|
144
|
-
| [`scripts/verify.mjs`](scripts/verify.mjs)
|
|
145
|
-
| [`scripts/preview.mjs`](scripts/preview.mjs)
|
|
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)
|
|
148
|
-
| [`scripts/search-docs.mjs`](scripts/search-docs.mjs)
|
|
149
|
-
| [`scripts/read-doc.mjs`](scripts/read-doc.mjs)
|
|
150
|
-
| [`scripts/add-example.mjs`](scripts/add-example.mjs)
|
|
151
|
-
| [`scripts/check-links.mjs`](scripts/check-links.mjs)
|
|
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...
|
|
165
|
-
|
|
166
|
-
| Write components, slots, props, attributes
|
|
167
|
-
| Pass shared `<head>` tags
|
|
168
|
-
| Use a library (petite-vue, Alpine.js, HTMX, Tailwind) | `/libraries`
|
|
169
|
-
| Use a third-party web component
|
|
170
|
-
| Share components between projects
|
|
171
|
-
| Find answers to edge cases
|
|
172
|
-
| Use the CLI and the dev 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
|
|
199
|
-
|
|
200
|
-
| `bascik-components`
|
|
201
|
-
| `bascik-compatibility` | Deciding whether an HTML, CSS, or JavaScript feature works in browsers and inside a component
|
|
202
|
-
| `bascik-scripts`
|
|
203
|
-
| `bascik-seo`
|
|
204
|
-
| `bascik-config`
|
|
205
|
-
| `bascik-testing`
|
|
206
|
-
| `bascik-migrate`
|
|
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
|
|
4
|
-
import { parseArgs } from
|
|
5
|
-
import { dirname, isAbsolute, join, relative, resolve } from
|
|
6
|
-
import {
|
|
7
|
-
|
|
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([
|
|
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(
|
|
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(
|
|
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)
|
|
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),
|
|
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(
|
|
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 (
|
|
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 ===
|
|
73
|
-
if (
|
|
74
|
-
|
|
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 ===
|
|
81
|
-
const tail = rest.join(
|
|
82
|
-
if (second ===
|
|
83
|
-
if (second ===
|
|
84
|
-
if (second ===
|
|
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(
|
|
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) =>
|
|
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
|
|
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
|
|
109
|
-
|
|
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 =
|
|
120
|
-
|
|
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:
|
|
137
|
-
|
|
138
|
-
force: { type:
|
|
139
|
-
source: { type:
|
|
140
|
-
help: { type:
|
|
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))
|
|
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 [
|
|
156
|
-
const examples = bundle.examples.filter(
|
|
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 ===
|
|
211
|
+
console.log(`\n${kind === "component" ? "Components" : "Sites"}`);
|
|
159
212
|
for (const example of examples) {
|
|
160
|
-
const firstSentence =
|
|
213
|
+
const firstSentence =
|
|
214
|
+
example.description.match(/^.*?[.!?](?=\s|$)/)?.[0] ??
|
|
215
|
+
example.description;
|
|
161
216
|
console.log(` ${example.name.padEnd(16)} ${firstSentence}`);
|
|
162
|
-
console.log(` ${
|
|
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)
|
|
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(
|
|
178
|
-
|
|
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 !==
|
|
258
|
+
if (file.path !== "README.md") printed.push({ example, file });
|
|
200
259
|
continue;
|
|
201
260
|
}
|
|
202
|
-
if (
|
|
203
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
220
|
-
|
|
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 (
|
|
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(
|
|
230
|
-
|
|
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(
|
|
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 ===
|
|
323
|
+
const packageJson = printed.find(({ file }) => file.path === "package.json");
|
|
240
324
|
if (packageJson) {
|
|
241
|
-
const wanted = Object.keys(
|
|
242
|
-
|
|
243
|
-
|
|
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)
|
|
336
|
+
if (missing.length)
|
|
337
|
+
console.log(
|
|
338
|
+
`\nInstall the dependencies this example uses: npm install ${missing.join(" ")}`,
|
|
339
|
+
);
|
|
246
340
|
}
|
|
247
|
-
if (!values[
|
|
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();
|