Reference
Web Standards & Scoping Compatibility
This page documents Bascik's support matrix against authoritative W3C Recommendations, WHATWG Living Standards, IETF RFCs, and ECMA-262 specifications, cross-referenced with MDN Web Docs documentation and Baseline browser compatibility tiers.
Legend
- Supported and tested
- Partially supported (see notes)
- Intentionally unsupported (see notes)
- Not yet supported
Web Standards Authority Matrix
Bascik operates as a zero-runtime build-time compiler and HTTP delivery server. All transformations, protocols, and APIs are designed to comply with official web specifications:
| Domain / Subsystem | Authoritative Standard | MDN / Baseline Status | Summary |
|---|---|---|---|
| HTML Elements & Composition | WHATWG HTML §3 / §4 | Baseline: Widely Available | Custom element naming, valid data-* attributes, slot fallback, raw-text masking |
| HTML Minification | CSS Text 3 / WHATWG HTML | Baseline: Widely Available | Safe phrasing content space preservation (INLINE_TAGS) and block collapsing |
| CSS Selectors & Nesting | W3C Selectors 4 / CSS Nesting | Baseline: Widely Available | Class scoping, 2023 relaxed nesting, pseudo-classes, combinators |
| Modern CSS At-Rules | W3C CSS Module Level 3 / 5 | Baseline: Newly / Widely Available | @layer, @container, @keyframes, @property, @counter-style, @starting-style, @position-try |
| JavaScript DOM Scoping | WHATWG DOM §4 / WebIDL | Baseline: Widely Available | Compile-time query rewriting for standard DOM element methods |
| Script Execution Isolation | ECMA-262 / WHATWG HTML §7.1 | Baseline: Widely Available | IIFE encapsulation for classic scripts, native module isolation for type="module" |
| HTTP/1.1 & HTTP/2 Protocols | IETF RFC 9112 / RFC 9113 | Baseline: Widely Available | ALPN negotiation, HTTP/2 multiplexing, pseudo-headers, stream lifecycle |
| HTTP Semantics & Caching | IETF RFC 9110 / RFC 6797 | Baseline: Widely Available | Strong/weak ETags, conditional 304 responses, Vary, Brotli, HSTS header |
| MIME Types | IETF RFC 9239 / IANA | Baseline: Widely Available | Current standard text/javascript; charset=utf-8 media types |
| Development Static Assets | Node.js filesystem | N/A | Optional relative symlinks for unchanged page assets; builds always emit regular files |
| Live Reload & Events | WHATWG EventSource | Baseline: Widely Available | Server-Sent Events (SSE) /bascik-live-reload endpoint |
| Sitemaps & Robots | Sitemaps 0.9 / IETF RFC 9309 | Standard Protocols | Canonical XML sitemap and robots exclusion directives |
| Content Security Policy | W3C CSP Level 3 | Baseline: Widely Available | SHA-256 hash manifests (dist/.bascik/csp-hashes.json) for inline scripts and styles; per-request nonces intentionally unsupported for static caching |
Component Template Structure & HTML Standards
Bascik supports flexible HTML, CSS, and JavaScript structures inside .html component files without requiring runtime frameworks.
| Capability | Standard / Spec | Status | Notes |
|---|---|---|---|
| Hyphenated custom element names | WHATWG HTML §4.13.1.2 | Component tags with hyphens (e.g. <my-button>, <site-nav>) follow the WHATWG custom element standard and prevent collisions with native tags. | |
| Unique component filenames | WHATWG HTML §4.13.1.2 | Component names are derived strictly from filenames. Subfolders are supported for organization, but duplicate filenames producing the same tag name error at build time. | |
| Single-word component filenames | WHATWG HTML §4.13.1.2 | Single-word component names (e.g. card.html -> <card></card>) compile for backward compatibility, but Bascik's CLI compiler and VS Code extension issue warnings recommending a hyphenated name (e.g. my-card.html). | |
| Native element shadowing guard | WHATWG HTML §4 | Bascik maintains a set of 115 native HTML elements and issues a build-time warning if a component filename shadows a native tag (e.g. header.html or dialog.html). | |
| Exact component tag matching | WHATWG HTML §13.2.5 | A component name matches only the complete tag name. A card component never claims a longer hyphenated tag such as <card-header>. | |
Custom data-* attributes | WHATWG HTML §3.2.6.6 | Internal directives (data-bascik-prop-*, data-bascik-attr-*, data-bascik-preserve, data-bascik-slot, data-bascik-build, data-bascik-routes, data-bascik-server, data-bascik-stream) strictly conform to XML NCName lower-case naming syntax. Directives match only as whole attribute names, so an unrelated attribute such as data-bascik-server-foo or data-bascik-build-x is left alone and the tag is treated as an ordinary client script. | |
| URL-safe dynamic route parameters | WHATWG URL | Dynamic route parameters reject illegal filename and URL characters (#, %, &, ', +, spaces, leading dots, Windows device names) to ensure predictable static serving. | |
| Self-closing custom tags | WHATWG HTML §13.1.2 | In HTML source code, custom tags can use self-closing syntax with or without a space (<my-comp /> or <my-comp/>). Both forms balance correctly when nested inside a paired instance and expand at build time into standard HTML. | |
| Multiple top-level HTML elements | WHATWG HTML | Supported naturally without requiring single wrapper elements or fragment tags. All root elements are inserted in document order. Inherited usage attributes merge onto the first content element after leading text, <link>, or <meta> nodes. Root opening tags are parsed quote-aware, so > inside an attribute value remains intact. | |
Multiple <style> blocks | WHATWG HTML §4.2.6 | Extracted and combined with any companion .css file before scoping and deduplication. Note: Using multiple <style> tags in a single component file is supported, but using a single stylesheet pattern per component is recommended for maintainability. | |
| Deterministic instance IDs | WHATWG HTML | Instance IDs for scoped id and name attributes are derived deterministically from (page path, component name, ordinal index), ensuring byte-identical builds across repeated runs. | |
Multiple <script> blocks | WHATWG HTML §4.12 | Client <script> blocks are each wrapped in an independent IIFE. Recommended for clean, maintainable code when separating unrelated logic within a component. Build (data-bascik-build), server (data-bascik-server), stream (data-bascik-stream), routes (data-bascik-routes), and data scripts (e.g. type="application/ld+json") are processed according to their script type. | |
| Raw-text comments and nested scripts | WHATWG HTML §4.12 | Comments and comment-like text inside <pre>, <textarea>, scripts, and <style> are shielded before HTML comments are stripped. CSS CDO/CDC tokens (<!-- ... -->) inside a <style> element are preserved so authored styles survive HTML minification. Scripts nested inside containers remain in place. | |
| Raw-text document closing tags | WHATWG HTML §13.2.5 | Literal </body> text inside <textarea> and </head> text inside <script> do not terminate document extraction or duplicate the remaining page during reassembly. | |
| Global class passthrough | WHATWG HTML | Class names in component HTML that are not declared in the component's stylesheet (such as global utility classes like skip-link, flex, hidden) pass through as unscoped global classes so global stylesheets continue to match them. A component with no stylesheet, an empty one, or one containing only comments declares no classes, so none of its classes are scoped. | |
| Class attribute whitespace | WHATWG HTML §2.3.7 | Class tokens are normalized across spaces, tabs, and newlines. Leading, trailing, and repeated whitespace never creates an empty scoped token. | |
| Quoted, unquoted, and whitespace-spaced attributes | WHATWG HTML §13.1.2.3 | Double-quoted (class="card"), single-quoted (class='card'), whitespace-spaced (class = "card"), and unquoted (class=card, id=hero, for=email) attributes are supported across HTML attribute scoping, element/ID class injection, and ID reference rewriting. | |
| Slot fallback semantics | WHATWG DOM §4 | Default and named slots preserve their inner placeholder markup when no replacement content is passed from the parent template. | |
| Prop value escaping | WHATWG HTML §13.2.5 | Prop values are HTML-escaped on injection. Entity-encoded quotes round-trip as text; slots are the raw-markup path. | |
| Prop-to-attribute binding | WHATWG HTML §3.2.6.6 | data-bascik-attr-{attribute}="{propName}" binds a supplied prop to plain and hyphenated attributes, then removes the directive. Missing props add nothing; existing targets warn and are replaced. Bound id, name, and class values use normal scoping rules. | |
| Default slot forwarding | WHATWG HTML §4.12.4 | A valueless data-bascik-slot marker written between a nested component's usage tags receives the outer component's default slot content, or its own fallback when the outer tag is empty. Named wrappers stay with the nested component. | |
| Nested prop boundary | WHATWG HTML §3.2.6.6 | Props are extracted only from a component's opening usage tag, so declarations on nested components inside slot content never leak into the parent. | |
| Tag-level preserve | WHATWG HTML §13.1.2 | scoping.preserve keeps each configured tag's id, name, and class attributes, contents, and descendants unscoped through shared restorable shielding. Entries accept * wildcard patterns (for example vendor-*) with the same semantics as exact names. | |
| External custom elements | WHATWG HTML §4.13 | components.external lists hyphenated tags owned by a custom element or library (exact names or * wildcards, case-insensitive). Matching tags are omitted from the Unresolved component tag build warning and the --check unmatched-tag findings. A real component of the same name still expands, and attribute scoping is unaffected. | |
| Element-level preserve | WHATWG HTML §3.2.6.6 | data-bascik-preserve applies to one subtree. A bare directive, or an empty value (data-bascik-preserve=""), preserves id, name, and class; a space-separated value preserves only listed attributes. The directive is removed from output. | |
| Internal raw-text mask | WHATWG HTML §13.1.2 | Internal scans use a hardcoded same-length discard mask for scripts, styles, textareas, and comments. It is not configurable and is distinct from author-facing preservation. | |
| Inline phrasing whitespace preservation | CSS Text Level 3 | HTML minification preserves single spaces between inline phrasing elements (INLINE_TAGS: span, a, strong, em, code, etc.) while safely collapsing block-level whitespace. Only spaces, tabs, and line breaks collapse: U+00A0 and other Unicode spaces, attribute values, and <pre>, <textarea>, <script>, and <style> content are kept. CSS is not read, so white-space: pre on another element is not detected. | |
<meta> tag preservation | WHATWG HTML §4.2.5 | Standard metadata attributes on <meta> tags (e.g. name="viewport", name="description") are shielded from attribute scoping. | |
Exclusive <details name> groups | WHATWG HTML §4.11.1 | name is scoped per instance like any other name, so the <details> elements of one component instance form one group and other instances stay independent. Add data-bascik-preserve="name" to group across instances or with page markup. | |
| Declarative shadow DOM | WHATWG HTML §4.12.3 | <template shadowrootmode="open"> in a component passes through as markup. Bascik does not scope inside a shadow root, which isolates its own styles. | |
| Custom elements defined in script | WHATWG HTML §4.13 | Real custom elements from a library or your own customElements.define work alongside Bascik components. List their tags in components.external so they are not reported as unresolved components. Their shadow roots are not scoped by Bascik. |
ID References
When an id declaration is scoped, Bascik rewrites references that resolve to that declaration in the same component. Unresolved references remain byte-identical so components can still target literal page-level IDs.
| Reference | Status | Notes |
|---|---|---|
for on <label> | Rewritten as one ID so label activation continues to focus the scoped control. | |
form, list, popovertarget, commandfor | Each single-ID reference is rewritten when its target is declared locally. | |
aria-activedescendant, aria-errormessage | Single-ID ARIA references resolve locally. | |
aria-labelledby, aria-describedby, aria-controls, aria-owns, aria-flowto, aria-details | Space-separated tokens resolve independently; nonlocal tokens remain unchanged. | |
itemref | Space-separated microdata item IDs resolve independently. | |
headers on <td> and <th> | Space-separated header IDs resolve independently. | |
for on <output> | Treated as a space-separated ID list, unlike the single-ID <label for>. | |
Fragment links on <a> and <area> | Fragment-only values such as href="#section" resolve locally. Bare hashes and other-document URLs remain unchanged. | |
SVG <use href> and xlink:href | Fragment-only references to local SVG IDs are rewritten. | |
| SVG presentation attributes | fill, stroke, mask, clip-path, filter, marker-start, marker-mid, and marker-end rewrite local url(#id) fragments. | |
Inline style attributes | Local url(#id) fragments are rewritten. Component <style> blocks and stylesheets are covered separately by CSS scoping. | |
CSS url(#id) fragments | Local fragments in component stylesheets and <style> blocks are rewritten for properties including fill, stroke, clip-path, mask, filter, and marker properties. Components with resolvable fragments automatically emit per-instance CSS. New in 1.0 hardening: url-like text inside string literals (e.g. content: "url(#local)") is preserved byte-for-byte, and only genuine url(...) fragments are rewritten. | |
| CSS cross-document fragments | Values such as url(other.svg#icon) are deliberately untouched because the fragment belongs to another document. Real, remote, and data URLs also remain unchanged. | |
usemap on <img> | Resolves against a local <map name>, not an ID, and follows scoping.attributes.name. | |
| Cross-component ID references | ✗ | IDs are scoped per instance, so references cannot resolve safely across component boundaries at build time. They remain unchanged. |
| Preserved subtrees | References and declarations inside scoping.preserve tags or data-bascik-preserve subtrees remain literal. A link inside a preserved subtree is never rewritten, even to a scoped target elsewhere in the component, so a link and its target must be both preserved or both scoped. A scoped link to a preserved target keeps working. |
Server & API Features
| Capability | Status | Notes |
|---|---|---|
| WHATWG Request / Response handlers | Standard handler signature (request: Request, context) => Response | Promise<Response> in src/api/*.ts. | |
| Supported HTTP methods | GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. | |
Automatic Allow headers | 405 responses automatically list exported methods in Allow. | |
Derived HEAD | HEAD derives automatically from GET when unexported, returning headers with empty body. | |
Auto OPTIONS | Auto-responds 204 with Allow header. | |
Multiple Set-Cookie headers | Preserved via Headers.getSetCookie() without comma-flattening. | |
| Streaming responses | WHATWG ReadableStream bodies stream directly to clients. | |
| Dynamic API route parameters | Extracted from [param] path segments into context.params. | |
Catch-all API routes ([...path].ts) | Required catch-all: one or more remaining segments as a decoded string[]. Static beats [param] beats catch-all. Unsafe captures return 400; single-dot and hidden segments are blocked before matching with 404. API routes only; same behavior on --server, HTTP/2, and the Cloudflare adapter. | |
| Client IP resolution | Forwarded client IP provided via context.remoteIp, respecting http.trustProxy. | |
| Request body streaming | Standard WHATWG request.body stream with duplex: 'half' support. | |
| Streaming body size limit | http.maxBodySize counts bytes on the fly (defaults to 1 MB) and aborts with 413 without buffering. | |
| Handler execution timeout | http.apiTimeout triggers 504 and cooperative AbortSignal for async work. | |
| Error information protection | Thrown handler errors return generic 500 without leaking stack traces or source paths. | |
| Header injection protection | CR and LF in handler-supplied header values are rejected. | |
| Directive scripts printed by a build script | Unsupported | A data-bascik-build, data-bascik-server, data-bascik-stream, or data-bascik-routes script in build script output is removed with a warning, so HTML from a CMS or an API cannot run code at build or request time. Ordinary scripts and handlers in printed HTML are not removed; sanitize it. |
Large dynamic route data | Any size the routes script prints. Over 32 KB, BASCIK_ROUTE is delivered through a temporary file, avoiding the OS environment limit (spawn E2BIG). | |
Middleware chains (_middleware.ts) | Not supported by design. Handlers compose plain functions directly. | |
| Built-in schema validation | Not supported by design. Validate payloads using standard libraries (e.g. Zod, Valibot). | |
| Automatic response compression on API routes | Deliberately omitted to prevent BREACH attacks on sensitive dynamic data. Handlers set Content-Encoding manually if needed. | |
| Automatic CORS headers | Not injected by default. Handlers return explicit Access-Control-* headers when required. |
CSS Scoping
CSS scoping applies to .css files paired with a component's HTML file. Place the .css file in the same directory as the component and give it the same base name.
Selectors
| Pattern | Example | Status | Notes |
|---|---|---|---|
| Class selector | .foo {} | Scoped with unique instance prefix | |
| Descendant with class | .foo .bar {} | All class names in selector scoped | |
| Multi-class | .foo.bar {} | Both class names scoped | |
| Standalone element selector | p {} | Converted to a generated class and injected only on elements with the exact tag name in component HTML, including indented selectors inside at-rules such as @media and inline <style> blocks. A selector for header does not match a custom element such as <header-link>, and hyphenated type selectors such as code-block are left unchanged rather than split at the hyphen. Note on specificity: converting element selectors (0, 0, 1) to class selectors (0, 1, 0) is an intentional zero-runtime build-time transformation that guarantees robust class isolation across components without requiring runtime shadow DOM. | |
| Element pseudo-class | p:hover {} | Element converted to class; pseudo-class preserved: .bascik__...__el__p:hover {} | |
| Element pseudo-element | p::before {} | Element converted to class; pseudo-element preserved: .bascik__...__el__p::before {} | |
@keyframes name | @keyframes spin {} | Name scoped; animation: and animation-name: references updated to match. Only whole names match, so a never rewrites part of a-b or of another scoped name. | |
@keyframes stops | 40% {}, 0%, 100% {}, from {} | Stop selectors are left as written, including percentage lists split across lines, so the browser keeps every stop. | |
@media query | @media (max-width: 600px) {} | Media condition untouched; class names inside scoped normally | |
@supports | @supports (display: grid) { .foo {} } | Class names inside @supports blocks are scoped normally. | |
@layer | @layer base { .foo {} } | Layer names are scoped in declaration blocks and single-name or comma-list ordering statements, including leading-hyphen names such as --utils. Only whole names match, so a never rewrites part of a-b or of another scoped name. | |
@container | @container sidebar (min-width: …) {} | Container names declared via container-name: or the container: shorthand are scoped; @container name (…) queries updated to match. Only whole names match, so a never rewrites part of a-b or of another scoped name. Unnamed queries untouched. | |
| CSS custom properties | --brand: #d3ff8d / var(--brand) | Declarations and all var() references in the same file scoped together. var(--prop, fallback) is fully supported, the fallback value is preserved and the property name is scoped. Only whole property names match, so --a never rewrites part of --a-b or of another scoped name. | |
Multiple animation: values | animation: a 1s, b 2s | Both keyframe name references are scoped when an animation: shorthand lists more than one animation. | |
| Child / sibling combinators | .a > .b, .a + .b, .a ~ .b | All class names on both sides of >, +, and ~ are scoped. An element after a combinator (.a > p, .a>p) is scoped with or without surrounding spaces. | |
:is() / :where() / :has() with class args | :is(.foo, .bar) {} | Class names inside :is(), :where(), and :has() are scoped normally. Element names inside these functions are not converted (see below). | |
Inline <style> in component HTML | <style>.foo {}</style> | Full CSS scoping pipeline applied to inline <style> blocks. Extracted from component HTML into component CSS, deduplicated across component instances, and injected into page <head>. | |
CSS #id selector | #btn {} | Converted to a component-scoped class selector (.bascik__comp__id__btn {}) using a context-aware lookahead that correctly distinguishes selector position from hex color values. The generated class is injected onto the HTML element across development and production builds with identifier minification, inside @media and paired stylesheets. Specificity drops from (0,1,0,0) to (0,0,1,0). String literals, comments, and attribute-selector values (a[href="#tab"]) are never treated as ID selectors; their contents are preserved byte-for-byte. | |
[id] / [id="…"] attribute selector | [id] {} | Stripped at compile time, including inside @media and @supports blocks or when paired in a comma-separated selector list (preserving sibling selectors). Attribute-selector forms cannot be scoped without DOM wrapping. | |
| Attribute selector | [data-foo="bar"] {} | Passed through untouched, not scoped. Will apply globally. Avoid in component CSS or use a class-based selector alongside it. Attribute-selector values containing # (e.g. a[href="#tab"]) or . in quoted and unquoted forms (a[href$=.pdf]) remain literal and are not rewritten as selector syntax. | |
| Compound / descendant element selectors | div p {}, .card p {}, #panel p {}, .list > li {} | Every element name in the chain is converted to a component-scoped class and injected onto the matching elements in the component's own template, whether anchored by an element, a class, :is(.a, .b) / :where(.a, .b), or a #id selector. Descendant, child (>), adjacent (+), and general sibling (~) combinators work, as do longer chains with pseudo-classes and the universal selector (nav a, #panel > h3 + p, p + p, ul li *, ul li:first-child img, ul a:hover img). Scoped in dev and production, including with identifier minification. The rules never match page markup or a child component's root element; see Styling a Child Component from Its Parent. Element names inside :is(), :where(), and :has() are a separate case, listed below. | |
| Parent element selector vs. child component root | nav a {} with <nav-link> inside <nav> | A parent's element selector matches only elements written in the parent's own template, never a child component's root element. Components are isolated by design. To style a child's root from the parent, put a class that the parent's CSS defines on the child's usage tag (<nav-link class="item">); Bascik scopes it to the parent and merges it onto the child's root. See Attribute Inheritance. | |
| Element selector vs. elements a script creates | td {} with document.createElement('td') | An element selector is scoped by adding a generated class to the elements written in the component's template, including the content of a <template>. An element a script creates has no such class, so the rule never matches it, with no warning. Clone a <template> that the component contains, or give the element a class the stylesheet defines and set it with classList.add or className. See From Vue. | |
| Comma-separated element selector list | h1, h2 {} | All elements in a comma list are converted, both multi-line (each at column 0) and same-line (h1, h2 {}). A ) stop in the lookahead prevents false positives inside :is(), :where(), :has(). | |
| Cross-boundary root element selectors | html[data-theme="dark"] .foo {} | html, body, and head are excluded from element-to-class conversion so cross-boundary selectors compile with the root element name intact. html[data-theme="light"] .component-class {} becomes html[data-theme="light"] .bascik__comp__class {} and correctly matches the component element when the document root carries a theme or state attribute. | |
:is() / :where() / :has() with element names | :is(p, h2) {} | Element names inside these functions are not converted. Class equivalents work fine: :is(.foo, .bar) {}. Anchored on a scoped class and a descendant combinator (.markdown :is(p, ul, ol)), the unconverted names match every descendant of that element, slotted content included, which is the usual intent for rendered Markdown, so css-is-element-names does not flag that form. | |
| Element selector vs. slotted content | .prose h2 {} with <h2> passed through a slot | .prose h2 becomes .prose .<scoped h2>, and the scoped class is added only to h2 elements in the component's own template. Content passed through a slot (including rendered Markdown a build script prints inside the usage tag) keeps plain tags, so the rule does not match it. Write .prose :is(h2) {}: Bascik leaves element names inside :is() as written, the wrapper class stays scoped, and specificity is the same as .prose h2. See Markdown. | |
| CSS nesting, class selectors | & .child {} | Class selectors inside nesting are scoped normally. | |
| CSS nesting, element selectors | & p {}, & > h2 {}, > h2 {}, p {} | Fully supported for explicit nesting (& p {}, & > h2 {}, &>h2 {}, & + li {}, & ~ span {}) and 2023 relaxed direct nesting (> h2 {}, + li {}, ~ span {}, direct nested element selectors without &). | |
@scope (native) | @scope (.foo) { .bar {} } | Class names in both the @scope (.selector) argument and the optional to (.selector) clause are scoped normally (handled by the global class-scoping pass). Class names inside the @scope block are also scoped. Element names in @scope arguments and indented element selectors inside the block follow the same rules as other at-rules. | |
:nth-child(An+B of .selector) | :nth-child(2n+1 of .item) {} | Class names in the of <selector> argument are scoped by the global class-scoping pass (the same (?<=\.) regex that handles :is(), :where(), and :has() class arguments). Works for :nth-child and :nth-last-child. |
Other CSS Features
| Feature | Status | Notes |
|---|---|---|
| CSS deduplication | Component CSS is normally injected once per type. Components containing resolvable url(#id) references emit per-instance CSS so each stylesheet targets that instance's scoped ID. | |
minify.identifiers | In production builds, verbose names like bascik__site-nav__a1b2c3__logo are hashed to short strings (e.g. ba1b2c3d) for name compression. | |
minify.css | Whitespace in the compiled <style> block is collapsed. A space before : that is a descendant combinator (.a :hover, .a :is(h2)) is kept; only declaration colons are tightened. | |
minify.html | Minifies HTML output. Style (and pre/textarea/script) raw text is shielded first, so CSS CDO/CDC tokens and comment-like strings inside those elements survive while ordinary document comments are stripped. | |
| Comments | Ordinary HTML comments (<!-- ... -->) are stripped from the page head and body. Comment-like content inside <pre>, <textarea>, <style>, and scripts is preserved. The development server and minify.html: false keep comments; a component tag named inside a comment, in a page or a component template, stays text and is never expanded. | |
| SVG elements in component HTML | class attributes on SVG elements (<svg>, <circle>, <path>, <rect>, etc.) are scoped with the same pipeline as HTML elements. JS classList and querySelector calls targeting SVG children are rewritten. | |
@font-face | Passed through untouched, the font-family name is not scoped. Both the declaration and all usage sites remain unmodified, so the font resolves correctly within the page. Declare @font-face in a shared global stylesheet rather than a component .css file to avoid duplicate declarations when a component is used multiple times. | |
@import | Local file imports (@import "./file.css") are inlined recursively at build time and scoped to the component. Remote URLs (@import "https://...") are preserved and hoisted to the top of the compiled stylesheet per W3C CSS spec requirements. A missing file becomes a not found comment and the rules after it are kept. @import text inside comments and strings is ignored. In dev, editing an imported file rebuilds the pages of every component that imports it. | |
@property | @property --name { } declaration names are scoped. Any matching --name: element declarations and var(--name) references in the same component file are scoped to match. | |
@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 are handled. | |
@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 component file are updated to match. Only whole names match, so a never rewrites part of a-b or of another scoped name. | |
view-transition-name | view-transition-name: name values are scoped to the component. Matching ::view-transition-old(name), ::view-transition-new(name), ::view-transition-group(name), and ::view-transition-image-pair(name) pseudo-element references in the same file are updated to match. Only whole names match, and the keywords none and auto are not scoped. | |
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 whole anchor names declared in the component's own CSS are scoped, and external anchor references are left untouched. |
Names Shared by Every Instance
Container names, layer names, anchor names, view transition names, keyframes, and custom properties declared in component CSS are scoped per component type: every instance of a component shares one name, and no other component can see it. That keeps components apart, but it changes how these features combine.
| Pattern | Example | Status | Notes |
|---|---|---|---|
| Child component querying a parent's named container | @container sidebar (…) in a child of side-bar | The parent's sidebar is renamed to the parent, so the child's query never matches. Use an unnamed query, @container (min-width: 30rem), which targets the nearest container, or declare the named container in a global stylesheet. | |
| Global layer order including component layers | @layer reset, components; in a page with @layer components {} in a component | The component's layer becomes its own private layer and is not placed by the global statement. Put base styles in layers in the global stylesheet and leave component CSS unlayered; unlayered rules win over layered ones. | |
anchor-name with several instances on a page | .trigger { anchor-name: --menu; } | Every instance declares the same scoped name, so each positioned element anchors to the last one. Use the implicit anchor of popover and popovertarget, or set the name per instance from the script: el.style.setProperty('anchor-name', `--${el.id}`). | |
view-transition-name with several instances on a page | .card { view-transition-name: card; } | Two elements with one name abort the transition. Use it on components that appear once per page, or name instances from the script: el.style.setProperty('view-transition-name', el.id). The same component on two pages shares its name, so it morphs across a navigation. | |
| Site-wide tokens declared in a component | :root { --brand: … } in component CSS | The property is renamed to the component, so no other component sees it. Declare tokens in a global stylesheet and only read them in components. |
JavaScript Scoping
Bascik rewrites DOM selector references inside component <script> tags to match scoped attribute values. All rewrites happen at build time with no runtime is added.
IIFE Isolation
| Pattern | Status | Notes |
|---|---|---|
<script> (no type) | Wrapped in an IIFE to prevent variable leakage between components. | |
<script type="text/javascript"> | Wrapped in an IIFE. | |
<script type="module"> | Not wrapped in an IIFE (modules are already isolated by spec). DOM selector references still rewritten. | |
JavaScript literal preservation in minify.js (built-in) | Strings, nested template literals (including ${} expressions that contain templates, strings, regexes, and comments), and regex literals are kept byte-for-byte. A / after ), ], a quote, or a backtick is division, except after an if, while, for, or with header, where it starts a regex statement. A / after } is treated as a possible regex only when it closes on the same line. | |
| JavaScript MIME type minification | Inline scripts with no type or text/javascript, module, application/javascript, text/ecmascript, and application/ecmascript are minified when minify.js is enabled. External scripts are left empty and unchanged. | |
<script type="application/json"> (and any non-JS type) | Left completely untouched. | |
Referenced .ts / .mts companion scripts (<script src="counter.ts">) | Erasable TypeScript is stripped automatically when the companion is inlined, with no configuration and before any minify.js step. Line structure is preserved so //# sourceURL line numbers stay accurate. Non-erasable syntax (enum, parameter properties, runtime namespaces) fails the build with the file path under the default compiler. | |
scripts.typescript compiler selection | true (default) is Node strip-only mode; a function is a bring-your-own compiler (esbuild, swc, tsc) receiving (code, { sourcePath, kind }) and returning JavaScript, which unlocks enum, decorators, and downleveling; false disables the transform so pre-compiled output passes through untouched. Applies to both browser paths, in dev and build, on the main thread and in workers. A compiler that throws or returns non-JavaScript fails the build with the source path. | |
<script type="text/typescript"> (inline, page or component) | Stripped to JavaScript and emitted as an ordinary <script> (the type attribute is removed), then scoped and optionally minified like any other inline script. Never applied to data-bascik-* directive scripts. | |
TypeScript syntax in an unmarked <script> | Unsupported | Left byte-for-byte on the JavaScript path because browsers do not execute TypeScript there. When Bascik can positively identify erasable TypeScript in such a block it logs a warning naming the file and the fix (type="text/typescript" or a companion .ts). Valid JavaScript is never rewritten or warned about. |
Module-shaped .ts companion or type="text/typescript" block targeting a classic script | Unsupported | Stripping types never removes import/export. A companion .ts inlined into a <script src="..."> with no type="module", or any type="text/typescript" block (always classic), fails the build with an actionable error naming the file if the stripped code still contains a static import/export declaration, since a classic script's IIFE wrapper would throw a SyntaxError in the browser. Fix by adding type="module" to the referencing tag, or by bundling the file first so no import/export remains. Dynamic import() and import.meta are unaffected. |
<script data-bascik-build> | Executed at transpile time as a Node.js ESM module. The script exports a default callable function returning an HTML string, which replaces the tag. Runs in both dev and build modes. Console methods log to the terminal for debugging. Top-level import and await are supported. | |
<script data-bascik-routes> | Executed at build time inside bracket templates (e.g. [slug].html). The script exports a default callable function returning an array of { params, data? } objects to expand the template into concrete static HTML pages. | |
<script data-bascik-server> | Executed on the server at request time in-process as a Node.js ESM module via ScriptRegistry. Returned markup replaces the tag dynamically. Receives a WHATWG Request, a { remoteIp } context, and { signal }, the same signature as API routes. Top-level import and await are supported. No helpers are injected or exported. Buffered: all server scripts resolve before the response is sent. | |
<script data-bascik-stream> | Same authoring model as data-bascik-server. Receives a WHATWG Request, a { remoteIp } context, and { signal }, the same signature as API routes. Top-level import and await are supported. No helpers are injected or exported. The server commits headers and every static byte before the tag without waiting for it, then writes its output when it resolves, in document order, as a chunked response. All server scripts on the page resolve before the first byte. A stream failure can never be a 500; the slot is emitted empty. Mutually exclusive with server, build, and routes. | |
Class names inside data-bascik-server and data-bascik-stream script source within a component | Scoped at build time like any other string in the component, so a class in the returned markup matches the component's scoped CSS in both readable and minified-identifier modes. Verified: class="result" becomes the same hashed name used in the emitted :has(> .result) selector. Classes built dynamically at request time (string concatenation, values from a database) are not visible to the build and are not scoped. | |
Markup printed by a component's data-bascik-build script | Scoped like the component's template, in dev and production and with identifier minification. Page-aware scripts (data-bascik-build="page") run at page time, after the template is scoped, so Bascik applies the component's class and element mapping to their output. That includes markup returned by an imported helper and class names computed while the script runs. Classes the component's stylesheet does not define stay global. Markup inside comments, <script>, <style>, <textarea>, and attribute values is left as written. | |
DevTools //# sourceURL directives | Automatically appended to every client, build, and server <script> block. Client scripts are padded so DevTools line numbers match the component file, including when markup or <style> blocks sit above the script (unless minify.js joins the lines). Companion scripts are numbered from line 1 of their own file. The second and later instances of a component on a page get ?instance=N, so each instance is its own DevTools file. The directive is held back from minify.js (built-in or custom) and re-attached on its own line afterwards, so a minifier that drops comments or collapses the IIFE to one line can neither lose it nor glue it onto })();. | |
Stack trace remapping (stack-trace.ts) | Build, routes, server, and stream script errors name the file the script was written in and the failing line and column in it, including in components whose markup and <style> blocks are moved before the script runs, and in columns after an import Bascik rewrote on the same line. Build and routes syntax errors show the line and a caret; server and stream syntax errors, which Node reports with no line, name the file and the line the script starts on. src= scripts and imported helpers keep their own paths and lines. Each error prints once, without Node.js internals or Bascik's own runner and request-handling frames. | |
Literal component tags inside <script>, <style>, or <textarea> | Treated as text, never resolved into components. Safe to mention tags like <my-card> in JSON-LD strings, inline scripts, or code examples. | |
| HTML comments containing component tags | HTML comments (<!-- <my-card> -->) are stripped during HTML minification, so commented custom tags are never expanded into components. |
DOM Selector Rewriting
| Method | Example | Attribute Scoped | Status | Notes |
|---|---|---|---|---|
document.getElementById | getElementById("my-id") | id | ||
document.querySelector with #id | querySelector("#my-id") | id | ||
document.querySelectorAll with #id | querySelectorAll("#my-id") | id | ||
document.getElementsByClassName | getElementsByClassName("my-cls") | class | Single and space-separated multi-class string arguments are rewritten. Scoped to the component type (document-wide across instances unless scoping.deduplicateCss is false); use getElementById or root.querySelector for per-instance lookup. getElementsByClassName("my-cls")[0] and .item(0) raise the js-document-class-query warning. | |
document.querySelector with .class | querySelector(".my-cls") | class | Single-token class selector only. Scoped to the component type (returns the first instance's element when scoping.deduplicateCss is true); use getElementById or root.querySelector for per-instance lookup. Any class selector without an #id (".card .title", "button.btn", backtick strings) raises the js-document-class-query warning; a selector that includes a scoped #id is per-instance and is not flagged. | |
document.querySelectorAll with .class | querySelectorAll(".my-cls") | class | Single-token class selector only. Scoped to the component type (matches all instances on the page when scoping.deduplicateCss is true); use root.querySelectorAll rooted on a per-instance id to query within one instance. | |
document.getElementsByName | getElementsByName("my-name") | name | ||
element.closest with #id | el.closest("#my-id") | id | ||
element.closest with .class | el.closest(".my-cls") | class | Single-token class selector only. | |
element.matches with #id | el.matches("#my-id") | id | ||
element.matches with .class | el.matches(".my-cls") | class | Single-token class selector only. Works for event delegation: e.target.matches(".my-cls"). | |
element.classList.add | el.classList.add("my-cls") | class | Single and multi-argument forms: classList.add("a", "b") rewrites all class names. | |
element.classList.remove | el.classList.remove("my-cls") | class | Single and multi-argument forms. | |
element.classList.toggle | el.classList.toggle("my-cls") | class | The optional boolean second argument is passed through unchanged. | |
element.classList.contains | el.classList.contains("my-cls") | class | ||
element.classList.replace | el.classList.replace("old", "new") | class | Both old and new class name arguments are rewritten. | |
Compound querySelector / querySelectorAll | querySelector(".foo .bar") | class / id | Space and combinator tokens (>, +, ~) rewritten. Adjacent .foo.bar rewrites leading token. | |
element.className setter | el.className = "my-cls" | class | Single and multi-class string assignments rewritten (= and +=). Reading className is unchanged. | |
element.setAttribute("class", …) | el.setAttribute("class", "my-cls") | class | Single and space-separated multi-class string literal values are rewritten. | |
element.setAttribute("id", …) | el.setAttribute("id", "my-id") | id | String literal values are rewritten. | |
element.setAttribute("name", …) | el.setAttribute("name", "my-name") | name | String literal values for known name attributes are rewritten. | |
innerHTML / insertAdjacentHTML strings | el.innerHTML = '<div class="box">' | class | Known class names in static HTML string literals are rewritten. | |
element.removeAttribute | el.removeAttribute("class") | Attribute names (not values) passed with no rewriting needed. | ||
element.hasAttribute | el.hasAttribute("id") | Same as removeAttribute: attribute name, not value. | ||
element.toggleAttribute | el.toggleAttribute("hidden") | Boolean attribute name only with no value to rewrite. | ||
element.style.setProperty for CSS vars | el.style.setProperty("--accent", v) | Runtime CSS custom property names are not rewritten. Use scoped property name explicitly. | ||
Template literal in className / selectors | el.className = `box ${state}` | Template literals with expressions are not rewritten. Use classList.add/remove. | ||
element.id setter | el.id = "my-id" | id | Not rewritten. Use getElementById to retrieve and operate on the reference. | |
querySelector attribute selector | querySelector("[id='my-id']") | id | Use getElementById instead. |
Notes on Gaps
The unsupported JS patterns above all involve dynamic attribute manipulation where static analysis cannot safely identify which component's attribute is being referenced from a string literal.
JS-only class discovery
Class names that only appear in JavaScript (never in a class="…" HTML attribute) are automatically discovered and added to the scope map before the JS rewrite runs. This covers all class-referencing patterns: classList.* arguments, .className tokens in querySelector / querySelectorAll / closest / matches selector strings, el.className = "…" assignments, and el.setAttribute("class", "…") values. CSS-only classes (only in the .css file, never in HTML or JS) are scoped in CSS only, which is fine since nothing in JS needs to reference them.
The exception is innerHTML / insertAdjacentHTML string scanning, which only recognizes classes that appear in the HTML template.
The recommended pattern is to query scoped elements by a single id or single-class selector first, store the reference, then use the reference for all further DOM operations:
<!-- source - works correctly -->
<div id="panel" class="card"></div>
<script>
const panel = document.getElementById("panel"); // ← rewritten by Bascik
panel.style.display = "none"; // ← operate on the reference
panel.dataset.state = "closed"; // ← data attributes for state
</script> Class queries are document-wide (not per-instance)
Class names are scoped to the component type, not to individual instances. This means querySelectorAll(".my-class") inside a component script, which Bascik rewrites to querySelectorAll(".bascik__comp__my-class"): will find matching elements across all instances of that component on the page, not just the current instance.
To operate only on the current instance's elements, query by id (which includes a per-instance hash) and traverse from the returned element:
// In component - gets only THIS instance's panel:
const panel = document.getElementById("panel"); // rewritten with instance hash FormData with scoped name attributes
When a component uses <input name="username">, Bascik scopes the name attribute to a per-instance value like bascik__comp__a1b2c3__username. As a result, new FormData(form) entries use the scoped name as the key. If your server-side code expects the unscoped field name, you will need to adapt it, or extract values using formData.get with the scoped name, or via form.elements iteration.
HTTP Protocols, Caching & Security Standards
When running Bascik's built-in HTTP/1.1 and HTTP/2 production server (bascik --server) or dev server (bascik), responses strictly adhere to modern IETF network and caching standards:
| Feature / Standard | Protocol Authority | Status | Implementation Details |
|---|---|---|---|
| HTTP/2 & ALPN Negotiation | IETF RFC 9113 / RFC 9112 | Uses http2.createSecureServer({ allowHTTP1: true }) for ALPN negotiation with automatic HTTP/1.1 fallback. Handles HTTP/2 pseudo-headers (:status, :path, :method, :scheme). | |
| Strong & Content-Hash ETags | IETF RFC 9110 §8.8.3 | Generates strong SHA-256 content-hash ETags for both HTML pages and static assets, with distinct encoding-specific ETags ("hash-br"). | |
| Conditional GET & 304 Responses | IETF RFC 9110 §13.1.1 | Evaluates incoming If-None-Match request headers against generated ETags and returns 304 Not Modified with zero response body. | |
| Content Negotiation & Vary | IETF RFC 9110 §12.5.5 | Sends Vary: Accept-Encoding and negotiates Brotli or gzip for compressible assets. Precompressed .br/.gz sidecars are served only when stamped with matching .bmeta provenance, which bascik --build emits under http.precompress: true; otherwise compression runs on demand. Assets above 2 MiB are streamed as identity with a weak validator. | |
| Method Guarding | IETF RFC 9110 §9.1 | Enforces GET and HEAD requests only. Rejects other methods with 405 Method Not Allowed and sends Allow: GET, HEAD. | |
| Strict-Transport-Security (HSTS) | IETF RFC 6797 | Automatically sends Strict-Transport-Security: max-age=31536000; includeSubDomains when serving HTTPS or when behind an SSL reverse proxy (x-forwarded-proto: https). | |
| Standard Security Headers | OWASP / IETF Guidelines | Every response sends X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, and Permissions-Policy: interest-cohort=(). | |
| Modern JavaScript MIME Types | IETF RFC 9239 | Serves JavaScript files (.js, .mjs, .cjs) as text/javascript; charset=utf-8 (application/javascript is legacy and deprecated per RFC 9239). | |
| Text MIME Charsets | IANA Media Types | All text and data MIME types (text/html, text/css, application/json, application/geo+json) include explicit charset=utf-8. | |
| Server-Sent Events (SSE) | WHATWG EventSource | Endpoint /bascik-live-reload sends Content-Type: text/event-stream; charset=utf-8, Cache-Control: no-cache, and standard double-newline frames (data: ...\n\n). Injected client includes automatic reconnect and focus re-sync. | |
| Path Traversal Security | WHATWG URL Standard | Decodes request paths and strictly enforces boundary containment within dist/. Rejects /../ path segments with 400 Bad Request. | |
| Sitemaps Protocol | Sitemaps.org / W3C | Generates valid XML sitemaps with namespace http://www.sitemaps.org/schemas/sitemap/0.9, escaping XML metacharacters and normalizing canonical URLs. | |
| Robots Exclusion Protocol | IETF RFC 9309 | Generates RFC 9309 compliant robots.txt pointing crawlers at the XML sitemap. |