Internals
Transpilation Pipeline
Bascik transforms source HTML into deployable HTML by replacing every custom component tag with its resolved, scoped content. The pipeline runs in two nested phases: the page phase and the component phase.
Overview
Every time a source page or component file changes, pageProcessing(filePath) in processing.ts is called. This is the top-level entry point for the whole pipeline.
Build scripts (<script data-bascik-build>) run first, before component resolution, so their output can contain component tags. Body HTML minification runs after component resolution so that whitespace-sensitive content from resolved components (e.g. <pre> blocks from <code-block>) is preserved intact.
Multi-Page Startup: processAllPages
On startup (and whenever a component is added), the watch system calls processAllPages() instead of invoking pageProcessing() once per file. This avoids redundant I/O:
- Hoist shared computation.
listComponents()andresolveInlineStylesHtml()each run once, in parallel, before any page is processed. The results are passed to every page rather than re-computed per page. - Transpile each page. By default, pages are transpiled sequentially on the main thread. If
useWorkers: trueis set inbascik.config.js, aWorkerPoolis created instead withMath.min(os.cpus().length, pageCount)workers, and each worker is initialised with the sharedcomponentListandglobalStylesHtmlviaworkerData. The main thread dispatches page paths through the pool's queue and awaits all results. Worker startup has a fixed cost (each worker loads the transpiler's module graph independently), so this only pays off for larger sites or CPU-heavy per-page work, see theuseWorkersconfig option. - Apply side effects on the main thread. After transpilation completes, the main thread runs
mem.storePage()and emits the"transpiled"event for each result. Brotli compression insidestorePage()runs in the background and does not block the page from being marked ready or served. - Write to disk only in build mode. In dev mode, pages are served entirely from the in-memory store, no
dist/writes happen, so the server is ready as soon as memory is populated.
Phase 1: Page Phase (pageProcessing)
The page phase prepares the source HTML document and orchestrates the component phase:
- Execute build scripts. Any
<script data-bascik-build>blocks are run as Node.js ESM modules. Their stdout replaces the script tag. The result can contain component tags, these will be resolved in step 4. - Extract body and head. The inner content of
<body>and<head>are extracted separately so component injection can happen in both zones independently. - Obtain component list. On the multi-page startup path (
processAllPages), the list is pre-computed once and passed in. On a single-page re-transpilation, it is loaded fromsrc/components/at this point. - Run component phase.
recursivelyTranspileis called on both the body and head HTML strings. Each call returns aTranspileResultcontaining the resolved HTML and the list of components that were used. - Collect and deduplicate CSS. All CSS from used components is gathered. Since multiple instances of the same component share identical scoped class names,
deduplicateCssemits a single<style>block regardless of how many times a component appears on the page. Any global stylesheets configured viainlineStylesare also injected into<head>at this stage. - Inject live-reload script. In dev mode only, a small
<script>that opens a Server-Sent Events connection to/bascik-live-reloadis appended to the body. - Minify. HTML comments are stripped and excess whitespace is collapsed via
minifyHtml. This runs after component resolution so that whitespace-sensitive content inside resolved components (e.g.<pre>blocks from<code-block>) is preserved intact. - Reassemble HTML. The resolved body and head are placed back into the original HTML document structure.
- Write output. In build mode, the finished HTML is written to
dist/. In dev mode, no disk write occurs, the result is stored in the in-memory page store so the HTTP/2 server can serve it instantly. - Emit transpiled event.
eventEmitter.emit("transpiled")triggers live-reload for any connected browser.
Phase 2: Component Phase (recursivelyTranspile)
The component phase recurses until no custom component tags remain in the HTML string. On each pass it finds the first component tag, fully resolves it, and substitutes it. It then repeats until no more tags are found.
For each component tag found:
Step 1: Scoping pipeline
A fresh instanceId (a random 8-byte hex string) is generated for this occurrence of the component. An ordered list of transform functions is assembled and applied in a pipeline (each step receives the output of the previous):
prefixElementAttribute(c, "id", instanceId): scopesidattributes and all corresponding JS DOM selector references.prefixElementAttribute(c, "name", instanceId): scopesnameattributes andgetElementsByNamecalls.prefixElementAttribute(c, "class", instanceId): scopes class names in HTML attributes, CSS, and JS selector calls.namespaceScriptTags(c): wraps every inline<script>in an IIFE.
Each step is skipped if disabled in bascik.config.js.
Step 2: Template resolution
- Props.
injectPropsreplaces everydata-bascik-prop-*placeholder in the component template with the corresponding attribute value from the usage tag. - Named slots.
replaceNamedSlotsfills eachdata-bascik-slot="name"zone in the template with the matching<div data-bascik-slot="name">content from the usage site. - Default slot. The inner content of the usage tag is placed into the element carrying
data-bascik-slot(no value). If the usage tag has no inner content, the template's fallback content is preserved. - Attribute inheritance.
mergeAttributesOntoRootcopies pass-through attributes (aria-*,data-*,class, etc.) from the usage tag onto the component's root element.
Step 3: Substitution
replaceTag replaces the original usage tag in the parent HTML string with the fully resolved component HTML. The outer loop runs again on the updated string. Because component templates can themselves contain other component tags, this naturally handles any depth of nesting.
Termination
The recursion terminates when getFirstComponent no longer finds any custom tag in the HTML string, i.e., when all recognised component names have been replaced with plain HTML.
Performance note: Each call to recursivelyTranspile uses the same in-memory ComponentList built once at the start of the pipeline. In the multi-page startup path, this list is pre-computed once and passed to every worker via workerData, components are never re-read from disk per page or per worker.
Selective Re-transpilation
When a component file changes during dev, Bascik does not reprocess every page. The memory store maintains a reverse index mapping each component name to the set of pages that use it. selectivelyProcessPages uses this index to retranspile only the affected pages.
Scoped Name Format
All scoped attribute names follow this pattern:
bascik______
# id and name attributes include instanceId for uniqueness:
bascik__my-nav__a1b2c3d4__search-input
# class attributes use component name only (no instanceId):
bascik__my-nav__toggle-btn When obfuscateAttributeNames is enabled (the default for builds), each full scoped name is hashed to a short hex string using SHAKE-256 before being written to the output, e.g. ba1c2d3e4f. See Scoping System for full details.