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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/SKILL.md CHANGED
@@ -787,25 +787,28 @@ Components work inside `<head>` to organize metadata and shared links:
787
787
 
788
788
  ## 8. Build-time Scripts
789
789
 
790
- `<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.
790
+ `<script data-bascik-build>` blocks are executed at transpile time as Node.js ESM modules. The script exports a default callable function whose returned HTML string replaces the tag. Runs in both dev and build modes.
791
791
 
792
792
  ```html
793
793
  <script data-bascik-build>
794
794
  import { readFile } from 'node:fs/promises';
795
795
  import { marked } from 'marked';
796
- const md = await readFile('./content/intro.md', 'utf8');
797
- console.log(marked(md));
796
+
797
+ export default async function () {
798
+ const md = await readFile('./content/intro.md', 'utf8');
799
+ return marked(md);
800
+ }
798
801
  </script>
799
802
  ```
800
803
 
801
- * Top-level `import` and top-level `await` are supported.
804
+ * Top-level `import` and top-level `await` are supported at module scope.
802
805
  * CWD is the project root. Quoted data paths (`readFile('./content/x.md')`) resolve from there.
803
806
  * **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.
804
807
  * **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.
805
808
  * **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.
806
- * 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.
809
+ * Define a callable default export function (`export default async function () { return ...; }`) that returns an HTML string. Standard console methods (`console.log()`, `console.error()`) log to the terminal for debugging only and never become generated markup.
807
810
  * Build scripts run before component resolution, so their output can contain component tags.
808
- * 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.
811
+ * Each uncached build script executes in its own fresh child process, isolated from every other script, regardless of how many siblings are cache misses. Results travel across a dedicated result transport. 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.
809
812
  * 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.
810
813
  * **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.
811
814
  * **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.
@@ -887,15 +890,17 @@ With `pipeline.workers: true`, multiple workers share the same cache directory.
887
890
 
888
891
  ### Rendering and Styling Markdown
889
892
 
890
- Install a Markdown parser such as `marked`, read the source in a build script, and write the resulting HTML to stdout:
893
+ Install a Markdown parser such as `marked`, read the source in a build script, and return the resulting HTML from the default export:
891
894
 
892
895
  ```html
893
896
  <script data-bascik-build>
894
897
  import { readFile } from 'node:fs/promises';
895
898
  import { marked } from 'marked';
896
899
 
897
- const md = await readFile('./content/article.md', 'utf8');
898
- console.log(marked(md));
900
+ export default async function () {
901
+ const md = await readFile('./content/article.md', 'utf8');
902
+ return marked(md);
903
+ }
899
904
  </script>
900
905
  ```
901
906
 
@@ -963,8 +968,8 @@ Use it from any page's `<head>`:
963
968
  ```html
964
969
  <head>
965
970
  <script data-bascik-build>
966
- import { canonical } from '../../scripts/canonical.ts';
967
- console.log(await canonical());
971
+ import { canonical } from '@/lib/canonical.ts';
972
+ export default async () => await canonical();
968
973
  </script>
969
974
  </head>
970
975
  ```
@@ -1016,25 +1021,28 @@ Dynamic routes allow you to generate multiple static HTML files from a single te
1016
1021
  ```html
1017
1022
  <!-- src/pages/blog/[slug].html -->
1018
1023
  <script data-bascik-routes>
1019
- const posts = [
1020
- { slug: 'hello-world', title: 'Hello World' },
1021
- { slug: 'second-post', title: 'Second Post' }
1022
- ];
1023
- const routes = posts.map(p => ({
1024
- params: { slug: p.slug },
1025
- data: p
1026
- }));
1027
- console.log(JSON.stringify(routes));
1024
+ export default async function () {
1025
+ const posts = [
1026
+ { slug: 'hello-world', title: 'Hello World' },
1027
+ { slug: 'second-post', title: 'Second Post' }
1028
+ ];
1029
+ return posts.map(p => ({
1030
+ params: { slug: p.slug },
1031
+ data: p
1032
+ }));
1033
+ }
1028
1034
  </script>
1029
1035
 
1030
1036
  <script data-bascik-build>
1031
- const { params, data } = JSON.parse(process.env.BASCIK_ROUTE || '{}');
1032
- console.log(`<h1>${data.title}</h1>`);
1037
+ export default function () {
1038
+ const { params, data } = JSON.parse(process.env.BASCIK_ROUTE || '{}');
1039
+ return `<h1>${data.title}</h1>`;
1040
+ }
1033
1041
  </script>
1034
1042
  ```
1035
1043
 
1036
1044
  Rules:
1037
- * Script must output a valid JSON array of route objects with required `params` (matching all bracket names in the template path) and optional `data`.
1045
+ * Script must export a callable default function returning an array of route objects with required `params` (matching all bracket names in the template path) and optional `data`. To generate zero routes, return `[]`.
1038
1046
  * Route parameters must be URL-safe tokens; characters like `#`, `%`, `&`, `'`, `+`, spaces, leading dots, and Windows device names are disallowed.
1039
1047
  * In build scripts, `process.env.BASCIK_ROUTE` provides the current `{ params, data }` payload.
1040
1048
  * 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.
@@ -1576,7 +1584,7 @@ What to check in compiled output:
1576
1584
  * **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.
1577
1585
  * **Scoped class names:** attributes like `class="bascik__site-nav__nav"` (or a short hash with `minify.identifiers`) confirm CSS scoping ran correctly.
1578
1586
  * **Injected `<style>` block:** the `<head>` should contain one combined `<style>` with CSS from all components used on that page.
1579
- * **Build script output:** `<script data-bascik-build>` is replaced with stdout; if missing, check the terminal for a `[bascik] build script error` line.
1587
+ * **Build script output:** `<script data-bascik-build>` is replaced with the returned HTML string; if missing, check the terminal for a `[bascik] build script error` line.
1580
1588
  * **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.
1581
1589
  * **Slot and prop values:** verify fallback and injected text appear in the right place.
1582
1590
 
@@ -1975,7 +1983,7 @@ Detailed per-framework migration guides live at `/switch/*`. Key patterns that a
1975
1983
  - **Props:** `defineProps` / component props → `data-bascik-prop-*` attributes (text only).
1976
1984
  - **Reactive state:** `ref`, `useState`, etc. → plain `<script>` with vanilla JS. Bascik scopes `id` values so multiple instances stay independent.
1977
1985
  - **Routing:** Client-side router → one `.html` file per URL in `src/pages/`. For parameterized routes (`[slug].astro`, `getStaticPaths`, Eleventy pagination), use a dynamic route template such as `src/pages/blog/[slug]/index.html` (trailing-slash URLs) with a `<script data-bascik-routes>` block. Do not generate page files into `src/pages/`. A route param cannot contain `/`; use one template per depth.
1978
- - **Build-time data:** `onMounted` / `getStaticProps` / frontmatter → `<script data-bascik-build>` (Node.js ESM, stdout injected).
1986
+ - **Build-time data:** `onMounted` / `getStaticProps` / frontmatter → `<script data-bascik-build>` (Node.js ESM, returned HTML string replaces the tag).
1979
1987
  - **Content collections (Astro `getCollection`, Eleventy `collections`):** No built-in equivalent. Write a `src/lib/posts.ts` helper that reads `content/` with `fs`, parses front matter with `gray-matter`, validates with `zod`, and sorts by the parsed date (not the file name). Escape every interpolated value. Drafts: skip them when `process.env.BASCIK_BUILD === '1'`. Exclude scripts that read `content/` from `scripts.cache` and add `content/` to `pipeline.watchPaths`.
1980
1988
  - **Template logic (Nunjucks/Liquid loops, filters, includes with variables):** Becomes TypeScript helpers in `src/lib/` that return HTML. Only static includes become components. Pages keep `<html>`/`<head>`/`<body>`; a component cannot own them.
1981
1989
  - **Feeds, sitemaps, images:** RSS/Atom is a `pipeline.exec` script (`phase: 'post'`) writing to `BASCIK_OUT_DIR`. Sitemap and robots are built in. No image optimization or syntax highlighting built in; do it in your own build step.
@@ -2016,10 +2024,10 @@ Full guide: `/switch/from-next`. Key Next.js-specific mappings:
2016
2024
  | Next.js | Bascik |
2017
2025
  |---------|--------|
2018
2026
  | `app/page.tsx`, `app/posts/[slug]/page.tsx` | `src/pages/index.html`, `src/pages/posts/[slug].html` (`/posts/<slug>`, also served with a trailing slash) |
2019
- | `generateStaticParams` | `<script data-bascik-routes>` printing `[{ "params": { "slug": "..." } }]` |
2020
- | `generateMetadata` / `metadata` export | A build script in `<head>` that prints `<title>` and `<meta>` from a `src/lib` helper; absolute URLs from `BASCIK_SITE_URL` |
2027
+ | `generateStaticParams` | `<script data-bascik-routes>` returning `[{ params: { slug: "..." } }]` |
2028
+ | `generateMetadata` / `metadata` export | A build script in `<head>` that returns `<title>` and `<meta>` from a `src/lib` helper; absolute URLs from `BASCIK_SITE_URL` |
2021
2029
  | `app/layout.tsx` | Pages keep `<html>`/`<head>`/`<body>`; a head component plus header/footer components |
2022
- | Server component that maps data to JSX | A `src/lib` helper returning escaped HTML strings, printed by a build script |
2030
+ | Server component that maps data to JSX | A `src/lib` helper returning escaped HTML strings, returned by a build script |
2023
2031
  | `"use client"` component with `useState`/`useEffect` | A component with a plain `<script>`; persistent state lives in the DOM or `localStorage` |
2024
2032
  | `next/link`, `next/image`, `next/font` | `<a>`, `<img>` with `width`/`height`, a self-hosted `@font-face` plus `<link rel="preload">` |
2025
2033
  | CSS Modules (`styles.x`) | Paired component `.css`; a class written in the template is scoped |
@@ -23,7 +23,7 @@ export declare const SITE_HEADER_CSS = ".header {\n position: sticky;\n top: 0
23
23
  export declare const SITE_HEADER_HTML = "<style>\n.header {\n position: sticky;\n top: 0;\n z-index: 100;\n background: rgba(24, 25, 27, 0.9);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--border);\n}\n\n.container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }\n\n.nav {\n display: flex;\n align-items: center;\n height: 60px;\n gap: 24px;\n}\n\n.logo {\n font-weight: 700;\n font-size: 1rem;\n color: var(--text);\n letter-spacing: -0.01em;\n flex-shrink: 0;\n}\n.logo:hover { opacity: 0.8; }\n\n.nav-menu {\n list-style: none;\n margin: 0 0 0 auto;\n padding: 0;\n display: flex;\n gap: 4px;\n}\n.nav-menu a {\n display: block;\n padding: 6px 12px;\n font-size: 0.9rem;\n color: var(--text-muted);\n border-radius: var(--r-sm);\n transition: color .15s, background .15s;\n}\n.nav-menu a:hover { color: var(--text); background: var(--elevated); opacity: 1; }\n\n.nav-toggle {\n display: none;\n flex-direction: column;\n justify-content: center;\n gap: 5px;\n width: 36px;\n height: 36px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 6px;\n margin-left: auto;\n border-radius: var(--r-sm);\n}\n.nav-toggle span {\n display: block;\n height: 2px;\n background: var(--text);\n border-radius: 2px;\n}\n\n@media (max-width: 640px) {\n .nav-toggle { display: flex; }\n\n .nav-menu {\n display: none;\n position: absolute;\n top: 60px;\n left: 0;\n right: 0;\n flex-direction: column;\n gap: 0;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n padding: 8px 16px;\n }\n .nav-menu.is-open { display: flex; }\n .nav-menu li { width: 100%; }\n .nav-menu a { padding: 10px 4px; }\n}\n</style>\n<header class=\"header\">\n <div class=\"container\">\n <nav class=\"nav\">\n <a href=\"/\" class=\"logo\" data-bascik-prop-brand>My Site</a>\n <button id=\"nav-toggle\" class=\"nav-toggle\" aria-label=\"Toggle menu\" aria-expanded=\"false\" data-testid=\"nav-toggle\">\n <span></span><span></span><span></span>\n </button>\n <ul id=\"nav-menu\" class=\"nav-menu\" data-testid=\"nav-menu\">\n <li><a href=\"/\">Home</a></li>\n <li><a href=\"/about\">About</a></li>\n <li><a href=\"/contact\">Contact</a></li>\n </ul>\n </nav>\n </div>\n</header>\n<script>\n var toggle = document.getElementById('nav-toggle');\n var menu = document.getElementById('nav-menu');\n toggle.addEventListener('click', function () {\n var open = toggle.getAttribute('aria-expanded') === 'true';\n toggle.setAttribute('aria-expanded', String(!open));\n menu.classList.toggle('is-open');\n });\n</script>\n";
24
24
  export declare const SITE_FOOTER_CSS = ".footer {\n border-top: 1px solid var(--border);\n padding: 28px 0;\n}\n\n.container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }\n\n.footer-inner {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n}\n\n.footer-copy { font-size: 0.85rem; color: var(--text-muted); }\n\n.footer-nav {\n display: flex;\n gap: 20px;\n}\n.footer-nav a {\n font-size: 0.85rem;\n color: var(--text-muted);\n}\n.footer-nav a:hover { color: var(--text); opacity: 1; }\n\n@media (max-width: 480px) {\n .footer-inner { flex-direction: column; align-items: flex-start; }\n}\n";
25
25
  /** Footer with brand prop and build-time year. */
26
- export declare const SITE_FOOTER_HTML = "<style>\n.footer {\n border-top: 1px solid var(--border);\n padding: 28px 0;\n}\n\n.container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }\n\n.footer-inner {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n}\n\n.footer-copy { font-size: 0.85rem; color: var(--text-muted); }\n\n.footer-nav {\n display: flex;\n gap: 20px;\n}\n.footer-nav a {\n font-size: 0.85rem;\n color: var(--text-muted);\n}\n.footer-nav a:hover { color: var(--text); opacity: 1; }\n\n@media (max-width: 480px) {\n .footer-inner { flex-direction: column; align-items: flex-start; }\n}\n</style>\n<footer class=\"footer\">\n <div class=\"container\">\n <div class=\"footer-inner\">\n <span class=\"footer-copy\">\n &copy; <script data-bascik-build>console.log(new Date().getFullYear())</script>\n <span data-bascik-prop-brand>My Site</span>\n </span>\n <nav class=\"footer-nav\" aria-label=\"Footer navigation\">\n <a href=\"/\">Home</a>\n <a href=\"/about\">About</a>\n <a href=\"/contact\">Contact</a>\n </nav>\n </div>\n </div>\n</footer>\n";
26
+ export declare const SITE_FOOTER_HTML = "<style>\n.footer {\n border-top: 1px solid var(--border);\n padding: 28px 0;\n}\n\n.container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }\n\n.footer-inner {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n}\n\n.footer-copy { font-size: 0.85rem; color: var(--text-muted); }\n\n.footer-nav {\n display: flex;\n gap: 20px;\n}\n.footer-nav a {\n font-size: 0.85rem;\n color: var(--text-muted);\n}\n.footer-nav a:hover { color: var(--text); opacity: 1; }\n\n@media (max-width: 480px) {\n .footer-inner { flex-direction: column; align-items: flex-start; }\n}\n</style>\n<footer class=\"footer\">\n <div class=\"container\">\n <div class=\"footer-inner\">\n <span class=\"footer-copy\">\n &copy; <script data-bascik-build>export default () => String(new Date().getFullYear());</script>\n <span data-bascik-prop-brand>My Site</span>\n </span>\n <nav class=\"footer-nav\" aria-label=\"Footer navigation\">\n <a href=\"/\">Home</a>\n <a href=\"/about\">About</a>\n <a href=\"/contact\">Contact</a>\n </nav>\n </div>\n </div>\n</footer>\n";
27
27
  export declare const FEAT_CARD_CSS = ".fcard {\n background: var(--elevated);\n border: 1px solid var(--border);\n border-radius: var(--r);\n padding: 24px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n transition: border-color .2s, box-shadow .2s, transform .2s;\n}\n.fcard:hover {\n border-color: var(--border-hover);\n box-shadow: 0 0 0 1px var(--accent-dim), 0 8px 32px rgba(0,0,0,0.3);\n transform: translateY(-2px);\n}\n\n.fcard-header {\n font-size: 0.7rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: .1em;\n color: var(--accent);\n}\n.fcard-header:empty { display: none; }\n\n.fcard-body { flex: 1; }\n.fcard-body h3 { margin: 0 0 6px; font-size: 1rem; color: var(--text); }\n.fcard-body p { margin: 0; font-size: 0.88rem; color: var(--text-muted); }\n\n.fcard-footer {\n padding-top: 12px;\n border-top: 1px solid var(--border);\n margin-top: auto;\n}\n.fcard-footer:empty { display: none; }\n";
28
28
  /**
29
29
  * Card component demonstrating named slots.
package/dist/scaffold.js CHANGED
@@ -26,10 +26,10 @@ export const PACKAGE_JSON = (name) => JSON.stringify({
26
26
  e2e: "playwright test --config e2e/playwright.config.ts",
27
27
  },
28
28
  dependencies: {
29
- "@bascik/bascik": "^1.0.0-rc.3",
29
+ "@bascik/bascik": "^1.0.0-rc.4",
30
30
  },
31
31
  devDependencies: {
32
- "@bascik/language-server": "^0.1.0-rc.1",
32
+ "@bascik/language-server": "^0.1.0-rc.2",
33
33
  "@playwright/test": "^1.62.0",
34
34
  "@types/node": "^24.0.0",
35
35
  "@vitest/coverage-v8": "^4.1.10",
@@ -456,7 +456,7 @@ ${SITE_FOOTER_CSS}</style>
456
456
  <div class="container">
457
457
  <div class="footer-inner">
458
458
  <span class="footer-copy">
459
- &copy; <script data-bascik-build>console.log(new Date().getFullYear())</script>
459
+ &copy; <script data-bascik-build>export default () => String(new Date().getFullYear());</script>
460
460
  <span data-bascik-prop-brand>My Site</span>
461
461
  </span>
462
462
  <nav class="footer-nav" aria-label="Footer navigation">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bascik",
3
3
  "description": "Create a new Bascik project",
4
- "version": "1.0.0-rc.3",
4
+ "version": "1.0.0-rc.4",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/bascikdev/bascik.git",
@@ -145,6 +145,9 @@ describe("scaffold-to-compiler integration", () => {
145
145
  const currentYear = String(new Date().getFullYear());
146
146
  expect(indexHtml).toContain(`&copy; ${currentYear}`);
147
147
  expect(aboutHtml).toContain(`&copy; ${currentYear}`);
148
+ // Confirm default export handler was scaffolded in site-footer
149
+ const footerHtml = await readFile(join(projectDir, "src/components/site-footer/site-footer.html"), "utf8");
150
+ expect(footerHtml).toContain("export default () => String(new Date().getFullYear())");
148
151
 
149
152
  // 6. Verify representative assets copied to dist
150
153
  await expect(access(join(distDir, "favicon.ico"))).resolves.toBeUndefined();
@@ -207,4 +210,28 @@ describe("scaffold-to-compiler integration", () => {
207
210
  const combinedOutput = `${buildResult.stdout}\n${buildResult.stderr}`;
208
211
  expect(combinedOutput.toLowerCase()).toMatch(/error|failed|syntaxerror/);
209
212
  }, 45_000);
213
+
214
+ it("negative control: rejects compilation when a build script relies on legacy stdout console.log", async () => {
215
+ const parentDir = await createTempDir("stdout-control");
216
+ const projectName = "legacy-stdout-site";
217
+ const projectDir = join(parentDir, projectName);
218
+
219
+ await scaffold(projectName, parentDir);
220
+
221
+ const legacyStdoutPage = `<!DOCTYPE html>
222
+ <html>
223
+ <head><title>Legacy Stdout</title></head>
224
+ <body>
225
+ <script data-bascik-build>
226
+ console.log("<p>legacy</p>");
227
+ </script>
228
+ </body>
229
+ </html>`;
230
+ await writeFile(join(projectDir, "src/pages/index.html"), legacyStdoutPage, "utf8");
231
+
232
+ const buildResult = await runBascikBuild(projectDir);
233
+ expect(buildResult.exitCode).not.toBe(0);
234
+ const combinedOutput = `${buildResult.stdout}\n${buildResult.stderr}`;
235
+ expect(combinedOutput).toContain("missing default export");
236
+ }, 45_000);
210
237
  });
@@ -134,7 +134,7 @@ describe("PACKAGE_JSON", () => {
134
134
 
135
135
  it("uses the current Bascik prerelease dependency", () => {
136
136
  const pkg = JSON.parse(PACKAGE_JSON("my-app"));
137
- expect(pkg.dependencies["@bascik/bascik"]).toBe("^1.0.0-rc.3");
137
+ expect(pkg.dependencies["@bascik/bascik"]).toBe("^1.0.0-rc.4");
138
138
  });
139
139
 
140
140
  it("uses the supplied name", () => {
@@ -286,6 +286,7 @@ describe("SITE_FOOTER_HTML", () => {
286
286
 
287
287
  it("uses a build-time script for the year", () => {
288
288
  expect(SITE_FOOTER_HTML).toContain("data-bascik-build");
289
+ expect(SITE_FOOTER_HTML).toContain("export default () =>");
289
290
  expect(SITE_FOOTER_HTML).toContain("getFullYear");
290
291
  });
291
292
  });
package/src/scaffold.ts CHANGED
@@ -32,10 +32,10 @@ export const PACKAGE_JSON = (name: string): string =>
32
32
  e2e: "playwright test --config e2e/playwright.config.ts",
33
33
  },
34
34
  dependencies: {
35
- "@bascik/bascik": "^1.0.0-rc.3",
35
+ "@bascik/bascik": "^1.0.0-rc.4",
36
36
  },
37
37
  devDependencies: {
38
- "@bascik/language-server": "^0.1.0-rc.1",
38
+ "@bascik/language-server": "^0.1.0-rc.2",
39
39
  "@playwright/test": "^1.62.0",
40
40
  "@types/node": "^24.0.0",
41
41
  "@vitest/coverage-v8": "^4.1.10",
@@ -491,7 +491,7 @@ ${SITE_FOOTER_CSS}</style>
491
491
  <div class="container">
492
492
  <div class="footer-inner">
493
493
  <span class="footer-copy">
494
- &copy; <script data-bascik-build>console.log(new Date().getFullYear())</script>
494
+ &copy; <script data-bascik-build>export default () => String(new Date().getFullYear());</script>
495
495
  <span data-bascik-prop-brand>My Site</span>
496
496
  </span>
497
497
  <nav class="footer-nav" aria-label="Footer navigation">