Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Vikaskumargd/85574a9ebc89e15aef347dfea6025acf to your computer and use it in GitHub Desktop.

Select an option

Save Vikaskumargd/85574a9ebc89e15aef347dfea6025acf to your computer and use it in GitHub Desktop.
Turn the fresh Astro 7 starter into a production-ready skeleton with strict TS, Tailwind v4, shadcn React components (server-rendered, zero JS), SEO head, native fonts, ESLint isolation rules, Prettier, and Vitest.

MP-0a — Repo skeleton & tooling (Design Spec)

Status: Approved in brainstorming session 2026-07-07 Parent spec: docs/superpowers/specs/2026-07-07-pink-pulsar-plan-of-plans-design.md (§7 MP-0a, §3 architecture, §5 UI/UX stack) Scope: One PR-sized increment — turn the fresh Astro 7 starter into a production-ready skeleton with strict TS, Tailwind v4, real shadcn React components (server-rendered = zero JS), SEO head, native fonts, ESLint isolation rules, Prettier, and Vitest. No business logic, no pages beyond a smoke-test home page.


1. Goal & scope

1.1 Goal

Turn the fresh Astro 7 starter (src/ has default Welcome.astro, Layout.astro, index.astro only) into a production-ready skeleton that every subsequent mini-plan builds on. MP-0a establishes: strict TypeScript, the design-token system, the component approach (real shadcn React via @astrojs/react, server-rendered by default), the SEO head, native font optimization, lint/format/test tooling, and the layer-isolation rule enforcement.

1.2 In scope

  • tsconfig.json: strict mode + noUncheckedIndexedAccess + exactOptionalPropertyTypes + @/* path alias (shadcn convention)
  • @astrojs/react integration + React 19
  • Tailwind v4 via @tailwindcss/vite + src/styles/global.css
  • Astro native Fonts API (stable in Astro 7): Inter via Fontsource, --font-sans
  • shadcn CLI init (components.json) + 4 base components: Button, Input, Card, Label as .tsx in src/components/ui/
  • shadcn CSS-variable design tokens (pink primary, light-only) in src/styles/tokens.css
  • cn() helper in src/lib/utils.ts (clsx + tailwind-merge)
  • MetaHead.astro SEO component (Astro — server-only, no interactivity)
  • Image.astro thin wrapper around Astro <Image> from astro:assets
  • Layout.astro with <head> (Font + MetaHead + global CSS) + slot
  • ESLint 10 flat config with no-restricted-imports (§3.5 isolation) + eslint-plugin-astro
  • Prettier with prettier-plugin-astro + prettier-plugin-tailwindcss
  • Vitest via getViteConfig() from astro/config + astro/container Container API
  • .dev.vars (gitignored) + .dev.vars.example (committed) skeleton
  • astro.config.mjs: site set, fonts config, @astrojs/react + Tailwind Vite plugins
  • package.json: bump Node engine >=22.13.0, add scripts (lint, lint:fix, format, format:check, test, test:watch, typecheck)
  • Smoke-test home page at src/pages/index.astro (Layout + Card + Button, zero JS)
  • Removal of starter cruft: Welcome.astro, src/assets/astro.svg, src/assets/background.svg, starter README content

1.3 Out of scope (later MPs)

  • D1/KV/R2/Analytics Engine bindings (MP-0b)
  • CI/CD pipeline (MP-0c)
  • Middleware, env accessor, logger, error taxonomy, rate-limit, /health, /rum (MP-0d)
  • Any feature pages (home/product/catalog/legal/admin — MP-2 onward)
  • Drizzle ORM (MP-1)
  • Interactive React islands with client:* (MP-3 onward — MP-0a uses zero client:*)
  • JSON-LD structured data (MP-2, MP-12)
  • Image responsive layout config (MP-2, MP-13)
  • Lighthouse CI thresholds (MP-0c configures CI; MP-0a just keeps the path clean)

1.4 Key principle — server-rendered shadcn = zero JS

shadcn React components in src/components/ui/ are used without client:* directives on prerendered pages. Astro renders them to static HTML at build time and ships zero React JS. The @astrojs/react integration enables server-side rendering of React components; React runtime only ships when a client:* directive is added (later MPs: cart, checkout, admin). This preserves the Lighthouse 100 target on the storefront while giving a consistent shadcn API everywhere.


2. Decisions locked

Decision Choice Rationale
Site URL https://pink-pulsar.workers.dev Cloudflare Workers default domain; update when custom domain is purchased
Brand name "Pink Pulsar" Drives <title>, footer, OG tags
Font Inter via Fontsource (variable, weights 400–700, latin, normal) Privacy-friendly (downloaded at build, served from own site), Lighthouse-friendly, single family
Color theme Pink primary (hsl(330 81% 60%)) on neutral (zinc) base, light-only Brand-aligned; dark mode deferred to a future MP
Base components Button, Input, Card, Label (4) Proves the token system end-to-end; covers common immediate needs; later MPs add more via shadcn CLI
Component approach Real shadcn React via @astrojs/react + shadcn CLI Per §5 amendment — server-rendered (zero JS) on prerendered pages, islands where interactive
Path alias @/*./src/* shadcn CLI convention — generated imports use @/components/ui/*, @/lib/utils
Linter ESLint 10 flat config + eslint-plugin-astro + typescript-eslint + no-restricted-imports (§3.5) Latest ESLint; Astro-recommended plugin; enforces layer isolation
Formatter Prettier + prettier-plugin-astro + prettier-plugin-tailwindcss (last in plugins) Official Astro Prettier plugin; Tailwind class sorting
Test runner Vitest via getViteConfig() from astro/config + astro/container Container API Per §4 amendment — no astro test CLI exists in Astro 7
Node engine >=22.13.0 ESLint 10 requirement
Starter cleanup Remove Welcome.astro, starter SVGs, starter README Fresh foundation

3. File tree

.
├── astro.config.mjs              # modify: site, fonts, @astrojs/react, vite.tailwind plugin
├── package.json                  # modify: engines, scripts, deps (react), devDeps
├── tsconfig.json                 # modify: paths (@/*), strict flags, jsx: react-jsx
├── pnpm-workspace.yaml           # modify: allowBuilds (esbuild, sharp, workerd = true)
├── components.json               # new — shadcn config (created by `shadcn init`)
├── .dev.vars                     # new (gitignored) — placeholder secrets
├── .dev.vars.example             # new (committed) — template
├── eslint.config.mjs             # new — flat config + §3.5 isolation rules
├── .prettierrc                   # new — astro + tailwind plugins
├── .prettierignore               # new
├── vitest.config.ts              # new — getViteConfig() from astro/config
├── src/
│   ├── styles/
│   │   ├── global.css            # @import "tailwindcss"; @import tokens; @theme inline; base layer
│   │   └── tokens.css            # shadcn CSS vars (:root, pink primary, light-only)
│   ├── lib/
│   │   └── utils.ts              # cn() helper (clsx + tailwind-merge) — created by shadcn init
│   ├── layouts/
│   │   └── Layout.astro          # modify: head + Font + MetaHead + slot
│   ├── components/
│   │   ├── ui/                   # shadcn React components (.tsx, created by `shadcn add`)
│   │   │   ├── button.tsx
│   │   │   ├── input.tsx
│   │   │   ├── card.tsx
│   │   │   └── label.tsx
│   │   ├── seo/
│   │   │   └── MetaHead.astro    # Astro — server-only SEO head
│   │   └── Image.astro           # Astro wrapper around astro:assets <Image>
│   ├── env.d.ts                  # Astro env type definitions (extend if needed)
│   └── pages/
│       └── index.astro           # modify: smoke-test home (Layout + Card + Button, zero JS)
├── tests/
│   ├── smoke.test.ts             # Layout renders, components render, no astro-island, meta tags present
│   └── lint.test.ts              # shells out to pnpm lint + pnpm format:check, asserts exit 0
└── (delete: src/components/Welcome.astro, src/assets/astro.svg, src/assets/background.svg)

4. Configs

4.1 astro.config.mjs

import { defineConfig, fontProviders } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  site: "https://pink-pulsar.workers.dev",
  adapter: cloudflare(),
  integrations: [react()],
  vite: { plugins: [tailwindcss()] },
  fonts: [
    {
      provider: fontProviders.fontsource(),
      name: "Inter",
      cssVariable: "--font-sans",
      weights: ["400 700"],
      styles: ["normal"],
      subsets: ["latin"],
    },
  ],
});
  • image responsive defaults deferred to MP-2/MP-13.
  • No output override — Astro 7 default is static (SSG), which is the Lighthouse-100 path. Later MPs add prerender = false on dynamic routes.

4.2 tsconfig.json

{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*", "./worker-configuration.d.ts"],
  "exclude": ["dist"],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] },
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "jsx": "react-jsx"
  }
}
  • @/* alias matches shadcn CLI convention (generated imports use @/components/ui/*).
  • jsx: "react-jsx" required for .tsx shadcn components.

4.3 package.json changes

Engines: "node": ">=22.13.0" (ESLint 10 requirement).

Add deps:

  • react, react-dom
  • @astrojs/react

Add devDeps:

  • tailwindcss, @tailwindcss/vite
  • eslint, @eslint/js, typescript-eslint, eslint-plugin-astro, eslint-config-prettier
  • prettier, prettier-plugin-astro, prettier-plugin-tailwindcss
  • vitest
  • clsx, tailwind-merge, class-variance-authority, lucide-react (shadcn deps — installed by shadcn init)
  • @types/react, @types/react-dom

Add scripts:

{
  "lint": "eslint .",
  "lint:fix": "eslint . --fix",
  "format": "prettier --write .",
  "format:check": "prettier --check .",
  "test": "vitest run",
  "test:watch": "vitest",
  "typecheck": "astro check"
}

4.4 pnpm-workspace.yaml

allowBuilds:
  esbuild: true
  sharp: true
  workerd: true

(Required for Astro build with Cloudflare adapter + image optimization.)

4.5 components.json (shadcn config — created by shadcn init)

{
  "tailwind": {
    "config": "",
    "css": "src/styles/global.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  },
  "iconLibrary": "lucide"
}

4.6 .dev.vars.example (committed)

# Razorpay (MP-5)
RAZORPAY_KEY=
RAZORPAY_SECRET=
# SMTP (MP-6b)
SMTP_URL=
# Admin shared secret (MP-7)
ADMIN_SECRET=
# HMAC signing key for signed links (MP-4, MP-8)
SIGNING_KEY=

.dev.vars is the same file with real placeholder values, gitignored (already covered by .gitignore .env rule — but MP-0a adds explicit .dev.vars entry to .gitignore).

4.7 .prettierrc

{
  "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
  "overrides": [{ "files": "*.astro", "options": { "parser": "astro" } }]
}

prettier-plugin-tailwindcss must be the last entry in the plugins array (per Tailwind docs).

4.8 .prettierignore

dist/
.astro/
.wrangler/
node_modules/
pnpm-lock.yaml

4.9 vitest.config.ts

/// <reference types="vitest/config" />
import { getViteConfig } from "astro/config";

export default getViteConfig({
  test: {
    environment: "node",
  },
});
  • Uses Astro's getViteConfig() helper so Vitest inherits the Astro project config (paths, integrations, Vite plugins).
  • Component rendering tests use experimental_AstroContainer from astro/container (imported in test files, not configured here).
  • environment: 'node' — Astro Container API renders to strings server-side; no DOM needed for MP-0a smoke tests.

5. Styles

5.1 src/styles/tokens.css — shadcn CSS variables (pink primary, light-only)

:root {
  --background: 0 0% 100%;
  --foreground: 240 10% 3.9%;
  --card: 0 0% 100%;
  --card-foreground: 240 10% 3.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 240 10% 3.9%;
  --primary: 330 81% 60%;
  --primary-foreground: 0 0% 100%;
  --secondary: 240 4.8% 95.9%;
  --secondary-foreground: 240 5.9% 10%;
  --muted: 240 4.8% 95.9%;
  --muted-foreground: 240 3.8% 46.1%;
  --accent: 240 4.8% 95.9%;
  --accent-foreground: 240 5.9% 10%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 0 0% 98%;
  --border: 240 5.9% 90%;
  --input: 240 5.9% 90%;
  --ring: 330 81% 60%;
  --radius: 0.5rem;
}
  • No .dark block — light-only for v1. A future dark-mode MP adds .dark { ... } overrides.
  • --ring matches --primary (pink) for focus rings.
  • shadcn components consume these via hsl(var(--token)) in their Tailwind classes.

5.2 src/styles/global.css

@import "tailwindcss";
@import "./tokens.css";

@theme inline {
  --font-sans: var(--font-sans);
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  --color-card: hsl(var(--card));
  --color-card-foreground: hsl(var(--card-foreground));
  --color-popover: hsl(var(--popover));
  --color-popover-foreground: hsl(var(--popover-foreground));
  --color-primary: hsl(var(--primary));
  --color-primary-foreground: hsl(var(--primary-foreground));
  --color-secondary: hsl(var(--secondary));
  --color-secondary-foreground: hsl(var(--secondary-foreground));
  --color-muted: hsl(var(--muted));
  --color-muted-foreground: hsl(var(--muted-foreground));
  --color-accent: hsl(var(--accent));
  --color-accent-foreground: hsl(var(--accent-foreground));
  --color-destructive: hsl(var(--destructive));
  --color-destructive-foreground: hsl(var(--destructive-foreground));
  --color-border: hsl(var(--border));
  --color-input: hsl(var(--input));
  --color-ring: hsl(var(--ring));
  --radius-lg: var(--radius);
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
    font-family: var(--font-sans);
  }
}
  • @import "tailwindcss" activates Tailwind v4.
  • @theme inline maps shadcn HSL tokens to Tailwind v4 theme tokens so bg-primary, text-foreground, border-border etc. work.
  • --font-sans is wired by the Astro Fonts API (the <Font cssVariable="--font-sans" /> component in Layout emits the @font-face + CSS variable).

6. Components

6.1 src/lib/utils.tscn() helper (shadcn standard, created by shadcn init)

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

6.2 src/components/ui/* — shadcn React components (.tsx)

Created by pnpm dlx shadcn@latest add button input card label. Standard shadcn output:

  • button.tsxcva variants (default / destructive / outline / secondary / ghost / link) + sizes (default / sm / lg / icon), uses cn().
  • input.tsx — styled <input>.
  • card.tsx — exports Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter.
  • label.tsx — wraps @radix-ui/react-label.

No manual authoring — CLI generates them. Imported via @/components/ui/button, @/components/ui/card, etc.

Server-rendered by default: in .astro pages, import and render directly in frontmatter. Astro renders these to static HTML at build time. Zero React JS shipped unless a client:* directive is added (which MP-0a does not do).

6.3 src/components/seo/MetaHead.astro — server-only SEO head

---
interface Props {
  title: string;
  description: string;
  canonical?: string;
  image?: string;
  noindex?: boolean;
}
const { title, description, canonical, image, noindex } = Astro.props;
const site = Astro.site ?? new URL(Astro.url.origin);
const canonicalURL = canonical
  ? new URL(canonical, site)
  : new URL(Astro.url.pathname, site);
const ogImage = image ? new URL(image, site) : undefined;
---

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL.href} />
<meta name="robots" content={noindex ? "noindex, nofollow" : "index, follow"} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL.href} />
{ogImage && <meta property="og:image" content={ogImage.href} />}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{ogImage && <meta name="twitter:image" content={ogImage.href} />}
  • No JSON-LD here — per-page JSON-LD (Product, Offer, BreadcrumbList) is added in MP-2.
  • MP-0a establishes the base meta component; later MPs extend it as needed (e.g. hreflang stub in MP-12).

6.4 src/components/Image.astro — thin Astro wrapper around astro:assets

---
import { Image as AstroImage } from "astro:assets";

interface Props {
  src: ImageMetadata;
  alt: string;
  width?: number;
  height?: number;
  loading?: "eager" | "lazy";
  class?: string;
}
const {
  src,
  alt,
  width,
  height,
  loading = "lazy",
  class: className,
} = Astro.props;
---

<AstroImage
  src={src}
  alt={alt}
  width={width}
  height={height}
  loading={loading}
  class={className}
/>
  • Astro-native (not shadcn) — for storefront product images in MP-1/MP-2.
  • Wraps astro:assets <Image> with project defaults (loading="lazy").

6.5 src/layouts/Layout.astro

---
import { Font } from "astro:assets";
import "../styles/global.css";
import MetaHead from "@/components/seo/MetaHead.astro";

interface Props {
  title: string;
  description: string;
  canonical?: string;
  image?: string;
  noindex?: boolean;
}
const props = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <Font cssVariable="--font-sans" preload />
    <MetaHead {...props} />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  </head>
  <body>
    <slot />
  </body>
</html>
  • Imports global.css (which imports tokens.css + Tailwind).
  • <Font> emits the @font-face for Inter + the --font-sans CSS variable + preload link.
  • <MetaHead> handles all SEO meta. Props passed through from the page.
  • No header/footer/nav yet — those come in MP-2 (storefront layout) and MP-9 (footer with legal links).

6.6 src/pages/index.astro — smoke-test home

---
import Layout from "@/layouts/Layout.astro";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
---

<Layout title="Pink Pulsar" description="Pink Pulsar storefront.">
  <main class="mx-auto max-w-2xl p-8">
    <Card>
      <CardHeader>
        <CardTitle>Pink Pulsar</CardTitle>
        <CardDescription
          >Skeleton ready. Storefront coming in MP-2.</CardDescription
        >
      </CardHeader>
      <CardContent>
        <Button>Get started</Button>
      </CardContent>
    </Card>
  </main>
</Layout>
  • Zero JS shipped — no client:* directive. Astro renders the React Card and Button to static HTML at build.
  • Replaces the starter Welcome.astro content.
  • This page will be replaced by the real home page in MP-2.

7. ESLint isolation rules (§3.5 enforcement)

7.1 eslint.config.mjs — full shape

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import astro from "eslint-plugin-astro";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  { ignores: ["dist/", ".astro/", ".wrangler/", "node_modules/"] },
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  ...astro.configs.recommended,
  // per-file no-restricted-imports blocks (§7.2)
  prettierConfig, // must be last — disables conflicting style rules
);

7.2 no-restricted-imports per file pattern

Enforced on .ts files (and .astro frontmatter imports via typescript-eslint). eslint-plugin-astro does not support no-restricted-imports cleanly on .astro template body, but frontmatter imports are TS and covered.

src/actions/**/*.ts — may import services + core only:

{
  files: ["src/actions/**/*.ts"],
  rules: {
    "no-restricted-imports": ["error", {
      patterns: [
        { group: ["src/lib/bindings/*", "@/lib/bindings/*"], message: "Actions may import services + core only, never concrete adapters." },
        { group: ["src/features/*/repository"], message: "Actions import services, not repositories directly." },
      ],
    }],
  },
}

src/features/**/service.ts — may import own repository (interface), binding types (interfaces only), db schemas, core; blocked from concrete adapters + other features' internals:

{
  files: ["src/features/**/service.ts"],
  rules: {
    "no-restricted-imports": ["error", {
      patterns: [
        { group: ["src/lib/bindings/*/!(types)*", "@/lib/bindings/*/!(types)*"], message: "Services import binding *types.ts* (interfaces) only — never concrete adapters. Wire adapters in src/lib/app/ instead." },
        { group: ["src/features/*/!(service|repository|types)"], message: "A feature service may not import another feature's internals." },
        { group: ["src/pages/*"], message: "Services must not import pages." },
        { group: ["src/actions/*"], message: "Services must not import actions." },
      ],
    }],
  },
}

src/lib/bindings/**/*.ts + src/lib/db/**/*.ts — must not import features/pages/actions (reverse dependency):

{
  files: ["src/lib/bindings/**/*.ts", "src/lib/db/**/*.ts"],
  rules: {
    "no-restricted-imports": ["error", {
      patterns: [
        { group: ["src/features/*", "@/features/*"], message: "Bindings/DB must not import features (reverse dependency)." },
        { group: ["src/pages/*", "src/actions/*"], message: "Bindings/DB must not import pages or actions." },
      ],
    }],
  },
}

src/lib/app/prod.ts + src/lib/app/test.ts — exception, these ARE allowed to import concrete adapters:

{
  files: ["src/lib/app/prod.ts", "src/lib/app/test.ts"],
  rules: { "no-restricted-imports": "off" },
}

src/pages/**/*.astro — pages import services + components + layouts only:

{
  files: ["src/pages/**/*.astro"],
  rules: {
    "no-restricted-imports": ["error", {
      patterns: [
        { group: ["src/lib/bindings/*", "@/lib/bindings/*"], message: "Pages import services + components + layouts only — never bindings." },
        { group: ["src/lib/db/*", "@/lib/db/*"], message: "Pages must not import DB layer directly." },
        { group: ["src/features/*/repository"], message: "Pages import services, not repositories." },
      ],
    }],
  },
}

7.3 Glob pattern caveat

The negation glob !(types) is a pattern approximation. The exact syntax may need tuning during implementation. The implementation plan includes a verification test that imports a forbidden path and asserts ESLint errors. If a pattern proves unenforceable via globs, fall back to a custom ESLint rule (~20 lines) that checks the import path against the §3.5 matrix. This is an implementation detail, not a spec change.

7.4 MP-0a enforcement scope

MP-0a creates the rule blocks. Most target directories (src/features/*, src/actions/*, src/lib/bindings/*, src/lib/db/*, src/lib/app/*) don't exist yet — they're created in MP-0b/MP-0d/MP-1+. The rules are still written now so that when those directories appear, isolation is enforced from day one. MP-0a's own files (src/components/ui/*, src/lib/utils.ts, src/components/seo/*, src/layouts/*, src/pages/*) are not blocked by any rule.


8. Tests

8.1 tests/smoke.test.ts

Uses Vitest + astro/container Container API:

import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { describe, it, expect } from "vitest";
import Layout from "@/layouts/Layout.astro";

describe("MP-0a smoke", () => {
  it("Layout renders with title, description, canonical, OG tags", async () => {
    const container = await AstroContainer.create();
    const result = await container.renderToString(Layout, {
      slots: { default: "<p>smoke</p>" },
      props: { title: "Test", description: "Test desc" },
    });
    expect(result).toContain("<title>Test</title>");
    expect(result).toContain('<meta name="description" content="Test desc"');
    expect(result).toContain('rel="canonical"');
    expect(result).toContain("og:title");
    expect(result).toContain("twitter:card");
    expect(result).toContain("smoke");
  });

  it("Layout emits Font CSS variable and preload", async () => {
    const container = await AstroContainer.create();
    const result = await container.renderToString(Layout, {
      props: { title: "T", description: "D" },
    });
    expect(result).toContain("--font-sans");
  });

  it("index page renders shadcn Card + Button as static HTML (no astro-island)", async () => {
    const container = await AstroContainer.create();
    // Render index.astro — assert Card title "Pink Pulsar" present
    // and NO <astro-island> wrapper in output (zero-JS guarantee)
    // (Implementation detail: import index.astro and renderToString it)
  });
});

Key assertion: shadcn components rendered without client:* produce plain HTML — no <astro-island> custom element, no hydration script. This is the Lighthouse-100 guarantee for the storefront path.

8.2 tests/lint.test.ts

Thin test that shells out to pnpm lint and pnpm format:check and asserts exit 0. Keeps the "lint passes" + "format passes" gates enforceable from vitest run.

import { describe, it, expect } from "vitest";
import { spawnSync } from "node:child_process";

describe("MP-0a lint + format", () => {
  it("pnpm lint exits 0", () => {
    const result = spawnSync("pnpm", ["lint"], { stdio: "pipe", shell: true });
    expect(result.status).toBe(0);
  });
  it("pnpm format:check exits 0", () => {
    const result = spawnSync("pnpm", ["format:check"], {
      stdio: "pipe",
      shell: true,
    });
    expect(result.status).toBe(0);
  });
});

(spawnSync returns an object with .status; execSync throws on non-zero exit and returns a Buffer, so spawnSync is the correct choice for exit-code assertions.)

8.3 Isolation rule verification

A test (or a manual verification step in the implementation plan) that creates a temporary file violating each no-restricted-imports rule and asserts ESLint errors. This proves the §3.5 enforcement is wired correctly. If done as a test, it lives in tests/isolation.test.ts and uses execSync('pnpm lint -- ...') on a fixture file.


9. Verification commands (run at end of MP-0a)

# 1. Install all new deps
pnpm install

# 2. Initialize shadcn (generates components.json + src/lib/utils.ts + src/styles/global.css)
pnpm dlx shadcn@latest init

# 3. Add the 4 base components (generates src/components/ui/*.tsx)
pnpm dlx shadcn@latest add button input card label

# 4. Typecheck (astro check — strict passes)
pnpm typecheck

# 5. Lint (ESLint 10 flat config + isolation rules pass)
pnpm lint

# 6. Format check (Prettier passes)
pnpm format:check

# 7. Tests (vitest run — smoke + lint tests green)
pnpm test

# 8. Build (astro build — SSG build succeeds, dist/ contains static HTML for /)
pnpm build

# 9. Dev server boots
pnpm dev --background
# visit http://localhost:4321 — see Card + Button rendered as static HTML
# view source — no React JS shipped, no <astro-island> for Card/Button
astro dev stop

10. Success criteria

  • pnpm typecheck exits 0 (strict TS passes, including noUncheckedIndexedAccess + exactOptionalPropertyTypes)
  • pnpm lint exits 0 (ESLint 10 flat config + §3.5 isolation rules pass)
  • pnpm format:check exits 0 (Prettier passes)
  • pnpm test exits 0 (all smoke + lint tests green)
  • pnpm build succeeds — dist/ contains static HTML for /
  • Built / page source has NO <astro-island> tags (zero JS shipped) — preserves Lighthouse 100 path
  • Built / page source contains <title>Pink Pulsar</title>, <meta name="description">, <link rel="canonical">, OG tags, Twitter card tags
  • Built / page source contains Inter font preload + --font-sans CSS variable
  • ESLint isolation rule verified: a test import of @/lib/bindings/* from a feature service triggers an error (or the rule pattern is documented as enforceable)
  • .dev.vars.example committed; .dev.vars gitignored
  • Welcome.astro, src/assets/astro.svg, src/assets/background.svg removed
  • package.json engines.node is >=22.13.0
  • @/* path alias resolves in both TS and Astro imports

11. Dependencies on other MPs

Depends on: None. MP-0a is the first mini-plan. The plan-of-plans §4 amendments (real shadcn via @astrojs/react, Vitest via getViteConfig(), astro/zod naming, stable Fonts API) are already applied to the parent spec.

Unblocks: Every subsequent MP. Specifically:

  • MP-0b (storage plumbing) — uses the @/* alias, ESLint isolation rules, Vitest setup
  • MP-0c (CI/CD) — uses pnpm typecheck, pnpm lint, pnpm test, pnpm build scripts
  • MP-0d (cross-cutting runtime) — uses the Layout, ESLint rules, Vitest setup
  • MP-1/MP-2 (catalog) — uses Image.astro, shadcn components, MetaHead.astro, design tokens
  • MP-3+ (cart/checkout/admin) — use shadcn components as islands with client:*

12. Open items — none

All decisions locked during the brainstorming session. No TBDs.

MP-0a — Repo Skeleton & Tooling Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Turn the fresh Astro 7 starter into a production-ready skeleton with strict TS, Tailwind v4, shadcn React components (server-rendered, zero JS), SEO head, native fonts, ESLint isolation rules, Prettier, and Vitest.

Architecture: Astro 7 static-first (SSG) on Cloudflare Workers via @astrojs/cloudflare. shadcn React components rendered server-side via @astrojs/react — no client:* directives means zero React JS shipped. Tailwind v4 via @tailwindcss/vite with shadcn CSS-variable design tokens (pink primary, light-only). Vitest via getViteConfig() from astro/config + astro/container Container API for smoke tests.

Tech Stack: Astro 7, @astrojs/cloudflare 14, @astrojs/react, React 19, Tailwind v4, shadcn, ESLint 10 flat config, Prettier, Vitest, TypeScript strict

Global Constraints

  • Node engine: >=22.13.0 (ESLint 10 requirement; note: eslint-plugin-astro requires >=22.22.3 — if lint fails on Node version, bump engines)
  • Package manager: pnpm
  • Astro 7 static output (SSG) — no output override in astro.config.mjs
  • shadcn components server-rendered only — zero client:* directives in MP-0a
  • Pink primary: hsl(330 81% 60%), light-only (no .dark block)
  • Font: Inter via Fontsource, --font-sans CSS variable
  • Path alias: @/*./src/*
  • TS strict: astro/tsconfigs/strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes + jsx: "react-jsx"
  • Design spec: docs/superpowers/specs/2026-07-07-MP-0a-repo-skeleton-tooling-design.md
  • Brand name: "Pink Pulsar"
  • Site URL: https://pink-pulsar.workers.dev

Spec corrections applied (from doc research)

  1. @astrojs/check + typescript devDeps — required for astro check to work. Not listed in spec §4.3 but mandatory. Added to Task 2.
  2. tailwindStylesheet in .prettierrcprettier-plugin-tailwindcss for Tailwind v4 requires tailwindStylesheet: "./src/styles/global.css". Not in spec §4.7. Added to Task 9.
  3. Container API renderer loading — rendering React components in tests requires loadRenderers from astro:container + getContainerRenderer from @astrojs/react/container-renderer. Spec §8.1 omits this. Added to Task 10.
  4. jsxImportSource: "react" in tsconfig — recommended by @astrojs/react docs. Added alongside jsx: "react-jsx".

File Structure

Create:

  • src/styles/tokens.css — shadcn CSS variables (pink primary, light-only)
  • src/styles/global.css — Tailwind v4 entry + token mapping + base layer
  • src/lib/utils.tscn() helper (clsx + tailwind-merge)
  • src/components/ui/button.tsx — shadcn Button (generated by CLI)
  • src/components/ui/input.tsx — shadcn Input (generated by CLI)
  • src/components/ui/card.tsx — shadcn Card (generated by CLI)
  • src/components/ui/label.tsx — shadcn Label (generated by CLI)
  • src/components/seo/MetaHead.astro — server-only SEO head
  • src/components/Image.astro — Astro wrapper around astro:assets
  • src/env.d.ts — Astro type reference
  • components.json — shadcn config
  • .dev.vars — placeholder secrets (gitignored)
  • .dev.vars.example — template (committed)
  • eslint.config.mjs — ESLint 10 flat config + isolation rules
  • .prettierrc — Prettier config (astro + tailwind plugins)
  • .prettierignore
  • vitest.config.tsgetViteConfig() from astro/config
  • tests/smoke.test.ts — Container API smoke tests
  • tests/lint.test.ts — lint + format gate tests
  • tests/isolation.test.ts — ESLint isolation rule verification

Modify:

  • astro.config.mjs — add site, fonts, react(), @tailwindcss/vite
  • package.json — engines bump, add deps + devDeps, add 7 scripts
  • tsconfig.json — add compilerOptions (paths, strict flags, jsx)
  • pnpm-workspace.yaml — fix allowBuilds (esbuild/sharp/workerd = true)
  • .gitignore — add .dev.vars
  • src/layouts/Layout.astro — full rewrite (Font + MetaHead + global.css)
  • src/pages/index.astro — full rewrite (smoke-test home with Card + Button)
  • README.md — replace starter content with project description

Delete:

  • src/components/Welcome.astro
  • src/assets/astro.svg
  • src/assets/background.svg

Task 1: Foundation configs

Files:

  • Modify: pnpm-workspace.yaml
  • Modify: package.json (engines field only)
  • Modify: tsconfig.json
  • Modify: .gitignore
  • Create: src/env.d.ts
  • Create: .dev.vars.example
  • Create: .dev.vars

Interfaces:

  • Produces: tsconfig.json with @/* path alias (consumed by all subsequent tasks); pnpm-workspace.yaml with valid allowBuilds (consumed by Task 2 pnpm install); worker-configuration.d.ts (consumed by astro check in later tasks)

  • Step 1: Fix pnpm-workspace.yaml

Replace the entire file (current has broken placeholder strings):

allowBuilds:
  esbuild: true
  sharp: true
  workerd: true
  • Step 2: Bump package.json engines.node

Change >=22.12.0 to >=22.13.0:

"engines": {
  "node": ">=22.13.0"
}
  • Step 3: Update tsconfig.json

Replace the entire file:

{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*", "./worker-configuration.d.ts"],
  "exclude": ["dist"],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] },
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}
  • Step 4: Add .dev.vars to .gitignore

Append after the .env.production line:

.env
.env.production
.dev.vars
  • Step 5: Create src/env.d.ts
/// <reference path="../.astro/types.d.ts" />
  • Step 6: Create .dev.vars.example
# Razorpay (MP-5)
RAZORPAY_KEY=
RAZORPAY_SECRET=
# SMTP (MP-6b)
SMTP_URL=
# Admin shared secret (MP-7)
ADMIN_SECRET=
# HMAC signing key for signed links (MP-4, MP-8)
SIGNING_KEY=
  • Step 7: Create .dev.vars

Same content as .dev.vars.example (placeholder empty values). This file is gitignored.

  • Step 8: Generate worker-configuration.d.ts

The generate-types npm script is defined in the existing package.json (it's one of the 5 starter scripts), so this works without waiting for Task 2.

Run:

pnpm generate-types

Expected: creates worker-configuration.d.ts at repo root with Cloudflare Worker type declarations. If this fails because wrangler can't find the dist/ directory, run pnpm build first, then retry.

  • Step 9: Verify install works with new workspace config

Run:

pnpm install

Expected: install succeeds, no errors about allowBuilds placeholders.

  • Step 10: Commit
git add pnpm-workspace.yaml package.json tsconfig.json .gitignore src/env.d.ts .dev.vars.example .dev.vars worker-configuration.d.ts
git commit -m "chore: foundation configs — strict tsconfig, pnpm workspace, env files"

Task 2: Install dependencies + package.json scripts

Files:

  • Modify: package.json (add deps, devDeps, scripts)

Interfaces:

  • Produces: all tooling deps installed; 7 new npm scripts (lint, lint:fix, format, format:check, test, test:watch, typecheck)

  • Step 1: Install React dependencies

pnpm add react react-dom @astrojs/react
  • Step 2: Install Tailwind v4 dependencies
pnpm add -D tailwindcss @tailwindcss/vite
  • Step 3: Install ESLint dependencies
pnpm add -D eslint @eslint/js typescript-eslint eslint-plugin-astro eslint-config-prettier
  • Step 4: Install Prettier dependencies
pnpm add -D prettier prettier-plugin-astro prettier-plugin-tailwindcss
  • Step 5: Install Vitest
pnpm add -D vitest
  • Step 6: Install shadcn dependencies
pnpm add -D clsx tailwind-merge class-variance-authority lucide-react @types/react @types/react-dom
  • Step 7: Install astro check dependencies
pnpm add -D @astrojs/check typescript
  • Step 8: Add npm scripts to package.json

Add these to the "scripts" object (keeping existing scripts):

"scripts": {
  "dev": "astro dev",
  "build": "astro build",
  "preview": "astro preview",
  "astro": "astro",
  "generate-types": "wrangler types",
  "lint": "eslint .",
  "lint:fix": "eslint . --fix",
  "format": "prettier --write .",
  "format:check": "prettier --check .",
  "test": "vitest run",
  "test:watch": "vitest",
  "typecheck": "astro check"
}
  • Step 9: Verify install

Run:

pnpm install

Expected: succeeds, lockfile updated.

  • Step 10: Commit
git add package.json pnpm-lock.yaml
git commit -m "chore: install dependencies and add package.json scripts"

Task 3: astro.config.mjs — site, fonts, react, tailwind

Files:

  • Modify: astro.config.mjs (full rewrite)

Interfaces:

  • Produces: Astro config with site set (consumed by MetaHead for canonical URLs), fonts config (consumed by <Font> component in Layout), react() integration (consumed by shadcn components), @tailwindcss/vite plugin (consumed by global.css)

  • Step 1: Rewrite astro.config.mjs

Replace the entire file:

// @ts-check
import { defineConfig, fontProviders } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  site: "https://pink-pulsar.workers.dev",
  adapter: cloudflare(),
  integrations: [react()],
  vite: { plugins: [tailwindcss()] },
  fonts: [
    {
      provider: fontProviders.fontsource(),
      name: "Inter",
      cssVariable: "--font-sans",
      weights: ["400 700"],
      styles: ["normal"],
      subsets: ["latin"],
    },
  ],
});
  • Step 2: Verify dev server boots

Run:

pnpm dev --background

Then:

astro dev status

Expected: server running on http://localhost:4321.

Stop:

astro dev stop
  • Step 3: Commit
git add astro.config.mjs
git commit -m "feat: configure astro.config — site, fonts, react, tailwind"

Task 4: Design tokens, global CSS, shadcn config, cn() helper

Files:

  • Create: src/styles/tokens.css
  • Create: src/styles/global.css
  • Create: components.json
  • Create: src/lib/utils.ts

Interfaces:

  • Produces: tokens.css with pink-primary shadcn CSS variables (consumed by shadcn components via hsl(var(--token))); global.css with Tailwind v4 + @theme inline token mapping (consumed by Layout); components.json (consumed by shadcn add in Task 5); cn() helper (consumed by shadcn components)

  • Step 1: Create src/styles/tokens.css

:root {
  --background: 0 0% 100%;
  --foreground: 240 10% 3.9%;
  --card: 0 0% 100%;
  --card-foreground: 240 10% 3.9%;
  --popover: 0 0% 100%;
  --popover-foreground: 240 10% 3.9%;
  --primary: 330 81% 60%;
  --primary-foreground: 0 0% 100%;
  --secondary: 240 4.8% 95.9%;
  --secondary-foreground: 240 5.9% 10%;
  --muted: 240 4.8% 95.9%;
  --muted-foreground: 240 3.8% 46.1%;
  --accent: 240 4.8% 95.9%;
  --accent-foreground: 240 5.9% 10%;
  --destructive: 0 84.2% 60.2%;
  --destructive-foreground: 0 0% 98%;
  --border: 240 5.9% 90%;
  --input: 240 5.9% 90%;
  --ring: 330 81% 60%;
  --radius: 0.5rem;
}
  • Step 2: Create src/styles/global.css
@import "tailwindcss";
@import "./tokens.css";

@theme inline {
  --font-sans: var(--font-sans);
  --color-background: hsl(var(--background));
  --color-foreground: hsl(var(--foreground));
  --color-card: hsl(var(--card));
  --color-card-foreground: hsl(var(--card-foreground));
  --color-popover: hsl(var(--popover));
  --color-popover-foreground: hsl(var(--popover-foreground));
  --color-primary: hsl(var(--primary));
  --color-primary-foreground: hsl(var(--primary-foreground));
  --color-secondary: hsl(var(--secondary));
  --color-secondary-foreground: hsl(var(--secondary-foreground));
  --color-muted: hsl(var(--muted));
  --color-muted-foreground: hsl(var(--muted-foreground));
  --color-accent: hsl(var(--accent));
  --color-accent-foreground: hsl(var(--accent-foreground));
  --color-destructive: hsl(var(--destructive));
  --color-destructive-foreground: hsl(var(--destructive-foreground));
  --color-border: hsl(var(--border));
  --color-input: hsl(var(--input));
  --color-ring: hsl(var(--ring));
  --radius-lg: var(--radius);
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
    font-family: var(--font-sans);
  }
}
  • Step 3: Create components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/styles/global.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks",
    "utils": "@/lib/utils"
  },
  "iconLibrary": "lucide"
}
  • Step 4: Create src/lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
  • Step 5: Verify typecheck

Run:

pnpm typecheck

Expected: exit 0 (no errors). If astro check reports errors about missing modules, run pnpm install first and retry.

  • Step 6: Commit
git add src/styles/tokens.css src/styles/global.css components.json src/lib/utils.ts
git commit -m "feat: add design tokens, global CSS, shadcn config, cn() helper"

Task 5: shadcn UI components — Button, Input, Card, Label

Files:

  • Create: src/components/ui/button.tsx (via CLI)
  • Create: src/components/ui/input.tsx (via CLI)
  • Create: src/components/ui/card.tsx (via CLI)
  • Create: src/components/ui/label.tsx (via CLI)

Interfaces:

  • Consumes: components.json (Task 4), src/lib/utils.ts cn() (Task 4), @astrojs/react integration (Task 3)

  • Produces: Button export from @/components/ui/button, Input from @/components/ui/input, Card + CardHeader + CardTitle + CardDescription + CardContent + CardFooter from @/components/ui/card, Label from @/components/ui/label (consumed by Task 7 smoke page)

  • Step 1: Run shadcn add

pnpm dlx shadcn@latest add button input card label

Expected: generates 4 .tsx files in src/components/ui/. May install @radix-ui/react-label for the Label component. If the CLI prompts for confirmation, answer yes.

If the CLI fails because shadcn init was not run first, run:

pnpm dlx shadcn@latest init -y

Then re-run the add command. After init, verify components.json and src/lib/utils.ts still match Task 4's content — restore them if init overwrote them.

  • Step 2: Verify all 4 files exist

Check that these files were created:

  • src/components/ui/button.tsx

  • src/components/ui/input.tsx

  • src/components/ui/card.tsx

  • src/components/ui/label.tsx

  • Step 3: Check if global.css was modified by shadcn

Read src/styles/global.css and verify it still matches Task 4 Step 2. If shadcn added extra @import lines (e.g. @import "tw-animate-css" or @import "shadcn/tailwind.css"), remove them — our global.css is the authoritative version.

  • Step 4: Verify typecheck

Run:

pnpm typecheck

Expected: exit 0. The shadcn .tsx files use @/lib/utils import which resolves via the @/* path alias.

  • Step 5: Commit
git add src/components/ui/ package.json pnpm-lock.yaml
git commit -m "feat: add shadcn UI components (button, input, card, label)"

Also add src/styles/global.css to the commit if it was modified and restored.


Task 6: MetaHead.astro + Image.astro

Files:

  • Create: src/components/seo/MetaHead.astro
  • Create: src/components/Image.astro

Interfaces:

  • Produces: MetaHead component accepting { title, description, canonical?, image?, noindex? } props (consumed by Layout in Task 7); Image component accepting { src, alt, width?, height?, loading?, class? } props (consumed by later MPs for product images)

  • Step 1: Create src/components/seo/MetaHead.astro

---
interface Props {
  title: string;
  description: string;
  canonical?: string;
  image?: string;
  noindex?: boolean;
}
const { title, description, canonical, image, noindex } = Astro.props;
const site = Astro.site ?? new URL(Astro.url.origin);
const canonicalURL = canonical
  ? new URL(canonical, site)
  : new URL(Astro.url.pathname, site);
const ogImage = image ? new URL(image, site) : undefined;
---

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL.href} />
<meta name="robots" content={noindex ? "noindex, nofollow" : "index, follow"} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL.href} />
{ogImage && <meta property="og:image" content={ogImage.href} />}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{ogImage && <meta name="twitter:image" content={ogImage.href} />}
  • Step 2: Create src/components/Image.astro
---
import { Image as AstroImage } from "astro:assets";

interface Props {
  src: ImageMetadata;
  alt: string;
  width?: number;
  height?: number;
  loading?: "eager" | "lazy";
  class?: string;
}
const {
  src,
  alt,
  width,
  height,
  loading = "lazy",
  class: className,
} = Astro.props;
---

<AstroImage
  src={src}
  alt={alt}
  width={width}
  height={height}
  loading={loading}
  class={className}
/>
  • Step 3: Verify typecheck

Run:

pnpm typecheck

Expected: exit 0.

  • Step 4: Commit
git add src/components/seo/MetaHead.astro src/components/Image.astro
git commit -m "feat: add MetaHead SEO component and Image wrapper"

Task 7: Layout rewrite, smoke-test home page, starter cleanup

Files:

  • Modify: src/layouts/Layout.astro (full rewrite)
  • Modify: src/pages/index.astro (full rewrite)
  • Modify: README.md
  • Delete: src/components/Welcome.astro
  • Delete: src/assets/astro.svg
  • Delete: src/assets/background.svg

Interfaces:

  • Consumes: MetaHead (Task 6), Font from astro:assets, global.css (Task 4), Card + Button from shadcn (Task 5)

  • Produces: Layout accepting { title, description, canonical?, image?, noindex? } props (consumed by all pages in later MPs); smoke-test home page at /

  • Step 1: Rewrite src/layouts/Layout.astro

Replace the entire file:

---
import { Font } from "astro:assets";
import "../styles/global.css";
import MetaHead from "@/components/seo/MetaHead.astro";

interface Props {
  title: string;
  description: string;
  canonical?: string;
  image?: string;
  noindex?: boolean;
}
const props = Astro.props;
---

<!doctype html>
<html lang="en">
  <head>
    <Font cssVariable="--font-sans" preload />
    <MetaHead {...props} />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  </head>
  <body>
    <slot />
  </body>
</html>
  • Step 2: Rewrite src/pages/index.astro

Replace the entire file:

---
import Layout from "@/layouts/Layout.astro";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
---

<Layout title="Pink Pulsar" description="Pink Pulsar storefront.">
  <main class="mx-auto max-w-2xl p-8">
    <Card>
      <CardHeader>
        <CardTitle>Pink Pulsar</CardTitle>
        <CardDescription
          >Skeleton ready. Storefront coming in MP-2.</CardDescription
        >
      </CardHeader>
      <CardContent>
        <Button>Get started</Button>
      </CardContent>
    </Card>
  </main>
</Layout>
  • Step 3: Delete starter cruft
Remove-Item -LiteralPath "src/components/Welcome.astro"
Remove-Item -LiteralPath "src/assets/astro.svg"
Remove-Item -LiteralPath "src/assets/background.svg"

If src/assets/ is now empty, leave the directory (Astro expects it may exist) or remove it:

Remove-Item -LiteralPath "src/assets" -Recurse -Force
  • Step 4: Rewrite README.md

Replace the entire file:

# Pink Pulsar

Astro 7 ecommerce storefront on Cloudflare Workers.

## Development

```sh
pnpm install
pnpm dev
```

Scripts

  • pnpm dev — start dev server
  • pnpm build — production build
  • pnpm preview — preview build
  • pnpm typecheck — TypeScript + Astro type checking
  • pnpm lint — ESLint
  • pnpm format — Prettier format
  • pnpm test — Vitest

- [ ] **Step 5: Verify dev server renders the smoke page**

Run:
```bash
pnpm dev --background

Then fetch the page (use curl.exe explicitly — in PowerShell, bare curl resolves to Invoke-WebRequest which returns a response object, not raw text):

curl.exe -s http://localhost:4321 | Select-String "Pink Pulsar"

Expected: output contains "Pink Pulsar" (Card title rendered).

Check for zero JS:

curl.exe -s http://localhost:4321 | Select-String "astro-island"

Expected: no output (no <astro-island> tags — React components server-rendered to static HTML).

Stop:

astro dev stop
  • Step 6: Verify typecheck

Run:

pnpm typecheck

Expected: exit 0.

  • Step 7: Commit
git add src/layouts/Layout.astro src/pages/index.astro README.md
git rm src/components/Welcome.astro src/assets/astro.svg src/assets/background.svg
git commit -m "feat: rewrite Layout with fonts/SEO, smoke-test home page, remove starter cruft"

Task 8: ESLint 10 flat config + layer-isolation rules

Files:

  • Create: eslint.config.mjs

Interfaces:

  • Produces: ESLint config that enforces §3.5 layer isolation via no-restricted-imports (consumed by pnpm lint in all subsequent tasks; verified by Task 11 isolation tests)

  • Step 1: Create eslint.config.mjs

// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import astro from "eslint-plugin-astro";
import prettierConfig from "eslint-config-prettier";

export default tseslint.config(
  { ignores: ["dist/", ".astro/", ".wrangler/", "node_modules/"] },
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  ...astro.configs.recommended,

  // src/actions/** — may import services + core only, never concrete adapters
  {
    files: ["src/actions/**/*.ts"],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: [
            {
              group: ["src/lib/bindings/*", "@/lib/bindings/*"],
              message:
                "Actions may import services + core only, never concrete adapters.",
            },
            {
              group: ["src/features/*/repository"],
              message: "Actions import services, not repositories directly.",
            },
          ],
        },
      ],
    },
  },

  // src/features/**/service.ts — may import own repository (interface), binding types, db schemas, core
  {
    files: ["src/features/**/service.ts"],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: [
            {
              group: ["src/lib/bindings/*", "@/lib/bindings/*"],
              message:
                "Services import binding types.ts (interfaces) only — never concrete adapters. Wire adapters in src/lib/app/ instead.",
            },
            {
              group: ["src/features/*"],
              message:
                "A feature service may not import another feature's internals.",
            },
            {
              group: ["src/pages/*"],
              message: "Services must not import pages.",
            },
            {
              group: ["src/actions/*"],
              message: "Services must not import actions.",
            },
          ],
        },
      ],
    },
  },

  // src/lib/bindings/** + src/lib/db/** — must not import features/pages/actions (reverse dependency)
  {
    files: ["src/lib/bindings/**/*.ts", "src/lib/db/**/*.ts"],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: [
            {
              group: ["src/features/*", "@/features/*"],
              message:
                "Bindings/DB must not import features (reverse dependency).",
            },
            {
              group: ["src/pages/*", "src/actions/*"],
              message: "Bindings/DB must not import pages or actions.",
            },
          ],
        },
      ],
    },
  },

  // src/lib/app/prod.ts + src/lib/app/test.ts — exception, allowed to import concrete adapters
  {
    files: ["src/lib/app/prod.ts", "src/lib/app/test.ts"],
    rules: { "no-restricted-imports": "off" },
  },

  // src/pages/**.astro — pages import services + components + layouts only
  {
    files: ["src/pages/**/*.astro"],
    rules: {
      "no-restricted-imports": [
        "error",
        {
          patterns: [
            {
              group: ["src/lib/bindings/*", "@/lib/bindings/*"],
              message:
                "Pages import services + components + layouts only — never bindings.",
            },
            {
              group: ["src/lib/db/*", "@/lib/db/*"],
              message: "Pages must not import DB layer directly.",
            },
            {
              group: ["src/features/*/repository"],
              message: "Pages import services, not repositories.",
            },
          ],
        },
      ],
    },
  },

  prettierConfig, // must be last — disables conflicting style rules
);

Note: the negation glob !(types) from spec §7.2 is omitted here because the glob syntax is unreliable in ESLint's no-restricted-imports. The broader rule (blocking all @/lib/bindings/* imports from services) is stricter and enforceable. The src/lib/app/prod.ts + src/lib/app/test.ts exception block allows the adapter wiring point. This is a documented implementation detail per spec §7.3.

  • Step 2: Run lint

Run:

pnpm lint

Expected: exit 0. MP-0a's own files (src/components/ui/*, src/lib/utils.ts, src/components/seo/*, src/layouts/*, src/pages/*) are not blocked by any rule.

If lint fails on .astro files with parser errors, verify eslint-plugin-astro is installed and the ...astro.configs.recommended spread is correct.

  • Step 3: Commit
git add eslint.config.mjs
git commit -m "feat: add ESLint 10 flat config with layer-isolation rules"

Task 9: Prettier config + format all files

Files:

  • Create: .prettierrc
  • Create: .prettierignore

Interfaces:

  • Produces: Prettier config with Astro + Tailwind plugins (consumed by pnpm format / pnpm format:check)

  • Step 1: Create .prettierrc

{
  "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
  "tailwindStylesheet": "./src/styles/global.css",
  "overrides": [{ "files": "*.astro", "options": { "parser": "astro" } }]
}

prettier-plugin-tailwindcss must be the last entry in plugins (per Tailwind docs). tailwindStylesheet is required for Tailwind v4 class sorting (points the plugin to the CSS file containing @theme).

  • Step 2: Create .prettierignore
dist/
.astro/
.wrangler/
node_modules/
pnpm-lock.yaml
  • Step 3: Format all files

Run:

pnpm format

Expected: all files formatted, exit 0. This will reformat existing files (configs, components, etc.) to Prettier style.

  • Step 4: Verify format check passes

Run:

pnpm format:check

Expected: exit 0.

  • Step 5: Verify lint still passes after formatting

Run:

pnpm lint

Expected: exit 0.

  • Step 6: Verify typecheck still passes

Run:

pnpm typecheck

Expected: exit 0.

  • Step 7: Commit
git add .prettierrc .prettierignore
git add -A
git commit -m "feat: add Prettier config with astro and tailwind plugins"

Task 10: Vitest config + smoke tests

Files:

  • Create: vitest.config.ts
  • Create: tests/smoke.test.ts

Interfaces:

  • Consumes: getViteConfig from astro/config (inherits Astro integrations, Vite plugins, path aliases); Layout (Task 7); index page (Task 7); React renderer from @astrojs/react/container-renderer (Task 2)

  • Produces: Vitest config + smoke test suite verifying Layout renders meta tags, Font CSS variable, and index page has no <astro-island> (zero-JS guarantee)

  • Step 1: Create vitest.config.ts

/// <reference types="vitest/config" />
import { getViteConfig } from "astro/config";

export default getViteConfig({
  test: {
    environment: "node",
  },
});

environment: 'node' is required — Astro 6+ removed the ability to render Astro components in Vitest client environments (jsdom, happy-dom). The Container API renders to strings server-side.

  • Step 2: Create tests/smoke.test.ts
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { getContainerRenderer as reactContainerRenderer } from "@astrojs/react/container-renderer";
import { loadRenderers } from "astro:container";
import { describe, it, expect } from "vitest";
import Layout from "@/layouts/Layout.astro";
import Index from "@/pages/index.astro";

describe("MP-0a smoke", () => {
  it("Layout renders with title, description, canonical, OG tags", async () => {
    const container = await AstroContainer.create();
    const result = await container.renderToString(Layout, {
      slots: { default: "<p>smoke</p>" },
      props: { title: "Test", description: "Test desc" },
      request: new Request("https://pink-pulsar.workers.dev/"),
    });
    expect(result).toContain("<title>Test</title>");
    expect(result).toContain('name="description" content="Test desc"');
    expect(result).toContain('rel="canonical"');
    expect(result).toContain("og:title");
    expect(result).toContain("twitter:card");
    expect(result).toContain("smoke");
  });

  it("Layout emits Font CSS variable and preload", async () => {
    const container = await AstroContainer.create();
    const result = await container.renderToString(Layout, {
      props: { title: "T", description: "D" },
      request: new Request("https://pink-pulsar.workers.dev/"),
    });
    expect(result).toContain("--font-sans");
  });

  it("index page renders shadcn Card + Button as static HTML (no astro-island)", async () => {
    const renderers = await loadRenderers([reactContainerRenderer()]);
    const container = await AstroContainer.create({ renderers });
    const result = await container.renderToString(Index, {
      request: new Request("https://pink-pulsar.workers.dev/"),
    });
    expect(result).toContain("Pink Pulsar");
    expect(result).toContain("Get started");
    expect(result).not.toContain("astro-island");
  });
});

Key points:

  • Test 1 & 2 render Layout only (pure Astro — no renderer needed).

  • Test 3 renders Index which contains React Card + Button — requires loadRenderers([reactContainerRenderer()]) to load the React server renderer.

  • request is passed so Astro.url is available in MetaHead (used for canonical URL).

  • expect(result).not.toContain("astro-island") is the zero-JS guarantee — server-rendered React without client:* produces plain HTML.

  • Step 3: Run smoke tests

Run:

pnpm test

Expected: all 3 smoke tests pass, exit 0.

If test 3 fails with "Cannot find module 'astro:container'", ensure vitest.config.ts uses getViteConfig() — the virtual module is only available through Astro's Vite plugins.

If test 3 fails with renderer errors, ensure @astrojs/react is in integrations in astro.config.mjs (Task 3).

  • Step 4: Commit
git add vitest.config.ts tests/smoke.test.ts
git commit -m "feat: add Vitest config and smoke tests via Container API"

Task 11: Lint/format gate tests + isolation rule verification

Files:

  • Create: tests/lint.test.ts
  • Create: tests/isolation.test.ts

Interfaces:

  • Consumes: pnpm lint + pnpm format:check scripts (Task 2); ESLint config with isolation rules (Task 8)

  • Produces: test suite that gates lint/format compliance and verifies §3.5 isolation rules are enforceable

  • Step 1: Create tests/lint.test.ts

import { describe, it, expect } from "vitest";
import { spawnSync } from "node:child_process";

describe("MP-0a lint + format gates", () => {
  it("pnpm lint exits 0", () => {
    const result = spawnSync("pnpm", ["lint"], { stdio: "pipe", shell: true });
    expect(result.status).toBe(0);
  });

  it("pnpm format:check exits 0", () => {
    const result = spawnSync("pnpm", ["format:check"], {
      stdio: "pipe",
      shell: true,
    });
    expect(result.status).toBe(0);
  });
});

spawnSync is used (not execSync) because it returns an object with .status instead of throwing on non-zero exit.

  • Step 2: Create tests/isolation.test.ts

This test uses the ESLint Node API (lintText) to verify that the no-restricted-imports rules trigger errors for forbidden imports. No temp files are created — lintText lints code as if it were at a given file path, which determines which rules apply.

import { describe, it, expect } from "vitest";
import { ESLint } from "eslint";

const eslint = new ESLint();

async function countErrors(code: string, filePath: string): Promise<number> {
  const results = await eslint.lintText(code, { filePath });
  return results[0]?.errorCount ?? 0;
}

describe("ESLint isolation rules (§3.5)", () => {
  it("actions cannot import concrete bindings", async () => {
    const code = `import { db } from "@/lib/bindings/d1";\nexport const x = db;\n`;
    const errors = await countErrors(code, "src/actions/test.ts");
    expect(errors).toBeGreaterThan(0);
  });

  it("actions cannot import repositories directly", async () => {
    const code = `import { repo } from "@/features/catalog/repository";\nexport const x = repo;\n`;
    const errors = await countErrors(code, "src/actions/test.ts");
    expect(errors).toBeGreaterThan(0);
  });

  it("services cannot import concrete bindings", async () => {
    const code = `import { d1 } from "@/lib/bindings/d1";\nexport const x = d1;\n`;
    const errors = await countErrors(code, "src/features/catalog/service.ts");
    expect(errors).toBeGreaterThan(0);
  });

  it("services cannot import pages", async () => {
    const code = `import { page } from "@/pages/index";\nexport const x = page;\n`;
    const errors = await countErrors(code, "src/features/catalog/service.ts");
    expect(errors).toBeGreaterThan(0);
  });

  it("bindings cannot import features (reverse dependency)", async () => {
    const code = `import { svc } from "@/features/catalog/service";\nexport const x = svc;\n`;
    const errors = await countErrors(code, "src/lib/bindings/d1.ts");
    expect(errors).toBeGreaterThan(0);
  });

  it("bindings cannot import pages", async () => {
    const code = `import { page } from "@/pages/index";\nexport const x = page;\n`;
    const errors = await countErrors(code, "src/lib/bindings/d1.ts");
    expect(errors).toBeGreaterThan(0);
  });
});

Note: .astro page isolation rules are not tested here because lintText does not reliably apply the astro-eslint-parser for .astro files via the API. The .ts isolation rules (actions, services, bindings) are the critical boundaries and are fully covered. The .astro page rules are verified manually in Task 12.

  • Step 3: Run all tests

Run:

pnpm test

Expected: all tests pass (3 smoke + 2 lint/format + 6 isolation = 11 tests), exit 0.

If isolation tests fail with "ESLint instance must be configured", ensure eslint.config.mjs exists (Task 8) and eslint is installed (Task 2).

  • Step 4: Commit
git add tests/lint.test.ts tests/isolation.test.ts
git commit -m "feat: add lint/format gate tests and isolation rule verification"

Task 12: Final verification

Files: none (verification only)

  • Step 1: Clean install
pnpm install

Expected: succeeds, no warnings about allowBuilds.

  • Step 2: Typecheck
pnpm typecheck

Expected: exit 0 (strict TS passes, including noUncheckedIndexedAccess + exactOptionalPropertyTypes).

  • Step 3: Lint
pnpm lint

Expected: exit 0 (ESLint 10 flat config + isolation rules pass).

  • Step 4: Format check
pnpm format:check

Expected: exit 0 (Prettier passes).

  • Step 5: Tests
pnpm test

Expected: exit 0 (all smoke + lint + isolation tests green).

  • Step 6: Build
pnpm build

Expected: SSG build succeeds, dist/ contains static HTML for /.

  • Step 7: Locate the built index.html

The @astrojs/cloudflare adapter may emit to dist/index.html or dist/client/index.html depending on adapter/wrangler defaults. Discover the actual path first:

Get-ChildItem dist -Recurse -Filter index.html | Select-Object -ExpandProperty FullName

Note the returned path (referred to below as <INDEX_PATH>). If multiple index.html files exist, pick the one under client/ or the root of dist/ — not under _worker.js/.

  • Step 8: Verify built page has zero JS (no astro-island)
Get-Content <INDEX_PATH> | Select-String "astro-island"

Expected: no output (zero <astro-island> tags).

  • Step 9: Verify built page has meta tags + font preload
Get-Content <INDEX_PATH> | Select-String "<title>Pink Pulsar</title>"
Get-Content <INDEX_PATH> | Select-String 'name="description"'
Get-Content <INDEX_PATH> | Select-String 'rel="canonical"'
Get-Content <INDEX_PATH> | Select-String "og:title"
Get-Content <INDEX_PATH> | Select-String "twitter:card"
Get-Content <INDEX_PATH> | Select-String "--font-sans"

Expected: all return matching lines.

  • Step 10: Verify dev server boots and renders
pnpm dev --background

Visit http://localhost:4321 — see Card + Button rendered. View source — no React JS shipped, no <astro-island> for Card/Button.

astro dev stop
  • Step 11: Verify .dev.vars is gitignored
git status --short

Expected: .dev.vars does NOT appear in untracked files (it's gitignored).

git ls-files .dev.vars

Expected: no output (file is not tracked).

  • Step 12: Confirm success criteria checklist

Verify each item from spec §10:

  • pnpm typecheck exits 0

  • pnpm lint exits 0

  • pnpm format:check exits 0

  • pnpm test exits 0

  • pnpm build succeeds — dist/ contains static HTML for /

  • Built / page source has NO <astro-island> tags

  • Built / page source contains <title>Pink Pulsar</title>, <meta name="description">, <link rel="canonical">, OG tags, Twitter card tags

  • Built / page source contains Inter font preload + --font-sans CSS variable

  • ESLint isolation rule verified: isolation tests pass (Task 11)

  • .dev.vars.example committed; .dev.vars gitignored

  • Welcome.astro, src/assets/astro.svg, src/assets/background.svg removed

  • package.json engines.node is >=22.13.0

  • @/* path alias resolves in both TS and Astro imports

  • Step 13: Final commit (if any files were reformatted during verification)

git status

If clean, no commit needed. If files changed:

git add -A
git commit -m "chore: MP-0a final verification cleanup"

Notes for implementers

  1. shadcn CLI interactivity: shadcn add may prompt interactively. If running in a non-interactive context, pre-create components.json (Task 4) and src/lib/utils.ts (Task 4) so the CLI has everything it needs. If shadcn add refuses to work without shadcn init, run pnpm dlx shadcn@latest init -y first, then re-run add. After init, verify components.json, utils.ts, and global.css still match Task 4's content.

  2. eslint-plugin-astro Node version: The plugin requires Node >=22.22.3, which is higher than the spec's >=22.13.0. If your Node is between 22.13.0 and 22.22.3 and lint fails, bump engines.node to >=22.22.3 in package.json.

  3. astro:container import in tests: The import { loadRenderers } from "astro:container" in tests/smoke.test.ts is a virtual module provided by Astro's Vite plugins. It only works because vitest.config.ts uses getViteConfig() from astro/config. If TypeScript complains about the module, ensure .astro/types.d.ts is in the tsconfig include (it is, by default).

  4. Container API is experimental: The experimental_AstroContainer class still has the experimental_ prefix in Astro 7. This is correct and expected.

  5. global.css after shadcn add: The shadcn CLI may try to modify src/styles/global.css during add. Always verify the file matches Task 4 Step 2 after running shadcn commands. Remove any extra @import lines shadcn adds.

  6. Starter dist/ directory: A stale dist/ from a previous build exists. It's gitignored. pnpm build will regenerate it. No action needed.

SDD Progress Ledger

This file tracks implementation progress across compaction.

Status

In progress: Task 3 dispatch

Tasks

  • Task 1: complete (commits 96bdda4..90205af, review approved 2026-07-08)

    • Minor #1 fixed in follow-up commit 6b115cb: untracked .dev.vars (was gitignored+tracked per plan; now gitignored+untracked). File still on disk for dev bootstrap; .dev.vars.example is the committed template.
    • Minor #2 logged for final review: 14,654-line worker-configuration.d.ts in commit (per plan)
  • Task 2: complete (commits 90205af..b2a33a8, review approved 2026-07-08)

    • Minor: package.json missing trailing newline (cosmetic, will be fixed by Prettier in Task 9)
  • Task 3: complete (commits b2a33a8..39c10f8, review approved 2026-07-08)

    • Includes Task 1 fix commit 6b115cb in the diff range (already pre-approved)
  • Task 4: complete (commits 39c10f8..6f728ff, review approved 2026-07-08, no findings)

  • Task 5: complete (commits 6f728ff..e6da0ea, review approved 2026-07-08, no findings)

    • shadcn CLI added radix-ui v1.6.1 to dependencies (bundles @radix-ui/react-slot and @radix-ui/react-label)
  • Task 6: complete (commits e6da0ea..611df4d, review approved 2026-07-08, no findings)

    • Implementer deviated from brief for Image.astro to use conditional spread ({...(width !== undefined ? { width } : {})}) for the three optional props. This is a necessary adaptation for exactOptionalPropertyTypes: true — direct prop assignment (width={width} where width: number | undefined) would fail TS2375. Behavior is identical to the brief's intent.
  • Task 7: complete (commits 611df4d..1fa1b5c, review approved 2026-07-08, no findings)

  • Task 8: complete (commits 1fa1b5c..a3a5d87, review approved 2026-07-08 after fix)

    • Initial review found 2 Important findings: (1) engines.node too permissive for eslint-plugin-astro; (2) services block missing @/features/* alias
    • Fix commit a3a5d87 addressed both
    • Implementer deviations (3) were all necessary and well-handled: src/env.d.ts and worker-configuration.d.ts in ignores, no-undef off for .astro
    • Minor finding (logged for final review): shallow globs in isolation rules (src/features/, etc.) could be deepened to src/features/**/ for robustness
  • Task 9: complete (commits a3a5d87..e856955, review approved 2026-07-08)

    • Prettier formatting pass reformatted many files (config files, components, docs)
    • per plan, pnpm format runs Prettier on all files; the diff includes reformatting of pre-existing content plus new .prettierrc/.prettierignore
  • Task 10: complete (commits e856955..cf3486e, review approved 2026-07-08)

    • vitest.config.ts + tests/smoke.test.ts created, 3/3 smoke tests pass
    • Adapter workaround: astro.config.mjs adapter is conditional on process.env.VITEST — node adapter for tests, cloudflare (with prerenderEnvironment: 'workerd') for production. User's idea, cleaner than the initial unconditional node switch.
    • Added @types/node (devDep) for process.env typecheck in astro.config.mjs
    • Commit was amended (77eacca → cf3486e) after user proposed the conditional adapter pattern
    • Minor findings for final review:
      • docs/superpowers/plans/2026-07-07-MP-0a-repo-skeleton-tooling.md included in commit (1456 lines, not in brief file list — committed for traceability, harmless)
      • Quote style inconsistency in astro.config.mjs line 6 (single vs double quotes — cosmetic)
      • @astrojs/node placed in dependencies not devDependencies — architectural consideration since the conditional adapter only uses node during tests
  • Task 11: complete (commits cf3486e..4cfc442, review approved 2026-07-08)

    • tests/lint.test.ts (2 tests: pnpm lint + pnpm format:check) + tests/isolation.test.ts (6 tests) created, 11/11 total tests pass
    • 3 documented deviations: (a) added @/ path alias variants to ESLint no-restricted-imports (improves rule coverage, consistent with Task 8 minor finding); (b) // eslint-disable-next-line no-undef for process.env.VITEST in astro.config.mjs (fix for no-undef on .mjs file — could be improved via languageOptions.globals later); (c) increased test timeouts (15s isolation, 30s spawnSync) for Windows ESLint init overhead
    • .prettierignore updated to add .superpowers/ (necessary so the auto-generated SDD report files don't fail format:check gate)
    • Minor findings for final review:
      • spawnSync with shell:true triggers DEP0190 deprecation warning (cosmetic; matches brief)
      • no-undef disable could be improved via ESLint languageOptions.globals (e.g. globals.node) for .mjs files
      • Commit touched .prettierignore + astro.config.mjs + eslint.config.mjs (beyond the brief's tests/lint.test.ts tests/isolation.test.ts example) but all changes are documented and necessary
  • Task 12: complete (verification only, no commit; 1 follow-up fix commit 4525949)

    • All 14 success criteria pass:
      • typecheck: 0 errors, 0 warnings, 1 hint
      • lint: exits 0
      • format:check: "All matched files use Prettier code style!"
      • test: 11/11 pass
      • build: dist/client/index.html generated with zero tags
      • meta tags, OG, Twitter, font preload, --font-sans all present in built HTML
      • .dev.vars.example committed; .dev.vars gitignored
      • engines.node >=22.22.3 (satisfies >=22.13.0 minimum)
    • Final whole-branch review found 1 MUST FIX: pages isolation rule was missing @/features/*/repository variant. Fixed in commit 4525949.
    • Final review verdict: ✅ APPROVED TO MERGE (after the MUST FIX)
    • Other minor findings classified as ACCEPT or NICE TO HAVE (logged in final review)
  • NICE TO HAVE cleanup: complete (commit 8d4a1ca, review approved 2026-07-08)

    • Addressed all 5 NICE TO HAVE items from final review in a single commit:
      1. Deepened src/features/*/repository to src/features/**/repository (and @/ variant) on actions and pages rules
      2. Moved @astrojs/node from dependencies to devDependencies
      3. Replaced inline no-undef disable with languageOptions.globals (globals package) for .mjs/.js files
      4. Migrated from tseslint.config to defineConfig from eslint/config (fixes ts(6387) deprecation hint)
      5. Added trailing newline to .gitignore
    • Added new isolation test (12/12 pass; was 11/11) that exercises the nested repository path
    • typecheck: 0 errors, 0 warnings, 0 hints (was: 0/0/1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment