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

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 (36) hide show
  1. package/README.md +2 -2
  2. package/assets/skills/README.md +64 -0
  3. package/assets/skills/bascik/SKILL.md +214 -0
  4. package/assets/skills/bascik/scripts/add-example.mjs +250 -0
  5. package/assets/skills/bascik/scripts/check-links.mjs +208 -0
  6. package/assets/skills/bascik/scripts/lib/components.mjs +137 -0
  7. package/assets/skills/bascik/scripts/lib/project.mjs +202 -0
  8. package/assets/skills/bascik/scripts/list-components.mjs +79 -0
  9. package/assets/skills/bascik/scripts/new-component.mjs +164 -0
  10. package/assets/skills/bascik/scripts/preview.mjs +156 -0
  11. package/assets/skills/bascik/scripts/read-doc.mjs +105 -0
  12. package/assets/skills/bascik/scripts/search-docs.mjs +115 -0
  13. package/assets/skills/bascik/scripts/verify.mjs +100 -0
  14. package/assets/skills/bascik-compatibility/SKILL.md +72 -0
  15. package/assets/skills/bascik-compatibility/scripts/baseline.mjs +132 -0
  16. package/assets/skills/bascik-compatibility/scripts/lookup.mjs +176 -0
  17. package/assets/skills/bascik-components/SKILL.md +95 -0
  18. package/assets/skills/bascik-components/scripts/check-tokens.mjs +145 -0
  19. package/assets/skills/bascik-config/SKILL.md +150 -0
  20. package/assets/skills/bascik-config/scripts/audit-config.mjs +327 -0
  21. package/assets/skills/bascik-config/scripts/check-workspace.mjs +255 -0
  22. package/assets/skills/bascik-migrate/SKILL.md +75 -0
  23. package/assets/skills/bascik-scripts/SKILL.md +144 -0
  24. package/assets/skills/bascik-scripts/scripts/new-post.mjs +77 -0
  25. package/assets/skills/bascik-seo/SKILL.md +61 -0
  26. package/assets/skills/bascik-seo/scripts/audit-seo.mjs +236 -0
  27. package/assets/skills/bascik-testing/SKILL.md +88 -0
  28. package/dist/scaffold.d.ts +7 -0
  29. package/dist/scaffold.js +18 -9
  30. package/dist/scaffold.js.map +1 -1
  31. package/package.json +16 -8
  32. package/src/scaffold.integration.test.ts +13 -7
  33. package/src/scaffold.test.ts +33 -12
  34. package/src/scaffold.ts +22 -10
  35. package/src/skill-sync.test.ts +24 -11
  36. package/assets/SKILL.md +0 -2162
package/README.md CHANGED
@@ -25,8 +25,8 @@ npm create bascik@latest
25
25
 
26
26
  ```text
27
27
  my-site/
28
- ├── .github/skills/bascik/SKILL.md ← AI Copilot skill file
29
- ├── .claude/skills/bascik/SKILL.md ← Claude skill file
28
+ ├── .agents/skills/ ← Bascik agent skills for Copilot, Cursor, Codex, and others
29
+ ├── .claude/skills/ ← The same skills for Claude Code
30
30
  ├── .vscode/
31
31
  │ ├── launch.json ← Debug launchers for dev server and tests
32
32
  │ └── extensions.json ← Recommends official Bascik extension
@@ -0,0 +1,64 @@
1
+ # Bascik Agent Skills
2
+
3
+ [Agent Skills](https://agentskills.io) that teach AI coding assistants (GitHub Copilot, Claude Code, Cursor, Codex, Gemini CLI, OpenCode, Windsurf, and others) how to build sites with [Bascik](https://bascik.dev).
4
+
5
+ ## Install
6
+
7
+ With the [skills CLI](https://skills.sh):
8
+
9
+ ```sh
10
+ npx skills add bascikdev/bascik
11
+ # or from the docs site's /.well-known/agent-skills/ index:
12
+ npx skills add https://bascik.dev
13
+ ```
14
+
15
+ Without it, download every skill into the shared `.agents/skills/` folder (use `.claude/skills` for Claude Code, or `.github/skills` if you prefer):
16
+
17
+ ```sh
18
+ mkdir -p .agents/skills && curl -fsSL https://bascik.dev/assets/skills.tar.gz | tar -xz -C .agents/skills
19
+ ```
20
+
21
+ New projects created with `npm create bascik@latest` include these skills already.
22
+
23
+ ## Skills
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 |
35
+
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
+
38
+ ## Scripts
39
+
40
+ The skills ship dependency-free Node.js scripts (Node 22.18+) that agents run from the project root:
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 |
59
+
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
+
62
+ ## Contributing
63
+
64
+ These files are the source of truth. The docs build publishes them to `https://bascik.dev/assets/skills.tar.gz`, `https://bascik.dev/.well-known/agent-skills/` (one archive per skill plus `index.json`), and the core skill to `https://bascik.dev/assets/SKILL.md`, and `create-bascik` bundles a copy in `create/assets/skills/` (`yarn create:prepack`). Contributor-only skills for working on Bascik itself live in `.github/skills/` and are marked `metadata.internal`, so `npx skills add` does not offer them.
@@ -0,0 +1,214 @@
1
+ ---
2
+ name: bascik
3
+ description: Build websites with Bascik, the zero-runtime HTML component build tool. Use whenever a project depends on @bascik/bascik, has a bascik.config.ts, or keeps components in src/components/*.html, or when the user mentions Bascik. Covers pages, components, slots, props, attribute inheritance, the bascik CLI (dev, --build, --server, --check, add), project layout, verifying compiled dist/ output, building a new site, and copying tested example components. Start here; companion skills cover components, compatibility, scripts, SEO, config, testing, and migration.
4
+ license: MIT
5
+ metadata:
6
+ author: bascik
7
+ homepage: https://bascik.dev
8
+ ---
9
+
10
+ # Bascik
11
+
12
+ Bascik is a build tool for HTML components. It expands custom tags such as `<site-nav />` into the matching component file, scopes the component's CSS, IDs, classes, and scripts, and writes plain HTML pages to `dist/`. It adds zero JavaScript: every script in the output is one you wrote.
13
+
14
+ It is not a framework. There is no virtual DOM, reactive state, client router, template language, Shadow DOM, or runtime. Write vanilla HTML, CSS, and JavaScript, and let Bascik do the isolation at build time.
15
+
16
+ Code in this skill is illustrative. Adapt tag names, paths, and values to the project instead of copying example names literally.
17
+
18
+ ## Recognize a Bascik project
19
+
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
+ - 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`).
23
+
24
+ ## Project layout
25
+
26
+ ```text
27
+ src/
28
+ components/ component templates; the file name is the tag name
29
+ site-nav/
30
+ site-nav.html <site-nav>
31
+ site-nav.css optional companion stylesheet, scoped to site-nav
32
+ site-nav.ts optional companion script, loaded with <script src="site-nav.ts">
33
+ pages/ one .html file per URL, plus public assets
34
+ index.html /
35
+ about.html /about
36
+ blog/post.html /blog/post
37
+ 404.html served for unknown routes
38
+ css/styles.css copied to dist/css/styles.css (global, unscoped)
39
+ lib/ shared Node helpers for build and server scripts (import with @/lib/...)
40
+ api/ optional API route handlers (bascik --server only)
41
+ dist/ compiled output, never edited by hand, never committed
42
+ ```
43
+
44
+ ## Write a component
45
+
46
+ Default pattern: `<style>` above the markup, markup in the middle, `<script>` below. Give every element a script must control an `id` and find it with `getElementById`.
47
+
48
+ ```html
49
+ <!-- src/components/like-button/like-button.html -->
50
+ <style>
51
+ .like { padding: 0.5rem 1rem; border-radius: 6px; }
52
+ .like.active { background: #d3ff8d; }
53
+ </style>
54
+ <button class="like" id="btn" type="button">
55
+ <span data-bascik-prop-label>Like</span>
56
+ </button>
57
+ <script>
58
+ const btn = document.getElementById('btn');
59
+ btn.addEventListener('click', () => btn.classList.toggle('active'));
60
+ </script>
61
+ ```
62
+
63
+ ```html
64
+ <!-- src/pages/index.html -->
65
+ <like-button data-bascik-prop-label="Like this post" />
66
+ <like-button data-bascik-prop-label="Like this comment" />
67
+ ```
68
+
69
+ Bascik rewrites `.like` to `.bascik__like-button__like`, gives each instance its own `id` (`bascik__like-button__<instance>__btn`), rewrites the `getElementById` string and the `classList` class to match, and wraps the script in an IIFE. Both buttons work independently. To see the exact output, run [`scripts/preview.mjs`](scripts/preview.mjs).
70
+
71
+ New components are picked up by the running dev server automatically; no restart is needed.
72
+
73
+ ## Rules (must follow)
74
+
75
+ 1. **Hyphenated, lowercase file names.** `feature-card.html` becomes `<feature-card>`. Never name a component after a native element (`header.html`, `dialog.html`): it replaces every such element on the page. Names must be unique across every component root.
76
+ 2. **Per-instance targeting uses `id`.** Use `getElementById` (or `getElementsByName`) for elements a script controls. `querySelector('.cls')` returns the first instance on the page, so the other instances look broken.
77
+ 3. **Order:** `<style>` above the markup, `<script>` below it.
78
+ 4. **Props carry text only.** `data-bascik-prop-*` values are HTML-escaped strings. Pass markup or nested components through slots (`data-bascik-slot`).
79
+ 5. **Self-close** components that take no slot content: `<site-head />`, `<site-footer />`.
80
+ 6. **HTML and CSS first.** Reach for JavaScript only when HTML and CSS cannot do the job cleanly (`<details>`, `<dialog>`, `:has()`, `popover` cover a lot).
81
+ 7. **Runtime state in `data-` attributes.** Toggle `data-state="open"` and style `.panel[data-state="open"]`; anchor attribute selectors on a scoped class.
82
+ 8. **Zero config.** Do not create `bascik.config.ts` unless a requirement differs from a default, and never put `siteUrl` in it. See the `bascik-config` skill.
83
+ 9. **Never write generated files into `src/`.** Generated output goes to `dist/` (`BASCIK_OUT_DIR`) from an existing `pipeline.exec` script.
84
+ 10. **Client scripts are not bundled.** Bare imports such as `import x from 'pkg'` fail in the browser. They work only in Node-side scripts (`data-bascik-build`, `data-bascik-server`).
85
+ 11. **Extract shared layout.** Repeated `<head>` tags, headers, and footers become components (`<site-head />`, `<site-header />`, `<site-footer />`). Pages keep their own `<html>`, `<head>`, and `<body>`.
86
+ 12. **Script directives are exclusive.** One `<script>` tag takes at most one of `data-bascik-build`, `data-bascik-routes`, `data-bascik-server`, `data-bascik-stream`.
87
+
88
+ ## Slots, props, and attributes
89
+
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 |
97
+
98
+ 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
+
100
+ ## Script tags at a glance
101
+
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 |
110
+
111
+ Node-side scripts export a default function that returns an HTML string. Details are in the `bascik-scripts` skill.
112
+
113
+ ## CLI
114
+
115
+ ```sh
116
+ bascik # dev server at http://localhost:8080 with live reload and watch
117
+ bascik --build # production build to dist/ (HTML, CSS, JS, and identifier minification on)
118
+ bascik --build --only "blog/**" # rebuild matching pages without cleaning dist/
119
+ 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
121
+ bascik add <package> # copy components from an installed npm package into src/components/
122
+ bascik init # add Bascik to an existing project
123
+ npx @bascik/language-server --check # lint scoping patterns, unclosed tags, and directives
124
+ ```
125
+
126
+ 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
+
128
+ ## Verify every change
129
+
130
+ Do not stop at "the file looks right". Run these from the project root:
131
+
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.
133
+ 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
+ 3. Read the relevant file in `dist/` after a build. A hyphenated tag still present in `dist/` means no component matched it.
135
+
136
+ `<skill-dir>` is the directory containing this file, for example `.agents/skills/bascik`.
137
+
138
+ ## Bundled scripts
139
+
140
+ All scripts are dependency-free Node.js (22.18+) and run from the project root. Each prints `--help`.
141
+
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` |
152
+
153
+ Run `list-components.mjs` before creating a component so you reuse what exists instead of duplicating it.
154
+
155
+ ## Look it up in the docs
156
+
157
+ The docs at https://bascik.dev are the reference; this skill only holds the rules. Read the page for any task below before writing code, instead of guessing:
158
+
159
+ ```sh
160
+ node <skill-dir>/scripts/read-doc.mjs /dynamic-routes
161
+ node <skill-dir>/scripts/search-docs.mjs "dynamic routes trailing slash"
162
+ ```
163
+
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` |
173
+
174
+ The full page index is at https://bascik.dev/llms.txt.
175
+
176
+ ## Start from an example
177
+
178
+ https://bascik.dev/example-components has tested components (tabs, dialog, popover menu, copy button, filter list, contact form, theme toggle). `add-example.mjs` also offers `starter-site`, a complete site (layout components, SEO helper, Markdown blog, feed, `llms.txt`, contact page) whose README is at https://github.com/bascikdev/bascik/tree/main/examples/starter-site. Copy instead of writing from scratch, then adapt:
179
+
180
+ ```sh
181
+ node <skill-dir>/scripts/add-example.mjs --list
182
+ node <skill-dir>/scripts/add-example.mjs tab-group
183
+ node <skill-dir>/scripts/add-example.mjs starter-site/src/lib/seo.ts
184
+ ```
185
+
186
+ The script downloads from bascik.dev, maps files to the project's configured folders, also copies example components a file uses, and writes nothing if any file would overwrite a different one. It prints `bascik.config.ts` and `package.json` instead of copying them.
187
+
188
+ ## Build a new site
189
+
190
+ 1. `npm create bascik@latest my-site -y`, or read the existing project first with `list-components.mjs`. Add `BASCIK_SITE_URL=https://example.com` to `.env`.
191
+ 2. Plan the pages and URLs (one purpose and one `<h1>` each), the shared layout (head, header, footer), repeated content (a collection in `content/`), and anything that needs a server (API routes, per-request markup). Ask when the plan is unclear.
192
+ 3. Copy the foundation: `add-example.mjs starter-site`, or only the parts you need. Then replace the example name, navigation (`src/components/site-header/links.ts`), colors (tokens in `src/pages/css/styles.css`), and every page's `seo({ title, description })`.
193
+ 4. Add pages, components (`new-component.mjs`), and interactive examples. Load the companion skill for each area.
194
+ 5. Before launch: `npx bascik --build`, then `verify.mjs`, `check-links.mjs`, the `bascik-seo` audit, and the `bascik-components` token check. The starter site README's "Build and Check It" section has the manual checklist.
195
+
196
+ ## Companion skills
197
+
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 |
207
+
208
+ If the companions or this skill's `scripts/` folder are missing, the skill was installed as a single file. Reinstall the full set:
209
+
210
+ ```sh
211
+ npx skills add bascikdev/bascik
212
+ # or, without npx skills:
213
+ mkdir -p .agents/skills && curl -fsSL https://bascik.dev/assets/skills.tar.gz | tar -xz -C .agents/skills
214
+ ```
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
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';
8
+
9
+ const HELP = `Usage: node add-example.mjs --list
10
+ node add-example.mjs <example>[/<path>] [...more] [--dry-run] [--force]
11
+
12
+ Copies examples from https://bascik.dev/example-components into this project. Every example is tested with the real compiler
13
+ and in a browser before it is published.
14
+
15
+ References:
16
+ tab-group a component, into your first component folder
17
+ starter-site a whole example site
18
+ starter-site/src/lib/seo.ts one file of a site
19
+ starter-site/src/pages/blog a folder of a site
20
+
21
+ Copying site files also copies the components they use, from the site or from the example components.
22
+ Site files map to your config's folders:
23
+ src/components -> first directory.components root, src/pages -> directory.pages, src/lib -> <scripts.importRoot>/lib,
24
+ src/api -> directory.api, and anything else (scripts/, content/) to the same path under the project root.
25
+ bascik.config.ts, package.json, and README.md are printed, never copied: merge them by hand.
26
+
27
+ Nothing is written when any file would overwrite a different existing file, unless you pass --force.
28
+
29
+ Options:
30
+ --list List every example
31
+ --dry-run Show what would be copied without writing
32
+ --force Overwrite files that exist and differ
33
+ --source <where> Read the examples from a URL or a local examples.json (default: <docs>/assets/examples.json)
34
+ --help Show this message
35
+
36
+ Environment:
37
+ BASCIK_DOCS_URL Docs origin (default https://bascik.dev)`;
38
+
39
+ const REFERENCE_ONLY = new Set(['bascik.config.ts', 'bascik.config.js', 'package.json', 'README.md']);
40
+
41
+ /** Load the examples bundle from a URL or a local file. Exported for tests. */
42
+ export async function loadBundle(source = `${docsBaseUrl()}/assets/examples.json`) {
43
+ let text;
44
+ if (/^https?:\/\//i.test(source)) {
45
+ let response;
46
+ try {
47
+ response = await fetch(source, { signal: AbortSignal.timeout(20_000) });
48
+ } catch (error) {
49
+ throw new Error(`could not reach ${source}: ${error.message}. Check the network, or pass --source <examples.json>.`);
50
+ }
51
+ if (!response.ok) throw new Error(`${source} returned HTTP ${response.status}`);
52
+ text = await response.text();
53
+ } else {
54
+ text = readFileSync(resolve(source), 'utf8');
55
+ }
56
+ const bundle = JSON.parse(text);
57
+ 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.`);
59
+ }
60
+ return bundle;
61
+ }
62
+
63
+ /**
64
+ * Where a file of an example lands in the project, or undefined for files that are only printed.
65
+ * `layout` holds absolute paths: root, componentRoot, pagesDir, importRoot, apiDir. Exported for tests.
66
+ */
67
+ export function destinationFor(example, filePath, layout) {
68
+ const segments = filePath.split(/[\\/]/);
69
+ if (isAbsolute(filePath) || segments.some((segment) => segment === '..' || segment === '' || segment === '.')) {
70
+ throw new Error(`unsafe path in the examples bundle: ${filePath}`);
71
+ }
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}`);
75
+ }
76
+ return join(layout.componentRoot, example.name, filePath);
77
+ }
78
+ 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);
85
+ return join(layout.importRoot, second, tail);
86
+ }
87
+ return join(layout.root, filePath);
88
+ }
89
+
90
+ /**
91
+ * The files a reference selects, plus the example components those files use. Returns
92
+ * [{ example, file }] with each file once. Throws on an unknown reference. Exported for tests.
93
+ */
94
+ export function selectFiles(bundle, references) {
95
+ const byName = new Map(bundle.examples.map((example) => [example.name, example]));
96
+ const picked = new Map();
97
+ const add = (example, file) => picked.set(`${example.name}/${file.path}`, { example, file });
98
+ // [example the using file belongs to, component name]
99
+ const queue = [];
100
+ for (const reference of references) {
101
+ const [name, ...rest] = reference.replace(/\\/g, '/').replace(/\/+$/, '').split('/');
102
+ const example = byName.get(name);
103
+ if (!example) {
104
+ const names = bundle.examples.map((e) => e.name).join(', ');
105
+ throw new Error(`no example named "${name}". Available: ${names}`);
106
+ }
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}`);
110
+ for (const file of files) {
111
+ add(example, file);
112
+ queue.push(...file.uses.map((name) => [example, name]));
113
+ }
114
+ }
115
+ const seen = new Set();
116
+ while (queue.length) {
117
+ const [owner, name] = queue.shift();
118
+ // 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 ?? []];
121
+ const key = `${example?.name}:${name}`;
122
+ if (!example || seen.has(key)) continue;
123
+ seen.add(key);
124
+ for (const file of files) {
125
+ add(example, file);
126
+ queue.push(...file.uses.map((used) => [example, used]));
127
+ }
128
+ }
129
+ return [...picked.values()];
130
+ }
131
+
132
+ async function main() {
133
+ const { values, positionals } = parseArgs({
134
+ allowPositionals: true,
135
+ 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 },
141
+ },
142
+ });
143
+ if (values.help || (!values.list && positionals.length === 0)) return console.log(HELP);
144
+
145
+ let bundle;
146
+ try {
147
+ bundle = await loadBundle(values.source);
148
+ } catch (error) {
149
+ fail(`add-example: ${error.message}`);
150
+ }
151
+ // An example's docs link is a docs path, or a full URL for examples without a docs page (sites link to GitHub).
152
+ const docsLink = (example) => new URL(example.docs, `${docsBaseUrl()}/`).href;
153
+
154
+ if (values.list) {
155
+ for (const kind of ['component', 'site']) {
156
+ const examples = bundle.examples.filter((example) => example.kind === kind);
157
+ if (!examples.length) continue;
158
+ console.log(`\n${kind === 'component' ? 'Components' : 'Sites'}`);
159
+ for (const example of examples) {
160
+ const firstSentence = example.description.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? example.description;
161
+ console.log(` ${example.name.padEnd(16)} ${firstSentence}`);
162
+ console.log(` ${''.padEnd(16)} ${docsLink(example)}`);
163
+ }
164
+ }
165
+ return;
166
+ }
167
+
168
+ const root = findProjectRoot();
169
+ const config = await loadProjectConfig(root);
170
+ if (config?.error) fail(`add-example: could not load ${config.path}: ${config.error.message}`);
171
+ const directory = config?.module?.default?.directory ?? {};
172
+ const roots = componentRoots(root, config?.module);
173
+ const layout = {
174
+ root,
175
+ componentRoot: roots[0],
176
+ pagesDir: pagesDirectory(root, config?.module),
177
+ importRoot: resolve(root, config?.module?.default?.scripts?.importRoot ?? 'src'),
178
+ apiDir: resolve(root, directory.api ?? 'src/api'),
179
+ };
180
+
181
+ let selected;
182
+ try {
183
+ selected = selectFiles(bundle, positionals);
184
+ } catch (error) {
185
+ fail(`add-example: ${error.message}`);
186
+ }
187
+
188
+ const existingComponents = listComponents(roots, root);
189
+ const plan = [];
190
+ const printed = [];
191
+ for (const { example, file } of selected) {
192
+ let destination;
193
+ try {
194
+ destination = destinationFor(example, file.path, layout);
195
+ } catch (error) {
196
+ fail(`add-example: ${error.message}`);
197
+ }
198
+ if (!destination) {
199
+ if (file.path !== 'README.md') printed.push({ example, file });
200
+ continue;
201
+ }
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);
204
+ if (clash) {
205
+ const same = readFileSync(resolve(root, clash.file), 'utf8') === file.content;
206
+ if (same) {
207
+ console.log(`same ${clash.file} (already in the project)`);
208
+ continue;
209
+ }
210
+ fail(`add-example: a component named <${example.name}> already exists at ${clash.file}. Rename one of them first.`);
211
+ }
212
+ }
213
+ plan.push({ example, file, destination });
214
+ }
215
+
216
+ const show = (path) => relative(root, path).replace(/\\/g, '/');
217
+ const conflicts = plan.filter(({ file, destination }) => existsSync(destination) && readFileSync(destination, 'utf8') !== file.content);
218
+ 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')}`);
221
+ }
222
+
223
+ let written = 0;
224
+ for (const { file, destination } of plan) {
225
+ if (existsSync(destination) && readFileSync(destination, 'utf8') === file.content) {
226
+ console.log(`same ${show(destination)}`);
227
+ continue;
228
+ }
229
+ console.log(`${values['dry-run'] ? 'would' : 'write'} ${show(destination)}`);
230
+ if (values['dry-run']) continue;
231
+ mkdirSync(dirname(destination), { recursive: true });
232
+ writeFileSync(destination, file.content);
233
+ written++;
234
+ }
235
+
236
+ 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()}`);
238
+ }
239
+ const packageJson = printed.find(({ file }) => file.path === 'package.json');
240
+ 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 ?? {})]);
244
+ const missing = wanted.filter((name) => !have.has(name));
245
+ if (missing.length) console.log(`\nInstall the dependencies this example uses: npm install ${missing.join(' ')}`);
246
+ }
247
+ if (!values['dry-run']) console.log(`\n${written} file${written === 1 ? '' : 's'} written. Read each file's comments for setup steps.`);
248
+ }
249
+
250
+ if (isMain(import.meta.url)) await main();