create-bascik 1.0.0-rc.1

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.
@@ -0,0 +1,2025 @@
1
+ ---
2
+ name: bascik
3
+ description: Comprehensive guide for building with Bascik, a file-based web component framework. Use when creating components, writing scoped CSS or JavaScript, configuring props or slots, using the CLI, setting up the build, or debugging Bascik-specific behavior.
4
+ ---
5
+
6
+ # Bascik Developer & Usage Copilot Skill (`SKILL.md`)
7
+
8
+ This file contains the **complete, centralized documentation and development skill guide** for Bascik. It is designed to provide Copilot and external developers with everything needed to build, compile, maintain, and write components for the Bascik project.
9
+
10
+ **Note on Code & Script Examples:** Code examples throughout this skill guide demonstrate patterns, capabilities, and API structures. Adapt file paths, script names, tag names, and configuration values to match the specific project requirements rather than copying illustrative names literally.
11
+
12
+ ---
13
+
14
+ ## 1. What Bascik Is & Does
15
+
16
+ > Bascik is a build tool for HTML components. It scopes and assembles reusable HTML component files into vanilla HTML pages at build time. It adds zero JavaScript to the output. You write HTML, CSS, and JavaScript; Bascik scopes and assembles them.
17
+
18
+ * **Resolves custom HTML tags** (e.g. `<my-nav></my-nav>`) to their component source HTML at build time.
19
+ * **Scopes CSS class names, element selectors, `@keyframes`, and CSS custom properties** per component so they never collide.
20
+ * **Rewrites DOM selector calls** (`getElementById`, `querySelector`, etc.) in component scripts to match scoped attribute names.
21
+ * **Wraps component scripts in IIFEs** so variables do not leak between components.
22
+ * **Derives instance IDs deterministically** from page path, component name, and ordinal index, ensuring byte-identical builds across repeated runs.
23
+ * **Internal build metadata in `dist/.bascik/`:** Build-internal artifacts (such as `dist/.bascik/manifest.json` when `generate.manifest` is enabled) are stored in dot-prefixed paths within `dist/`. These are never served over HTTP because requests containing dot-segments always return 404.
24
+ * **Outputs a `dist/` directory of plain `.html` files** with zero runtime dependencies and no client-side JS added by Bascik itself.
25
+
26
+ ### What Bascik Does NOT Do
27
+ * It is not a JavaScript framework. There is no virtual DOM, no reactive state, no client-side routing.
28
+ * It does not add any JavaScript to pages. Every script in the output was written by you.
29
+ * It does not require Web Components, Shadow DOM, or any browser-specific API.
30
+
31
+ ### End-to-End Example: Input → Output
32
+
33
+ One component file, its usage, and the scoped build output:
34
+
35
+ ```html
36
+ <!-- src/components/my-card.html -->
37
+ <style>
38
+ .card {
39
+ padding: 26px 28px;
40
+ border: 1px solid #3a3d40;
41
+ border-radius: 10px;
42
+ &.active { border-color: #d3ff8d; }
43
+ }
44
+ </style>
45
+ <div class="card" id="card">
46
+ <div data-bascik-slot></div>
47
+ </div>
48
+ <script>
49
+ const card = document.getElementById('card');
50
+ card.addEventListener('click', () => {
51
+ card.classList.toggle('active');
52
+ });
53
+ </script>
54
+ ```
55
+
56
+ ```html
57
+ <!-- src/pages/index.html -->
58
+ <my-card>
59
+ <h3>My Card</h3>
60
+ <p>Any HTML goes inside as slot content.</p>
61
+ </my-card>
62
+ ```
63
+
64
+ ```html
65
+ <!-- dist/index.html - classes and selectors scoped, script wrapped in IIFE -->
66
+ <style>
67
+ .bascik__my-card__card {
68
+ padding: 26px 28px;
69
+ border: 1px solid #3a3d40;
70
+ border-radius: 10px;
71
+ &.bascik__my-card__active { border-color: #d3ff8d; }
72
+ }
73
+ </style>
74
+ <div class="bascik__my-card__card" id="bascik__my-card__a1b__card">
75
+ <h3>My Card</h3>
76
+ <p>Any HTML goes inside as slot content.</p>
77
+ </div>
78
+ <script>
79
+ (function () {
80
+ const card = document.getElementById('bascik__my-card__a1b__card');
81
+ card.addEventListener('click', () => {
82
+ card.classList.toggle('bascik__my-card__active');
83
+ });
84
+ })();
85
+ </script>
86
+ ```
87
+
88
+ Because IDs are scoped per instance, the same component can appear multiple times on one page and each instance's JS stays fully isolated. Plain `getElementById` / `querySelector` calls just work.
89
+
90
+
91
+ ### How Bascik Compares to Other Tools
92
+ * **Vanilla HTML with Build-Time Components:** Bascik is built for content sites, landing pages, and documentation portals where shipping 80+ KB of React or framework runtime is unnecessary.
93
+ * **Contrast with Svelte:** Svelte compiles to a JavaScript runtime for reactive DOM management. Bascik compiles to plain HTML with zero runtime added.
94
+ * **Complements HTMX and Alpine.js:** Bascik resolves components at build time, while HTMX and Alpine add behavior at runtime. They compose cleanly together.
95
+ * **Contrast with Eleventy / Hugo / Jekyll:** Those tools focus on content pipelines (Markdown collections and template engines). Bascik focuses on HTML page composition and component reuse without a template language.
96
+
97
+ ### Source Vocabulary and Browser Compatibility
98
+ * The custom component tags in a Bascik project are the components the developer creates; Bascik does not provide a framework-owned component catalog.
99
+ * Build instructions such as `data-bascik-slot`, `data-bascik-prop-*`, and `data-bascik-build` use HTML's standards-valid `data-*` extension mechanism.
100
+ * Bascik consumes those instructions at build time. They are not runtime directives that require a client library to interpret them.
101
+ * **Tag structure ordering rule:** When writing component files containing both styles and scripts alongside HTML, always place `<style>` tags above the HTML markup as a style guide, and always place `<script>` tags below the HTML markup.
102
+
103
+ ### Repository Layout and the Create App
104
+
105
+ The repo is split into four top-level folders:
106
+
107
+ ```text
108
+ bascik/
109
+ pkg/ ← the @bascik/bascik npm package
110
+ create/ ← the standalone generator used by `npm create bascik@latest`
111
+ docs/ ← the docs site and internal developer guide
112
+ extensions/ ← editor tooling, including the VS Code extension for component navigation and scoping checks
113
+ ```
114
+
115
+ The `create/` folder is intentionally separate from `pkg/`. Contributor work in this monorepo uses Yarn 4 with `yarn.lock`, while generated projects intentionally use npm and receive their own `package-lock.json`. That split keeps contributor workflows Yarn-based while preserving the standard npm onboarding flow for generated apps.
116
+
117
+ The editor package in `extensions/vscode-bascik/` is intentionally separate from `pkg/`. It provides context-aware IntelliSense (component, prop, and slot completions), command-click component navigation, rich hover details, syntax highlighting, and real-time scoping warnings. Linting and language server capabilities are powered by `@bascik/language-server`, which also runs standalone via CLI (`npm run lint` or `npx @bascik/language-server --check`) and implements LSP so it works in Neovim, Helix, Zed, and other editors. The compatibility rules are generated from `docs/content/compatibility.md` via `docs/scripts/generate-compatibility-rules.ts`, so the editor and the published capability table stay in sync automatically instead of drifting apart.
118
+
119
+ The generator in `create/src/index.ts` validates input, then calls `create/src/scaffold.ts` to write the project files. The generated app is not coupled to the monorepo layout. It just uses the published `@bascik/bascik` package and then runs as a normal Bascik site.
120
+
121
+ For local contributor testing of the generator itself, rebuild from `create/`, link it with `npm link`, and invoke it via `npx create-bascik ...`; that remains the working flow for exercising the local scaffold end-to-end. `npm link` runs the `prepare` script, which copies the latest SKILL.md from `docs/` and rebuilds `dist/` automatically, so no separate build step is needed after a fresh checkout.
122
+
123
+ ---
124
+
125
+ ## 2. Component Format
126
+
127
+ A component is one `.html` file inside `src/components/`. Its tag name is derived from the file name.
128
+
129
+ ### Component Naming Conventions
130
+ Under the **WHATWG HTML standard (§4.13.1.2)**, custom elements should contain at least one hyphen (such as `<site-nav>`, `<hello-card>`, or `<my-button>`).
131
+ * Single-word component filenames (e.g., `card.html`) compile for backward compatibility, but Bascik's CLI compiler and VS Code extension issue warnings encouraging a hyphenated name (`my-card.html`).
132
+ * Filenames matching native HTML elements (e.g., `header.html` or `dialog.html`) issue a shadowing warning.
133
+ * Always use lowercase hyphenated filenames in `src/components/` (e.g., `src/components/feature-card.html` for `<feature-card>`).
134
+
135
+ **HTML-only component:**
136
+ ```html
137
+ <!-- src/components/page-footer.html -->
138
+ <footer>
139
+ <p>© 2025 My Site. All rights reserved.</p>
140
+ </footer>
141
+ ```
142
+
143
+ **HTML with inline CSS:**
144
+ ```html
145
+ <!-- src/components/promo-banner.html -->
146
+ <style>
147
+ .banner { padding: 1rem 1.5rem; background: #e8f4fd; border-left: 4px solid #3b82f6; }
148
+ </style>
149
+ <aside class="banner">…</aside>
150
+ ```
151
+
152
+ **HTML with inline JavaScript** (use `id` + `getElementById` for per-instance targeting):
153
+ ```html
154
+ <!-- src/components/scroll-top.html -->
155
+ <button id="btn" type="button">Back to top</button>
156
+ <script>
157
+ document.getElementById('btn').addEventListener('click', () => {
158
+ window.scrollTo({ top: 0, behavior: 'smooth' });
159
+ });
160
+ </script>
161
+ ```
162
+
163
+ **All three in one file** (HTML + `<style>` + `<script>`):
164
+ ```html
165
+ <!-- src/components/alert-box.html -->
166
+ <style>
167
+ .alert { display: flex; align-items: center; gap: 0.75rem; padding: 1rem; border: 1px solid #f59e0b; border-radius: 6px; background: #fffbeb; }
168
+ .alert-close { margin-left: auto; background: none; border: none; cursor: pointer; }
169
+ </style>
170
+ <div class="alert" id="alert">
171
+ <span>Maintenance on Sunday, 2am–4am UTC.</span>
172
+ <button id="close" class="alert-close" aria-label="Dismiss">×</button>
173
+ </div>
174
+ <script>
175
+ document.getElementById('close').addEventListener('click', () => {
176
+ document.getElementById('alert').hidden = true;
177
+ });
178
+ </script>
179
+ ```
180
+
181
+ Use it in any page or other component:
182
+ ```html
183
+ <alert-box></alert-box>
184
+ ```
185
+ *Self-closing tags are also supported:* `<site-nav />` or `<site-nav class="top" />`
186
+
187
+ **Separate CSS file:** pair a `.css` file with the same base name:
188
+ ```
189
+ src/components/
190
+ alert-box.html
191
+ alert-box.css ← scoped to alert-box
192
+ ```
193
+
194
+ **Subfolder layout** (subfolders are allowed for organization, but the filename determines the tag name; duplicate names anywhere in components error):
195
+ ```
196
+ src/components/
197
+ alert-box/
198
+ alert-box.html
199
+ alert-box.css
200
+ alert-box.ts ← inlined and scoped via <script src="alert-box.ts"></script>
201
+ ```
202
+
203
+ ### Companion CSS and Script Files
204
+ Companion `.css` files in the component directory are merged automatically. Companion script files (`.ts`, `.js`, `.mjs`) explicitly referenced via `<script src="counter.ts"></script>` inside component HTML are resolved, inlined, and scoped at build time. Path resolution is strictly scoped to the component directory or base filename.
205
+
206
+ ### Component Metadata Comments (`<!-- @bascik ... -->`)
207
+ To document a component's public contract and provide rich hover and autocomplete information in editor tooling (such as the Bascik VS Code extension), add an optional leading `@bascik` metadata comment block at the very top of the component file:
208
+
209
+ ```html
210
+ <!-- @bascik
211
+ Interactive demo box with Preview, Source, and Output panes.
212
+ @prop no-preview - Hides the Preview tab and collapses margins.
213
+ @prop file - Monospace file path displayed in the demo meta bar.
214
+ @slot example - Content rendered in the live preview pane.
215
+ @slot source-usage - Usage code snippet shown in the source tab.
216
+ @slot default - Default slot content if an un-named slot is exposed.
217
+ -->
218
+ <div class="demo-box">
219
+ <span data-bascik-prop-no-preview hidden></span>
220
+ <div class="demo-meta" data-bascik-prop-file></div>
221
+ <div data-bascik-slot="example"></div>
222
+ <div data-bascik-slot="source-usage"></div>
223
+ <div data-bascik-slot></div>
224
+ </div>
225
+ ```
226
+
227
+ **Metadata rules:**
228
+ - **Placement:** Must be placed before any markup, `<style>`, or `<script>` tags. Whitespace and standard HTML comments may precede it.
229
+ - **Markup is authoritative:** `@prop` and `@slot` annotations enrich inferred members with descriptions; they cannot declare members that do not exist in the markup.
230
+ - **Syntax:** `@prop <name> - <description>` and `@slot <name> - <description>`. Use `@slot default - <description>` for the default slot. Text is rendered literally.
231
+
232
+ ### Component Decomposition & Shared Head Tags
233
+ When structuring or migrating a site with Bascik, identify repeating HTML structures (especially shared `<head>` markup such as meta charset, viewport, favicons, fonts, Open Graph tags, and global CSS links, as well as site headers/footers) and extract them into reusable components.
234
+
235
+ Component tags can be placed inside `<head>` element blocks in your page shells:
236
+
237
+ ```html
238
+ <!-- src/components/site-head.html -->
239
+ <meta charset="UTF-8" />
240
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
241
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
242
+ <link rel="stylesheet" href="/styles.css" />
243
+ ```
244
+
245
+ Page shells can then include `<site-head />` inside their `<head>`:
246
+
247
+ ```html
248
+ <!-- src/pages/index.html -->
249
+ <!DOCTYPE html>
250
+ <html lang="en">
251
+ <head>
252
+ <site-head />
253
+ <title>Home - My Site</title>
254
+ </head>
255
+ <body>
256
+ <site-nav />
257
+ <main>
258
+ <h1>Welcome</h1>
259
+ </main>
260
+ <site-footer />
261
+ </body>
262
+ </html>
263
+ ```
264
+
265
+ At build time, Bascik expands `<site-head>` inside `<head>` and merges any component styles into the document `<head>` naturally. Proactively extract shared head elements, site headers, and site footers whenever initializing or refactoring a site into Bascik.
266
+
267
+ ### Multiple Root Elements
268
+ Unlike other frameworks that require a single wrapper element or fragment, Bascik component templates support multiple top-level HTML elements in a single `.html` file. All root elements are inserted in order. If non-`data-bascik-*` attributes are passed on a usage tag, Bascik merges them onto the first root HTML element.
269
+
270
+ **No restart needed.** The dev server watches the components directory. Drop a new `.html` (or paired `.css`) file in and all pages that use that tag are automatically re-transpiled and reloaded with no server restart required.
271
+
272
+ ---
273
+
274
+ ## 3. Scoped CSS
275
+
276
+ Scoped CSS can live in a paired `.css` file or one or more inline `<style>` tags inside component HTML. Both go through the same scoping pipeline.
277
+
278
+ At build time, Bascik extracts all inline `<style>` blocks, combines them with any companion `.css` file, scopes them, and injects them into the document `<head>`. Using multiple `<style>` tags (or mixing them with a companion `.css` file) is supported but not recommended for readability and maintainability. Choose a single stylesheet pattern per component.
279
+
280
+ ### Global Class Passthrough
281
+ Bascik checks whether a class name in a component template is defined in the component's CSS (paired `.css` or inline `<style>`). Only class names defined in the component's stylesheet are scoped (prefixed/hashed). Classes in component HTML that are NOT defined in the component's stylesheet (such as global utility classes like `skip-link`, `flex`, `hidden`, or design system classes) pass through as unscoped global classes so global stylesheets continue to match them.
282
+
283
+ Pair a `.css` file alongside the HTML in a same-named directory:
284
+
285
+ ```
286
+ src/components/
287
+ site-nav/
288
+ site-nav.html
289
+ site-nav.css
290
+ ```
291
+
292
+ All class names, element selectors, `#id` selectors, `@keyframes`, `@layer`, `@container`, `:is()/.class`, `:where()/.class`, `:has()/.class`, child/sibling combinator selectors, and CSS custom properties in component CSS are automatically scoped to that component. SVG elements with `class` attributes inside component HTML are also scoped. CSS `#id` selectors are converted to generated class selectors and the class is injected on the matching HTML element.
293
+
294
+ `html`, `body`, and `head` are **never** converted to scoped classes. Cross-boundary selectors like `html[data-theme="light"] .foo {}` compile correctly: the root element name is preserved verbatim and only `.foo` is scoped, producing `html[data-theme="light"] .bascik__comp__foo {}`. This means theme-switching, dark/light mode, and other document-root state selectors in component CSS work as expected.
295
+
296
+ These rewrites compose normally in one component: bare element selectors receive generated classes, locally declared custom properties and their `var()` references are renamed together, and keyframe declarations stay synchronized with `animation` references.
297
+
298
+ Fragment-only CSS references such as `fill: url(#gradient)` are rewritten when the ID is declared in the same component. Quoted fragment forms are supported. Real URLs, data URLs, remote URLs, and cross-document fragments such as `url(sprite.svg#icon)` remain unchanged. Because IDs are per instance, a component with a resolvable CSS fragment automatically emits per-instance CSS even when `deduplicateCss: true`; components without fragments still deduplicate normally. Fragment rewriting runs before base-path URL processing.
299
+
300
+ ```css
301
+ /* site-nav.css: source */
302
+ .nav a {
303
+ color: white;
304
+ }
305
+ p {
306
+ margin: 0;
307
+ }
308
+ @keyframes fade {
309
+ from { opacity: 0; }
310
+ to { opacity: 1; }
311
+ }
312
+ ```
313
+
314
+ ```css
315
+ /* compiled output */
316
+ .bascik__site-nav__nav a { color: white; }
317
+ .bascik__site-nav__el__p { margin: 0; }
318
+ @keyframes bascik__site-nav__keyframe__fade { ... }
319
+ ```
320
+
321
+ ### Scoped CSS Custom Properties
322
+ CSS custom properties declared in the file are also scoped. `var(--prop, fallback)` with fallback values is fully supported, the property name is scoped and the fallback preserved:
323
+
324
+ ```css
325
+ /* source */
326
+ :root {
327
+ --brand: #d3ff8d;
328
+ }
329
+ .title {
330
+ color: var(--brand);
331
+ border-color: var(--brand, rgb(150, 150, 150)); /* fallback preserved */
332
+ }
333
+
334
+ /* compiled */
335
+ :root {
336
+ --bascik__site-nav__brand: #d3ff8d;
337
+ }
338
+ .bascik__site-nav__title {
339
+ color: var(--bascik__site-nav__brand);
340
+ border-color: var(--bascik__site-nav__brand, rgb(150, 150, 150));
341
+ }
342
+ ```
343
+
344
+ #### Using global design tokens in a component
345
+ Define your design tokens once in a global stylesheet, then consume them inside any component. Because the component never declares those properties locally, Bascik leaves the `var()` references as-is and they resolve from the global stylesheet at render time.
346
+
347
+ ```css
348
+ /* src/styles.css: design tokens, linked in every page <head> */
349
+ :root {
350
+ --brand: #d3ff8d;
351
+ --card-bg: #1e2022;
352
+ --text-muted: #8d929e;
353
+ }
354
+ ```
355
+
356
+ ```html
357
+ <!-- src/components/brand-card.html -->
358
+ <style>
359
+ .card {
360
+ padding: 24px 28px;
361
+ background: var(--card-bg); /* global: Bascik leaves untouched */
362
+ border-top: 3px solid var(--brand);
363
+ border-radius: 10px;
364
+ }
365
+ .card-label {
366
+ color: var(--text-muted);
367
+ }
368
+ </style>
369
+ <div class="card">
370
+ <p class="card-label" data-bascik-prop-label></p>
371
+ <div data-bascik-slot></div>
372
+ </div>
373
+ ```
374
+
375
+ ```css
376
+ /* dist/ output: class names scoped, var() refs preserved */
377
+ .bascik__brand-card__card {
378
+ padding: 24px 28px;
379
+ background: var(--card-bg);
380
+ border-top: 3px solid var(--brand);
381
+ border-radius: 10px;
382
+ }
383
+ .bascik__brand-card__card-label {
384
+ color: var(--text-muted);
385
+ }
386
+ ```
387
+
388
+ ### CSS Scoping Compatibility Notes
389
+ * CSS nesting: fully supports explicit `&` nesting (`& p`, `& > h2`, `&>h2`) and 2023 W3C relaxed direct nesting without `&` (`> h2`, `+ li`, `~ span`, direct nested element selectors)
390
+ * `@property`: `@property --name { }` declaration names are scoped along with any matching `--name:` declarations and `var(--name)` references in the same component
391
+ * `@starting-style`: class names and element selectors inside `@starting-style` blocks are scoped by the same passes that handle other at-rules; both standalone `@starting-style { .foo { } }` and nested `.foo { @starting-style { } }` forms work
392
+ * `@counter-style`: `@counter-style name { }` declaration names are scoped; references in `list-style`, `list-style-type`, `counter(counter, name)`, and `counters(counter, sep, name)` in the same CSS file are updated to match
393
+ * `view-transition-name`: ✓ values are scoped per component (`bascik__<comp>__vtn__<name>`); matching `::view-transition-old/new/group/image-pair()` pseudo-element references in the same file are updated; `none` and `auto` are not scoped
394
+ * `anchor-name` / `@position-try`: `anchor-name: --name` declarations are scoped per component; matching `position-anchor: --name` references and `@position-try --name { }` at-rules in the same CSS file are updated to match; only anchors declared in the component's own CSS are scoped
395
+ * `@scope` (native): class names in `@scope (.foo)` argument and optional `to (.clause)` are scoped normally, and class names inside the `@scope` block are scoped
396
+ * `:nth-child(An+B of .selector)`: class names in the `of <selector>` argument are scoped (same global `(?<=\.)` pass as `:is()`, `:where()`, `:has()`); works for `:nth-child` and `:nth-last-child`
397
+ * `@font-face`: passed through untouched; declare in a shared stylesheet to avoid duplicate injections
398
+ * `@import`: local file imports (`@import "./file.css"`) are inlined recursively and scoped to the component; remote URLs (`@import "https://..."`) are preserved and hoisted to the top of the compiled stylesheet
399
+ * Standalone attribute selectors (e.g. `[data-state]`): not scoped and can leak globally; anchor with a scoped class: `.card[data-state]`
400
+ * `[id]` selectors: `[id]` and `[id="..."]` attribute selectors in CSS are stripped at compile time because they cannot be scoped without DOM wrapping
401
+ * Compound element selectors: `.class element {}` and `.class > element {}` are scoped (element converted to class and injected on matching HTML elements); patterns with two bare elements (`div p {}`) still require a class anchor on the left
402
+ * Element names inside `:is()`, `:where()`, and `:has()` are not converted; use class selectors inside those pseudo-classes instead
403
+
404
+ ---
405
+
406
+ ## 4. Scoped JavaScript
407
+
408
+ **Key rule:** Use `id` attributes to identify elements you need to control per-instance, and `getElementById` to find them. Bascik rewrites selector strings at build time, you write your component as if it's the only one on the page.
409
+
410
+ DOM selectors in component scripts are rewritten to match scoped names:
411
+
412
+ ```html
413
+ <!-- source -->
414
+ <button id="my-btn">Click</button>
415
+ <script>
416
+ document
417
+ .getElementById("my-btn")
418
+ .addEventListener("click", () => alert("hi"));
419
+ </script>
420
+ ```
421
+
422
+ ```html
423
+ <!-- compiled -->
424
+ <button id="bascik__my-btn__a1b2__my-btn">Click</button>
425
+ <script>
426
+ (function () {
427
+ document
428
+ .getElementById("bascik__my-btn__a1b2__my-btn")
429
+ .addEventListener("click", () => alert("hi"));
430
+ })();
431
+ </script>
432
+ ```
433
+
434
+ ### Supported DOM Methods (auto-rewritten)
435
+ * `document.getElementById("id")`
436
+ * `document.querySelector("#id")` / `document.querySelectorAll("#id")`
437
+ * `document.querySelector(".cls")` / `document.querySelectorAll(".cls")`: single OR compound selectors
438
+ * `document.querySelector(".foo .bar")` / `querySelector("#id .child")`: compound selectors supported
439
+ * `document.getElementsByClassName("cls")`
440
+ * `document.getElementsByName("name")`
441
+ * `element.closest("#id")` / `element.closest(".cls")`: compound-aware
442
+ * `element.matches("#id")` / `element.matches(".cls")`: works for event delegation too
443
+ * `element.classList.add/remove/toggle/contains("cls")`: all single and multi-argument forms
444
+ * `element.classList.replace("old", "new")`: both arguments rewritten
445
+ * `element.setAttribute("class", "cls")` / `setAttribute("id", "id-value")` / `setAttribute("name", "value")`: string literal values
446
+ * `element.className = "cls"` or `"cls1 cls2"` or `+= " cls"`: setter forms; space-separated multi-class strings fully rewritten
447
+ * `innerHTML` / `insertAdjacentHTML` string literals, known class names inside the HTML string are rewritten
448
+ * `removeAttribute`, `hasAttribute`, `toggleAttribute`: take attribute names (not values), no rewriting needed
449
+ * **JS-only class discovery:** class names referenced in `classList.*`, `.className` in selector strings, `el.className = "..."`, and `setAttribute("class", "...")` are automatically discovered and added to the class scope map before JS rewriting.
450
+
451
+ ### Not Rewritten (known limitations)
452
+ * `el.id = "value"`: property setter not rewritten; use `getElementById` then work from the reference
453
+ * `el.style.setProperty("--my-var", value)`: runtime CSS custom property names are not rewritten; the scoped var name (e.g. `--bascik__comp__my-var`) is different from the source name
454
+ * Template literals: `` el.className = `box ${state}` ``, not rewritten; use `classList.add/remove` instead
455
+ * Template-literal replacement arguments in `classList.replace(oldName, \`${dynamic}\`)` are not rewritten safely; prefer `classList.add/remove/toggle` with static class names
456
+ * `querySelector("[name='username']")`: attribute-selector form for `name`; use `getElementsByName` instead
457
+ * `innerHTML` / `insertAdjacentHTML` string literals: scanning only recognizes class names that appear in the HTML template.
458
+
459
+ ### Scoping Model
460
+ `id` and `name` attributes are scoped **per-instance:** each use of a component generates a different `instanceId`, guaranteeing unique DOM IDs even when the same component appears multiple times on a page. `class` attributes are scoped to the component name only (no instanceId), so all instances share the same class names and CSS deduplication emits a single `<style>` block.
461
+
462
+ HTML references to locally declared IDs are rewritten automatically in the same component. This includes `<label for>`, form, ARIA, and microdata `itemref` ID attributes, fragment-only `href` values, SVG fragment attributes, and inline `style` `url(#id)` values. Space-separated lists resolve one token at a time. `usemap` resolves against `<map name>` and follows name scoping. References that target page-shell IDs or IDs in another component remain unchanged because cross-component instance resolution is not possible at build time. Use `data-bascik-preserve` when an external system requires a literal ID graph.
463
+
464
+ Form inputs using `<input name="username">` receive per-instance scoped names (e.g. `bascik__comp__a1b2c3__username`), so `new FormData(form)` keys reflect the scoped name.
465
+
466
+ Literal component tags inside `<script>`, `<style>`, `<textarea>`, or HTML comments (`<!-- <my-card> -->`) are treated as text and never resolved into components.
467
+
468
+ ```
469
+ class → bascik__<componentName>__<originalName>
470
+ id → bascik__<componentName>__<instanceId>__<originalName>
471
+ name → bascik__<componentName>__<instanceId>__<originalName>
472
+ ```
473
+
474
+ ### Multiple Script Blocks
475
+ Component templates can contain multiple `<script>` tags. Bascik processes each script tag according to its attributes:
476
+ * **Client scripts:** Standard JavaScript blocks are each wrapped in an isolated IIFE `(function() { ... })();` when `scoping.scriptBlocks` is enabled. If you include multiple client `<script>` tags in a single component, each runs in its own IIFE so local variables do not collide.
477
+ * **Build scripts (`<script data-bascik-build>`):** Executed during build or dev time in Node.js to generate dynamic markup.
478
+ * **Server scripts (`<script data-bascik-server>`):** Executed on the server at request time in Node.js.
479
+ * **Data scripts (e.g. `type="application/ld+json"`):** Left untouched without IIFE wrapping or JavaScript minification.
480
+
481
+ Recommended Pattern: Keeping separate, unrelated concerns in dedicated client `<script>` tags (like form validation and UI animation) is recommended for code readability and maintainability.
482
+
483
+ ### Multiple Instances
484
+ Using a component more than once works automatically, each use gets a different `instanceId`, so IDs never collide and each instance's scripts reference only its own elements.
485
+
486
+ ### Class Selectors and Multiple Instances (PITFALL)
487
+ Because class names are scoped to the component **name** (not per-instance), `querySelector('.my-class')` and `querySelectorAll('.my-class')` **always return the first matching element in the document**, even after Bascik rewrites the class name. When the same component is used more than once on a page, every instance's script will target the first instance's elements, the other instances' buttons/inputs/etc. will appear non-functional.
488
+
489
+ **Rule:** In component scripts, always use `getElementById` (or `getElementsByName`) to locate specific elements, both are per-instance scoped and always resolve to the correct element. Never use `querySelector`/`querySelectorAll` with a class selector to find an element you need to control per-instance.
490
+
491
+ ```html
492
+ <!-- ❌ Broken for multiple instances -->
493
+ <button class="my-btn">Click</button>
494
+ <script>
495
+ document.querySelector('.my-btn').addEventListener('click', () => { … });
496
+ </script>
497
+
498
+ <!-- ✅ Correct: id is per-instance -->
499
+ <button id="my-btn" class="my-btn">Click</button>
500
+ <script>
501
+ document.getElementById('my-btn').addEventListener('click', () => { … });
502
+ </script>
503
+ ```
504
+
505
+ **Escape hatch:** Set `scoping.deduplicateCss` to `false` in `bascik.config.ts` to switch to per-instance class scoping. By default, all instances of the same component share identical scoped class names so Bascik emits one shared `<style>` block per component. With `scoping.deduplicateCss: false`, class selectors become unique per instance (like IDs), but Bascik emits a separate `<style>` block for each component instance.
506
+
507
+ ### `deduplicateCss` Trade-Off Comparison
508
+
509
+ Setting `scoping.deduplicateCss` in `bascik.config.ts` controls whether class names are scoped per component type or per component instance.
510
+
511
+ | Feature or Aspect | `deduplicateCss: true` (Default) | `deduplicateCss: false` |
512
+ |---|---|---|
513
+ | **Class Scoping Scheme** | `bascik__card__wrapper` (shared per component) | `bascik__card__a1b2c3d4__wrapper` (unique per instance) |
514
+ | **CSS Payload** | Single `<style>` block per component type, zero CSS duplication | Multiplied `<style>` blocks (one block per component instance) |
515
+ | **`querySelector('.cls')` Behavior** | Targets the first instance on the page | Targets the matching element inside that specific instance |
516
+ | **Instance Isolation Model** | Use `id` and `getElementById()` for per-instance script isolation | Class selectors inherently isolate per instance |
517
+ | **Best For** | Virtually all production sites and design systems | Migrating legacy or third-party code that relies on class queries |
518
+
519
+ #### Side-by-Side Code Example
520
+
521
+ Given two instances of `<my-card>` on the same page:
522
+
523
+ ```html
524
+ <!-- Input Page HTML -->
525
+ <my-card></my-card>
526
+ <my-card></my-card>
527
+ ```
528
+
529
+ **Output with `deduplicateCss: true` (Default, Shared Class Scope):**
530
+
531
+ ```html
532
+ <!-- HTML Output: Shared classes, unique IDs -->
533
+ <div class="bascik__my-card__wrapper" id="bascik__my-card__a1b2c3d4__root">...</div>
534
+ <div class="bascik__my-card__wrapper" id="bascik__my-card__e5f6g7h8__root">...</div>
535
+
536
+ <!-- CSS Output: Emitted once in <head> -->
537
+ <style>
538
+ .bascik__my-card__wrapper { padding: 1rem; }
539
+ </style>
540
+ ```
541
+
542
+ **Output with `deduplicateCss: false` (Per-Instance Class Scope):**
543
+
544
+ ```html
545
+ <!-- HTML Output: Unique classes per instance -->
546
+ <div class="bascik__my-card__a1b2c3d4__wrapper" id="bascik__my-card__a1b2c3d4__root">...</div>
547
+ <div class="bascik__my-card__e5f6g7h8__wrapper" id="bascik__my-card__e5f6g7h8__root">...</div>
548
+
549
+ <!-- CSS Output: Emitted for every instance -->
550
+ <style>
551
+ .bascik__my-card__a1b2c3d4__wrapper { padding: 1rem; }
552
+ .bascik__my-card__e5f6g7h8__wrapper { padding: 1rem; }
553
+ </style>
554
+ ```
555
+
556
+ Using the `id`-based pattern with `getElementById()` is recommended because it gives you per-instance JS isolation while keeping `deduplicateCss: true` for minimal CSS payload.
557
+
558
+ ### TypeScript in Component Scripts
559
+
560
+ Bascik ships vanilla JavaScript to the browser and strips browser TypeScript automatically on two supported paths. No configuration is required and `minify.js` is not involved.
561
+
562
+ * **Default: companion `.ts` file.** `<script src="counter.ts"></script>` inside a component inlines `counter.ts` with its erasable types stripped. Line structure is preserved so `//# sourceURL` line numbers match the `.ts` source.
563
+ * **Inline opt-in:** `<script type="text/typescript">...</script>` in a page or component is stripped and emitted as an ordinary `<script>` (the `type` attribute is removed).
564
+ * **Ordinary `<script>` is JavaScript.** Browsers do not run TypeScript in an unmarked block, so Bascik never rewrites it. If it can positively identify erasable TypeScript there, the build logs a warning naming the file and the two fixes above. Do not put TypeScript annotations in an unmarked `<script>`.
565
+
566
+ Pipeline order is fixed: TypeScript strip, then scoping (IIFE, selector rewriting, `//# sourceURL`), then optional `minify.js`. A `minify.js` function (built-in or custom such as esbuild with `loader: 'js'`) therefore always receives valid JavaScript.
567
+
568
+ **Default compiler is erasable syntax only** (Node 22.18+ strip-only mode): annotations, interfaces, type aliases, `as` casts, `!` assertions, `import type`. Non-erasable syntax (`enum`, parameter properties, namespaces with runtime code) fails the build with the file path. Prefer `as const` objects over `enum`.
569
+
570
+ **Compiler selection: `scripts.typescript`** (`true` default | `false` | function). Set a function to bring your own compiler when a project needs `enum`, decorators, an older browser `target`, or the same toolchain it uses elsewhere. It receives `(code, { sourcePath, kind })` where `kind` is `'companion'` or `'inline'`, must return plain JavaScript, and runs at the same pipeline point (before scoping and `minify.js`), including inside worker threads. Set `false` only when another tool has already compiled the files Bascik reads; marked blocks then pass through untouched, `type` attribute included.
571
+
572
+ ```ts
573
+ // bascik.config.ts (illustrative: esbuild as the browser TypeScript compiler)
574
+ import { transform } from 'esbuild';
575
+ import { defineConfig } from '@bascik/bascik/config';
576
+
577
+ export default defineConfig({
578
+ scripts: {
579
+ typescript: async (code, { sourcePath }) =>
580
+ (await transform(code, { loader: 'ts', target: 'es2020', sourcefile: sourcePath })).code,
581
+ },
582
+ });
583
+ ```
584
+
585
+ This option is browser-only. Build, routes, server, and API route `.ts` files are executed by Node directly; Node handles their TypeScript (pass `--experimental-transform-types` via `NODE_OPTIONS` for non-erasable syntax there).
586
+
587
+ Never configure `minify.js` merely to strip TypeScript. Bascik performs supported browser TypeScript stripping automatically before the minifier hook. Add a custom `minify.js` function only when the project requires non-default JavaScript minification.
588
+
589
+ ### Debugging Component Scripts & Virtual Source Files
590
+
591
+ When debugging component scripts in browser DevTools (Cmd+Option+I or F12), Bascik provides virtual source files and accurate stack traces:
592
+
593
+ * **Virtual Source Files:** Bascik appends a `//# sourceURL=src/components/name.html` directive and preserves line-offset padding in every component client `<script>` block. In browser DevTools under the **Sources** (or Debugger) panel, your component scripts appear as virtual files matching your project folder structure (e.g., `src/components/card.html`).
594
+ * **Console Logs & Breakpoints:** Because component scripts are listed as virtual files, you can search for them directly using `Cmd + P` or `Ctrl + P` in DevTools, set breakpoints, and step-debug. Runtime console logs and uncaught exceptions map directly to the original component file and line offset rather than the generated HTML page.
595
+
596
+ ---
597
+
598
+ ## 5. Dynamic Runtime Class Scoping & JS-Only Class Discovery
599
+
600
+ Class names referenced in JavaScript (`classList.add/remove/toggle/replace`, `.className` in selector strings, `el.className = "..."`, and `setAttribute("class", "...")`) are automatically discovered by Bascik's JS scanner and added to the component's class scope map before the JS rewrite runs.
601
+
602
+ This means dynamic modifier classes (such as `is-open` or `btn--active`) that only appear inside a `<script>` block and never in a `class="..."` HTML attribute are scoped automatically in both CSS and JS without needing any template annotations.
603
+
604
+ ```html
605
+ <section class="card" id="card">
606
+ <p id="status">Panel closed</p>
607
+ <button id="toggle" type="button">Toggle</button>
608
+ </section>
609
+ <script>
610
+ const toggle = document.getElementById('toggle');
611
+ const status = document.getElementById('status');
612
+ const card = document.getElementById('card');
613
+
614
+ toggle.addEventListener('click', () => {
615
+ const isOpen = card.classList.toggle('is-open'); // is-open is automatically discovered and scoped in CSS and JS
616
+ status.textContent = isOpen ? 'Panel open' : 'Panel closed';
617
+ });
618
+ </script>
619
+ ```
620
+
621
+ The only exception is `innerHTML` / `insertAdjacentHTML` HTML string scanning, which only recognizes class names that appear as static `class="..."` attributes in the component HTML template.
622
+
623
+ ---
624
+
625
+ ## 6. Slots & Props
626
+
627
+ ### Slots (Default & Named)
628
+
629
+ #### Default Slot
630
+
631
+ Add `data-bascik-slot` (no value) to any element in the component template. The element is replaced by the slot content at the usage site. Use the element's inner content as fallback when no slot content is provided.
632
+
633
+ Slot content remains raw HTML, participates in scoping, and may contain nested Bascik components. Use slots, not props, for rich markup.
634
+
635
+ ```html
636
+ <!-- my-card.html -->
637
+ <div class="card">
638
+ <div data-bascik-slot><p>No content provided.</p></div>
639
+ </div>
640
+
641
+ <!-- usage -->
642
+ <my-card>
643
+ <h2>Card Title</h2>
644
+ <p>Card body text.</p>
645
+ </my-card>
646
+
647
+ <!-- output -->
648
+ <div class="card">
649
+ <h2>Card Title</h2>
650
+ <p>Card body text.</p>
651
+ </div>
652
+ ```
653
+
654
+ #### Named Slots
655
+
656
+ Use `data-bascik-slot="name"` in the template to define named zones. At the usage site, wrap content with the same attribute.
657
+
658
+ ```html
659
+ <!-- page-layout.html -->
660
+ <div class="layout">
661
+ <header><div data-bascik-slot="header"></div></header>
662
+ <main><div data-bascik-slot></div></main>
663
+ <aside><div data-bascik-slot="sidebar"></div></aside>
664
+ </div>
665
+
666
+ <!-- usage -->
667
+ <page-layout>
668
+ <p>Main body content.</p>
669
+ <div data-bascik-slot="header"><h1>Page Title</h1></div>
670
+ <div data-bascik-slot="sidebar"><nav>Sidebar nav</nav></div>
671
+ </page-layout>
672
+ ```
673
+
674
+ ### Slot Whitespace
675
+ Leading and trailing whitespace is trimmed from all slot content at build time. Whitespace *within* slot content is preserved exactly as written.
676
+ By default Bascik skips transpilation inside `<code>` elements, so raw code examples stay literal.
677
+
678
+ ### Props
679
+ Inject text values into a component at usage time.
680
+
681
+ ```html
682
+ <!-- alert-box.html -->
683
+ <div class="alert">
684
+ <strong data-bascik-prop-title></strong>
685
+ <p data-bascik-prop-message></p>
686
+ </div>
687
+
688
+ <!-- usage -->
689
+ <alert-box
690
+ data-bascik-prop-title="Success"
691
+ data-bascik-prop-message="Your changes were saved."
692
+ ></alert-box>
693
+ ```
694
+ Props in Bascik follow the same basic idea as React props, but the mechanism is vanilla HTML through `data-bascik-prop-*` attributes.
695
+ The `data-bascik-prop-*` marker is removed from compiled output, while the target element's other attributes are preserved.
696
+ Prop values are HTML-escaped on injection. Entity-encode delimiting quotes in usage attributes. Props are read only from the component's opening tag, so nested component props do not leak upward.
697
+ *Props accept text values only. For rich HTML content or nested components, use slots.*
698
+
699
+ To send a prop value to an element attribute, use `data-bascik-attr-{attribute}="{propName}"` in the component template:
700
+
701
+ ```html
702
+ <img data-bascik-attr-src="image" data-bascik-attr-alt="alt">
703
+ <a data-bascik-attr-href="link">Read more</a>
704
+ ```
705
+
706
+ This is the same prop value with a different destination, not templating or variables. The directive is removed from output. A missing prop adds no attribute. If the target already exists, the prop wins and Bascik warns. Bound `id`, `name`, and `class` values are scoped normally.
707
+
708
+ ---
709
+
710
+ ## 7. Attribute Inheritance & Tags
711
+
712
+ ### Attribute Inheritance
713
+ Non-`data-bascik-*` attributes on a usage tag are merged onto the component's root element when `scoping.inheritAttributes` is `true` (the default). `id` is forwarded too unless the template root already defines its own `id`. Class names are appended, not replaced.
714
+
715
+ If a component template contains multiple root elements, inherited attributes are merged onto the first root HTML element in the component template.
716
+
717
+ ```html
718
+ <!-- usage: attributes here are forwarded onto the component root -->
719
+ <site-nav class="sticky" aria-label="main navigation"></site-nav>
720
+
721
+ <!-- site-nav.html: component template -->
722
+ <nav class="nav"><a href="/">Home</a></nav>
723
+
724
+ <!-- compiled output: class appended, aria-label forwarded -->
725
+ <nav class="bascik__site-nav__nav sticky" aria-label="main navigation">
726
+ <a href="/">Home</a>
727
+ </nav>
728
+ ```
729
+
730
+ Inherited class names are not scoped, they are treated as global page-level classes. To disable inheritance, set `scoping.inheritAttributes` to `false` in `bascik.config.ts`.
731
+
732
+ ### Internal Masking vs. Preserving Element Contents
733
+
734
+ These two mechanisms serve distinct purposes:
735
+
736
+ * **Internal scanning mask (internal, hardcoded, not configurable):** Bascik temporarily blanks the contents of `<script>`, `<style>`, `<textarea>`, and HTML comments while scanning with regular expressions, so a component tag inside a JavaScript string, style block, or comment is not mistaken for real markup. The mask is discarded immediately after scanning. Authors never interact with or configure this behavior.
737
+ * **Preserve scoping (authoring choice):** Keep scoping enabled by default. Use `scoping.preserve` (default `['code']`) when every matching tag and subtree must keep literal `id`, `name`, and `class` values. Entries are exact tag names or `*` wildcard patterns (for example `vendor-*` preserves every tag whose name starts with `vendor-`); both use the same semantics. Use bare `data-bascik-preserve` for one element and subtree, or list selected attributes such as `data-bascik-preserve="name"`. Nesting only widens. Preserving `name` gives up per-instance radio-group isolation, so reserve it for external form endpoints that require literal field names.
738
+
739
+ ### Self-Closing Tags
740
+ Components that do not contain inner slot content should always use self-closing void syntax:
741
+ ```html
742
+ <site-nav />
743
+ <site-head />
744
+ <site-footer />
745
+ <site-nav class="top" />
746
+ ```
747
+
748
+ ### Head Components
749
+ Components work inside `<head>` to organize metadata and shared links:
750
+ ```html
751
+ <!-- src/components/site-meta.html -->
752
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
753
+ <meta name="description" content="My site" />
754
+
755
+ <!-- usage in page shell -->
756
+ <head>
757
+ <site-meta />
758
+ <title>Home</title>
759
+ </head>
760
+ ```
761
+
762
+ ---
763
+
764
+ ## 8. Build-time Scripts
765
+
766
+ `<script data-bascik-build>` blocks are executed at transpile time as Node.js ESM modules. The script's stdout is injected in place of the tag. Runs in both dev and build modes.
767
+
768
+ ```html
769
+ <script data-bascik-build>
770
+ import { readFile } from 'node:fs/promises';
771
+ import { marked } from 'marked';
772
+ const md = await readFile('./content/intro.md', 'utf8');
773
+ console.log(marked(md));
774
+ </script>
775
+ ```
776
+
777
+ * Top-level `import` and top-level `await` are supported.
778
+ * CWD is the project root. Quoted data paths (`readFile('./content/x.md')`) resolve from there.
779
+ * **Importing shared helpers: use `@/` by default.** `import { renderMd } from '@/lib/md-renderer.ts'` resolves against `scripts.importRoot` (default `src`), so the identical import line works from any page or component at any nesting depth. Relative `./` and `../` specifiers still work and resolve against the file that contains the script; use them for helpers that live next to the page or component. Both forms also apply to `data-bascik-server`, `data-bascik-routes`, and the `src="…"` attribute on those tags.
780
+ * **Never use a bare leading `/`.** `import x from '/lib/x.ts'` or `src="/lib/x.ts"` inside a Bascik script is a hard compile error (and a red diagnostic in the VS Code extension), regardless of `onBuildScriptError`. A bare slash is ambiguous between filesystem root and site root. The error names the two valid rewrites: `@/lib/x.ts` (import root) or `./lib/x.ts` (relative). Plain client `<script>` tags are unaffected.
781
+ * **Alias gotchas:** only the exact `@/` prefix is an alias (`@scope/pkg` is a normal package). Aliases are rewritten only inside script blocks; a helper file importing another helper must use `./` or `../`. Add shared build helpers to `pipeline.watchPaths` when outside the pages/components directories. The import-root watcher only invalidates request-time modules, not page compilation caches.
782
+ * Use `console.log()` or `process.stdout.write()` to output HTML. Build scripts execute as top-level Node.js ESM modules rather than wrapped functions, so top-level `return` is a JavaScript syntax error; output is sent to stdout instead.
783
+ * Build scripts run before component resolution, so their output can contain component tags.
784
+ * Each uncached build script executes in its own fresh child process, isolated from every other script, regardless of how many siblings are cache misses. Output is assembled in document order once all scripts complete. Bounded concurrency is enforced by a memory-aware semaphore, but scripts never share a process or global ESM registry, so detached async output from one script can never bleed into a neighbor.
785
+ * On error, behavior is controlled by three script-specific options in `bascik.config.ts`: `scripts.onBuildScriptError`, `scripts.onRoutesScriptError`, and `scripts.onServerScriptError` (each supports `'warn'`, `'error'`, or `'ignore'`). Defaults are mode-aware: `'warn'` in dev, `'error'` during `--build` and `--server`. For `data-bascik-stream` scripts, an error cannot produce an HTTP 500 because headers are already committed; the slot is emitted empty, the failure is logged at the configured severity, and the document completes.
786
+ * **Stack Trace Remapping:** For `<script data-bascik-build>`, `<script data-bascik-server>`, and `<script data-bascik-stream>` blocks, Bascik automatically intercepts child-process stack traces, filters out noisy Node.js internal files, stack frames, and `Command failed:` headers, and remaps temporary execution files back to your source HTML file and line offset (e.g., `src/pages/dashboard.html:25`). This filters out the noise of internal V8 loader frames and child process execution headers, leaving only the clean, actionable stack trace of your template and helper scripts. In VS Code or terminal emulators, you can Cmd+Click (or Ctrl+Click) the file reference in the error log to jump directly to the failing script's exact line.
787
+ * **Hard error:** combining any of `data-bascik-build`, `data-bascik-routes`, `data-bascik-server`, or `data-bascik-stream` on the same tag throws and aborts the build. Directives are mutually exclusive.
788
+
789
+ ### Build Script Environment Variables
790
+
791
+ Build scripts receive these `process.env` variables:
792
+
793
+ | Variable | Description |
794
+ |---|---|
795
+ | `BASCIK_PAGE_PATH` | Normalized root-relative route path of the current page shell (e.g. `/getting-started`, `/switch/from-vue`, `/`). |
796
+ | `BASCIK_SOURCE_FILE` | Absolute path to the file currently being transpiled (page or component template). |
797
+ | `BASCIK_PAGE_FILE` | Absolute path of the current page file (e.g. `/project/src/pages/about.html`). Use this to generate page-specific output like canonical URLs. |
798
+ | `BASCIK_PAGES_DIR` | Absolute path to the configured pages directory. |
799
+ | `BASCIK_OUT_DIR` | Absolute path to the configured output directory (exec scripts). |
800
+ | `BASCIK_BUILD` | `"1"` during `bascik --build`, `"0"` during dev. Use to produce different output per mode. |
801
+ | `BASCIK_SITE_URL` | The site URL resolved from `--site-url`, the `BASCIK_SITE_URL` env var, or `.env`. Absent when unset, never an empty string. |
802
+ | `BASCIK_BASE` | Normalized deployment base from config. Always present and defaults to `/`. Use it when a build script must expose the prefix for JavaScript-built URLs. |
803
+ | `BASCIK_ROUTE` | JSON string `{ params, data }` passed to build scripts inside dynamic route templates. |
804
+
805
+ These are critical for scripts that generate per-page output. A script using `BASCIK_SOURCE_FILE`, `BASCIK_PAGE_FILE`, or `BASCIK_PAGE_PATH` gets a separate cache entry per page automatically.
806
+
807
+ ### The Fetch-Once Pattern
808
+
809
+ When a data-driven page needs the same data in several places, read or fetch it **once at page level** in a single `data-bascik-build` script, then apply it everywhere on the page. Not once per component instance. The same data object can feed a second script, a JSON payload for client JavaScript, or a component prop, without a second network round trip or file read.
810
+
811
+ **Cache exclusion gotcha:** the build script cache keys on script content, statically scanned local dependencies, and resolved installed-package identity. It **cannot see network fetches**, `readdir` directory reads, computed file paths, or dynamic computed imports. A script whose output depends on a remote API will be served from cache with stale data across builds, and nothing will tell you. A script containing a dynamic non-literal import (`import(someName)` or `` import(`../${x}`) ``) is treated as non-cacheable and re-runs every build. Exclude scripts that read external data via `scripts.cache.exclude` (see below), or disable the cache for them.
812
+
813
+ ### Build Script Output Cache
814
+
815
+ Each `<script data-bascik-build>` spawns a Node.js child process (~50–150 ms startup each). Bascik caches script output on disk so unchanged scripts skip the spawn on subsequent builds or server restarts.
816
+
817
+ **Cache location:** `node_modules/.cache/bascik/script-cache/<sha256>.json`
818
+
819
+ **Cache key:** SHA-256 of the script content + dev/build mode + the source file path (`BASCIK_SOURCE_FILE`) + the site URL + the deployment base + dynamic route parameters + the full content of any local dependency files referenced as quoted path literals in the script + the resolved identity of every installed package the script (or its local dependencies) imports. Package identity covers the package manifest and the resolved entry, plus a bounded transitive hash of the packages those imports themselves pull in, so upgrading a build-time npm dependency invalidates affected entries automatically regardless of cache temperature.
820
+
821
+ **Excluding scripts from cache:** Statically scanned cache keys cannot detect runtime network calls, `readdir` directory reads, or dynamic computed file paths. For scripts reading from external APIs or computed paths, exclude them via `scripts.cache.exclude`:
822
+
823
+ ```ts
824
+ // bascik.config.ts
825
+ import { defineConfig } from '@bascik/bascik/config';
826
+
827
+ export default defineConfig({
828
+ scripts: {
829
+ cache: {
830
+ enabled: true,
831
+ exclude: ['src/pages/live-feed/**', 'src/components/api-cards/**'],
832
+ },
833
+ },
834
+ });
835
+ ```
836
+
837
+ **Environment variable cache keys (`scripts.cache.environment`):** By default, `process.env` reads are invisible to the cache key. Declare the exact variable names a script reads in `scripts.cache.environment` so their resolved values become part of the key:
838
+
839
+ ```ts
840
+ export default defineConfig({
841
+ scripts: {
842
+ cache: {
843
+ enabled: true,
844
+ environment: ['MY_FEATURE_FLAG', 'API_BASE_URL'],
845
+ },
846
+ },
847
+ });
848
+ ```
849
+
850
+ When a declared variable's value changes, the cache key changes and the script re-runs. Values are hashed into the key and never persisted. Missing vs. empty string produce different keys. Only exact names are supported (no globs). Bascik warns about statically visible `process.env.NAME` reads that are not declared; the warning is advisory and never fails the build.
851
+
852
+ **To bust the entire cache** (waive cache correctness, e.g. after an unusual external change the identity walk cannot see):
853
+
854
+ ```sh
855
+ rm -rf node_modules/.cache/bascik/script-cache
856
+ ```
857
+
858
+ With `pipeline.workers: true`, multiple workers share the same cache directory. Workers that independently miss the same key both spawn a child process; last write wins with identical content. This is a minor inefficiency on a cold first build only.
859
+
860
+ ### Rendering and Styling Markdown
861
+
862
+ Install a Markdown parser such as `marked`, read the source in a build script, and write the resulting HTML to stdout:
863
+
864
+ ```html
865
+ <script data-bascik-build>
866
+ import { readFile } from 'node:fs/promises';
867
+ import { marked } from 'marked';
868
+
869
+ const md = await readFile('./content/article.md', 'utf8');
870
+ console.log(marked(md));
871
+ </script>
872
+ ```
873
+
874
+ Given this Markdown source:
875
+
876
+ ```md
877
+ ## A practical heading
878
+
879
+ Markdown stays comfortable for authors, while the published page stays **vanilla HTML**.
880
+
881
+ > Add an editorial treatment with ordinary CSS.
882
+ ```
883
+
884
+ `marked()` emits:
885
+
886
+ ```html
887
+ <h2>A practical heading</h2>
888
+ <p>Markdown stays comfortable for authors, while the published page stays <strong>vanilla HTML</strong>.</p>
889
+ <blockquote>
890
+ <p>Add an editorial treatment with ordinary CSS.</p>
891
+ </blockquote>
892
+ ```
893
+
894
+ The parser emits ordinary HTML, so it can be styled by a global stylesheet or wrapped in a Bascik component. For reusable scoped styles, have the script emit `<markdown-content>${marked(md)}</markdown-content>` and give that component a default slot.
895
+
896
+ Use wrapper descendant selectors for generated slot content:
897
+
898
+ ```css
899
+ .markdown-content h2 { margin-block: 2.5rem 0.75rem; }
900
+ .markdown-content blockquote { border-left: 4px solid currentColor; }
901
+ ```
902
+
903
+ Do not rely on a bare `h2 {}` component rule for Markdown passed through a slot. Bare element rules are transformed before slot content is inserted; a scoped wrapper selector continues to match the generated descendants.
904
+
905
+ ### Page-Aware Scripts
906
+
907
+ Some pages need content that is specific to the current page, such as a canonical URL in the head, an Open Graph image, a structured-data block, or even a page-specific sidebar. Hardcoding those values in every page file works. However, a shared script is easier to maintain. You can change the logic once and every page picks it up automatically.
908
+
909
+ Bascik makes this possible by injecting environment variables into every `data-bascik-build` subprocess: `BASCIK_SOURCE_FILE`, `BASCIK_PAGE_FILE`, `BASCIK_PAGES_DIR`, and `BASCIK_SITE_URL` (described in the Environment Variables table above).
910
+
911
+ #### Canonical URL Example
912
+
913
+ A canonical URL tag tells search engines which URL is the authoritative version of a page. Every docs page on this site uses a shared `scripts/canonical.ts` that derives the URL from `BASCIK_SOURCE_FILE`:
914
+
915
+ ```ts
916
+ // scripts/canonical.ts
917
+ export async function canonical(): Promise<string> {
918
+ const siteUrl = (process.env.BASCIK_SITE_URL ?? '').replace(/\/$/, '');
919
+ const pageFile = process.env.BASCIK_SOURCE_FILE ?? process.env.BASCIK_PAGE_FILE ?? '';
920
+ const pagesDir = process.env.BASCIK_PAGES_DIR ?? '';
921
+
922
+ if (!siteUrl || !pageFile || !pagesDir) return '';
923
+
924
+ const relPath = pageFile.slice(pagesDir.length).replace(/^[\\/]/, '').replace(/\\/g, '/');
925
+ const withoutExt = relPath.replace(/\.html$/, '');
926
+ const route = withoutExt === 'index' ? '' : withoutExt.replace(/\/index$/, '/');
927
+ const urlPath = route ? `/${route}` : '/';
928
+
929
+ return `<link rel="canonical" href="${siteUrl}${urlPath}" />`;
930
+ }
931
+ ```
932
+
933
+ Use it from any page's `<head>`:
934
+
935
+ ```html
936
+ <head>
937
+ <script data-bascik-build>
938
+ import { canonical } from '../../scripts/canonical.ts';
939
+ console.log(await canonical());
940
+ </script>
941
+ </head>
942
+ ```
943
+
944
+ #### Reading the Page's Own HTML
945
+
946
+ For richer outputs, including Open Graph tags or JSON-LD structured data, a script can also read the page file itself to extract metadata. `BASCIK_SOURCE_FILE` is an absolute path, so `readFile` works directly:
947
+
948
+ ```ts
949
+ // scripts/article-schema.ts (simplified)
950
+ import { readFile } from 'node:fs/promises';
951
+
952
+ export async function articleSchema(): Promise<string> {
953
+ const pageFile = process.env.BASCIK_SOURCE_FILE ?? process.env.BASCIK_PAGE_FILE ?? '';
954
+ const siteUrl = (process.env.BASCIK_SITE_URL ?? '').replace(/\/$/, '');
955
+ if (!pageFile || !siteUrl) return '';
956
+
957
+ const html = await readFile(pageFile, 'utf8');
958
+ const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/);
959
+ const descMatch = html.match(/<meta\s+name="description"\s+content="([^"]+)"/);
960
+ if (!titleMatch || !descMatch) return '';
961
+
962
+ const headline = titleMatch[1].trim();
963
+ const description = descMatch[1].trim();
964
+ // ... compute url, build schema object, return JSON-LD script tag
965
+ }
966
+ ```
967
+
968
+ The script runs on the source HTML before any other build scripts have fired, so `<title>` and `<meta name="description">` are always present as written.
969
+
970
+ ### data-bascik-dev
971
+
972
+ Tag a browser script with `data-bascik-dev` to mark it as dev-only. In development the attribute is stripped and the script runs normally in the browser. In production builds (`bascik --build`) the entire script tag is removed.
973
+
974
+ ```html
975
+ <script data-bascik-dev>
976
+ console.log('dev only: stripped from production build');
977
+ </script>
978
+ ```
979
+
980
+ Useful for debug logging, development overlays, or any browser script that should never ship to production.
981
+
982
+ > **dev vs. build:** `data-bascik-build` executes at transpile time and injects HTML into the page. `data-bascik-dev` runs in the browser, but only in dev mode.
983
+
984
+ ### Dynamic Routes (data-bascik-routes)
985
+
986
+ Dynamic routes allow you to generate multiple static HTML files from a single template at build time. Use square brackets in the template filename (such as `src/pages/blog/[slug].html` or `src/pages/[category]/[id].html`) and include a `<script data-bascik-routes>` block:
987
+
988
+ ```html
989
+ <!-- src/pages/blog/[slug].html -->
990
+ <script data-bascik-routes>
991
+ const posts = [
992
+ { slug: 'hello-world', title: 'Hello World' },
993
+ { slug: 'second-post', title: 'Second Post' }
994
+ ];
995
+ const routes = posts.map(p => ({
996
+ params: { slug: p.slug },
997
+ data: p
998
+ }));
999
+ console.log(JSON.stringify(routes));
1000
+ </script>
1001
+
1002
+ <script data-bascik-build>
1003
+ const { params, data } = JSON.parse(process.env.BASCIK_ROUTE || '{}');
1004
+ console.log(`<h1>${data.title}</h1>`);
1005
+ </script>
1006
+ ```
1007
+
1008
+ Rules:
1009
+ * Script must output a valid JSON array of route objects with required `params` (matching all bracket names in the template path) and optional `data`.
1010
+ * Route parameters must be URL-safe tokens; characters like `#`, `%`, `&`, `'`, `+`, spaces, leading dots, and Windows device names are disallowed.
1011
+ * In build scripts, `process.env.BASCIK_ROUTE` provides the current `{ params, data }` payload.
1012
+ * Dynamic route templates are expanded into concrete static HTML files during `bascik --build` and dev server startup. Route collisions with static pages or other templates cause build errors.
1013
+ * Concrete route URLs are automatically added to `sitemap.xml` with percent-encoding.
1014
+ * `data-bascik-routes`, `data-bascik-build`, `data-bascik-server`, and `data-bascik-stream` are mutually exclusive and cannot be combined on the same tag.
1015
+
1016
+ ### data-bascik-server
1017
+
1018
+ Tag a `<script>` block with `data-bascik-server` to run it **at request time** on the server. Server scripts execute in-process via `ScriptRegistry` on every request; handler results are not cached, but loaded modules are reused. Use them to personalize pages per visitor, reading cookies, querying a database, rendering content based on query parameters. All `data-bascik-server` scripts on a page resolve before the response is sent, returning a complete buffered response with `Content-Length`.
1019
+
1020
+ ```html
1021
+ <script data-bascik-server>
1022
+ import { escape } from '@/lib/server.ts';
1023
+
1024
+ export default function (request) {
1025
+ const name = escape(request.headers.get('x-display-name') ?? 'Guest');
1026
+ return `<p>Welcome, ${name}!</p>`;
1027
+ }
1028
+ </script>
1029
+ ```
1030
+
1031
+ Handlers receive `(request, context, { signal })`:
1032
+
1033
+ * `request`: standard WHATWG `Request`, identical to API routes (`request.method`, `new URL(request.url).searchParams`, `request.headers.get(...)`)
1034
+ * `context`: `{ remoteIp }`
1035
+ * `{ signal }`: standard `AbortSignal` for timeouts and client disconnects
1036
+
1037
+ > **Escaping gotcha:** Bascik does not escape output and ships no escaper. Output is raw HTML by contract. Write a five-line escaper in `src/lib/server.ts` or install `escape-html`. Untrusted data includes anything from `request`, database rows containing user input, and third-party APIs.
1038
+
1039
+ | Sink | Correct treatment |
1040
+ | :--- | :--- |
1041
+ | Text between tags / Quoted attribute | HTML entity escape (`& < > " '`) |
1042
+ | Unquoted attribute | Never do this; quote the attribute |
1043
+ | `href`, `src`, `action` | Validate URL (`new URL(v, base)`), allow-list `http:`/`https:` (entity escaping does not stop `javascript:`) |
1044
+ | Inline `<script>` / `on*` attributes / `<style>` | Never interpolate untrusted data (use `data-*` attributes for static client scripts) |
1045
+ | Already-rendered HTML | HTML sanitizer at data layer |
1046
+
1047
+ Rules:
1048
+ * Top-level `import` and `await` are supported.
1049
+ * `data-bascik-server` blocks are stripped from emitted HTML into a sidecar file (`dist/.bascik/server-scripts.json`) leaving an inert placeholder (`<script type="text/bascik-server">`), so Node server source is never shipped to browsers in static builds.
1050
+ * When served with `bascik --server` or the dev server, the sidecar is loaded and scripts execute in-process on each request.
1051
+ * They are NOT executed during `bascik --build` itself.
1052
+ * Execution is bounded by `scripts.timeout` in `bascik.config.ts`.
1053
+ * On error, behavior is governed by `scripts.onServerScriptError` (`'error'`, `'warn'`, or `'ignore'`). Under `'error'`, the request halts and returns an HTTP 500. Under `'warn'`, the error is logged and the tag is replaced with an empty string without failing other scripts or the page.
1054
+ * In-process caveat: synchronous blocking code blocks Node's single-threaded event loop and cannot be interrupted by timeouts; always use async APIs.
1055
+
1056
+ #### Request Module Lifetime
1057
+
1058
+ * Node retains evaluated ESM identities, including old development generations, until process exit. Clearing framework caches does not reclaim them; release authored timers and resources explicitly and keep request-specific data in handler arguments.
1059
+ * Inline `server` and `stream` loads use weak script-job owners. Replacing a page does not retain its inline source history globally; accepted requests can finish using their old jobs without publishing loads into the replacement page.
1060
+ * In development, generations of literal file imports contribute to inline module identity. Tracked helper edits reload affected inline modules, while unrelated edits preserve singleton state. Computed imports do not gain this guarantee; watcher and tracked-root limits still apply. Production uses stable identities and immutable releases.
1061
+
1062
+ ### data-bascik-stream
1063
+
1064
+ Tag a `<script>` block with `data-bascik-stream` to run it at request time with chunked HTTP streaming. It uses the exact same authoring model as `data-bascik-server` (`export default async function (request, context, { signal })`, in-process execution, sidecar registry, and stack remapping). The only difference: the server does not wait for it. Response headers and all static HTML bytes preceding the tag are sent immediately. When the script resolves, its returned markup is written in document order via chunked transfer.
1065
+
1066
+ ```html
1067
+ <script data-bascik-stream>
1068
+ import { escape } from '@/lib/server.ts';
1069
+
1070
+ export default async function (request, context, { signal }) {
1071
+ const data = await loadData(request, signal);
1072
+ return `<article>${escape(data.title)}</article>`;
1073
+ }
1074
+ </script>
1075
+ ```
1076
+
1077
+ Decision rule:
1078
+ * Use `data-bascik-server` when the output must be present before anything is sent, or when a failure must be an HTTP 500.
1079
+ * Use `data-bascik-stream` when the page shell and static layout should paint while this slow block loads.
1080
+
1081
+ Rules & Gotchas:
1082
+ * **Mixed-page ordering:** All `data-bascik-server` scripts on the page resolve first. If any `server` script fails under `onServerScriptError: 'error'`, the response aborts with an HTTP 500 before any bytes are sent. Once all `server` scripts resolve, headers commit and `stream` chunks flow in document order. A slow `data-bascik-server` script on a page that also streams delays the first byte for the whole page.
1083
+ * **Error handling:** A `stream` script failure can never produce an HTTP 500 because headers are already committed. The slot emits an empty string, the error is logged at the configured `scripts.onServerScriptError` severity (with the dev overlay under `'error'`), and the document streams to completion.
1084
+ * **Header consequences:** Stream pages use `transfer-encoding: chunked` on HTTP/1.1 (DATA frames on HTTP/2) with `cache-control: private, no-store`. They omit `content-length`, `etag`, and Brotli `content-encoding`. `HEAD` requests always take the buffered execution path and execute the scripts.
1085
+ * **No config key:** There is no configuration key for streaming. Opt in per script with `data-bascik-stream`; opt out by using `data-bascik-server`.
1086
+ * **Zero-CLS Placeholders:** Everything the author writes before a `stream` tag is delivered and painted before the script runs. Use a component with a reserved-size container (`min-block-size` or `aspect-ratio`), `aria-busy`/`role="status"`, and either a one-cell grid stack (`grid-area: 1 / 1`) or a `:has(> .result)` rule to swap the placeholder visually. Bascik adds no CSS or JS for this. Put slow `stream` blocks last in source and place them with `grid-template-areas` or `order` so the rest of the layout paints first.
1087
+ * Note that code examples are illustrative; adjust module paths and database queries to your project.
1088
+
1089
+ ---
1090
+
1091
+ ## 9. Configuration (`bascik.config.ts`)
1092
+
1093
+ Bascik is **zero-config by default**. Start without `bascik.config.ts`. Create one only after identifying a concrete requirement that differs from built-in behavior, such as `generate.sitemapLastmod`, an existing custom `exec` build script, or a non-default production minification override. `bascik --build` and `bascik --server` already enable HTML, CSS, JavaScript, and identifier minification.
1094
+
1095
+ Use `bascik.config.ts` (preferred) or `bascik.config.js` (takes precedence if both exist). Import `defineConfig` for full editor autocomplete and inline docs.
1096
+
1097
+ ### Configuration is validated at startup
1098
+
1099
+ Bascik validates the config before anything reads it and reports every problem together: each error names the key, the received value, and the expectation. Unknown keys are rejected with a "did you mean" suggestion, so a typo like `minfy:` or `directroy:` fails loudly instead of being silently ignored. Referenced paths (`directory.pages`, `pipeline.exec[].script`, `pipeline.watchPaths`, `assets.inlineStyles`, TLS key/cert files) are checked for existence. `base` is normalized to a leading and trailing slash (`docs` becomes `/docs/`); only a full URL is rejected.
1100
+
1101
+ `base` defaults to `/`, which is a complete output and routing no-op. A non-root value such as `/docs/` rewrites root-relative HTML, CSS, and web app manifest URLs, serves dev and production-server requests below that prefix, and is included in sitemap, robots, and canonical URLs. Requests outside the prefix return `404`. Bascik does not rewrite URLs constructed in JavaScript; emit `BASCIK_BASE` from a build script when client code needs the prefix.
1102
+
1103
+ ### The site URL is not a config key
1104
+
1105
+ `siteUrl` is a per-deployment value, so it never goes in `bascik.config.ts` (setting it there is an error). Three sources, in precedence order:
1106
+
1107
+ ```text
1108
+ --site-url flag > BASCIK_SITE_URL env var > .env file
1109
+ ```
1110
+
1111
+ The value must be an absolute `http` or `https` URL. Bascik loads `./.env` automatically (silently skipped when absent); `--env-file <path>` is repeatable with later files winning, and a missing explicit file is an error. A real environment variable always beats a file value. When `generate.sitemap` or `generate.robots` is enabled (both default to `true`) and no source provides the URL, the build fails with a message showing all three ways to set it.
1112
+
1113
+ ### Create a Config Only for a Non-Default Requirement
1114
+
1115
+ Do not create `bascik.config.ts` preemptively. When a verified non-default requirement makes one necessary, include only that setting:
1116
+
1117
+ ```ts
1118
+ // bascik.config.ts (minimal example)
1119
+ import { defineConfig } from '@bascik/bascik/config';
1120
+
1121
+ export default defineConfig({
1122
+ generate: { sitemapLastmod: true },
1123
+ });
1124
+ ```
1125
+
1126
+ ### Anti-Pattern: Defaults-Only Configuration
1127
+
1128
+ Create `bascik.config.ts` only when a value differs from the default. If the configuration file would contain only defaults, **do not create the file**.
1129
+
1130
+ ```ts
1131
+ // DO NOT write this. Every value here is already the default for `bascik --build`.
1132
+ export const build = {
1133
+ minify: true,
1134
+ };
1135
+ ```
1136
+
1137
+ ### Full Configuration Reference (Built-In Defaults)
1138
+
1139
+ You do not need to populate default options in `bascik.config.ts`. The reference below displays all available configuration options populated with their built-in default values for illustrative purposes only:
1140
+
1141
+ ```ts
1142
+ // bascik.config.ts (reference showing all default values)
1143
+ import { defineConfig } from '@bascik/bascik/config';
1144
+
1145
+ export default defineConfig({
1146
+ directory: {
1147
+ pages: 'src/pages',
1148
+ components: 'src/components',
1149
+ out: 'dist',
1150
+ api: 'src/api',
1151
+ },
1152
+ scoping: {
1153
+ scriptBlocks: true,
1154
+ inheritAttributes: true,
1155
+ attributes: {
1156
+ class: true,
1157
+ id: true,
1158
+ name: true,
1159
+ },
1160
+ preserve: ['code'],
1161
+ deduplicateCss: true,
1162
+ },
1163
+ minify: {
1164
+ html: false,
1165
+ css: false,
1166
+ js: false,
1167
+ identifiers: false,
1168
+ },
1169
+ assets: {
1170
+ inlineStyles: false,
1171
+ exclude: [],
1172
+ },
1173
+ generate: {
1174
+ sitemap: true,
1175
+ robots: true,
1176
+ sitemapLastmod: false,
1177
+ cspHashes: false, // writes dist/.bascik/csp-hashes.json with exact sha256 hashes for inline blocks
1178
+ manifest: false,
1179
+ },
1180
+ pipeline: {
1181
+ watchPaths: [],
1182
+ exec: [],
1183
+ workers: false,
1184
+ },
1185
+ scripts: {
1186
+ cache: { enabled: true },
1187
+ typescript: true,
1188
+ onBuildScriptError: 'error',
1189
+ onRoutesScriptError: 'error',
1190
+ onServerScriptError: 'error',
1191
+ timeout: 30000,
1192
+ importRoot: 'src', // directory that @/ and / script imports resolve against
1193
+ },
1194
+ onMinifyError: 'warn',
1195
+ http: {
1196
+ port: undefined,
1197
+ hostname: 'localhost',
1198
+ tls: {
1199
+ enabled: false,
1200
+ },
1201
+ rateLimit: true,
1202
+ trustProxy: false,
1203
+ cacheControl: 'public, max-age=3600',
1204
+ compression: true,
1205
+ maxBodySize: 1048576,
1206
+ apiTimeout: 10000,
1207
+ },
1208
+ logging: {
1209
+ level: 'info',
1210
+ requests: true,
1211
+ copies: true,
1212
+ deletes: true,
1213
+ transpiles: true,
1214
+ },
1215
+ base: '/',
1216
+ });
1217
+ ```
1218
+
1219
+ > **Reverse Proxy & CDN Note (`trustProxy`):** When deploying `bascik --server` behind a CDN or load balancer, set `http.trustProxy: true` in `bascik.config.ts`. This ensures per-IP rate limiting and HSTS headers accurately resolve the client from `X-Forwarded-For` and `X-Forwarded-Proto`. Do not enable `trustProxy` if the server is exposed directly to the Internet without a reverse proxy.
1220
+
1221
+ > **Health Endpoints & Port Conflicts:** `bascik --server` provides `GET /_health` (returns 200 when ready, 503 during boot or drain). Under `--server`, port conflicts (`EADDRINUSE`) fail fast rather than binding an unexpected port.
1222
+
1223
+ > **Live Reload & Error Overlay:** In dev mode, live reload uses SSE with periodic comment heartbeats to survive proxies. Build errors surface directly in an in-browser overlay (`data-testid="bascik-build-error-overlay"`) and clear on the next successful build. Live reload scripts never ship to production.
1224
+
1225
+ > **Watch Mode & Config Changes:** Changes to `bascik.config.ts` require a server restart because configuration is deep-frozen at boot time. Bascik prints a restart hint when config files change.
1226
+
1227
+ > **Content Security Policy (CSP):** Bascik does not set a `Content-Security-Policy` header. Enable `generate.cspHashes: true` to generate SHA-256 hashes of all inline styles and scripts, and apply the CSP header at your host or reverse proxy.
1228
+
1229
+ ### Agent Guidelines for Configuration
1230
+
1231
+ When creating or modifying `bascik.config.ts`:
1232
+ * **Do not create a config without a verified need:** Bascik is zero-config by default. Before creating `bascik.config.ts`, identify the exact requested behavior and verify that it differs from a built-in default. If no such requirement exists, omit the file entirely.
1233
+ * **Keep `bascik.config.ts` minimal:** Do NOT add redundant default options like `directory: { pages: 'src/pages', components: 'src/components' }`, `scoping.scriptBlocks: true`, `scoping.inheritAttributes: true`, `scoping.deduplicateCss: true`, `pipeline.watchPaths: []`, or empty `pipeline.exec` arrays. Never add an all-`true` production minification block because `bascik --build` and `bascik --server` already apply those values. Include only options that differ from defaults.
1234
+ * **Monorepo layout:** `directory.pages`, `directory.components`, and `scripts.importRoot` are three independent directories that never need a common parent. `directory.components` accepts `string | string[]`; every listed root is scanned, watched, and known to `--check`, and roots may point outside the project (for example `components: ['../../shared/components', 'src/components']` with `scripts: { importRoot: '../../shared/scripts' }`) so several Bascik sites in one repository share components and script helpers with no copying while each keeps private components. Component names must be unique across ALL roots (filename-derived tags; duplicates are a build error, never shadowed), roots may not be nested inside one another, and duplicate roots (including via symlink) are rejected. `bascik add` copies into the first listed root. See `/how-to/monorepos`.
1235
+ * **Package.json `"type": "module"` and NPM scripts:** When initializing or configuring a Bascik project, ensure `package.json` specifies `"type": "module"` (since `bascik.config.ts` uses ES module imports) and includes standard npm scripts (`"dev": "bascik"`, `"build": "bascik --build"`, `"check": "bascik --check"`).
1236
+ * **Proactively decompose shared layout components:** When creating or migrating a site, identify repeating layout sections (especially shared `<head>` tags such as `<site-head>`, site headers, and footers) and extract them into reusable components.
1237
+ * **Prefer self-closing void syntax for components without slots:** Use self-closing void syntax (`<site-head />`, `<site-nav />`, `<site-footer />`) for any component tag that does not enclose inner slot content.
1238
+ * **Do NOT invent non-existent `pipeline.exec` scripts:** `pipeline.exec` is only for executing existing custom lifecycle script files. If no custom script file exists in the workspace, omit it.
1239
+ * **Array Replacement in `build`:** Array properties like `pipeline.exec`, `pipeline.watchPaths`, and `assets.inlineStyles` are replaced as atomic values (not concatenated) when specified in `export const build`. When defining `build.pipeline.exec`, include all scripts that should run in production builds.
1240
+ * **Write artifacts to `dist/`:** Lifecycle scripts must write generated artifacts only to the output directory, never source files or watched paths. Never configure `pipeline.watchPaths` or `pipeline.exec[].watch` for generated outputs. There is no `outputs` option. Exec receives `BASCIK_BUILD`, `BASCIK_PAGES_DIR`, `BASCIK_OUT_DIR`, `BASCIK_BASE`, and `BASCIK_SITE_URL`. Use `pre` for required generated dependencies, not `parallel`.
1241
+ * **Source-owned exec phases:** `pipeline.exec[].watch` selects matching scripts after a source edit. One cycle coalesces overlap with pages, components, and `pipeline.watchPaths`: pre finishes before compilation, parallel starts alongside it, post starts after compilation and disk writes. Known source dependents rebuild once; unknown external dependencies conservatively rebuild all pages without rerunning unmatched scripts. Exec-only inputs need no duplicate `pipeline.watchPaths`. Completion never queues another compile or reload. Edits during work are retained; failed paths retry on the next source edit. Pre/compile/post failures discard success reloads. Dev parallel stays nonblocking and reports every failure; build joins parallel and compilation/post before success. Never suppress watched script edits as presumed self-writes. Bascik cannot safely identify source-writing loops. `scripts.importRoot` and `assets.inlineStyles` do not add compilation watches.
1242
+ * **Stick to recommended defaults without restating them:** Preserve `scoping.deduplicateCss: true`, `scoping.scriptBlocks: true`, and `scoping.inheritAttributes: true` unless specifically instructed otherwise or integrating global utility frameworks like Tailwind CSS. Do not write these values into a config just to preserve them.
1243
+ * **Set `BASCIK_SITE_URL` for production features:** Provide the site URL via the environment (e.g. `BASCIK_SITE_URL=https://example.com bascik --build`) when page-aware canonical scripts, sitemaps, or `robots.txt` generation are enabled. Never put `siteUrl` in `bascik.config.ts`.
1244
+ * **Use `base` for subdirectory deployments:** Set a literal path prefix such as `base: '/docs/'`. Do not include a query, fragment, percent escape, backslash, or dot segment, and do not use a full URL. Bascik does not rewrite paths assembled inside JavaScript; use the build-time `BASCIK_BASE` value for those paths.
1245
+
1246
+ **`minify.js`:** `true` (default) strips comments and collapses whitespace; it does not mangle identifiers. Pass a custom async function to plug in esbuild or terser. The function always receives valid JavaScript (browser TypeScript on supported paths is stripped first) and Bascik re-attaches its own `//# sourceURL` directive on a fresh line after the function returns, so minifiers that drop comments are safe:
1247
+
1248
+ ```ts
1249
+ import { transform } from 'esbuild';
1250
+ import { defineConfig } from '@bascik/bascik/config';
1251
+
1252
+ export const build = defineConfig({
1253
+ minify: {
1254
+ js: async (js) => (await transform(js, { minify: true, loader: 'js' })).code,
1255
+ },
1256
+ });
1257
+ ```
1258
+
1259
+ ---
1260
+
1261
+ ## 10. Folder Structure, Static Assets, 404, and 500 pages
1262
+
1263
+ ```
1264
+ src/
1265
+ components/ ← component .html (+ optional .css) templates
1266
+ pages/ ← HTML routes and publishable assets
1267
+ index.html → dist/index.html
1268
+ css/styles.css → dist/css/styles.css (auto-minified)
1269
+ js/main.js → dist/js/main.js (auto-minified)
1270
+ images/logo.svg → dist/images/logo.svg
1271
+ 404.html → dist/404.html
1272
+ ```
1273
+
1274
+ ### Static Assets and Subdirectories
1275
+ * **One Publish Tree:** Put images, fonts, downloads, standalone browser JavaScript, CSS, and other public assets under `directory.pages`. Colocate them with a route or use shared subdirectories such as `src/pages/assets/`. Eligible files copy to `directory.out` with relative structure preserved.
1276
+ * **Project-Specific Exclusions:** `assets.exclude` adds glob exclusions matched relative to `directory.pages`. Keep tests and source-only helpers outside the publish tree.
1277
+ * **Auto-Minification:** CSS and JS files placed in `src/pages/` are automatically minified by `bascik --build` and `bascik --server`; no config is required. Custom BYOMinifier minifier/transformer functions (e.g. PostCSS/Autoprefixer, LightningCSS, esbuild, terser) can be assigned to `minify.css` and `minify.js` only when a project needs non-default processing.
1278
+ * **Built-In Deny-List:** Dotfiles, dot-directories, `node_modules`, `.html`, `.ts`, `.mjs`, `.cjs`, `.mts`, `.cts`, `.map`, `.md`, test/spec files, and inlined stylesheets never copy. This deny-list always applies, including when `assets.exclude` is configured.
1279
+ * **External Copying:** When assets must come from a separate source tree, use a `pipeline.exec` script that writes intentionally selected files to `directory.out`.
1280
+
1281
+ ### Custom 404 & 500 Error Pages
1282
+ * **404 Page (`src/pages/404.html`):** Picked up automatically by path convention in the dev server and under `bascik --server` as a fallback for any non-existent routes (with a 404 status code). When built with `bascik --build`, it compiles to `dist/404.html`, which static hosting platforms (GitHub Pages, Cloudflare Pages, Netlify, Vercel) recognize and serve automatically.
1283
+ * **500 Error Pages in Static vs. Server Contexts:**
1284
+ * **Static builds (`bascik --build`):** A 500 error page is meaningless in a fully static build, because static web hosts serve their own error pages if an infrastructure or server issue occurs. Do not port a 500 error page from Next.js, Remix, or any server-rendered framework into a static Bascik site.
1285
+ * **Production server (`bascik --server`):** Custom `src/pages/500.html` support under `bascik --server` is convention-based with no config key. When an unhandled error occurs, `src/pages/500.html` is served with status 500 and security headers, falling back to a built-in minimal document when absent. Static hosts serve their own error pages.
1286
+
1287
+ ---
1288
+
1289
+ ## 11. CLI & Development Workflow
1290
+
1291
+ ### Scaffold a New Project
1292
+
1293
+ The zero-friction way to start a new Bascik project:
1294
+
1295
+ ```sh
1296
+ npm create bascik@latest my-site -y
1297
+ ```
1298
+
1299
+ This scaffolds the project, installs dependencies, and starts the dev server in one shot. You're live at **http://localhost:8080**. Pass a different name to use it as both the directory name and the site title. Omit the name to be prompted for one (defaulting to `bascik-app`). Drop `-y` to step through the install and dev server prompts manually.
1300
+
1301
+ The scaffold creates a complete starter site: pages, components with unit tests, Playwright E2E browser tests, global CSS, `.vscode/extensions.json` (recommends the official Bascik extension), and AI assistant skills at `.github/skills/bascik/SKILL.md` and `.claude/skills/bascik/SKILL.md`. It includes a `lint` script in `package.json` powered by `@bascik/language-server` (`npm run lint`), and omits `bascik.config.ts` because the starter uses Bascik's built-in defaults. When the dev server stops, the CLI prints a reminder:
1302
+
1303
+ ```
1304
+ To start again: cd my-site && npm run dev
1305
+ ```
1306
+
1307
+ ### Adding to an Existing Project
1308
+
1309
+ ```sh
1310
+ npm install @bascik/bascik
1311
+ ```
1312
+
1313
+ Then run `bascik init` to scaffold starter files (`src/pages/index.html`, `src/components/`), append `.gitignore` entries (`dist/`, `node_modules/.cache/bascik/`), and patch `package.json` (`"type": "module"` when absent plus standard scripts and dependency), or add the scripts manually to `package.json`:
1314
+
1315
+ ```json
1316
+ {
1317
+ "type": "module",
1318
+ "scripts": {
1319
+ "dev": "bascik",
1320
+ "build": "bascik --build"
1321
+ }
1322
+ }
1323
+ ```
1324
+
1325
+ ### CLI Commands
1326
+
1327
+ ```sh
1328
+ bascik # dev: transpile, start plaintext HTTP server at http://localhost:8080, watch
1329
+ bascik --build # production: transpile to dist/ only
1330
+ bascik --build --log [path] # write build output to a log file (default: .bascik/build.log; only valid with --build)
1331
+ bascik --build --only <glob> # targeted build: rebuild matching pages only without cleaning dist/ (repeatable)
1332
+ bascik --server # production server: serve a pre-built dist/ folder
1333
+ bascik --check # static analysis: validate pages, components, and config without building
1334
+ bascik add <package> # copy-in components from an installed npm package into src/components/
1335
+ ```
1336
+
1337
+ More flags: `--config <path>` (load a specific config file), `--port <n>`, `--host <name>`, `--log-level <silent|error|warn|info|debug>`, `--site-url <url>`, `--only <glob>` (repeatable, only with `--build`), `--force` (overwrite locally modified components during `add`), `--dry-run` (preview what `add` would copy), `--yes`/`-y` (skip interactive prompts during `add`), and `--env-file <path>` (repeatable). Every value flag accepts both `--flag value` and `--flag=value`. Precedence: a flag beats the environment variable (`BASCIK_SERVER_PORT`, `BASCIK_SERVER_HOST`, `BASCIK_LOG_LEVEL`), which beats the config file. The parser rejects `--build` combined with `--server`, `--only` without `--build`, unknown flags, and stray positionals: `bascik build` (no dashes) errors with a `Did you mean "--build"?` suggestion instead of silently starting the dev server.
1338
+
1339
+ **`bascik add`:** copies components from an installed npm package into `src/components/`. Files are **project files** committed to version control, not resolved runtime dependencies. Component tag names derive from filenames; if a package component collides with an existing project component, `bascik add` refuses to write anything and reports both paths. `bascik add` maintains a `bascik-lock.json` file in the project root with file hashes. Unmodified files update cleanly on re-add; locally modified files are never overwritten without `--force`.
1340
+
1341
+ **Gotchas with `--only`:** Targeted builds skip cleaning `directory.out` so existing compiled pages survive. Sitemap and robots generation is skipped with a warning to avoid delisting unbuilt pages. Artifacts (`dist/.bascik/manifest.json`, `dist/.bascik/csp-hashes.json`) merge updated entries into existing files.
1342
+
1343
+ **`bascik --build --target <name>`:** also emits a serverless deployment bundle under `dist/.bascik/<name>/` (`cloudflare`, `cloudflare-workers`, or `cloudflare-pages`). The normal `dist/` is unchanged. The bundle has a `public/` upload tree (static files plus the generated Worker and routing) and compiles every server script, stream script, API route, and dynamic page template into the Worker; dynamic templates are never in `public/`, so a routing miss is a 404, not inert placeholders. Official targets resolve to `@bascik/adapter-cloudflare` (`npm install --save-dev @bascik/adapter-cloudflare`). Rejected with `--only`. Unsupported `node:*` imports (for example `node:child_process`) fail the build with the import chain. Streamed pages are sent with `Content-Encoding: identity` so the edge does not buffer them. See the Cloudflare deployment guide.
1344
+
1345
+ **`bascik --server`:** starts the production server against a pre-built `dist/` directory (HTTP/1.1 by default; HTTP/2 when TLS is enabled). **Needed when the site uses `data-bascik-server` or `data-bascik-stream` scripts or API routes (`src/api/`)** for per-request dynamic content, unless you deploy a serverless bundle instead. Sites with only static pages and none of those can be deployed to any static host. Run `bascik --build` first, then `bascik --server`. Unlike the dev server, `--server` does not watch files or inject live-reload. `data-bascik-server` scripts and API routes execute per-request in both modes.
1346
+
1347
+ ### API Routes (`src/api/`)
1348
+
1349
+ Define HTTP endpoints in TypeScript/JavaScript files under `src/api/` (`directory.api`). Handlers take a standard WHATWG `Request` and return a standard `Response`:
1350
+
1351
+ ```ts
1352
+ // src/api/contact.ts
1353
+ export const POST = async (request: Request): Promise<Response> => {
1354
+ const data = await request.json();
1355
+ if (!data.name) return Response.json({ error: "name is required" }, { status: 400 });
1356
+ return Response.json({ ok: true }, { status: 201 });
1357
+ };
1358
+ ```
1359
+
1360
+ * **Method exports:** `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `HEAD`. Unexported methods return `405 Method Not Allowed` with an accurate `Allow` header.
1361
+ * **Derived HEAD & Auto OPTIONS:** `HEAD` auto-derives from `GET` (headers only, body stripped); `OPTIONS` auto-responds 204 with `Allow`.
1362
+ * **Context argument:** `(request, { params, remoteIp }, { signal })`. Route params are parsed from `[param]` segments. `remoteIp` is proxy-aware when `http.trustProxy` is true. `signal` provides cooperative abort on `http.apiTimeout`.
1363
+ * **Streaming request body:** `request.body` is a WHATWG stream with `duplex: 'half'`. Handlers call `.json()`, `.text()`, etc. `http.maxBodySize` (default 1 MB) counts streamed bytes and aborts with 413 without memory buffering.
1364
+ * **Security & secrets:** Handlers run in-process with access to `process.env`. Handler source files in `src/api/` are never served or copied to static builds. Thrown errors return 500 with zero stack/path info leaked to clients. No CORS headers are added by default.
1365
+ * **Portability:** The handler function is reusable because it takes a standard `Request`, but routing, method dispatch, and imports still need a host. Bascik supplies all three on `bascik --server` and on the Cloudflare target (`bascik --build --target cloudflare|cloudflare-workers|cloudflare-pages`, see below). Other hosts are manual porting: write the platform wrapper yourself.
1366
+ * **`context.platform`:** optional host capabilities. `{ name: "node" }` on the built-in server; `{ name: "cloudflare", env, waitUntil }` in a generated Worker, where `env` carries KV/D1/secrets bindings. Never copied to globals; a handler that ignores it runs unchanged everywhere.
1367
+ * **Gotchas:** Plain static builds (`bascik --build`) cannot serve API routes and warn at build time; add `--target <name>` to package them into a serverless bundle. Handlers run in-process on the server; synchronous infinite loops cannot be interrupted by timers. No automatic compression or CORS headers are added by default.
1368
+
1369
+ **`http` config block:** configure the server in `bascik.config.ts`:
1370
+ ```ts
1371
+ export default {
1372
+ http: {
1373
+ port: 8080, // default (8080 HTTP, 8443 HTTPS)
1374
+ hostname: 'localhost', // set '0.0.0.0' to bind all interfaces
1375
+ httpCache: true, // default in --server; false in dev
1376
+ tls: {
1377
+ enabled: false, // default; set true for HTTP/2 HTTPS
1378
+ keyFile: 'bascik-privkey.pem',
1379
+ certFile: 'bascik-cert.pem',
1380
+ },
1381
+ },
1382
+ };
1383
+ ```
1384
+ When `tls.enabled: true` is set, TLS certs are generated automatically (mkcert if available, openssl fallback) when `keyFile`/`certFile` are absent. Provide your own certs from a public CA for production.
1385
+
1386
+ **`http.httpCache`:** defaults to `true` in `--server` and `false` in the dev server. When `true`: pages and static assets receive strong SHA-256 content-hash `ETag` headers (not mtime-derived, so identical bytes produce identical ETags across separate server instances/replicas) and the server returns `304 Not Modified` for unchanged content, carrying `vary` and `cache-control`. Set `false` if a CDN manages caching externally.
1387
+
1388
+ **`http.cacheControl`:** a string (applied to every static asset) or a per-extension map, e.g. `{ '.woff2': 'public, max-age=31536000, immutable', '.png': 'public, max-age=86400' }`. Unmapped extensions fall back to the default `public, max-age=3600`. Only pair `immutable` with a URL whose content cannot change (in practice, a fingerprinted filename).
1389
+
1390
+ **`http.compression`:** negotiates Brotli or Gzip based on `Accept-Encoding`. Static HTML pages are pre-compressed with both Brotli and Gzip in the background at startup; static assets negotiate Brotli or Gzip with sidecar support (`.br`/`.gz`). Compression is skipped below a size threshold and for already-compressed formats (images, video, audio, archives, `woff2`). Each encoded representation gets a distinct ETag from the identity representation (e.g. `"hash-br"`, `"hash-gz"`).
1391
+
1392
+ **Production hardening (automatic in `--server`):**
1393
+ * **Security headers:** every response includes `x-content-type-options: nosniff`, `x-frame-options: SAMEORIGIN`, `referrer-policy: strict-origin-when-cross-origin`, `permissions-policy: interest-cohort=()`. When serving over HTTPS/TLS (`enableTls: true` or `x-forwarded-proto: https`), `strict-transport-security: max-age=31536000; includeSubDomains` (HSTS) is automatically included per RFC 6797.
1394
+ * **URL routing (dev and production):** pages are served at their filename without the `.html` extension. `dist/about.html` is at `/about`, `dist/blog/post.html` is at `/blog/post`, `dist/index.html` is at `/`. Unmatched paths fall through to `/404` if a `dist/404.html` exists.
1395
+ * **Rate limiting:** 500 requests per 10 seconds per IP. Clients over the limit get `429 Too Many Requests` with `Retry-After`. Not active in the dev server. When behind a reverse proxy the limit applies to the proxy's IP; use the proxy's own rate limiting for per-client control.
1396
+ * **Graceful shutdown:** SIGTERM and SIGINT mark readiness as draining, stop accepting connections, start registered cleanup handlers, and close protocol resources. If the server has not closed within `http.timeouts.drain` (default 5000 ms), Bascik force-exits with a nonzero status.
1397
+ * **Path traversal protection:** static asset URLs are validated against `dist/`; requests that escape with `/../` sequences get `400 Bad Request`.
1398
+ * **Hidden-path protection:** after URL decoding, any request path containing a segment that starts with `.` gets `404 Not Found` before static file lookup.
1399
+
1400
+ **Deployment:** Bascik's server runs over unencrypted HTTP/1.1 by default. Edge platforms (Heroku, Fly.io, AWS ECS, Render) that terminate TLS at the load balancer can forward cleartext HTTP directly to the container. Always run production servers under a process supervisor (systemd, Docker restart policy, or Kubernetes supervisor) as Bascik's process-level crash net terminates with a non-zero exit code on unhandled errors to avoid undefined state. Key patterns:
1401
+
1402
+ * **VPS / dedicated**: bind `hostname: '0.0.0.0'`, supply Let's Encrypt certs via `keyFile`/`certFile` with `enableTls: true`, run as a `systemd` service with `Restart=on-failure`.
1403
+ * **Docker**: multi-stage build (build stage: `npx bascik --build`; serve stage: `npx bascik --server`) with `--restart=unless-stopped`.
1404
+ * **PaaS (Railway, Render, Fly.io)**: set start command to `bascik --build && bascik --server` and bind port `8080`.
1405
+
1406
+ When using a reverse proxy, forward `X-Real-IP` and any auth headers so `data-bascik-server` scripts receive them via `request.headers` or `context.remoteIp`.
1407
+
1408
+ ### Development Workflow & Server Output
1409
+ Bascik's CLI is designed to provide clean, minimal, and informative terminal output.
1410
+
1411
+ #### 1. Starting the Dev Server
1412
+ When you start the dev server, Bascik starts the HTTP server concurrently with page transpilation so the server is already bound to its port by the time the last page finishes. The `Server running at` line prints immediately after the transpilation summary with no gap between them. Pages are served from memory with no disk I/O at request time in dev mode. If port 8080 is in use, Bascik automatically tries the next available port (8081, 8082, etc.).
1413
+
1414
+ ```terminal
1415
+ Starting transpiling...
1416
+ transpiled: pages/getting-started.html in 0.4ms
1417
+ transpiled: pages/index.html in 0.6ms
1418
+ transpiled: pages/about.html in 0.3ms
1419
+
1420
+ ✓ 3 pages transpiled in 45ms
1421
+ Server running at http://localhost:8080
1422
+ ```
1423
+
1424
+ On startup, Bascik cleans `directory.out` before pre-phase lifecycle scripts run, then computes the full component list and global styles **once** and transpiles all pages. By default pages transpile sequentially on the main thread; setting `pipeline.workers: true` in `bascik.config.ts` distributes them across a pool of CPU-core worker threads instead. Worker startup has a fixed cost (each worker loads the transpiler's module graph independently), so `pipeline.workers` is opt-in and best suited to larger sites or CPU-heavy per-page work, small sites are usually faster with the sequential default. Brotli and Gzip compression for each page runs in the background after storage and does not block the page from being marked ready or served; the server falls back to serving uncompressed content for any request that arrives before compression finishes. The server becomes ready as soon as memory is populated. In dev mode, Bascik stores each page in memory first, then writes its compiled HTML to `dist/` asynchronously. Serving never waits for that disk write.
1425
+
1426
+ #### 2. Watching for File Changes (Watch Mode)
1427
+ While the dev server is active, Bascik watches your file system and incrementally updates your build as files are added, updated, or removed:
1428
+
1429
+ * **Modifying/Adding Pages:** Editing or adding an HTML page in your pages directory (e.g., `src/pages/about.html`) triggers incremental transpilation of just that page:
1430
+ ```terminal
1431
+ transpiled: pages/about.html in 0.3ms
1432
+ ```
1433
+ * **Modifying Components:** Editing a component (e.g., `src/components/site-nav/site-nav.html`) triggers selective transpilation. Bascik tracks dependency mappings and only rebuilds pages that actually reference that component:
1434
+ ```terminal
1435
+ transpiled: pages/index.html in 0.5ms
1436
+ transpiled: pages/about.html in 0.3ms
1437
+ ```
1438
+ * **Open-Page Prioritization:** When a batch rebuild is triggered (such as an inlined stylesheet or shared component), Bascik checks active live-reload browser connections and prioritizes compiling currently open pages first, emitting reload signals immediately before compiling background pages.
1439
+ * **Static Assets:** Replicating any non-HTML static assets (like custom CSS, JS files, or images) from pages directly into the output directory:
1440
+ ```terminal
1441
+ copied: pages/css/custom.css
1442
+ ```
1443
+ * **Deleting Pages:** Removing a page from your pages directory automatically cleans up its compiled output counterpart to prevent dead files:
1444
+ ```terminal
1445
+ deleted file: pages/old-page.html
1446
+ ```
1447
+
1448
+ #### 3. Transpilation & Build Errors
1449
+ Development and production builds handle page failures differently. The dev server logs a failed page, completes boot, continues serving healthy pages, and retries the failed page on the next save. A production `bascik --build` waits for every page job, reports all failures together, and exits nonzero rather than reporting success with missing output.
1450
+
1451
+ Hard build failures include a missing or unreadable configured pages directory, a page without a non-empty `<body>`, runaway component expansion, output directory creation failure, and page write failure. `ENOENT` write errors are not ignored.
1452
+
1453
+ ```terminal
1454
+ Build failed with 2 page errors:
1455
+ src/pages/about.html
1456
+ validate markup: Page does not contain a non-empty <body> element
1457
+ src/pages/blog/post.html
1458
+ write output: EACCES: permission denied
1459
+ ```
1460
+
1461
+ * **Component Transpilation Failure:** If a component markup or CSS scoping parser fails during transpilation:
1462
+ ```terminal
1463
+ [bascik] Transpilation failed for component <site-nav> during css-scoping in "pages/about.html" at (line 22, column 8)
1464
+ Defined in component template: "components/site-nav/site-nav.html"
1465
+ Error: ParseError: CSS Selector is invalid or could not be parsed.
1466
+ ```
1467
+ * **Build Script Failure:** If a build-time JavaScript execution block (using `<script data-bascik-build>`) encounters an error:
1468
+ ```terminal
1469
+ [bascik] build script error in "pages/index.html" at (line 12, column 5):
1470
+ ReferenceError: marked is not defined
1471
+ ```
1472
+ * **Unknown Component Tags:** If a page references a hyphenated tag with no matching component file, Bascik warns during transpilation:
1473
+ ```terminal
1474
+ [bascik] Unresolved component tag in "pages/about.html": <my-mistyped> - no matching component file found. Run `bascik --check` for a full report.
1475
+ ```
1476
+ Use `bascik --check` (or `bascik --check --strict`) as the CI gate for project references and diagnostics.
1477
+
1478
+ #### 4. Static Analysis (`bascik --check`)
1479
+ Run `bascik --check` from your project root to validate pages, component files, config, and API route files without starting the dev server or writing any output:
1480
+
1481
+ ```sh
1482
+ bascik --check
1483
+ ```
1484
+
1485
+ Bascik scans project sources and reports:
1486
+ * **Warnings (exit code 0):** Unmatched hyphenated tags (including third-party web components), unused component files, unknown `data-bascik-*` attributes, and component ordering conventions (`<style>` above markup, `<script>` below).
1487
+ ```terminal
1488
+ Components with no matching file (1)
1489
+ These are either typos, or third-party web components. Bascik does not
1490
+ transpile them; they are passed through to the browser unchanged.
1491
+
1492
+ <model-viewer> src/pages/gallery.html:42
1493
+ ```
1494
+ * **Errors (exit code 1):** Config validation failures, missing site URL for sitemap/robots generation, duplicate component names, circular component references, script mode conflicts (`data-bascik-build` + `data-bascik-server` on one tag), duplicate route resolution, API route files missing method handlers, API route collisions, and WHATWG HTML5 parse errors in compiled `dist/` HTML files (requires [parse5](https://parse5.js.org/) as a dev dependency; when parse5 is absent, `--check` prints a one-line install hint and does not fail).
1495
+ * **Strict mode:** Pass `--strict` to treat warnings as errors and exit with code `1`.
1496
+ * **JSON output:** Pass `--json` to output structured findings for CI integration. The JSON schema includes `distHtmlChecked` (number of `dist/` files parse5 validated, or `null`) and `distHtmlSpecHintNeeded` (`true` when parse5 is absent but `dist/` HTML exists).
1497
+ * **Success**: Exits with code `0` when no errors are found (or under `--strict` when no errors or warnings).
1498
+
1499
+ `missing-required-prop` is intentionally not emitted. The cheap whole-project heuristic is noisy for real projects and creates speculative warnings.
1500
+
1501
+ Run in CI:
1502
+ ```sh
1503
+ bascik --check && bascik --build
1504
+ ```
1505
+
1506
+ **What `bascik --check` does not cover:** it does not replace CSS/JS syntax linting. Use external tools alongside `--check`:
1507
+
1508
+ | Tool | What it catches |
1509
+ |---|---|
1510
+ | VS Code built-in CSS | CSS syntax errors (squiggly lines, no install needed) |
1511
+ | [Stylelint](https://stylelint.io) | CSS syntax, invalid properties, conventions |
1512
+ | [parse5](https://parse5.js.org/) | WHATWG HTML5 parse errors in compiled `dist/` output (`npm install -D parse5` then `bascik --check`) |
1513
+ | [HTMLHint](https://htmlhint.com) | HTML structure errors in `.html` files |
1514
+ | [ESLint](https://eslint.org) | JS syntax and logic errors in `<script>` blocks |
1515
+
1516
+ Recommended CI pipeline with CSS validation:
1517
+ ```sh
1518
+ npx stylelint "src/**/*.css" && bascik --check && bascik --build
1519
+ ```
1520
+
1521
+ #### Editor Configuration
1522
+
1523
+ Editors validate all `<script>` blocks in an HTML file as sharing one scope, causing false "variable already declared" errors. Bascik wraps each block in an IIFE at build time, so the errors are false positives.
1524
+
1525
+ **VS Code fix:** add `.vscode/settings.json`:
1526
+ ```json
1527
+ { "html.validate.scripts": false }
1528
+ ```
1529
+
1530
+ Commit this file so all contributors get the correct behavior automatically. Alternatively, add `// @ts-nocheck` as the first line inside any individual script block that triggers the warning.
1531
+
1532
+ #### 5. Inspecting `dist/` Output
1533
+
1534
+ Both `bascik --build` and dev mode (`bascik`) write compiled HTML and static assets to `dist/` on disk. Dev stores pages in memory before issuing asynchronous disk writes, so the server can return updated pages immediately while `dist/` remains available for inspection. The `dist/` structure mirrors `src/pages/` with the leading directory stripped:
1535
+
1536
+ ```
1537
+ src/pages/about.html → dist/about.html
1538
+ src/pages/blog/post.html → dist/blog/post.html
1539
+ ```
1540
+
1541
+ What to check in compiled output:
1542
+ * **Component resolution:** every custom tag (e.g. `<site-nav>`) should be replaced with expanded HTML. A hyphenated tag still present in `dist/` means no component file matched.
1543
+ * **Scoped class names:** attributes like `class="bascik__site-nav__nav"` (or a short hash with `minify.identifiers`) confirm CSS scoping ran correctly.
1544
+ * **Injected `<style>` block:** the `<head>` should contain one combined `<style>` with CSS from all components used on that page.
1545
+ * **Build script output:** `<script data-bascik-build>` is replaced with stdout; if missing, check the terminal for a `[bascik] build script error` line.
1546
+ * **Server script output:** `<script data-bascik-server>` is replaced at request time; if output is missing on a live request, check the terminal for a `[bascik] server script error` line. Remember these scripts run in Node.js, not the browser, they require `bascik --server` or the dev server to execute.
1547
+ * **Slot and prop values:** verify fallback and injected text appear in the right place.
1548
+
1549
+ The browser's **View Source** (or DevTools **Sources** panel) is equivalent to reading `dist/` and is often faster during development.
1550
+
1551
+ ---
1552
+
1553
+ ## 12. JavaScript Libraries & Progressive Enhancement
1554
+
1555
+ Bascik adds zero JavaScript to output pages by default, but places no restrictions on including external libraries.
1556
+
1557
+ **Note for AI Assistants:** The integration patterns below (petite-vue, Alpine.js, Tailwind CSS) demonstrate how to combine Bascik with external runtime libraries. Do not add or configure these libraries unless the project explicitly uses them or the user requests them.
1558
+
1559
+ ### How to Include
1560
+ Add a CDN `<script src>` tag to the page `<head>` or a shared head component. Bascik passes external script tags through completely unchanged.
1561
+
1562
+ **Bare specifiers do not work in client scripts.** Bascik does not rewrite `import x from 'some-package'` in a `<script type="module">`; the browser receives it verbatim and fails with `Failed to resolve module specifier`. Bare specifiers work only in `data-bascik-build` and `data-bascik-server` scripts, which run in Node.js. For an npm package with no CDN build, bundle it with esbuild as a `pipeline.exec` step (`phase: 'pre'`), write the bundle into `dist/assets/js/`, and import it from the page with a root-relative URL.
1563
+
1564
+ ```html
1565
+ <head>
1566
+ <script src="https://unpkg.com/petite-vue" defer init></script>
1567
+ </head>
1568
+ ```
1569
+
1570
+ ### petite-vue
1571
+ petite-vue (~5 KB) is a Vue-compatible progressive enhancement library. Declare `v-scope` on any element to give it isolated reactive state:
1572
+
1573
+ ```html
1574
+ <!-- src/components/my-counter.html -->
1575
+ <div v-cloak v-scope="{ count: 0 }">
1576
+ {{ count }}
1577
+ <button @click="count++">+</button>
1578
+ </div>
1579
+ ```
1580
+
1581
+ Load it once in the page `<head>` with `defer init` to auto-mount all `v-scope` elements:
1582
+ ```html
1583
+ <script src="https://unpkg.com/petite-vue" defer init></script>
1584
+ ```
1585
+
1586
+ ### Alpine.js
1587
+ Alpine.js uses `x-data` for state and `@click` / `x-show` for events and visibility:
1588
+
1589
+ ```html
1590
+ <div x-data="{ open: false }">
1591
+ <button @click="open = !open">Toggle</button>
1592
+ <p x-show="open">Hidden content.</p>
1593
+ </div>
1594
+ <script src="https://unpkg.com/alpinejs" defer></script>
1595
+ ```
1596
+
1597
+ ### Tailwind CSS
1598
+ Tailwind utility classes are global by design. Bascik's class scoping would rename `class="flex gap-4"` to `class="bascik__comp__flex bascik__comp__gap-4"`, breaking Tailwind's CSS.
1599
+
1600
+ **Required config:** disable class scoping in `bascik.config.ts`:
1601
+ ```ts
1602
+ import { defineConfig } from '@bascik/bascik/config';
1603
+
1604
+ export default defineConfig({
1605
+ scoping: {
1606
+ attributes: {
1607
+ class: false, // let Tailwind utility classes pass through unchanged
1608
+ },
1609
+ },
1610
+ });
1611
+ ```
1612
+
1613
+ Include Tailwind via CDN (development) or Tailwind CLI (production):
1614
+ ```html
1615
+ <script src="https://cdn.tailwindcss.com"></script>
1616
+ ```
1617
+
1618
+ With `class: false`, utility classes work normally inside any component:
1619
+ ```html
1620
+ <!-- src/components/feature-card.html -->
1621
+ <div class="rounded-xl border border-gray-200 p-6 shadow-sm">
1622
+ <h3 class="mb-2 text-lg font-semibold" data-bascik-prop-title></h3>
1623
+ <p class="text-sm text-gray-600" data-bascik-prop-body></p>
1624
+ </div>
1625
+ ```
1626
+
1627
+ Trade-off: with `class: false`, Bascik no longer isolates component class names. IDs and names remain scoped independently.
1628
+
1629
+ ### HTMX, Stimulus, and Others
1630
+ * **HTMX:** Uses `hx-get`, `hx-post` attributes for server-driven partial updates.
1631
+ * **Stimulus:** Uses `data-controller` for attaching behavior to DOM structures.
1632
+ * **Chart.js, D3, Leaflet:** Mounts to elements via `getElementById`; Bascik rewrites the ID and matching selectors at build-time so they stay in sync.
1633
+
1634
+ ---
1635
+
1636
+ ## 13. Testing
1637
+
1638
+ Bascik provides per-package commands and workspace-wide aggregators run from the root.
1639
+
1640
+ ### Monorepo Aggregators
1641
+
1642
+ ```sh
1643
+ yarn typecheck:all # typecheck pkg, create, docs, ext
1644
+ yarn check:all # spelling and web standards
1645
+ yarn unit:all # unit tests across all packages
1646
+ yarn e2e:all # Playwright E2E suites across all packages
1647
+ yarn coverage:all # update coverage across all packages
1648
+ yarn test:all # typecheck:all + check:all + unit:all + e2e:all + docs:lighthouse
1649
+ ```
1650
+
1651
+ ### Unit Tests (Vitest)
1652
+
1653
+ ```sh
1654
+ yarn pkg:unit # single run (@bascik/bascik)
1655
+ yarn pkg:test # watch mode
1656
+ yarn create:unit # create-bascik unit tests
1657
+ yarn docs:unit # bascik-docs unit tests
1658
+ yarn ext:unit # bascik-vscode unit tests
1659
+ yarn pkg:bench # benchmarks
1660
+ ```
1661
+
1662
+ Each `pkg/src/lib/*.ts` module has a paired `*.test.ts`. Because modules depend on `BascikConfig` (a singleton), unit tests use `vi.mock('../config.ts', ...)` to stub configuration, then import the module under test **after** the mock call.
1663
+
1664
+ ### Runtime Profiling and Retention
1665
+
1666
+ Use `yarn workspace @bascik/bascik profile:workload --report-dir <private-directory>` for bounded runtime captures, separate from throughput benchmarks. Replace the illustrative placeholder with a new, empty, absolute directory outside the repository and all served or watched trees; existing roots must be user-owned and mode `0700`. Run tools sequentially and keep manifests, profiles, and snapshots private.
1667
+
1668
+ Captures require complete byte-checked work and valid artifacts, not merely exit code zero. Main-isolate CPU profiles do not establish worker or child-process coverage. Worker CPU recording is unsupported on Node v24.17.0/macOS; unprofiled workers remain available. Allocation samples do not prove reclamation: use the separate module-retention experiments and heap retainer paths described in the testing docs. Failed profiling tests retain private diagnostic paths; do not retry failures into success or infer Node ESM eviction from framework cache cleanup.
1669
+
1670
+ ### End-to-End Tests (Playwright)
1671
+
1672
+ ```sh
1673
+ yarn pkg:build # build package first
1674
+ yarn pkg:e2e # run static production suite
1675
+ yarn pkg:e2e:dev # run dev server live-reload and watch suite
1676
+ yarn pkg:e2e:prod # run both HTTP/1.1 and HTTP/2 production server suites
1677
+ yarn pkg:e2e:prod:http1 # run cleartext HTTP/1.1 prod server suite
1678
+ yarn pkg:e2e:prod:http2 # run TLS HTTP/2 prod server suite
1679
+ ```
1680
+
1681
+ The E2E suite lives in `pkg/e2e/` and supports four execution modes:
1682
+ 1. **Static production suite (`playwright.config.ts`)**: builds the fixture site with `bascik --build` and serves static files via `server.ts` on port 4200.
1683
+ 2. **HTTP/1.1 production server suite (`playwright.server.config.ts`)**: boots cleartext `bascik --server` over HTTP/1.1 on port 9443 to test `data-bascik-server` request-time script execution and cleartext server behavior.
1684
+ 3. **HTTP/2 production server suite (`playwright.server-http2.config.ts`)**: boots TLS-enabled `bascik --server` over HTTP/2 on port 9444 to test `data-bascik-server` request-time script execution and encrypted server behavior.
1685
+ 4. **Dev server watch suite (`playwright.dev.config.ts`)**: boots `bascik --dev` on port 8080 to run the full test suite and live-reload watcher tests directly against the live dev server with SSE tracking and open-page priority re-transpilation.
1686
+
1687
+ Keep each mode's `testIgnore` list on its `default` project. Playwright project arrays replace matching top-level arrays instead of extending them, which can silently select server-only tests in the wrong mode. The config-selection unit test must cover all four project exclusion lists.
1688
+
1689
+ The fixture config sets `minify.identifiers: false` so Playwright selectors can use readable scoped names like `bascik__my-comp__btn` instead of opaque hashes. However, in production builds (where `minify.identifiers: true` is enabled), Bascik compiles and minifies element IDs and class names. Consequently, relying on raw CSS selectors like `page.locator('.my-class')` or `page.locator('#my-id')` will fail because those identifiers are hashed and compressed.
1690
+
1691
+ Choose locators by test intent:
1692
+ * **Behavior-oriented E2E tests:** Prefer accessibility-based locators such as `page.getByRole(...)`, `page.getByLabel(...)`, and `page.getByPlaceholder(...)`, or explicit `data-testid` attributes with `page.getByTestId(...)`. These locators remain stable when identifier minification changes classes and IDs.
1693
+ * **Compiler-output verification tests:** When the transformed selector or identifier is itself under test, deliberately assert generated scoped class names or rewritten IDs. Do not use a behavior-only locator as a substitute for asserting the compiler output that the test is meant to verify.
1694
+
1695
+ **Adding a new e2e test:**
1696
+ 1. Add a component in `pkg/e2e/src/components/my-feature/`
1697
+ 2. Add a page in `pkg/e2e/src/pages/my-feature-test.html` with two or more instances (to verify isolation)
1698
+ 3. Add `pkg/e2e/tests/my-feature.test.ts` using the standard pattern:
1699
+
1700
+ ```ts
1701
+ import { test, expect } from '@playwright/test';
1702
+
1703
+ test.describe('my-feature-test page', () => {
1704
+ test.beforeEach(async ({ page }) => { await page.goto('/my-feature-test'); });
1705
+
1706
+ test('instances are isolated', async ({ page }) => {
1707
+ const a = page.locator('.bascik__my-feature__wrapper').nth(0);
1708
+ const b = page.locator('.bascik__my-feature__wrapper').nth(1);
1709
+ // assert A and B are independent
1710
+ });
1711
+ });
1712
+ ```
1713
+
1714
+ There are 44 e2e test files covering CSS scoping, JS scoping, slots, props, attribute inheritance, animations, observers, SVG, and head components.
1715
+
1716
+ ### Debugging with VS Code and Node.js
1717
+
1718
+ Bascik works smoothly with debuggers because it operates directly on vanilla HTML, CSS, and JavaScript without complex runtime abstractions or heavy bundle transformations.
1719
+
1720
+ #### Source Location Preservation
1721
+
1722
+ Debugging server-side scripts, build-time scripts, and browser component code in Bascik requires no special source map plugins or transpilation step:
1723
+
1724
+ * **Server and Build Scripts:** Bascik automatically appends `//# sourceURL=${relPath}` comments to temporary script modules created during compilation (`<script data-bascik-build>` and `<script data-bascik-server>`). Stack traces and debugger breakpoints point directly back to your original source file paths.
1725
+ * **Client Component Scripts:** Bascik preserves newline padding when scoping component `<script>` tags. Line numbers reported in console errors or hit during step-debugging match the exact line numbers in your component `.html` source files.
1726
+ * **Native TypeScript Debugging:** Because Node 22.18.0+ natively executes TypeScript files by stripping type annotations, setting breakpoints in `.ts` modules pauses execution immediately with full access to local scope variables and call stacks.
1727
+
1728
+ #### Pre-Configured VS Code Launch Configurations
1729
+
1730
+ Projects created using `npm create bascik@latest` include a pre-configured `.vscode/launch.json` file focused on testing and debugging your application. You can start debugging immediately by pressing `F5` or selecting a profile from VS Code's **Run and Debug** panel:
1731
+
1732
+ ```json
1733
+ {
1734
+ "version": "0.2.0",
1735
+ "configurations": [
1736
+ {
1737
+ "name": "Debug Dev Server",
1738
+ "type": "node",
1739
+ "request": "launch",
1740
+ "runtimeExecutable": "npx",
1741
+ "runtimeArgs": ["bascik"],
1742
+ "console": "integratedTerminal",
1743
+ "restart": true,
1744
+ "skipFiles": ["<node_internals>/**"]
1745
+ },
1746
+ {
1747
+ "name": "Debug Unit Tests",
1748
+ "type": "node",
1749
+ "request": "launch",
1750
+ "runtimeExecutable": "npx",
1751
+ "runtimeArgs": ["vitest", "run"],
1752
+ "console": "integratedTerminal",
1753
+ "skipFiles": ["<node_internals>/**"]
1754
+ },
1755
+ {
1756
+ "name": "Launch Chrome",
1757
+ "type": "chrome",
1758
+ "request": "launch",
1759
+ "url": "http://localhost:8080",
1760
+ "webRoot": "${workspaceFolder}"
1761
+ }
1762
+ ]
1763
+ }
1764
+ ```
1765
+
1766
+ #### Debugging Server Scripts and Build Steps
1767
+
1768
+ To debug server scripts (`<script data-bascik-server>`), build-time scripts (`<script data-bascik-build>`), or custom server logic modules:
1769
+
1770
+ 1. Open your component `.html` file or imported `.ts` logic file in VS Code.
1771
+ 2. Click to the left of any line number inside your script block or function to set a breakpoint.
1772
+ 3. Select **Debug Dev Server** in the Run and Debug panel and press `F5`.
1773
+ 4. Open your browser and navigate to the page triggering the request.
1774
+ 5. Node.js pauses execution on your breakpoint inside VS Code. You can inspect variables, evaluate expressions in the Debug Console, step through functions, and view call stacks.
1775
+
1776
+ #### Debugging Unit Tests in VS Code
1777
+
1778
+ To step through unit test logic or component contract assertions in VS Code:
1779
+
1780
+ 1. Open your test file (such as `my-counter.test.ts`) and set a breakpoint inside an `it()` or `describe()` block.
1781
+ 2. Select **Debug Unit Tests** from the Run and Debug panel and press `F5`.
1782
+ 3. Vitest runs the test suite under the Node.js debugger and pauses at your breakpoint before assertions complete.
1783
+
1784
+ #### Debugging Client Component Scripts in the Browser
1785
+
1786
+ To debug interactive client component scripts in Google Chrome or Microsoft Edge directly from VS Code:
1787
+
1788
+ 1. Start the dev server using **Debug Dev Server** or `npm run dev`.
1789
+ 2. Select **Launch Chrome** from the Run and Debug panel and press `F5`.
1790
+ 3. VS Code launches a new Chrome window attached to the debugger.
1791
+ 4. Set breakpoints directly in your component `.html` files in VS Code, or open Chrome DevTools (`F12`), press `Cmd + P` (or `Ctrl + P`), and open virtual source files like `src/components/my-counter.html`.
1792
+
1793
+ ### Validating Compiled Output Against the HTML Spec
1794
+
1795
+ Install `parse5` (the reference WHATWG HTML5 parser used by jsdom and Playwright) as a dev dependency and add a Vitest test that walks every `dist/**/*.html` file. Use `onParseError` with `sourceCodeLocationInfo: true` to fail on any spec violation with an actionable `file:line:col [error-code]` message:
1796
+
1797
+ ```ts
1798
+ // src/lib/dist-sanity.test.ts
1799
+ import { describe, it, expect } from 'vitest';
1800
+ import { readdir, readFile } from 'node:fs/promises';
1801
+ import path from 'node:path';
1802
+ import { parse, type ParserError } from 'parse5';
1803
+
1804
+ const DIST_DIR = path.resolve(process.cwd(), 'dist');
1805
+
1806
+ async function walk(dir: string, ext: string): Promise<string[]> {
1807
+ const out: string[] = [];
1808
+ let entries;
1809
+ try { entries = await readdir(dir, { withFileTypes: true }); }
1810
+ catch { return out; }
1811
+ for (const entry of entries) {
1812
+ const full = path.join(dir, entry.name);
1813
+ if (entry.isDirectory()) out.push(...(await walk(full, ext)));
1814
+ else if (entry.isFile() && entry.name.endsWith(ext)) out.push(full);
1815
+ }
1816
+ return out;
1817
+ }
1818
+
1819
+ describe('dist HTML — WHATWG spec compliance', () => {
1820
+ it('every page is free of parse errors', async () => {
1821
+ const files = await walk(DIST_DIR, '.html');
1822
+ const failures: string[] = [];
1823
+ for (const file of files) {
1824
+ const html = await readFile(file, 'utf-8');
1825
+ const errors: ParserError[] = [];
1826
+ parse(html, { sourceCodeLocationInfo: true, onParseError: (e) => errors.push(e) });
1827
+ for (const err of errors) {
1828
+ failures.push(`${path.relative(DIST_DIR, file)}:${err.startLine}:${err.startCol} [${err.code}]`);
1829
+ }
1830
+ }
1831
+ expect(failures, failures.join('\n')).toHaveLength(0);
1832
+ });
1833
+ });
1834
+ ```
1835
+
1836
+ Run after `bascik --build`. Catches corrupted script bodies, mismatched tags, and unreplaced internal tokens that string-based assertions miss. Bascik is a build tool — hold output to the full spec, not browser recovery behavior.
1837
+
1838
+ ### Testing Site Logic in a Bascik Project
1839
+
1840
+ Browser component scripts are IIFE-based and not directly importable. The recommended pattern for testing complex client-side logic:
1841
+
1842
+ 1. **Extract pure functions** (no DOM, no `fetch`) into a sibling TypeScript `.ts` file, such as `search-logic.ts` alongside `docs-search.html`.
1843
+ 2. **Reference the companion directly** with `<script src="search-logic.ts"></script>`. Bascik strips erasable TypeScript before scoping and JavaScript minification, so do not add a build script or `minify.js` hook for type stripping.
1844
+ 3. **Test the module with Vitest**: import the `.ts` file directly in a `*.test.ts` file. No browser or DOM is required for pure function tests.
1845
+
1846
+ **What to test vs. skip:** DOM wiring (adding event listeners, toggling visibility) is low value to test because it depends on the compiled output. Pure data functions (parsing, scoring, formatting) are high value. Extract those into a `.ts` module and test them with Vitest. Configure Vitest in `vite.config.ts`:
1847
+
1848
+ ```ts
1849
+ // vite.config.ts
1850
+ import { defineConfig } from 'vite';
1851
+ export default defineConfig({
1852
+ test: {
1853
+ include: ['src/**/*.test.ts'],
1854
+ },
1855
+ });
1856
+ ```
1857
+
1858
+ **Server scripts** (`data-bascik-server`): test the business logic as pure functions in a `.ts` module. Node 24 runs `.ts` files natively with no separate build or compilation step required. Test the HTTP layer with integration tests that POST to the dev server, not by importing the script.
1859
+
1860
+ ---
1861
+
1862
+ ## 14. CI / CD
1863
+
1864
+ Two GitHub Actions workflows handle all automation.
1865
+
1866
+ **CI** (`.github/workflows/ci.yml`): runs on every push to `main` and every PR, two parallel jobs on Node 24:
1867
+ - `test`: runs `yarn pkg:test:ci` (unit tests with coverage)
1868
+ - `e2e`: builds the package (`yarn pkg:build`), installs Chromium via `playwright install chromium`, then runs `yarn pkg:e2e` (Playwright tests)
1869
+ - Both jobs have `permissions: contents: read`
1870
+
1871
+ **Release** (`.github/workflows/release.yml`): triggers on version tags:
1872
+
1873
+ | Package | Tag | Job guard |
1874
+ |---|---|---|
1875
+ | `@bascik/bascik` | `v*.*.*` | `if: startsWith(github.ref_name, 'v')` |
1876
+ | `create-bascik` | `create-v*.*.*` | `if: startsWith(github.ref_name, 'create-v')` |
1877
+ | `@bascik/adapter-cloudflare` | `adapter-cloudflare-v*.*.*` | `if: startsWith(github.ref_name, 'adapter-cloudflare-v')` |
1878
+ | `@bascik/language-server` | `lsp-v*.*.*` | `if: startsWith(github.ref_name, 'lsp-v')` |
1879
+
1880
+ Each release job: install → test → build → `npm publish --provenance --access public --tag <dist-tag>`. The tag must equal the prefix plus the package version; prereleases such as `1.0.0-rc.1` publish under the `rc` dist-tag instead of `latest`.
1881
+
1882
+ - `--provenance` requires `id-token: write` permission; generates a signed attestation on npmjs.com linking the package to the Actions run.
1883
+ - Both `package.json` files also declare `"publishConfig": { "access": "public" }`.
1884
+ - Requires the `NPM_TOKEN` repository secret (Settings → Secrets and variables → Actions).
1885
+
1886
+ **Tagging a release:**
1887
+ ```sh
1888
+ # @bascik/bascik
1889
+ git tag v1.2.0 && git push origin main --tags
1890
+
1891
+ # create-bascik
1892
+ git tag create-v1.0.3 && git push origin main --tags
1893
+ ```
1894
+
1895
+ `dist/` is never committed to git; the workflow always builds it fresh.
1896
+
1897
+ ---
1898
+
1899
+ ## 15. VS Code Extension
1900
+
1901
+ The `extensions/vscode-bascik/` package provides editor tooling:
1902
+
1903
+ * **Command-click navigation:** click a component tag like `<site-nav>` to jump to `src/components/site-nav/site-nav.html`.
1904
+ * **Inline warnings:** flags CSS patterns Bascik cannot safely scope (standalone attribute selectors, element names inside `:is()`/`:where()`/`:has()`) and JS patterns that won't be rewritten (`.id =` setter, template-literal class names, `style.setProperty('--var', …)`).
1905
+ * **Rules generated from the compatibility matrix:** `docs/scripts/generate-compatibility-rules.ts` reads `docs/content/compatibility.md` and writes the warning rules, so editor diagnostics stay in sync with the documented capability table automatically.
1906
+
1907
+ To install locally: open `extensions/vscode-bascik/` in VS Code and press F5.
1908
+
1909
+ ---
1910
+
1911
+ ## 16. Lighthouse 100s & Performance
1912
+
1913
+ Bascik gives you an enormous head start on Lighthouse scores. Because it outputs vanilla HTML with zero framework runtime, you begin every page with near-perfect scores. Reaching 100 across all four Lighthouse categories is a matter of applying a small, well-known set of HTML patterns.
1914
+
1915
+ ### What Bascik Does
1916
+ * **Zero runtime:** The most impactful thing Bascik does is what it does not add: no framework bundle, no hydration script, and no client-side router. The only JavaScript on any page is what you wrote.
1917
+ * **CSS deduplication:** When a component appears multiple times on a page, Bascik emits a single `<style>` block regardless of instance count.
1918
+ * **HTML minification:** HTML comments are stripped and excess whitespace is collapsed in every built page. Content inside `<pre>` blocks is left intact.
1919
+ * **Script minification:** `minify.js` is `true` by default, stripping comments and whitespace.
1920
+ * **Inline styles:** Set `assets.inlineStyles` in `bascik.config.ts` to inject a stylesheet directly into `<head>`, eliminating the render-blocking HTTP request.
1921
+
1922
+ ### Performance Patterns for Developers
1923
+ * **Responsive Images (`srcset`):** Avoid sending oversized images. Use `srcset` density or width descriptors, and always include explicit `width` and `height` attributes to prevent Cumulative Layout Shift (CLS).
1924
+ * **Lazy Loading (`loading="lazy"`):** Add `loading="lazy"` to below-the-fold images and iframes. Do not lazy-load above-the-fold hero images.
1925
+ * **Preloading (`<link rel="preload">`):** Preload critical first-render assets, such as your hero image, critical web fonts, or early stylesheets.
1926
+ * **Prefetching (`<link rel="prefetch">`):** Fetch resources or future pages during idle time if a navigation is highly likely.
1927
+ * **Preconnect / DNS Prefetch:** For critical third-party domains, resolve DNS and negotiate connections early using `dns-prefetch` and `preconnect`.
1928
+ * **Async and Deferred Scripts:** Use `defer` for scripts that need the DOM ready, and `async` for fully independent scripts like analytics.
1929
+
1930
+ ---
1931
+
1932
+ ## 17. Switch to Bascik
1933
+
1934
+ Detailed per-framework migration guides live at `/switch/*`. Key patterns that apply across all migrations:
1935
+
1936
+ - **Component files:** Rename to hyphenated `.html` files in `src/components/<name>/`. Remove framework-specific syntax (`<template>`, JSX, `.astro` frontmatter fences). The HTML file is just the component markup.
1937
+ - **Scoped styles:** Delete framework scoped-style blocks (`<style scoped>`, `.module.css`). Create a paired `.css` file alongside the component HTML. Class names stay the same; Bascik scopes them at build time.
1938
+ - **Slots:** `<slot />` / `children` → `data-bascik-slot` (no value) for default, `data-bascik-slot="name"` for named slots.
1939
+ - **Props:** `defineProps` / component props → `data-bascik-prop-*` attributes (text only).
1940
+ - **Reactive state:** `ref`, `useState`, etc. → plain `<script>` with vanilla JS. Bascik scopes `id` values so multiple instances stay independent.
1941
+ - **Routing:** Client-side router → one `.html` file per URL in `src/pages/`. No dynamic segments; generate static files for parameterized routes.
1942
+ - **Build-time data:** `onMounted` / `getStaticProps` / frontmatter → `<script data-bascik-build>` (Node.js ESM, stdout injected).
1943
+
1944
+ ### From Svelte
1945
+
1946
+ Full guide: `/switch/from-svelte`. Key Svelte-specific mappings:
1947
+
1948
+ | Svelte | Bascik |
1949
+ |--------|--------|
1950
+ | `.svelte` file (`<script>` + markup + `<style>`) | `.html` + `.css` paired files |
1951
+ | `children` snippet / `{@render children()}` (Svelte 5) | `data-bascik-slot` (no value) |
1952
+ | `<slot />` (Svelte 4, deprecated) | `data-bascik-slot` (no value) |
1953
+ | Named snippet prop / `{#snippet header()}` (Svelte 5) | `data-bascik-slot="x"` |
1954
+ | `<slot name="x" />` (Svelte 4, deprecated) | `data-bascik-slot="x"` |
1955
+ | `$props()` / `export let` | `data-bascik-prop-*` attributes |
1956
+ | `$state()` / reactive variables | Vanilla JS `<script>` |
1957
+ | `{#if}` / `{#each}` | Static HTML or `<script data-bascik-build>` |
1958
+ | SvelteKit file routing (`+page.svelte`) | One `.html` per route in `src/pages/` |
1959
+ | `onMount` data fetch | `<script data-bascik-build>` (build time) or `<script>` (runtime) |
1960
+
1961
+ ### From Vue
1962
+
1963
+ Full guide: `/switch/from-vue`. Key Vue-specific mappings:
1964
+
1965
+ | Vue | Bascik |
1966
+ |-----|--------|
1967
+ | `<template>` + `<style scoped>` | `.html` + `.css` paired files |
1968
+ | `<slot />` | `data-bascik-slot` (no value) |
1969
+ | `<slot name="x" />` | `data-bascik-slot="x"` |
1970
+ | `defineProps` | `data-bascik-prop-*` attributes |
1971
+ | `ref` / `reactive` | Vanilla JS `<script>` |
1972
+ | `v-if` / `v-show` | CSS `display:none` or JS toggle |
1973
+ | `vue-router` | One `.html` per route in `src/pages/` |
1974
+ | `onMounted` data fetch | `<script data-bascik-build>` |
1975
+
1976
+ ### Migrating Existing Sitemap & Robots Files
1977
+
1978
+ When migrating an existing website to Bascik, follow this procedure for existing `sitemap.xml` and `robots.txt` files:
1979
+
1980
+ 1. **Open and inspect the existing files:** Read the existing `sitemap.xml` and `robots.txt`.
1981
+ 2. **Determine if custom directives exist:** Decide whether they contain anything Bascik would not generate automatically: hand-curated URLs, `Disallow` rules for paths that still exist, a `Sitemap:` pointer to an external index, or crawl directives specific to a single bot.
1982
+ 3. **If they contain nothing special, delete them:** Delete the old files and let Bascik generate fresh ones automatically at build time. Do not copy them into `src/pages/`, and do not set `generate.sitemap: false` or `generate.robots: false` in `bascik.config.ts`.
1983
+ 4. **If they contain custom directives, keep the authored file:** Keep the authored file in `src/pages/`, turn off the matching generator in `bascik.config.ts` (`generate: { sitemap: false }` or `generate: { robots: false }`), and add a comment in the config explaining why. Authored files in `src/pages/` take precedence over generated ones; if generation remains enabled, Bascik preserves the authored file and emits a warning. To exclude individual redirect-only pages from `sitemap.xml`, use `<meta name="bascik-sitemap" content="exclude">` in the page template.
1984
+
1985
+ **Failure Mode Warning:** Copying old sitemap and robots files into `src/pages/` while disabling Bascik's built-in generation looks superficially correct, but silently freezes the sitemap at the old site's URL list forever as new pages are created or routes change.
1986
+
1987
+ ---
1988
+
1989
+ ## 18. Key Constraints & Rules for AI Code Generation (MUST FOLLOW)
1990
+
1991
+ When generating code, pages, or components for a Bascik project, the following conventions are strictly enforced:
1992
+
1993
+ 1. **Hyphenated Custom Tags:** Component tag names must be hyphenated (e.g. `my-nav`, `site-header`). Single-word tags are not valid custom element names.
1994
+ 2. **Scoping Rules:** CSS scoping only applies to paired `.css` files and inline `<style>` tags inside component HTML.
1995
+ 3. **Selector Rewrites:** CSS `#id {}` hash selectors are converted to component-scoped class selectors; the class is automatically injected onto the matching element. The `[id]` attribute-selector form is stripped.
1996
+ 4. **Script Selectors:** Use `id` and `class` selectors in JS that exactly match the attributes in the component HTML, Bascik rewrites them at build time.
1997
+ 5. **DOM Traversal:** For compound DOM queries, query by a single scoped `id` first, then traverse from the returned element.
1998
+ 6. **Dynamic Toggles:** Use `data-` attributes for runtime state that changes via JavaScript (e.g. `data-state="open"`). Scoped class names are assigned at build time and cannot be reliably looked up by JS string manipulation *unless* you utilize a scoping helper (Section 5).
1999
+ 7. **Text Props:** Props accept text only. For rich HTML content, use slots.
2000
+ 8. **Script Modules:** `<script type="module">` scripts are not wrapped in an IIFE, but their selectors are still rewritten.
2001
+ 9. **Non-JS Script Types:** Script tags with any `type` other than `text/javascript` (e.g. `type="application/json"`) are left completely untouched: no IIFE wrapping, no selector rewriting.
2002
+ 10. **Literal Tag Text Is Safe:** Component tag text inside `<script>`, `<style>`, or `<textarea>` content (e.g. `<my-card>` in a JSON-LD string or code example) is treated as text and never resolved into a component.
2003
+ 11. **HTML and CSS First:** Always try to use HTML and CSS before resorting to JavaScript. If a feature or layout can be implemented cleanly and without causing issues using only HTML and CSS, do it in HTML and CSS.
2004
+
2005
+ ---
2006
+
2007
+ ## 19. FAQ
2008
+
2009
+ **How do you pronounce Bascik? Where does the name come from?** Just like "basic." The idea is basic, the implementation is basic in theory, and the usage is basic. The spelling comes from the author's maternal grandmother's maiden name, so it's unique and means something personal.
2010
+
2011
+ **Isn't BASIC already a programming language?** Yes, the classic one from the 1960s. Used it on an old Texas Instruments calculator. Enough time has passed; it can take on new meaning.
2012
+
2013
+ **Who made Bascik?** Collin Thomas.
2014
+
2015
+ **Why was Bascik created?** To build the fastest possible websites and dashboards with components. It uses only foundational languages (HTML, CSS, and JavaScript) to let you leverage what you already know, without abstraction layers or runtime JavaScript bottlenecks.
2016
+
2017
+ **What happens if a component file is named after a native HTML element (e.g. `nav.html`)?**
2018
+ Bascik logs a warning and still loads the component, but it will replace every occurrence of that element in pages with the component content, almost certainly breaking the site. Always use a hyphenated component name (e.g. `site-nav.html`).
2019
+
2020
+ **What happens with uppercase letters in a component filename (e.g. `My-Card.html`)?**
2021
+ Component names are normalized to lowercase at load time. `My-Card.html` registers as `my-card` and is referenced as `<my-card>`. If two files differ only in case, the last one loaded wins. Convention: always use lowercase, hyphenated filenames.
2022
+
2023
+ **What happens if I reference a component that doesn't exist?**
2024
+ During a build (`bascik --build`), Bascik emits a warning naming the unresolved tag, and the tag ships to the HTML output unchanged without failing the build. Similarly, `bascik --check` treats an unknown hyphenated tag as a warning (exit code 0), allowing third-party custom elements (such as `<model-viewer>` or `<ion-icon>`) to pass without failing the check. Pass `--strict` if you want warnings to fail the check.
2025
+