create-bascik 1.0.0-rc.5 → 1.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/assets/skills/README.md +64 -0
- package/assets/skills/bascik/SKILL.md +222 -0
- package/assets/skills/bascik/scripts/add-example.mjs +347 -0
- package/assets/skills/bascik/scripts/check-links.mjs +293 -0
- package/assets/skills/bascik/scripts/lib/components.mjs +184 -0
- package/assets/skills/bascik/scripts/lib/project.mjs +228 -0
- package/assets/skills/bascik/scripts/list-components.mjs +106 -0
- package/assets/skills/bascik/scripts/new-component.mjs +213 -0
- package/assets/skills/bascik/scripts/preview.mjs +198 -0
- package/assets/skills/bascik/scripts/read-doc.mjs +118 -0
- package/assets/skills/bascik/scripts/search-docs.mjs +141 -0
- package/assets/skills/bascik/scripts/verify.mjs +174 -0
- package/assets/skills/bascik-compatibility/SKILL.md +73 -0
- package/assets/skills/bascik-compatibility/scripts/baseline.mjs +149 -0
- package/assets/skills/bascik-compatibility/scripts/lookup.mjs +248 -0
- package/assets/skills/bascik-components/SKILL.md +95 -0
- package/assets/skills/bascik-components/scripts/check-tokens.mjs +175 -0
- package/assets/skills/bascik-config/SKILL.md +158 -0
- package/assets/skills/bascik-config/scripts/audit-config.mjs +434 -0
- package/assets/skills/bascik-config/scripts/check-workspace.mjs +379 -0
- package/assets/skills/bascik-migrate/SKILL.md +74 -0
- package/assets/skills/bascik-scripts/SKILL.md +145 -0
- package/assets/skills/bascik-scripts/scripts/new-post.mjs +111 -0
- package/assets/skills/bascik-seo/SKILL.md +61 -0
- package/assets/skills/bascik-seo/scripts/audit-seo.mjs +384 -0
- package/assets/skills/bascik-testing/SKILL.md +88 -0
- 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.d.ts +7 -0
- package/dist/scaffold.js +21 -14
- package/dist/scaffold.js.map +1 -1
- package/package.json +16 -8
- 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 +77 -25
- package/src/scaffold.test.ts +121 -28
- package/src/scaffold.ts +201 -86
- package/src/skill-sync.test.ts +39 -11
- package/assets/SKILL.md +0 -2163
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ npm create bascik@latest
|
|
|
25
25
|
|
|
26
26
|
```text
|
|
27
27
|
my-site/
|
|
28
|
-
├── .
|
|
29
|
-
├── .claude/skills/
|
|
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,222 @@
|
|
|
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 --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 {
|
|
52
|
+
padding: 0.5rem 1rem;
|
|
53
|
+
border-radius: 6px;
|
|
54
|
+
}
|
|
55
|
+
.like.active {
|
|
56
|
+
background: #d3ff8d;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
<button class="like" id="btn" type="button">
|
|
60
|
+
<span data-bascik-prop-label>Like</span>
|
|
61
|
+
</button>
|
|
62
|
+
<script>
|
|
63
|
+
const btn = document.getElementById("btn");
|
|
64
|
+
btn.addEventListener("click", () => btn.classList.toggle("active"));
|
|
65
|
+
</script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<!-- src/pages/index.html -->
|
|
70
|
+
<like-button data-bascik-prop-label="Like this post" />
|
|
71
|
+
<like-button data-bascik-prop-label="Like this comment" />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
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).
|
|
75
|
+
|
|
76
|
+
New components are picked up by the running dev server automatically; no restart is needed.
|
|
77
|
+
|
|
78
|
+
## Rules (must follow)
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
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.
|
|
82
|
+
3. **Order:** `<style>` above the markup, `<script>` below it.
|
|
83
|
+
4. **Props carry text only.** `data-bascik-prop-*` values are HTML-escaped strings. Pass markup or nested components through slots (`data-bascik-slot`).
|
|
84
|
+
5. **Self-close** components that take no slot content: `<site-head />`, `<site-footer />`.
|
|
85
|
+
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).
|
|
86
|
+
7. **Runtime state in `data-` attributes.** Toggle `data-state="open"` and style `.panel[data-state="open"]`; anchor attribute selectors on a scoped class.
|
|
87
|
+
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.
|
|
88
|
+
9. **Never write generated files into `src/`.** Generated output goes to `dist/` (`BASCIK_OUT_DIR`) from an existing `pipeline.exec` script.
|
|
89
|
+
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`).
|
|
90
|
+
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>`.
|
|
91
|
+
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`.
|
|
92
|
+
|
|
93
|
+
## Slots, props, and attributes
|
|
94
|
+
|
|
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 |
|
|
102
|
+
|
|
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`.
|
|
104
|
+
|
|
105
|
+
## Script tags at a glance
|
|
106
|
+
|
|
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 |
|
|
115
|
+
|
|
116
|
+
Node-side scripts export a default function that returns an HTML string. Details are in the `bascik-scripts` skill.
|
|
117
|
+
|
|
118
|
+
## CLI
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
bascik # dev server at http://localhost:8080 with live reload and watch
|
|
122
|
+
bascik --build # production build to dist/ (HTML, CSS, JS, and identifier minification on)
|
|
123
|
+
bascik --build --only "blog/**" # rebuild matching pages without cleaning dist/
|
|
124
|
+
bascik --server # serve a built dist/ (needed for server/stream scripts and API routes)
|
|
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)
|
|
127
|
+
bascik add <package> # copy components from an installed npm package into src/components/
|
|
128
|
+
bascik init # add Bascik to an existing project
|
|
129
|
+
npx @bascik/language-server --check # the per-file checks on their own (bascik --check already runs them)
|
|
130
|
+
```
|
|
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
|
+
|
|
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`.
|
|
135
|
+
|
|
136
|
+
## Verify every change
|
|
137
|
+
|
|
138
|
+
Do not stop at "the file looks right". Run these from the project root:
|
|
139
|
+
|
|
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.
|
|
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.
|
|
142
|
+
3. Read the relevant file in `dist/` after a build. A hyphenated tag still present in `dist/` means no component matched it.
|
|
143
|
+
|
|
144
|
+
`<skill-dir>` is the directory containing this file, for example `.agents/skills/bascik`.
|
|
145
|
+
|
|
146
|
+
## Bundled scripts
|
|
147
|
+
|
|
148
|
+
All scripts are dependency-free Node.js (22.18+) and run from the project root. Each prints `--help`.
|
|
149
|
+
|
|
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` |
|
|
160
|
+
|
|
161
|
+
Run `list-components.mjs` before creating a component so you reuse what exists instead of duplicating it.
|
|
162
|
+
|
|
163
|
+
## Look it up in the docs
|
|
164
|
+
|
|
165
|
+
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:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
node <skill-dir>/scripts/read-doc.mjs /dynamic-routes
|
|
169
|
+
node <skill-dir>/scripts/search-docs.mjs "dynamic routes trailing slash"
|
|
170
|
+
```
|
|
171
|
+
|
|
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` |
|
|
181
|
+
|
|
182
|
+
The full page index is at https://bascik.dev/llms.txt.
|
|
183
|
+
|
|
184
|
+
## Start from an example
|
|
185
|
+
|
|
186
|
+
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:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
node <skill-dir>/scripts/add-example.mjs --list
|
|
190
|
+
node <skill-dir>/scripts/add-example.mjs tab-group
|
|
191
|
+
node <skill-dir>/scripts/add-example.mjs starter-site/src/lib/seo.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
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.
|
|
195
|
+
|
|
196
|
+
## Build a new site
|
|
197
|
+
|
|
198
|
+
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`.
|
|
199
|
+
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.
|
|
200
|
+
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 })`.
|
|
201
|
+
4. Add pages, components (`new-component.mjs`), and interactive examples. Load the companion skill for each area.
|
|
202
|
+
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.
|
|
203
|
+
|
|
204
|
+
## Companion skills
|
|
205
|
+
|
|
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 |
|
|
215
|
+
|
|
216
|
+
If the companions or this skill's `scripts/` folder are missing, the skill was installed as a single file. Reinstall the full set:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
npx skills add bascikdev/bascik
|
|
220
|
+
# or, without npx skills:
|
|
221
|
+
mkdir -p .agents/skills && curl -fsSL https://bascik.dev/assets/skills.tar.gz | tar -xz -C .agents/skills
|
|
222
|
+
```
|
|
@@ -0,0 +1,347 @@
|
|
|
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 {
|
|
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";
|
|
16
|
+
|
|
17
|
+
const HELP = `Usage: node add-example.mjs --list
|
|
18
|
+
node add-example.mjs <example>[/<path>] [...more] [--dry-run] [--force]
|
|
19
|
+
|
|
20
|
+
Copies examples from https://bascik.dev/example-components into this project. Every example is tested with the real compiler
|
|
21
|
+
and in a browser before it is published.
|
|
22
|
+
|
|
23
|
+
References:
|
|
24
|
+
tab-group a component, into your first component folder
|
|
25
|
+
starter-site a whole example site
|
|
26
|
+
starter-site/src/lib/seo.ts one file of a site
|
|
27
|
+
starter-site/src/pages/blog a folder of a site
|
|
28
|
+
|
|
29
|
+
Copying site files also copies the components they use, from the site or from the example components.
|
|
30
|
+
Site files map to your config's folders:
|
|
31
|
+
src/components -> first directory.components root, src/pages -> directory.pages, src/lib -> <scripts.importRoot>/lib,
|
|
32
|
+
src/api -> directory.api, and anything else (scripts/, content/) to the same path under the project root.
|
|
33
|
+
bascik.config.ts, package.json, and README.md are printed, never copied: merge them by hand.
|
|
34
|
+
|
|
35
|
+
Nothing is written when any file would overwrite a different existing file, unless you pass --force.
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--list List every example
|
|
39
|
+
--dry-run Show what would be copied without writing
|
|
40
|
+
--force Overwrite files that exist and differ
|
|
41
|
+
--source <where> Read the examples from a URL or a local examples.json (default: <docs>/assets/examples.json)
|
|
42
|
+
--help Show this message
|
|
43
|
+
|
|
44
|
+
Environment:
|
|
45
|
+
BASCIK_DOCS_URL Docs origin (default https://bascik.dev)`;
|
|
46
|
+
|
|
47
|
+
const REFERENCE_ONLY = new Set([
|
|
48
|
+
"bascik.config.ts",
|
|
49
|
+
"bascik.config.js",
|
|
50
|
+
"package.json",
|
|
51
|
+
"README.md",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Load the examples bundle from a URL or a local file. Exported for tests. */
|
|
55
|
+
export async function loadBundle(
|
|
56
|
+
source = `${docsBaseUrl()}/assets/examples.json`,
|
|
57
|
+
) {
|
|
58
|
+
let text;
|
|
59
|
+
if (/^https?:\/\//i.test(source)) {
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await fetch(source, { signal: AbortSignal.timeout(20_000) });
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`could not reach ${source}: ${error.message}. Check the network, or pass --source <examples.json>.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
throw new Error(`${source} returned HTTP ${response.status}`);
|
|
70
|
+
text = await response.text();
|
|
71
|
+
} else {
|
|
72
|
+
text = readFileSync(resolve(source), "utf8");
|
|
73
|
+
}
|
|
74
|
+
const bundle = JSON.parse(text);
|
|
75
|
+
if (bundle?.version !== 1 || !Array.isArray(bundle.examples)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${source} is not a supported examples bundle (version ${bundle?.version}). Update the skills.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return bundle;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Where a file of an example lands in the project, or undefined for files that are only printed.
|
|
85
|
+
* `layout` holds absolute paths: root, componentRoot, pagesDir, importRoot, apiDir. Exported for tests.
|
|
86
|
+
*/
|
|
87
|
+
export function destinationFor(example, filePath, layout) {
|
|
88
|
+
const segments = filePath.split(/[\\/]/);
|
|
89
|
+
if (
|
|
90
|
+
isAbsolute(filePath) ||
|
|
91
|
+
segments.some(
|
|
92
|
+
(segment) => segment === ".." || segment === "" || segment === ".",
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(`unsafe path in the examples bundle: ${filePath}`);
|
|
96
|
+
}
|
|
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
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return join(layout.componentRoot, example.name, filePath);
|
|
107
|
+
}
|
|
108
|
+
if (REFERENCE_ONLY.has(filePath)) return undefined;
|
|
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);
|
|
115
|
+
return join(layout.importRoot, second, tail);
|
|
116
|
+
}
|
|
117
|
+
return join(layout.root, filePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The files a reference selects, plus the example components those files use. Returns
|
|
122
|
+
* [{ example, file }] with each file once. Throws on an unknown reference. Exported for tests.
|
|
123
|
+
*/
|
|
124
|
+
export function selectFiles(bundle, references) {
|
|
125
|
+
const byName = new Map(
|
|
126
|
+
bundle.examples.map((example) => [example.name, example]),
|
|
127
|
+
);
|
|
128
|
+
const picked = new Map();
|
|
129
|
+
const add = (example, file) =>
|
|
130
|
+
picked.set(`${example.name}/${file.path}`, { example, file });
|
|
131
|
+
// [example the using file belongs to, component name]
|
|
132
|
+
const queue = [];
|
|
133
|
+
for (const reference of references) {
|
|
134
|
+
const [name, ...rest] = reference
|
|
135
|
+
.replace(/\\/g, "/")
|
|
136
|
+
.replace(/\/+$/, "")
|
|
137
|
+
.split("/");
|
|
138
|
+
const example = byName.get(name);
|
|
139
|
+
if (!example) {
|
|
140
|
+
const names = bundle.examples.map((e) => e.name).join(", ");
|
|
141
|
+
throw new Error(`no example named "${name}". Available: ${names}`);
|
|
142
|
+
}
|
|
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
|
+
);
|
|
153
|
+
for (const file of files) {
|
|
154
|
+
add(example, file);
|
|
155
|
+
queue.push(...file.uses.map((name) => [example, name]));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
while (queue.length) {
|
|
160
|
+
const [owner, name] = queue.shift();
|
|
161
|
+
// A site's own src/components/<name>/ wins over the example component of the same name.
|
|
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 ?? []];
|
|
171
|
+
const key = `${example?.name}:${name}`;
|
|
172
|
+
if (!example || seen.has(key)) continue;
|
|
173
|
+
seen.add(key);
|
|
174
|
+
for (const file of files) {
|
|
175
|
+
add(example, file);
|
|
176
|
+
queue.push(...file.uses.map((used) => [example, used]));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [...picked.values()];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function main() {
|
|
183
|
+
const { values, positionals } = parseArgs({
|
|
184
|
+
allowPositionals: true,
|
|
185
|
+
options: {
|
|
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 },
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
if (values.help || (!values.list && positionals.length === 0))
|
|
194
|
+
return console.log(HELP);
|
|
195
|
+
|
|
196
|
+
let bundle;
|
|
197
|
+
try {
|
|
198
|
+
bundle = await loadBundle(values.source);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
fail(`add-example: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
// An example's docs link is a docs path, or a full URL for examples without a docs page (sites link to GitHub).
|
|
203
|
+
const docsLink = (example) => new URL(example.docs, `${docsBaseUrl()}/`).href;
|
|
204
|
+
|
|
205
|
+
if (values.list) {
|
|
206
|
+
for (const kind of ["component", "site"]) {
|
|
207
|
+
const examples = bundle.examples.filter(
|
|
208
|
+
(example) => example.kind === kind,
|
|
209
|
+
);
|
|
210
|
+
if (!examples.length) continue;
|
|
211
|
+
console.log(`\n${kind === "component" ? "Components" : "Sites"}`);
|
|
212
|
+
for (const example of examples) {
|
|
213
|
+
const firstSentence =
|
|
214
|
+
example.description.match(/^.*?[.!?](?=\s|$)/)?.[0] ??
|
|
215
|
+
example.description;
|
|
216
|
+
console.log(` ${example.name.padEnd(16)} ${firstSentence}`);
|
|
217
|
+
console.log(` ${"".padEnd(16)} ${docsLink(example)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const root = findProjectRoot();
|
|
224
|
+
const config = await loadProjectConfig(root);
|
|
225
|
+
if (config?.error)
|
|
226
|
+
fail(`add-example: could not load ${config.path}: ${config.error.message}`);
|
|
227
|
+
const directory = config?.module?.default?.directory ?? {};
|
|
228
|
+
const roots = componentRoots(root, config?.module);
|
|
229
|
+
const layout = {
|
|
230
|
+
root,
|
|
231
|
+
componentRoot: roots[0],
|
|
232
|
+
pagesDir: pagesDirectory(root, config?.module),
|
|
233
|
+
importRoot: resolve(
|
|
234
|
+
root,
|
|
235
|
+
config?.module?.default?.scripts?.importRoot ?? "src",
|
|
236
|
+
),
|
|
237
|
+
apiDir: resolve(root, directory.api ?? "src/api"),
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
let selected;
|
|
241
|
+
try {
|
|
242
|
+
selected = selectFiles(bundle, positionals);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
fail(`add-example: ${error.message}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const existingComponents = listComponents(roots, root);
|
|
248
|
+
const plan = [];
|
|
249
|
+
const printed = [];
|
|
250
|
+
for (const { example, file } of selected) {
|
|
251
|
+
let destination;
|
|
252
|
+
try {
|
|
253
|
+
destination = destinationFor(example, file.path, layout);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
fail(`add-example: ${error.message}`);
|
|
256
|
+
}
|
|
257
|
+
if (!destination) {
|
|
258
|
+
if (file.path !== "README.md") printed.push({ example, file });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
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
|
+
);
|
|
269
|
+
if (clash) {
|
|
270
|
+
const same =
|
|
271
|
+
readFileSync(resolve(root, clash.file), "utf8") === file.content;
|
|
272
|
+
if (same) {
|
|
273
|
+
console.log(`same ${clash.file} (already in the project)`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
fail(
|
|
277
|
+
`add-example: a component named <${example.name}> already exists at ${clash.file}. Rename one of them first.`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
plan.push({ example, file, destination });
|
|
282
|
+
}
|
|
283
|
+
|
|
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
|
+
);
|
|
290
|
+
if (conflicts.length && !values.force) {
|
|
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
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let written = 0;
|
|
301
|
+
for (const { file, destination } of plan) {
|
|
302
|
+
if (
|
|
303
|
+
existsSync(destination) &&
|
|
304
|
+
readFileSync(destination, "utf8") === file.content
|
|
305
|
+
) {
|
|
306
|
+
console.log(`same ${show(destination)}`);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
console.log(
|
|
310
|
+
`${values["dry-run"] ? "would" : "write"} ${show(destination)}`,
|
|
311
|
+
);
|
|
312
|
+
if (values["dry-run"]) continue;
|
|
313
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
314
|
+
writeFileSync(destination, file.content);
|
|
315
|
+
written++;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const { example, file } of printed) {
|
|
319
|
+
console.log(
|
|
320
|
+
`\n--- ${example.name}/${file.path} (not copied: merge what you need by hand) ---\n${file.content.trimEnd()}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const packageJson = printed.find(({ file }) => file.path === "package.json");
|
|
324
|
+
if (packageJson) {
|
|
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
|
+
]);
|
|
335
|
+
const missing = wanted.filter((name) => !have.has(name));
|
|
336
|
+
if (missing.length)
|
|
337
|
+
console.log(
|
|
338
|
+
`\nInstall the dependencies this example uses: npm install ${missing.join(" ")}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
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
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (isMain(import.meta.url)) await main();
|