@warlock.js/core 5.0.1 → 5.1.0
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/CHANGELOG.md +47 -0
- package/esm/cli/cli-commands.manager.mjs +36 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs +207 -0
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/config.check.mjs +5 -2
- package/esm/cli/commands/doctor/checks/config.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/connectors.check.mjs +38 -12
- package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +8 -4
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/health.check.mjs +77 -11
- package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/index.mjs +5 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +116 -53
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs +27 -9
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/routes.check.mjs +51 -12
- package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/doctor-command.action.mjs +16 -6
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor/run-checks.mjs +15 -6
- package/esm/cli/commands/doctor/run-checks.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +17 -9
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/connectors/describe-server-address.mjs +89 -6
- package/esm/connectors/describe-server-address.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts +18 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +36 -3
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +1 -4
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +3 -7
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +1 -1
- package/esm/dev-server/ready-block.mjs +62 -0
- package/esm/dev-server/ready-block.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +2 -575
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +76 -0
- package/esm/generations/features/access.feature.mjs.map +1 -0
- package/esm/generations/features/ai-anthropic.feature.mjs +10 -0
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -0
- package/esm/generations/features/ai-bedrock.feature.mjs +10 -0
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -0
- package/esm/generations/features/ai-google.feature.mjs +10 -0
- package/esm/generations/features/ai-google.feature.mjs.map +1 -0
- package/esm/generations/features/ai-ollama.feature.mjs +10 -0
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -0
- package/esm/generations/features/ai-openai.feature.mjs +10 -0
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -0
- package/esm/generations/features/ai-panoptic.feature.mjs +13 -0
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -0
- package/esm/generations/features/ai-tools.feature.mjs +13 -0
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -0
- package/esm/generations/features/ai-workspace.feature.mjs +13 -0
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -0
- package/esm/generations/features/ai.feature.mjs +15 -0
- package/esm/generations/features/ai.feature.mjs.map +1 -0
- package/esm/generations/features/herald.feature.mjs +19 -0
- package/esm/generations/features/herald.feature.mjs.map +1 -0
- package/esm/generations/features/image.feature.mjs +9 -0
- package/esm/generations/features/image.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +72 -0
- package/esm/generations/features/index.mjs.map +1 -0
- package/esm/generations/features/mail.feature.mjs +10 -0
- package/esm/generations/features/mail.feature.mjs.map +1 -0
- package/esm/generations/features/mongodb.feature.mjs +9 -0
- package/esm/generations/features/mongodb.feature.mjs.map +1 -0
- package/esm/generations/features/mysql.feature.mjs +9 -0
- package/esm/generations/features/mysql.feature.mjs.map +1 -0
- package/esm/generations/features/notifications.feature.mjs +43 -0
- package/esm/generations/features/notifications.feature.mjs.map +1 -0
- package/esm/generations/features/postgres.feature.mjs +9 -0
- package/esm/generations/features/postgres.feature.mjs.map +1 -0
- package/esm/generations/features/react-email.feature.mjs +70 -0
- package/esm/generations/features/react-email.feature.mjs.map +1 -0
- package/esm/generations/features/react.feature.mjs +16 -0
- package/esm/generations/features/react.feature.mjs.map +1 -0
- package/esm/generations/features/redis.feature.mjs +9 -0
- package/esm/generations/features/redis.feature.mjs.map +1 -0
- package/esm/generations/features/s3.feature.mjs +13 -0
- package/esm/generations/features/s3.feature.mjs.map +1 -0
- package/esm/generations/features/scheduler.feature.mjs +9 -0
- package/esm/generations/features/scheduler.feature.mjs.map +1 -0
- package/esm/generations/features/ses.feature.mjs +9 -0
- package/esm/generations/features/ses.feature.mjs.map +1 -0
- package/esm/generations/features/shadcn.feature.mjs +397 -0
- package/esm/generations/features/shadcn.feature.mjs.map +1 -0
- package/esm/generations/features/shared/link-ai-package-import.mjs +37 -0
- package/esm/generations/features/shared/link-ai-package-import.mjs.map +1 -0
- package/esm/generations/features/shared/migration-timestamp.mjs +17 -0
- package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -0
- package/esm/generations/features/socket.feature.mjs +15 -0
- package/esm/generations/features/socket.feature.mjs.map +1 -0
- package/esm/generations/features/tailwind.feature.mjs +174 -0
- package/esm/generations/features/tailwind.feature.mjs.map +1 -0
- package/esm/generations/features/test.feature.mjs +96 -0
- package/esm/generations/features/test.feature.mjs.map +1 -0
- package/esm/generations/features/web.feature.mjs +179 -0
- package/esm/generations/features/web.feature.mjs.map +1 -0
- package/esm/generations/stubs.mjs +95 -8
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -0
- package/esm/http/index.mjs +1 -0
- package/esm/http/ready-report.d.mts +56 -0
- package/esm/http/ready-report.d.mts.map +1 -0
- package/esm/http/ready-report.mjs +24 -0
- package/esm/http/ready-report.mjs.map +1 -0
- package/esm/index.d.mts +2 -1
- package/esm/index.mjs +2 -1
- package/llms-full.txt +322 -1
- package/llms.txt +2 -0
- package/package.json +12 -12
- package/skills/use-request-locals/SKILL.md +24 -1
- package/skills/warlock-conventions/SKILL.md +5 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadcn.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/shadcn.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { FeatureDefinition } from \"./types\";\n\n/**\n * `warlock add shadcn` installs the PREREQUISITES for shadcn/ui. It does not\n * wrap `shadcn add`, and it never will.\n *\n * shadcn/ui is a copy-in generator with its own CLI and its own registry, not a\n * dependency: components are written into your source tree and become yours the\n * moment they land. Wrapping their CLI would make their moving target our bug\n * reports, for a command that adds nothing but a rename.\n *\n * What is worth owning is the part their CLI gets wrong here. Measured against\n * this framework's layout on 2026-08-25, `shadcn add button card` exited 0 and\n * produced components that rendered COMPLETELY UNSTYLED. It writes components\n * that import `cn` and reference `bg-primary` / `ring-ring`, but only\n * `shadcn init` creates `lib/utils` and the theme tokens those names resolve\n * against. The class was in the generated CSS and the element carried it, and\n * the rule still evaluated to an empty `var()` — a silent failure with a zero\n * exit code, which is the worst kind to hand a user.\n *\n * So this feature ships the five things that make `npx shadcn add <component>`\n * work the first time:\n *\n * 1. `components.json` written for OUR layout (their defaults assume `src/app`\n * or a bare `components/`, neither of which is where pages live here).\n * 2. `src/web/lib/utils.ts` exporting `cn`.\n * 3. The design tokens appended to `src/web/app.css`.\n * 4. A `web/*` entry in tsconfig `paths`, so the generated imports typecheck.\n * 5. The packages `init` would have installed — see `printNextStep` below.\n * Skipping `init` is correct; skipping its dependency list was a bug, and\n * it cost a generated button that imports `cva` and cannot compile.\n *\n * After that, the user talks to shadcn directly, and their docs are true.\n */\n\n/**\n * The aliases shadcn's CLI rewrites every generated import against.\n *\n * These are the whole reason this feature exists. shadcn's defaults do not fit\n * `src/web`, and no user would guess this mapping: the alias keys are shadcn's\n * vocabulary, the values are tsconfig `paths` prefixes (hence `web/...`, not\n * `src/web/...`), and `tailwind.css` is a real path from the project root\n * (hence `src/web/app.css`, WITH the `src`). Getting one of them wrong produces\n * components in the wrong folder importing `cn` from somewhere that does not\n * exist.\n *\n * `tailwind.config` is deliberately empty: v4 is CSS-first and there is no\n * config file for it to point at. `rsc: false` because these are SSR React\n * pages rendered by the Warlock HTTP server, not React Server Components.\n */\nconst componentsJsonStub = `{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": false,\n \"tsx\": true,\n \"tailwind\": {\n \"config\": \"\",\n \"css\": \"src/web/app.css\",\n \"baseColor\": \"neutral\",\n \"cssVariables\": true,\n \"prefix\": \"\"\n },\n \"aliases\": {\n \"components\": \"web/components\",\n \"utils\": \"web/lib/utils\",\n \"ui\": \"web/components/ui\",\n \"lib\": \"web/lib\",\n \"hooks\": \"web/hooks\"\n },\n \"iconLibrary\": \"lucide\"\n}\n`;\n\n/**\n * `cn` — the one import every single shadcn component makes.\n *\n * `clsx` resolves the conditional/array/object class syntax, and `tailwind-merge`\n * then de-duplicates conflicting Tailwind utilities so a caller's `className`\n * actually beats the component's own default rather than depending on which one\n * happens to come later in the generated stylesheet. Both halves are required:\n * clsx alone leaves `px-2 px-4` in the attribute and the loser wins at random.\n */\nconst cnUtilStub = `import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names, with later Tailwind utilities winning over earlier ones.\n *\n * Every shadcn/ui component imports this. Keep the export name and signature.\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n`;\n\n/**\n * Marker for \"the tokens are already in this stylesheet\".\n *\n * A comment rather than a token name, because a user is free to retune every\n * value below and we must still recognise our own block on a re-run.\n */\nconst SHADCN_TOKENS_MARKER = \"shadcn/ui design tokens\";\n\n/**\n * The shadcn token set, as a plain `@theme` block with LITERAL values.\n *\n * Not `@theme inline` over `:root` custom properties, which is what shadcn's own\n * `init` writes. That form was verified against this stylesheet pipeline on\n * 2026-08-25 and it produces utilities that resolve to nothing: the class is in\n * the generated CSS, the class is on the element, and the rule still evaluates\n * to an empty `var()`. Literal values in `@theme` are what make `bg-primary`\n * emit a colour instead of a dangling reference.\n *\n * Dark mode still works, and works the way shadcn expects: `@theme` emits these\n * as real custom properties, so the `.dark` block re-declares the same\n * `--color-*` names and the cascade does the rest. `@custom-variant dark` is\n * what points Tailwind's `dark:` prefix at that class instead of the OS setting.\n *\n * Values are shadcn's `neutral` base. They are the starting point, not the\n * answer — this is the block to edit when the project gets a real palette.\n */\nconst shadcnTokensStub = `\n/* ${SHADCN_TOKENS_MARKER} — edit freely, this is your palette now.\n\n These are LITERAL values in a plain @theme block, on purpose. shadcn's own\n \\`init\\` writes \\`@theme inline\\` over :root custom properties; that form emits\n utilities which resolve to an empty var() here, so every component renders\n unstyled while the class sits right there on the element. Do not convert it.\n*/\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: oklch(1 0 0);\n --color-foreground: oklch(0.145 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.145 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.145 0 0);\n --color-primary: oklch(0.205 0 0);\n --color-primary-foreground: oklch(0.985 0 0);\n --color-secondary: oklch(0.97 0 0);\n --color-secondary-foreground: oklch(0.205 0 0);\n --color-muted: oklch(0.97 0 0);\n --color-muted-foreground: oklch(0.556 0 0);\n --color-accent: oklch(0.97 0 0);\n --color-accent-foreground: oklch(0.205 0 0);\n --color-destructive: oklch(0.577 0.245 27.325);\n --color-destructive-foreground: oklch(0.985 0 0);\n --color-border: oklch(0.922 0 0);\n --color-input: oklch(0.922 0 0);\n --color-ring: oklch(0.708 0 0);\n\n --color-chart-1: oklch(0.646 0.222 41.116);\n --color-chart-2: oklch(0.6 0.118 184.704);\n --color-chart-3: oklch(0.398 0.07 227.392);\n --color-chart-4: oklch(0.828 0.189 84.429);\n --color-chart-5: oklch(0.769 0.188 70.08);\n\n --color-sidebar: oklch(0.985 0 0);\n --color-sidebar-foreground: oklch(0.145 0 0);\n --color-sidebar-primary: oklch(0.205 0 0);\n --color-sidebar-primary-foreground: oklch(0.985 0 0);\n --color-sidebar-accent: oklch(0.97 0 0);\n --color-sidebar-accent-foreground: oklch(0.205 0 0);\n --color-sidebar-border: oklch(0.922 0 0);\n --color-sidebar-ring: oklch(0.708 0 0);\n\n /* shadcn components reach for rounded-lg/md/sm and expect them to track one\n radius. Changing --radius-lg here also retunes Tailwind's own rounded-lg,\n which is the intended trade: one radius scale per project, not two. */\n --radius-sm: 0.375rem;\n --radius-md: 0.5rem;\n --radius-lg: 0.625rem;\n --radius-xl: 1rem;\n}\n\n.dark {\n --color-background: oklch(0.145 0 0);\n --color-foreground: oklch(0.985 0 0);\n --color-card: oklch(0.205 0 0);\n --color-card-foreground: oklch(0.985 0 0);\n --color-popover: oklch(0.205 0 0);\n --color-popover-foreground: oklch(0.985 0 0);\n --color-primary: oklch(0.922 0 0);\n --color-primary-foreground: oklch(0.205 0 0);\n --color-secondary: oklch(0.269 0 0);\n --color-secondary-foreground: oklch(0.985 0 0);\n --color-muted: oklch(0.269 0 0);\n --color-muted-foreground: oklch(0.708 0 0);\n --color-accent: oklch(0.269 0 0);\n --color-accent-foreground: oklch(0.985 0 0);\n --color-destructive: oklch(0.704 0.191 22.216);\n --color-destructive-foreground: oklch(0.985 0 0);\n --color-border: oklch(1 0 0 / 10%);\n --color-input: oklch(1 0 0 / 15%);\n --color-ring: oklch(0.556 0 0);\n\n --color-chart-1: oklch(0.488 0.243 264.376);\n --color-chart-2: oklch(0.696 0.17 162.48);\n --color-chart-3: oklch(0.769 0.188 70.08);\n --color-chart-4: oklch(0.627 0.265 303.9);\n --color-chart-5: oklch(0.645 0.246 16.439);\n\n --color-sidebar: oklch(0.205 0 0);\n --color-sidebar-foreground: oklch(0.985 0 0);\n --color-sidebar-primary: oklch(0.488 0.243 264.376);\n --color-sidebar-primary-foreground: oklch(0.985 0 0);\n --color-sidebar-accent: oklch(0.269 0 0);\n --color-sidebar-accent-foreground: oklch(0.985 0 0);\n --color-sidebar-border: oklch(1 0 0 / 10%);\n --color-sidebar-ring: oklch(0.556 0 0);\n}\n`;\n\n/**\n * Write `components.json` — the sentinel for \"this feature already ran\".\n *\n * Nothing in the project template creates this file, so its presence means\n * `add shadcn` has been here and a human may since have retuned the aliases,\n * the style, or the base colour. It is never rewritten: shadcn's CLI reads this\n * file on every `add`, so overwriting it would silently relocate a project's\n * component folder out from under the components already in it.\n */\nasync function writeComponentsJson(): Promise<void> {\n const componentsJsonPath = rootPath(\"components.json\");\n\n if (await fileExistsAsync(componentsJsonPath)) {\n console.log(`${colors.yellowBright(\"components.json\")} already exists, skipping...`);\n\n return;\n }\n\n await putFileAsync(componentsJsonPath, componentsJsonStub);\n console.log(`${colors.green(\"✓\")} Created components.json`);\n}\n\n/**\n * Write `src/web/lib/utils.ts`.\n *\n * Guarded on its own rather than on the sentinel above, because `lib/utils` is\n * a name a project may well already own — and if it does, whatever is in there\n * is user code with other callers. We print instead of merging.\n */\nasync function writeCnUtil(): Promise<void> {\n const utilsFile = srcPath(\"web/lib/utils.ts\");\n\n if (await fileExistsAsync(utilsFile)) {\n const current = await getFileAsync(utilsFile).catch(() => \"\");\n\n if (/export\\s+(function|const)\\s+cn\\b/.test(current)) {\n console.log(`${colors.yellowBright(\"src/web/lib/utils.ts\")} already exports cn, skipping...`);\n } else {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/lib/utils.ts\")} exists but does not export ` +\n `${colors.yellowBright(\"cn\")} — add it yourself:\\n` +\n \" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }\\n\" +\n \" Every shadcn component imports it, and none of them will compile until it is there.\",\n );\n }\n\n return;\n }\n\n await ensureDirectoryAsync(srcPath(\"web/lib\"));\n await putFileAsync(utilsFile, cnUtilStub);\n console.log(`${colors.green(\"✓\")} Created src/web/lib/utils.ts`);\n}\n\n/**\n * Append the token block to `src/web/app.css`.\n *\n * The stylesheet is guaranteed to exist by the time this runs: `requires:\n * [\"tailwind\"]` makes the add command resolve `tailwind` first and run its\n * `onExecuting` ahead of this one, and that is what creates the file. The check\n * below is for a stylesheet a human has since moved or deleted — worth a\n * printed instruction, not a failure.\n *\n * Appended, never rewritten. Everything already in that file is either\n * Tailwind's own `@import` or the project's design system.\n */\nasync function appendThemeTokens(): Promise<void> {\n const cssFile = srcPath(\"web/app.css\");\n\n if (!(await fileExistsAsync(cssFile))) {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/app.css\")} not found — ` +\n \"append the shadcn token block to your Tailwind stylesheet yourself.\\n\" +\n \" Without the tokens, shadcn components render unstyled: the classes are emitted and applied, \" +\n \"but `bg-primary` and friends resolve to an empty var().\",\n );\n\n return;\n }\n\n const current = await getFileAsync(cssFile);\n\n if (current.includes(SHADCN_TOKENS_MARKER) || current.includes(\"--color-primary-foreground\")) {\n console.log(`${colors.yellowBright(\"src/web/app.css\")} already has the tokens, skipping...`);\n\n return;\n }\n\n await putFileAsync(cssFile, `${current.trimEnd()}\\n${shadcnTokensStub}`);\n console.log(`${colors.green(\"✓\")} Appended the shadcn tokens to src/web/app.css`);\n}\n\n/** The tsconfig `paths` entry the generated imports resolve through. */\nconst WEB_PATH_ALIAS = '\"web/*\": [\"./src/web/*\"]';\n\n/**\n * Add `web/*` to tsconfig `compilerOptions.paths`.\n *\n * The template declares only `app/*`, so every import shadcn generates against\n * the aliases above (`web/lib/utils`, `web/components/ui/button`) would fail to\n * typecheck the moment it lands. This is the first `add` feature to patch\n * `paths` rather than `include`.\n *\n * String surgery, and NOT the parse-and-write that the `include` patches use.\n * The project template's `tsconfig.json` carries `//` comments — it is JSONC,\n * and `JSON.parse` throws on it — so a parse-first patch would fail on exactly\n * the projects this feature is for. Editing the text also preserves those\n * comments, which are load-bearing documentation in that file.\n */\nasync function addWebPathAlias(): Promise<void> {\n const tsconfigPath = rootPath(\"tsconfig.json\");\n\n const printManualInstruction = (reason: string) => {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"tsconfig.json\")} ${reason} — ` +\n \"add this to `compilerOptions.paths` yourself:\\n\" +\n ` ${WEB_PATH_ALIAS}\\n` +\n \" Without it, every import shadcn generates against the `web/*` aliases fails to typecheck.\",\n );\n };\n\n if (!(await fileExistsAsync(tsconfigPath))) {\n printManualInstruction(\"not found\");\n\n return;\n }\n\n const current = await getFileAsync(tsconfigPath);\n\n // Matches the alias whichever quote style and spacing the file uses, so a\n // re-run against a hand-edited tsconfig does not stack a second entry.\n if (/[\"']web\\/\\*[\"']\\s*:/.test(current)) {\n console.log(`${colors.yellowBright(\"tsconfig.json\")} already maps web/*, skipping...`);\n\n return;\n }\n\n let next: string;\n\n if (/\"paths\"\\s*:\\s*\\{/.test(current)) {\n next = current.replace(/\"paths\"\\s*:\\s*\\{/, `$&\\n ${WEB_PATH_ALIAS},`);\n } else if (/\"compilerOptions\"\\s*:\\s*\\{/.test(current)) {\n next = current.replace(\n /\"compilerOptions\"\\s*:\\s*\\{/,\n `$&\\n \"paths\": {\\n ${WEB_PATH_ALIAS}\\n },`,\n );\n } else {\n printManualInstruction(\"has no recognisable compilerOptions block\");\n\n return;\n }\n\n await putFileAsync(tsconfigPath, next);\n console.log(`${colors.green(\"✓\")} Added ${WEB_PATH_ALIAS} to tsconfig.json paths`);\n}\n\n/**\n * Tell the user the next command is theirs to run.\n *\n * This is the seam. Everything above is prerequisite; from here the shadcn docs\n * apply verbatim, which is the entire point of not wrapping their CLI.\n *\n * WHY THIS FEATURE DECLARES cva AND lucide-react, and where the list came from.\n *\n * shadcn's registry splits dependencies across two levels. Each component item\n * (`.../new-york-v4/button.json`) declares only what that file pulls beyond the\n * baseline — for button, `radix-ui` and nothing else. Everything the baseline\n * assumes lives on the STYLE INDEX (`.../new-york-v4/index.json`), which is\n * fetched by `init` and only by `init`:\n *\n * dependencies: class-variance-authority, lucide-react, radix-ui\n * devDependencies: tw-animate-css, shadcn\n *\n * We skip `init` on purpose — it would rewrite components.json and replace the\n * literal tokens with the `@theme inline` block that resolves to nothing here.\n * Skipping it is right; inheriting nothing from it was the bug. `shadcn add\n * button` exits 0 and writes `import { cva } from \"class-variance-authority\"`\n * against a package no one installed. Zero exit code, TS2307, blank page.\n *\n * `radix-ui` stays off our list: it is the one style-index dependency that is\n * ALSO declared per-component, so `add` really does install it on demand, and\n * declaring it here would pull the whole primitive set into projects using two\n * components. `shadcn` itself stays off too — it is the CLI, and the user is\n * invoking it via `npx`.\n *\n * `tw-animate-css` was checked and deliberately EXCLUDED. It is not imported by\n * any component; it is a plain stylesheet whose only entry point is the\n * `@import \"tw-animate-css\"` line that `init` writes into the CSS — and we do\n * not write that line, so the package would install and never load. Nothing\n * fails to compile or render without it. What you lose is the enter/exit\n * animation on overlay components (dialog, dropdown, tooltip, sheet): their\n * `animate-in` / `fade-in-0` classes are simply never generated, so the overlay\n * appears instantly instead of fading. That is opt-in, and the note below is how\n * a user opts in — the `@import` has to go at the TOP of app.css, next to\n * Tailwind's own, which is why this feature cannot append it to the token block.\n */\nfunction printNextStep(): void {\n console.log(\n `\\n${colors.green(\"✓\")} shadcn/ui prerequisites are in place. Add components with shadcn's own CLI:\\n` +\n ` ${colors.yellowBright(\"npx shadcn@latest add button card\")}\\n` +\n \" Skip `shadcn init` — this feature did its job, and running it would rewrite components.json\\n\" +\n \" and replace the theme tokens with an `@theme inline` block that resolves to nothing here.\\n\" +\n ` ${colors.yellowBright(\"class-variance-authority\")} and ${colors.yellowBright(\"lucide-react\")} are already installed: shadcn declares\\n` +\n \" them on the style index that only `init` reads, so `add` would never install them for you.\\n\" +\n ` ${colors.yellowBright(\"radix-ui\")} (the unified package, not @radix-ui/react-*) IS declared per component,\\n` +\n \" so shadcn's CLI installs that one itself as each component needs it.\\n\" +\n ` For overlay animations, add ${colors.yellowBright(\"tw-animate-css\")} and put ${colors.yellowBright('@import \"tw-animate-css\";')}\\n` +\n \" at the TOP of src/web/app.css, under the Tailwind import. Without it dialogs and dropdowns\\n\" +\n \" still work, they just appear instantly instead of animating.\",\n );\n}\n\n/**\n * Lay the ground shadcn's CLI expects to find, and nothing more.\n *\n * Four files, none of which needs `node_modules` to be populated: on the\n * `create-warlock` path this runs under `--no-install`, so the dependencies\n * declared below are only recorded in `package.json` and nothing here may\n * import, resolve, or execute shadcn, clsx, or Tailwind.\n */\nasync function completeShadcnInstallation(_options: CommandActionData) {\n await writeComponentsJson();\n await writeCnUtil();\n await appendThemeTokens();\n await addWebPathAlias();\n printNextStep();\n}\n\nexport const shadcnFeature: FeatureDefinition = {\n description:\n \"Sets up the prerequisites for shadcn/ui so `npx shadcn add <component>` works first time: components.json aliased to src/web, src/web/lib/utils.ts (cn), the design tokens in src/web/app.css, and a web/* tsconfig path. It does NOT wrap shadcn's CLI — components stay theirs to generate and yours to own.\",\n // `tailwind` owns src/web/app.css, which the token block is appended to.\n // Requiring it also fixes the order: the add command resolves requirements\n // depth-first, so the stylesheet exists before this feature writes into it.\n requires: [\"tailwind\"],\n // Everything `shadcn init` would have installed, minus the parts their CLI\n // genuinely does install per-component. See the note above `printNextStep`\n // for how this list was derived and what is deliberately NOT in it.\n dependencies: {\n // Both are runtime dependencies of `cn`, which every generated component\n // calls on every render — not build-time tooling.\n clsx: \"^2.1.1\",\n // v3 is the Tailwind v4 line; tailwind-merge v2 knows the v3 utility set and\n // silently fails to de-duplicate against v4 class names.\n \"tailwind-merge\": \"^3.3.1\",\n // `cva` is imported on line 2 of the generated button — and of every other\n // component with a `variant` prop. The registry declares it ONCE, on the\n // style index that only `init` applies, so `shadcn add button` resolves the\n // component's own deps, exits 0, and leaves TS2307 on a file it just wrote.\n // Still 0.x upstream, so this caret pins to 0.7.x; expect 0.7.1 to land.\n \"class-variance-authority\": \"^0.7.1\",\n // Same trap, one layer further in. `components.json` declares\n // `iconLibrary: \"lucide\"`, and dialog/select/checkbox/dropdown-menu all\n // import `lucide-react` in their source while declaring only `radix-ui` —\n // verified against the new-york-v4 registry items. So the CLI installs it\n // for nobody, and the first icon-bearing component fails to resolve.\n // Peer range covers React 19, which is what `web` brings.\n \"lucide-react\": \"^1.34.0\",\n },\n onExecuting: completeShadcnInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC3B,MAAM,aAAa;;;;;;;;;;;;;;;;;;AAmBnB,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;AAoB7B,MAAM,mBAAmB;KACpB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqG1B,eAAe,sBAAqC;CAClD,MAAM,qBAAqB,SAAS,iBAAiB;CAErD,IAAI,MAAM,gBAAgB,kBAAkB,GAAG;EAC7C,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,6BAA6B;EAEnF;CACF;CAEA,MAAM,aAAa,oBAAoB,kBAAkB;CACzD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yBAAyB;AAC5D;;;;;;;;AASA,eAAe,cAA6B;CAC1C,MAAM,YAAY,QAAQ,kBAAkB;CAE5C,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,MAAM,UAAU,MAAM,aAAa,SAAS,CAAC,CAAC,YAAY,EAAE;EAE5D,IAAI,mCAAmC,KAAK,OAAO,GACjD,QAAQ,IAAI,GAAG,OAAO,aAAa,sBAAsB,EAAE,iCAAiC;OAE5F,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,sBAAsB,EAAE,8BACtE,OAAO,aAAa,IAAI,EAAE;sFAGjC;EAGF;CACF;CAEA,MAAM,qBAAqB,QAAQ,SAAS,CAAC;CAC7C,MAAM,aAAa,WAAW,UAAU;CACxC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,8BAA8B;AACjE;;;;;;;;;;;;;AAcA,eAAe,oBAAmC;CAChD,MAAM,UAAU,QAAQ,aAAa;CAErC,IAAI,CAAE,MAAM,gBAAgB,OAAO,GAAI;EACrC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,iBAAiB,EAAE;wJAIxE;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,OAAO;CAE1C,IAAI,QAAQ,SAAS,oBAAoB,KAAK,QAAQ,SAAS,4BAA4B,GAAG;EAC5F,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,qCAAqC;EAE3F;CACF;CAEA,MAAM,aAAa,SAAS,GAAG,QAAQ,QAAQ,EAAE,IAAI,kBAAkB;CACvE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,+CAA+C;AAClF;;AAGA,MAAM,iBAAiB;;;;;;;;;;;;;;;AAgBvB,eAAe,kBAAiC;CAC9C,MAAM,eAAe,SAAS,eAAe;CAE7C,MAAM,0BAA0B,WAAmB;EACjD,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,GAAG,OAAO;IAEvE,eAAe,gGAExB;CACF;CAEA,IAAI,CAAE,MAAM,gBAAgB,YAAY,GAAI;EAC1C,uBAAuB,WAAW;EAElC;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,YAAY;CAI/C,IAAI,sBAAsB,KAAK,OAAO,GAAG;EACvC,QAAQ,IAAI,GAAG,OAAO,aAAa,eAAe,EAAE,iCAAiC;EAErF;CACF;CAEA,IAAI;CAEJ,IAAI,mBAAmB,KAAK,OAAO,GACjC,OAAO,QAAQ,QAAQ,oBAAoB,aAAa,eAAe,EAAE;MACpE,IAAI,6BAA6B,KAAK,OAAO,GAClD,OAAO,QAAQ,QACb,8BACA,6BAA6B,eAAe,SAC9C;MACK;EACL,uBAAuB,2CAA2C;EAElE;CACF;CAEA,MAAM,aAAa,cAAc,IAAI;CACrC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,SAAS,eAAe,wBAAwB;AACnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAS,gBAAsB;CAC7B,QAAQ,IACN,KAAK,OAAO,MAAM,GAAG,EAAE,kFAChB,OAAO,aAAa,mCAAmC,EAAE;;IAGzD,OAAO,aAAa,0BAA0B,EAAE,OAAO,OAAO,aAAa,cAAc,EAAE;IAE3F,OAAO,aAAa,UAAU,EAAE;gCAEJ,OAAO,aAAa,gBAAgB,EAAE,WAAW,OAAO,aAAa,6BAA2B,EAAE;+DAGvI;AACF;;;;;;;;;AAUA,eAAe,2BAA2B,UAA6B;CACrE,MAAM,oBAAoB;CAC1B,MAAM,YAAY;CAClB,MAAM,kBAAkB;CACxB,MAAM,gBAAgB;CACtB,cAAc;AAChB;AAEA,MAAa,gBAAmC;CAC9C,aACE;CAIF,UAAU,CAAC,UAAU;CAIrB,cAAc;EAGZ,MAAM;EAGN,kBAAkB;EAMlB,4BAA4B;EAO5B,gBAAgB;CAClB;CACA,aAAa;AACf"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { srcPath } from "../../../utils/paths.mjs";
|
|
2
|
+
import "../../../utils/index.mjs";
|
|
3
|
+
import { colors } from "@mongez/copper";
|
|
4
|
+
import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
5
|
+
|
|
6
|
+
//#region ../core/src/generations/features/shared/link-ai-package-import.ts
|
|
7
|
+
/**
|
|
8
|
+
* Link a satellite AI package into src/config/ai.ts via a side-effect import.
|
|
9
|
+
*
|
|
10
|
+
* The satellites (@warlock.js/ai-tools, ai-panoptic, ai-workspace) augment the
|
|
11
|
+
* `ai` object on import — they register their runtime surface (ai.tools / ai.mcp,
|
|
12
|
+
* ai.workspace, panoptic's ai.config({ panoptic }) wiring) AND the matching TS
|
|
13
|
+
* declaration-merging so ai.* members resolve. config-loader runs config/ai.ts at
|
|
14
|
+
* boot, so dropping a bare `import "<specifier>";` at the top of that file is what
|
|
15
|
+
* actually loads the augmentation before the ai connector applies the config.
|
|
16
|
+
*
|
|
17
|
+
* No-op if config/ai.ts is missing (the `ai` feature ejects it) or the import is
|
|
18
|
+
* already present. Inserts right after the `warlock:ai-packages` marker when it
|
|
19
|
+
* exists so the satellite imports stay grouped; otherwise prepends to the top.
|
|
20
|
+
*/
|
|
21
|
+
async function linkAiPackageImport(specifier) {
|
|
22
|
+
const aiConfigPath = srcPath("config/ai.ts");
|
|
23
|
+
if (!await fileExistsAsync(aiConfigPath)) return;
|
|
24
|
+
const current = await getFileAsync(aiConfigPath);
|
|
25
|
+
const importLine = `import "${specifier}";`;
|
|
26
|
+
if (current.includes(importLine)) return;
|
|
27
|
+
const marker = "// >>> warlock:ai-packages (auto-managed) >>>";
|
|
28
|
+
let next;
|
|
29
|
+
if (current.includes(marker)) next = current.replace(marker, `${marker}\n${importLine}`);
|
|
30
|
+
else next = `${importLine}\n${current}`;
|
|
31
|
+
await putFileAsync(aiConfigPath, next);
|
|
32
|
+
console.log(`${colors.green("✓")} Linked ${specifier} in src/config/ai.ts`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { linkAiPackageImport };
|
|
37
|
+
//# sourceMappingURL=link-ai-package-import.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link-ai-package-import.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/link-ai-package-import.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { srcPath } from \"../../../utils\";\n\n/**\n * Link a satellite AI package into src/config/ai.ts via a side-effect import.\n *\n * The satellites (@warlock.js/ai-tools, ai-panoptic, ai-workspace) augment the\n * `ai` object on import — they register their runtime surface (ai.tools / ai.mcp,\n * ai.workspace, panoptic's ai.config({ panoptic }) wiring) AND the matching TS\n * declaration-merging so ai.* members resolve. config-loader runs config/ai.ts at\n * boot, so dropping a bare `import \"<specifier>\";` at the top of that file is what\n * actually loads the augmentation before the ai connector applies the config.\n *\n * No-op if config/ai.ts is missing (the `ai` feature ejects it) or the import is\n * already present. Inserts right after the `warlock:ai-packages` marker when it\n * exists so the satellite imports stay grouped; otherwise prepends to the top.\n */\nexport async function linkAiPackageImport(specifier: string): Promise<void> {\n const aiConfigPath = srcPath(\"config/ai.ts\");\n\n if (!(await fileExistsAsync(aiConfigPath))) {\n return;\n }\n\n const current = await getFileAsync(aiConfigPath);\n const importLine = `import \"${specifier}\";`;\n\n if (current.includes(importLine)) {\n return;\n }\n\n const marker = \"// >>> warlock:ai-packages (auto-managed) >>>\";\n let next: string;\n\n if (current.includes(marker)) {\n next = current.replace(marker, `${marker}\\n${importLine}`);\n } else {\n next = `${importLine}\\n${current}`;\n }\n\n await putFileAsync(aiConfigPath, next);\n\n console.log(`${colors.green(\"✓\")} Linked ${specifier} in src/config/ai.ts`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkBA,eAAsB,oBAAoB,WAAkC;CAC1E,MAAM,eAAe,QAAQ,cAAc;CAE3C,IAAI,CAAE,MAAM,gBAAgB,YAAY,GACtC;CAGF,MAAM,UAAU,MAAM,aAAa,YAAY;CAC/C,MAAM,aAAa,WAAW,UAAU;CAExC,IAAI,QAAQ,SAAS,UAAU,GAC7B;CAGF,MAAM,SAAS;CACf,IAAI;CAEJ,IAAI,QAAQ,SAAS,MAAM,GACzB,OAAO,QAAQ,QAAQ,QAAQ,GAAG,OAAO,IAAI,YAAY;MAEzD,OAAO,GAAG,WAAW,IAAI;CAG3B,MAAM,aAAa,cAAc,IAAI;CAErC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,UAAU,UAAU,qBAAqB;AAC5E"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region ../core/src/generations/features/shared/migration-timestamp.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build a migration filename timestamp prefix in the framework's
|
|
4
|
+
* MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this
|
|
5
|
+
* prefix and orders migrations deterministically by it. Pass `offsetSeconds` to
|
|
6
|
+
* stamp sibling migrations created in the same scaffold a second apart so they
|
|
7
|
+
* never collide and keep a stable relative order.
|
|
8
|
+
*/
|
|
9
|
+
function migrationTimestamp(offsetSeconds = 0) {
|
|
10
|
+
const now = new Date(Date.now() + offsetSeconds * 1e3);
|
|
11
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
12
|
+
return `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { migrationTimestamp };
|
|
17
|
+
//# sourceMappingURL=migration-timestamp.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-timestamp.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/migration-timestamp.ts"],"sourcesContent":["/**\n * Build a migration filename timestamp prefix in the framework's\n * MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this\n * prefix and orders migrations deterministically by it. Pass `offsetSeconds` to\n * stamp sibling migrations created in the same scaffold a second apart so they\n * never collide and keep a stable relative order.\n */\nexport function migrationTimestamp(offsetSeconds = 0): string {\n const now = new Date(Date.now() + offsetSeconds * 1000);\n const pad = (value: number) => String(value).padStart(2, \"0\");\n\n return (\n `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_` +\n `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`\n );\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,mBAAmB,gBAAgB,GAAW;CAC5D,MAAM,MAAM,IAAI,KAAK,KAAK,IAAI,IAAI,gBAAgB,GAAI;CACtD,MAAM,OAAO,UAAkB,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG;CAE5D,OACE,GAAG,IAAI,IAAI,SAAS,IAAI,CAAC,EAAE,GAAG,IAAI,IAAI,QAAQ,CAAC,EAAE,GAAG,IAAI,YAAY,EAAE,GACnE,IAAI,IAAI,SAAS,CAAC,EAAE,GAAG,IAAI,IAAI,WAAW,CAAC,EAAE,GAAG,IAAI,IAAI,WAAW,CAAC;AAE3E"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { socketConfigStub } from "../stubs.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/generations/features/socket.feature.ts
|
|
4
|
+
const socketFeature = {
|
|
5
|
+
description: "Installs socket.io for the realtime socket server (Socket Connector)",
|
|
6
|
+
dependencies: { "socket.io": "^4.8.3" },
|
|
7
|
+
ejectConfig: {
|
|
8
|
+
content: socketConfigStub,
|
|
9
|
+
name: "socket"
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { socketFeature };
|
|
15
|
+
//# sourceMappingURL=socket.feature.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/socket.feature.ts"],"sourcesContent":["import { socketConfigStub } from \"../stubs\";\r\nimport { FeatureDefinition } from \"./types\";\r\n\r\nexport const socketFeature: FeatureDefinition = {\r\n description: \"Installs socket.io for the realtime socket server (Socket Connector)\",\r\n dependencies: {\r\n \"socket.io\": \"^4.8.3\",\r\n },\r\n ejectConfig: {\r\n content: socketConfigStub,\r\n name: \"socket\",\r\n },\r\n};\r\n"],"mappings":";;;AAGA,MAAa,gBAAmC;CAC9C,aAAa;CACb,cAAc,EACZ,aAAa,SACf;CACA,aAAa;EACX,SAAS;EACT,MAAM;CACR;AACF"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
|
+
import "../../utils/index.mjs";
|
|
3
|
+
import { colors } from "@mongez/copper";
|
|
4
|
+
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
5
|
+
|
|
6
|
+
//#region ../core/src/generations/features/tailwind.feature.ts
|
|
7
|
+
/**
|
|
8
|
+
* The stylesheet entry, and the whole of Tailwind's configuration.
|
|
9
|
+
*
|
|
10
|
+
* Tailwind v4 is CSS-first: there is no `tailwind.config.js` and no `content`
|
|
11
|
+
* globs to keep in sync with the project layout — the engine discovers the
|
|
12
|
+
* templates that reach it through the bundler graph, and everything a v3 config
|
|
13
|
+
* held (`theme`, `plugins`, `darkMode`) is expressed in CSS beside the import.
|
|
14
|
+
* So this one file is the config, which is why the feature writes it rather
|
|
15
|
+
* than an `ejectConfig` entry: `ejectConfig` lands in `src/config/*.ts`, and a
|
|
16
|
+
* stylesheet that must sit in the bundler's path does not belong there.
|
|
17
|
+
*/
|
|
18
|
+
const appCssStub = `@import "tailwindcss";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
Tailwind v4 configures itself from CSS — there is no tailwind.config.js.
|
|
22
|
+
|
|
23
|
+
Design tokens go in an @theme block, and each one becomes both a CSS variable
|
|
24
|
+
and a utility class:
|
|
25
|
+
|
|
26
|
+
@theme {
|
|
27
|
+
--color-brand: oklch(0.62 0.19 259);
|
|
28
|
+
--font-display: "Inter", sans-serif;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
gives you \`bg-brand\`, \`text-brand\`, \`font-display\` and \`var(--color-brand)\`.
|
|
32
|
+
|
|
33
|
+
Plugins are imported here too — \`@plugin "@tailwindcss/typography";\` — and
|
|
34
|
+
your own non-utility CSS can simply follow this comment.
|
|
35
|
+
*/
|
|
36
|
+
`;
|
|
37
|
+
/**
|
|
38
|
+
* PostCSS rather than a Vite config file, and deliberately at the project root.
|
|
39
|
+
*
|
|
40
|
+
* Vite discovers `postcss.config.mjs` from the project root on its own, in the
|
|
41
|
+
* dev server and in `vite build` alike, without the application owning a Vite
|
|
42
|
+
* config at all. That matters here: the only app-facing Vite plugin array in
|
|
43
|
+
* this framework is `webConnector({ plugins })`, which is wired into the DEV
|
|
44
|
+
* server exclusively — the client build composes its own plugin list from its
|
|
45
|
+
* caller. Registering `@tailwindcss/vite` there would produce styles in `dev`
|
|
46
|
+
* and silently drop them from a production build, which is a worse failure than
|
|
47
|
+
* any config file, because it only shows up after deploy.
|
|
48
|
+
*
|
|
49
|
+
* `.mjs` because a Warlock project's `package.json` is not guaranteed to set
|
|
50
|
+
* `"type": "module"`, and `export default` in a `.js` file would throw in a
|
|
51
|
+
* CommonJS project.
|
|
52
|
+
*/
|
|
53
|
+
const postcssConfigStub = `/**
|
|
54
|
+
* Tailwind v4 runs as a PostCSS plugin. Vite loads this file automatically —
|
|
55
|
+
* in dev and in build — so no Vite configuration is required.
|
|
56
|
+
*
|
|
57
|
+
* The plugin lives in its own package in v4: \`tailwindcss\` is the engine,
|
|
58
|
+
* \`@tailwindcss/postcss\` is the adapter. Naming \`tailwindcss\` here directly is
|
|
59
|
+
* the v3 spelling and will not work.
|
|
60
|
+
*/
|
|
61
|
+
export default {
|
|
62
|
+
plugins: {
|
|
63
|
+
"@tailwindcss/postcss": {},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
`;
|
|
67
|
+
/**
|
|
68
|
+
* Every filename Vite/PostCSS will pick up from the project root.
|
|
69
|
+
*
|
|
70
|
+
* Checked as a set rather than just writing our own: two PostCSS configs in one
|
|
71
|
+
* directory is not a merge, it is a coin toss over which one loads, so a project
|
|
72
|
+
* that already has one gets instructions instead of a second file.
|
|
73
|
+
*/
|
|
74
|
+
const POSTCSS_CONFIG_FILES = [
|
|
75
|
+
"postcss.config.mjs",
|
|
76
|
+
"postcss.config.js",
|
|
77
|
+
"postcss.config.cjs",
|
|
78
|
+
"postcss.config.ts",
|
|
79
|
+
"postcss.config.json",
|
|
80
|
+
".postcssrc",
|
|
81
|
+
".postcssrc.js",
|
|
82
|
+
".postcssrc.json"
|
|
83
|
+
];
|
|
84
|
+
/** The side-effect import that pulls the stylesheet into the bundler graph. */
|
|
85
|
+
const APP_CSS_IMPORT = "import \"./app.css\";";
|
|
86
|
+
/**
|
|
87
|
+
* Create `src/web/app.css` — the sentinel for "this feature already ran".
|
|
88
|
+
*
|
|
89
|
+
* Nothing in the project template creates this file (the scaffold ships a
|
|
90
|
+
* static `public/home.css` instead), so its presence means `add tailwind` has
|
|
91
|
+
* been here, and its contents may since have been edited into a real design
|
|
92
|
+
* system. It is never rewritten.
|
|
93
|
+
*/
|
|
94
|
+
async function createStylesheet() {
|
|
95
|
+
const cssFile = srcPath("web/app.css");
|
|
96
|
+
if (await fileExistsAsync(cssFile)) {
|
|
97
|
+
console.log(`${colors.yellowBright("src/web/app.css")} already exists, skipping...`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
await ensureDirectoryAsync(srcPath("web"));
|
|
101
|
+
await putFileAsync(cssFile, appCssStub);
|
|
102
|
+
console.log(`${colors.green("✓")} Created src/web/app.css`);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Write `postcss.config.mjs`, unless the project already has a PostCSS config.
|
|
107
|
+
*
|
|
108
|
+
* An existing config is left completely alone. It is an app-owned build file
|
|
109
|
+
* that may already register autoprefixer, nesting, or a CSS-modules pass, and
|
|
110
|
+
* appending Tailwind to it by string surgery would be guessing at both its
|
|
111
|
+
* module format and its plugin order.
|
|
112
|
+
*/
|
|
113
|
+
async function writePostcssConfig() {
|
|
114
|
+
for (const name of POSTCSS_CONFIG_FILES) {
|
|
115
|
+
if (!await fileExistsAsync(rootPath(name))) continue;
|
|
116
|
+
if ((await getFileAsync(rootPath(name)).catch(() => "")).includes("@tailwindcss/postcss")) console.log(`${colors.yellowBright(name)} already registers Tailwind, skipping...`);
|
|
117
|
+
else console.log(`${colors.yellowBright("!")} ${colors.yellowBright(name)} already exists — add Tailwind to it yourself:\n plugins: { "@tailwindcss/postcss": {} }\n Two PostCSS configs in one project is undefined behaviour, so this feature did not write a second one.`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await putFileAsync(rootPath("postcss.config.mjs"), postcssConfigStub);
|
|
121
|
+
console.log(`${colors.green("✓")} Created postcss.config.mjs`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Import the stylesheet from `src/web/root.tsx`.
|
|
125
|
+
*
|
|
126
|
+
* The root is the one module every SSR page renders through, so importing the
|
|
127
|
+
* stylesheet there is what puts it in the client bundle for every route.
|
|
128
|
+
*
|
|
129
|
+
* `root.tsx` is guaranteed to exist by the time this runs: `requires: ["web"]`
|
|
130
|
+
* makes the add command resolve `web` ahead of `tailwind` and run its
|
|
131
|
+
* `onExecuting` first, and that is what creates the file. The check below is
|
|
132
|
+
* therefore for a root a human has since moved or deleted — a case worth a
|
|
133
|
+
* printed instruction, not a failure.
|
|
134
|
+
*/
|
|
135
|
+
async function importStylesheetFromRoot() {
|
|
136
|
+
const rootFile = srcPath("web/root.tsx");
|
|
137
|
+
if (!await fileExistsAsync(rootFile)) {
|
|
138
|
+
console.log(`${colors.yellowBright("!")} ${colors.yellowBright("src/web/root.tsx")} not found — add ${colors.yellowBright(APP_CSS_IMPORT)} to your application root yourself.\n Until the stylesheet is imported from a module the bundler reaches, no Tailwind CSS is emitted.`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const current = await getFileAsync(rootFile);
|
|
142
|
+
if (/import\s+["'].*app\.css["']/.test(current)) {
|
|
143
|
+
console.log(`${colors.yellowBright("src/web/root.tsx")} already imports app.css, skipping...`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
await putFileAsync(rootFile, `${APP_CSS_IMPORT}\n${current}`);
|
|
147
|
+
console.log(`${colors.green("✓")} Imported app.css in src/web/root.tsx`);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Wire Tailwind v4 into the page layer.
|
|
151
|
+
*
|
|
152
|
+
* Three files, none of which needs `node_modules` to be populated: on the
|
|
153
|
+
* `create-warlock` path this runs under `--no-install`, so the dependencies
|
|
154
|
+
* declared below are only recorded in `package.json` and nothing here may
|
|
155
|
+
* import, resolve, or execute Tailwind itself.
|
|
156
|
+
*/
|
|
157
|
+
async function completeTailwindInstallation(_options) {
|
|
158
|
+
await createStylesheet();
|
|
159
|
+
await writePostcssConfig();
|
|
160
|
+
await importStylesheetFromRoot();
|
|
161
|
+
}
|
|
162
|
+
const tailwindFeature = {
|
|
163
|
+
description: "Installs Tailwind CSS v4 for the SSR page layer. Creates src/web/app.css (the CSS-first config — v4 has no tailwind.config.js), imports it from src/web/root.tsx, and registers the engine through postcss.config.mjs, which Vite loads in dev and build alike.",
|
|
164
|
+
requires: ["web"],
|
|
165
|
+
devDependencies: {
|
|
166
|
+
tailwindcss: "^4.1.16",
|
|
167
|
+
"@tailwindcss/postcss": "^4.1.16"
|
|
168
|
+
},
|
|
169
|
+
onExecuting: completeTailwindInstallation
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
export { tailwindFeature };
|
|
174
|
+
//# sourceMappingURL=tailwind.feature.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tailwind.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/tailwind.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n} from \"@warlock.js/fs\";\nimport { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { FeatureDefinition } from \"./types\";\n\n/**\n * The stylesheet entry, and the whole of Tailwind's configuration.\n *\n * Tailwind v4 is CSS-first: there is no `tailwind.config.js` and no `content`\n * globs to keep in sync with the project layout — the engine discovers the\n * templates that reach it through the bundler graph, and everything a v3 config\n * held (`theme`, `plugins`, `darkMode`) is expressed in CSS beside the import.\n * So this one file is the config, which is why the feature writes it rather\n * than an `ejectConfig` entry: `ejectConfig` lands in `src/config/*.ts`, and a\n * stylesheet that must sit in the bundler's path does not belong there.\n */\nconst appCssStub = `@import \"tailwindcss\";\n\n/*\n Tailwind v4 configures itself from CSS — there is no tailwind.config.js.\n\n Design tokens go in an @theme block, and each one becomes both a CSS variable\n and a utility class:\n\n @theme {\n --color-brand: oklch(0.62 0.19 259);\n --font-display: \"Inter\", sans-serif;\n }\n\n gives you \\`bg-brand\\`, \\`text-brand\\`, \\`font-display\\` and \\`var(--color-brand)\\`.\n\n Plugins are imported here too — \\`@plugin \"@tailwindcss/typography\";\\` — and\n your own non-utility CSS can simply follow this comment.\n*/\n`;\n\n/**\n * PostCSS rather than a Vite config file, and deliberately at the project root.\n *\n * Vite discovers `postcss.config.mjs` from the project root on its own, in the\n * dev server and in `vite build` alike, without the application owning a Vite\n * config at all. That matters here: the only app-facing Vite plugin array in\n * this framework is `webConnector({ plugins })`, which is wired into the DEV\n * server exclusively — the client build composes its own plugin list from its\n * caller. Registering `@tailwindcss/vite` there would produce styles in `dev`\n * and silently drop them from a production build, which is a worse failure than\n * any config file, because it only shows up after deploy.\n *\n * `.mjs` because a Warlock project's `package.json` is not guaranteed to set\n * `\"type\": \"module\"`, and `export default` in a `.js` file would throw in a\n * CommonJS project.\n */\nconst postcssConfigStub = `/**\n * Tailwind v4 runs as a PostCSS plugin. Vite loads this file automatically —\n * in dev and in build — so no Vite configuration is required.\n *\n * The plugin lives in its own package in v4: \\`tailwindcss\\` is the engine,\n * \\`@tailwindcss/postcss\\` is the adapter. Naming \\`tailwindcss\\` here directly is\n * the v3 spelling and will not work.\n */\nexport default {\n plugins: {\n \"@tailwindcss/postcss\": {},\n },\n};\n`;\n\n/**\n * Every filename Vite/PostCSS will pick up from the project root.\n *\n * Checked as a set rather than just writing our own: two PostCSS configs in one\n * directory is not a merge, it is a coin toss over which one loads, so a project\n * that already has one gets instructions instead of a second file.\n */\nconst POSTCSS_CONFIG_FILES = [\n \"postcss.config.mjs\",\n \"postcss.config.js\",\n \"postcss.config.cjs\",\n \"postcss.config.ts\",\n \"postcss.config.json\",\n \".postcssrc\",\n \".postcssrc.js\",\n \".postcssrc.json\",\n];\n\n/** The side-effect import that pulls the stylesheet into the bundler graph. */\nconst APP_CSS_IMPORT = 'import \"./app.css\";';\n\n/**\n * Create `src/web/app.css` — the sentinel for \"this feature already ran\".\n *\n * Nothing in the project template creates this file (the scaffold ships a\n * static `public/home.css` instead), so its presence means `add tailwind` has\n * been here, and its contents may since have been edited into a real design\n * system. It is never rewritten.\n */\nasync function createStylesheet(): Promise<boolean> {\n const cssFile = srcPath(\"web/app.css\");\n\n if (await fileExistsAsync(cssFile)) {\n console.log(`${colors.yellowBright(\"src/web/app.css\")} already exists, skipping...`);\n\n return false;\n }\n\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(cssFile, appCssStub);\n console.log(`${colors.green(\"✓\")} Created src/web/app.css`);\n\n return true;\n}\n\n/**\n * Write `postcss.config.mjs`, unless the project already has a PostCSS config.\n *\n * An existing config is left completely alone. It is an app-owned build file\n * that may already register autoprefixer, nesting, or a CSS-modules pass, and\n * appending Tailwind to it by string surgery would be guessing at both its\n * module format and its plugin order.\n */\nasync function writePostcssConfig(): Promise<void> {\n for (const name of POSTCSS_CONFIG_FILES) {\n if (!(await fileExistsAsync(rootPath(name)))) continue;\n\n const current = await getFileAsync(rootPath(name)).catch(() => \"\");\n\n if (current.includes(\"@tailwindcss/postcss\")) {\n console.log(`${colors.yellowBright(name)} already registers Tailwind, skipping...`);\n } else {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(name)} already exists — add Tailwind to it yourself:\\n` +\n ` plugins: { \"@tailwindcss/postcss\": {} }\\n` +\n \" Two PostCSS configs in one project is undefined behaviour, so this feature did not write a second one.\",\n );\n }\n\n return;\n }\n\n await putFileAsync(rootPath(\"postcss.config.mjs\"), postcssConfigStub);\n console.log(`${colors.green(\"✓\")} Created postcss.config.mjs`);\n}\n\n/**\n * Import the stylesheet from `src/web/root.tsx`.\n *\n * The root is the one module every SSR page renders through, so importing the\n * stylesheet there is what puts it in the client bundle for every route.\n *\n * `root.tsx` is guaranteed to exist by the time this runs: `requires: [\"web\"]`\n * makes the add command resolve `web` ahead of `tailwind` and run its\n * `onExecuting` first, and that is what creates the file. The check below is\n * therefore for a root a human has since moved or deleted — a case worth a\n * printed instruction, not a failure.\n */\nasync function importStylesheetFromRoot(): Promise<void> {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (!(await fileExistsAsync(rootFile))) {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/root.tsx\")} not found — ` +\n `add ${colors.yellowBright(APP_CSS_IMPORT)} to your application root yourself.\\n` +\n \" Until the stylesheet is imported from a module the bundler reaches, no Tailwind CSS is emitted.\",\n );\n\n return;\n }\n\n const current = await getFileAsync(rootFile);\n\n // Matches the import whichever quote style and specifier the file uses, so a\n // re-run against a hand-edited root does not stack a second copy.\n if (/import\\s+[\"'].*app\\.css[\"']/.test(current)) {\n console.log(`${colors.yellowBright(\"src/web/root.tsx\")} already imports app.css, skipping...`);\n\n return;\n }\n\n // FIRST line, above the framework imports. A side-effect CSS import has no\n // binding to order against, and putting it at the top keeps it from being\n // swept away by an import sorter that only ranks module specifiers.\n await putFileAsync(rootFile, `${APP_CSS_IMPORT}\\n${current}`);\n console.log(`${colors.green(\"✓\")} Imported app.css in src/web/root.tsx`);\n}\n\n/**\n * Wire Tailwind v4 into the page layer.\n *\n * Three files, none of which needs `node_modules` to be populated: on the\n * `create-warlock` path this runs under `--no-install`, so the dependencies\n * declared below are only recorded in `package.json` and nothing here may\n * import, resolve, or execute Tailwind itself.\n */\nasync function completeTailwindInstallation(_options: CommandActionData) {\n await createStylesheet();\n await writePostcssConfig();\n await importStylesheetFromRoot();\n}\n\nexport const tailwindFeature: FeatureDefinition = {\n description:\n \"Installs Tailwind CSS v4 for the SSR page layer. Creates src/web/app.css (the CSS-first config — v4 has no tailwind.config.js), imports it from src/web/root.tsx, and registers the engine through postcss.config.mjs, which Vite loads in dev and build alike.\",\n // `web` owns root.tsx and the CSS pipeline that serves this stylesheet.\n // Requiring it also fixes the order: the add command resolves requirements\n // depth-first, so `web` scaffolds src/web before this feature writes into it.\n requires: [\"web\"],\n devDependencies: {\n // Build-time only, both of them — Tailwind compiles to a plain stylesheet\n // and nothing it ships is imported at runtime.\n tailwindcss: \"^4.1.16\",\n // The engine and its PostCSS adapter are separate packages in v4 and are\n // released in lockstep; keep these two ranges identical.\n \"@tailwindcss/postcss\": \"^4.1.16\",\n },\n onExecuting: completeTailwindInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAsBA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnB,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;AAsB1B,MAAM,uBAAuB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,MAAM,iBAAiB;;;;;;;;;AAUvB,eAAe,mBAAqC;CAClD,MAAM,UAAU,QAAQ,aAAa;CAErC,IAAI,MAAM,gBAAgB,OAAO,GAAG;EAClC,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,6BAA6B;EAEnF,OAAO;CACT;CAEA,MAAM,qBAAqB,QAAQ,KAAK,CAAC;CACzC,MAAM,aAAa,SAAS,UAAU;CACtC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yBAAyB;CAE1D,OAAO;AACT;;;;;;;;;AAUA,eAAe,qBAAoC;CACjD,KAAK,MAAM,QAAQ,sBAAsB;EACvC,IAAI,CAAE,MAAM,gBAAgB,SAAS,IAAI,CAAC,GAAI;EAI9C,KAAI,MAFkB,aAAa,SAAS,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,EAEtD,CAAC,SAAS,sBAAsB,GACzC,QAAQ,IAAI,GAAG,OAAO,aAAa,IAAI,EAAE,yCAAyC;OAElF,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,IAAI,EAAE,oMAG3D;EAGF;CACF;CAEA,MAAM,aAAa,SAAS,oBAAoB,GAAG,iBAAiB;CACpE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,4BAA4B;AAC/D;;;;;;;;;;;;;AAcA,eAAe,2BAA0C;CACvD,MAAM,WAAW,QAAQ,cAAc;CAEvC,IAAI,CAAE,MAAM,gBAAgB,QAAQ,GAAI;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,kBAAkB,EAAE,mBAC9D,OAAO,aAAa,cAAc,EAAE,uIAE/C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,QAAQ;CAI3C,IAAI,8BAA8B,KAAK,OAAO,GAAG;EAC/C,QAAQ,IAAI,GAAG,OAAO,aAAa,kBAAkB,EAAE,sCAAsC;EAE7F;CACF;CAKA,MAAM,aAAa,UAAU,GAAG,eAAe,IAAI,SAAS;CAC5D,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,sCAAsC;AACzE;;;;;;;;;AAUA,eAAe,6BAA6B,UAA6B;CACvE,MAAM,iBAAiB;CACvB,MAAM,mBAAmB;CACzB,MAAM,yBAAyB;AACjC;AAEA,MAAa,kBAAqC;CAChD,aACE;CAIF,UAAU,CAAC,KAAK;CAChB,iBAAiB;EAGf,aAAa;EAGb,wBAAwB;CAC1B;CACA,aAAa;AACf"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
|
+
import "../../utils/index.mjs";
|
|
3
|
+
import { colors } from "@mongez/copper";
|
|
4
|
+
import { fileExistsAsync, putFileAsync } from "@warlock.js/fs";
|
|
5
|
+
|
|
6
|
+
//#region ../core/src/generations/features/test.feature.ts
|
|
7
|
+
async function completeTestInstallation(options) {
|
|
8
|
+
const testGlobalSetupPath = srcPath("test-global-setup.ts");
|
|
9
|
+
if (!await fileExistsAsync(testGlobalSetupPath)) {
|
|
10
|
+
await putFileAsync(testGlobalSetupPath, `/**
|
|
11
|
+
* Global Test Setup
|
|
12
|
+
*
|
|
13
|
+
* Runs ONCE before all test workers.
|
|
14
|
+
* Starts the HTTP server for integration tests.
|
|
15
|
+
*/
|
|
16
|
+
import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests";
|
|
17
|
+
|
|
18
|
+
export async function setup() {
|
|
19
|
+
await startHttpTestServer();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function teardown() {
|
|
23
|
+
await stopHttpTestServer();
|
|
24
|
+
}
|
|
25
|
+
`);
|
|
26
|
+
console.log(`${colors.green("✓")} Created src/test-global-setup.ts`);
|
|
27
|
+
}
|
|
28
|
+
const testSetupPath = srcPath("test-setup.ts");
|
|
29
|
+
if (!await fileExistsAsync(testSetupPath)) {
|
|
30
|
+
await putFileAsync(testSetupPath, `/**
|
|
31
|
+
* Test Setup - runs before EVERY test file
|
|
32
|
+
*
|
|
33
|
+
* Vitest runs setupFiles before each test file and rebuilds the module
|
|
34
|
+
* registry with it, so this pair boots and closes the test runtime once per
|
|
35
|
+
* test file.
|
|
36
|
+
*
|
|
37
|
+
* setupTest() is called with no options on purpose: an explicit connectors
|
|
38
|
+
* value outranks tests.connectors from src/config/tests.ts, so passing one
|
|
39
|
+
* here would erase your project config. Omitting it leaves the config in
|
|
40
|
+
* charge.
|
|
41
|
+
*
|
|
42
|
+
* afterAll(teardownTest) is the other half of the pair: whoever calls
|
|
43
|
+
* setupTest() owns closing it in the same runtime context.
|
|
44
|
+
*/
|
|
45
|
+
import { setupTest, teardownTest } from "@warlock.js/core/tests";
|
|
46
|
+
import { afterAll } from "vitest";
|
|
47
|
+
|
|
48
|
+
await setupTest();
|
|
49
|
+
|
|
50
|
+
afterAll(teardownTest);
|
|
51
|
+
`);
|
|
52
|
+
console.log(`${colors.green("✓")} Created src/test-setup.ts`);
|
|
53
|
+
}
|
|
54
|
+
const viteConfigPath = rootPath("vite.config.ts");
|
|
55
|
+
if (!await fileExistsAsync(viteConfigPath)) {
|
|
56
|
+
await putFileAsync(viteConfigPath, `import { lowerStage3Decorators } from "@warlock.js/core/vite";
|
|
57
|
+
import mongezVite from "@mongez/vite";
|
|
58
|
+
import { defineConfig } from "vitest/config";
|
|
59
|
+
|
|
60
|
+
export default defineConfig({
|
|
61
|
+
// lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)
|
|
62
|
+
// decorators with esbuild before oxc / the SSR rewrite can mangle them, so
|
|
63
|
+
// decorated Cascade models load under Vitest.
|
|
64
|
+
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
65
|
+
test: {
|
|
66
|
+
globalSetup: "./src/test-global-setup.ts", // HTTP server - runs once
|
|
67
|
+
setupFiles: ["./src/test-setup.ts"], // DB/cache - runs per test file
|
|
68
|
+
environment: "node",
|
|
69
|
+
globals: false,
|
|
70
|
+
include: ["src/app/**/*.test.ts"],
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
`);
|
|
74
|
+
console.log(`${colors.green("✓")} Created vite.config.ts`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const testFeature = {
|
|
78
|
+
description: "Installs warlock test for testing",
|
|
79
|
+
onExecuting: completeTestInstallation,
|
|
80
|
+
script: {
|
|
81
|
+
test: "vitest run",
|
|
82
|
+
"test:coverage": "vitest run --coverage",
|
|
83
|
+
"test:ui": "vitest --ui",
|
|
84
|
+
"test:watch": "vitest --watch"
|
|
85
|
+
},
|
|
86
|
+
devDependencies: {
|
|
87
|
+
"@mongez/vite": "^2.0.4",
|
|
88
|
+
vite: "^8.0.16",
|
|
89
|
+
vitest: "^4.1.8",
|
|
90
|
+
"@vitest/coverage-v8": "^4.1.8"
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
export { testFeature };
|
|
96
|
+
//# sourceMappingURL=test.feature.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/test.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { fileExistsAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { FeatureDefinition } from \"./types\";\r\n\r\nasync function completeTestInstallation(options: CommandActionData) {\r\n // Create test-global-setup.ts (runs once before all tests)\r\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\r\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\r\n\r\n if (!testGlobalSetupExists) {\r\n await putFileAsync(\r\n testGlobalSetupPath,\r\n `/**\r\n * Global Test Setup\r\n *\r\n * Runs ONCE before all test workers.\r\n * Starts the HTTP server for integration tests.\r\n */\r\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\r\n\r\nexport async function setup() {\r\n await startHttpTestServer();\r\n}\r\n\r\nexport async function teardown() {\r\n await stopHttpTestServer();\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\r\n }\r\n\r\n // Create test-setup.ts (runs before EVERY test file)\r\n const testSetupPath = srcPath(\"test-setup.ts\");\r\n const testSetupExists = await fileExistsAsync(testSetupPath);\r\n\r\n if (!testSetupExists) {\r\n await putFileAsync(\r\n testSetupPath,\r\n `/**\r\n * Test Setup - runs before EVERY test file\r\n *\r\n * Vitest runs setupFiles before each test file and rebuilds the module\r\n * registry with it, so this pair boots and closes the test runtime once per\r\n * test file.\r\n *\r\n * setupTest() is called with no options on purpose: an explicit connectors\r\n * value outranks tests.connectors from src/config/tests.ts, so passing one\r\n * here would erase your project config. Omitting it leaves the config in\r\n * charge.\r\n *\r\n * afterAll(teardownTest) is the other half of the pair: whoever calls\r\n * setupTest() owns closing it in the same runtime context.\r\n */\r\nimport { setupTest, teardownTest } from \"@warlock.js/core/tests\";\r\nimport { afterAll } from \"vitest\";\r\n\r\nawait setupTest();\r\n\r\nafterAll(teardownTest);\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\r\n }\r\n\r\n // Create vite.config.ts\r\n const viteConfigPath = rootPath(\"vite.config.ts\");\r\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\r\n\r\n if (!viteConfigExists) {\r\n await putFileAsync(\r\n viteConfigPath,\r\n `import { lowerStage3Decorators } from \"@warlock.js/core/vite\";\r\nimport mongezVite from \"@mongez/vite\";\r\nimport { defineConfig } from \"vitest/config\";\r\n\r\nexport default defineConfig({\r\n // lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)\r\n // decorators with esbuild before oxc / the SSR rewrite can mangle them, so\r\n // decorated Cascade models load under Vitest.\r\n plugins: [lowerStage3Decorators(), mongezVite()],\r\n test: {\r\n globalSetup: \"./src/test-global-setup.ts\", // HTTP server - runs once\r\n setupFiles: [\"./src/test-setup.ts\"], // DB/cache - runs per test file\r\n environment: \"node\",\r\n globals: false,\r\n include: [\"src/app/**/*.test.ts\"],\r\n },\r\n});\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\r\n }\r\n}\r\n\r\nexport const testFeature: FeatureDefinition = {\r\n description: \"Installs warlock test for testing\",\r\n onExecuting: completeTestInstallation,\r\n script: {\r\n test: \"vitest run\",\r\n \"test:coverage\": \"vitest run --coverage\",\r\n \"test:ui\": \"vitest --ui\",\r\n \"test:watch\": \"vitest --watch\",\r\n },\r\n devDependencies: {\r\n \"@mongez/vite\": \"^2.0.4\",\r\n vite: \"^8.0.16\",\r\n vitest: \"^4.1.8\",\r\n \"@vitest/coverage-v8\": \"^4.1.8\",\r\n },\r\n};\r\n"],"mappings":";;;;;;AAMA,eAAe,yBAAyB,SAA4B;CAElE,MAAM,sBAAsB,QAAQ,sBAAsB;CAG1D,IAAI,CAAC,MAF+B,gBAAgB,mBAAmB,GAE3C;EAC1B,MAAM,aACJ,qBACA;;;;;;;;;;;;;;;CAgBF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kCAAkC;CACrE;CAGA,MAAM,gBAAgB,QAAQ,eAAe;CAG7C,IAAI,CAAC,MAFyB,gBAAgB,aAAa,GAErC;EACpB,MAAM,aACJ,eACA;;;;;;;;;;;;;;;;;;;;;CAsBF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2BAA2B;CAC9D;CAGA,MAAM,iBAAiB,SAAS,gBAAgB;CAGhD,IAAI,CAAC,MAF0B,gBAAgB,cAAc,GAEtC;EACrB,MAAM,aACJ,gBACA;;;;;;;;;;;;;;;;;CAkBF;EACA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,wBAAwB;CAC3D;AACF;AAEA,MAAa,cAAiC;CAC5C,aAAa;CACb,aAAa;CACb,QAAQ;EACN,MAAM;EACN,iBAAiB;EACjB,WAAW;EACX,cAAc;CAChB;CACA,iBAAiB;EACf,gBAAgB;EAChB,MAAM;EACN,QAAQ;EACR,uBAAuB;CACzB;AACF"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
|
+
import "../../utils/index.mjs";
|
|
3
|
+
import { webHomePageStub, webRootStub } from "../stubs.mjs";
|
|
4
|
+
import { colors } from "@mongez/copper";
|
|
5
|
+
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
6
|
+
|
|
7
|
+
//#region ../core/src/generations/features/web.feature.ts
|
|
8
|
+
/**
|
|
9
|
+
* Register the WebConnector in `warlock.config.ts`, and ONLY there.
|
|
10
|
+
*
|
|
11
|
+
* It belongs to the config array or to app code, never both. Both halves are
|
|
12
|
+
* registered before app code loads — the CLI preloader in dev, the generated
|
|
13
|
+
* entry in production — so also calling `connectorsManager.register(...)` in
|
|
14
|
+
* `src/app/main.ts` boots the connector twice and installs every page route
|
|
15
|
+
* twice. That surfaces at PRODUCTION boot as `Route name "..." is already
|
|
16
|
+
* taken`, because pages and API routes share one route-name namespace.
|
|
17
|
+
*
|
|
18
|
+
* The config array is the half to prefer: `warlock build` reads the same array
|
|
19
|
+
* to drain each connector's build contribution, so "built for" and "boots with"
|
|
20
|
+
* cannot drift.
|
|
21
|
+
*
|
|
22
|
+
* String surgery rather than a TypeScript parse: `warlock.config.ts` is an
|
|
23
|
+
* app-owned file that may carry any formatting, and a parse-and-print would
|
|
24
|
+
* reformat the parts we did not come to change.
|
|
25
|
+
*/
|
|
26
|
+
async function registerWebConnector() {
|
|
27
|
+
const configPath = rootPath("warlock.config.ts");
|
|
28
|
+
if (!await fileExistsAsync(configPath)) {
|
|
29
|
+
console.log(`${colors.yellowBright("warlock.config.ts")} not found — add this yourself:\n import { webConnector } from "@warlock.js/web/connector";\n export default defineConfig({ connectors: [webConnector()] });`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const current = await getFileAsync(configPath);
|
|
33
|
+
if (current.includes("webConnector")) {
|
|
34
|
+
console.log(`${colors.yellowBright("webConnector")} already registered, skipping...`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const importLine = "import { webConnector } from \"@warlock.js/web/connector\";";
|
|
38
|
+
let next = current.includes(importLine) ? current : `${importLine}\n${current}`;
|
|
39
|
+
if (/connectors:\s*\[/.test(next)) next = next.replace(/connectors:\s*\[/, "connectors: [webConnector(),");
|
|
40
|
+
else if (next.includes("defineConfig({")) next = next.replace("defineConfig({", "defineConfig({\n connectors: [webConnector()],");
|
|
41
|
+
else {
|
|
42
|
+
console.log(`${colors.yellowBright("warlock.config.ts")} has no recognisable defineConfig({...}) — add \`connectors: [webConnector()]\` yourself.`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await putFileAsync(configPath, next);
|
|
46
|
+
console.log(`${colors.green("✓")} Registered webConnector in warlock.config.ts`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The app routes file the project template registers `GET /` in. Only this one
|
|
50
|
+
* path is inspected: `warlock add web` is not a codebase-wide route auditor, and
|
|
51
|
+
* a project that keeps its routes elsewhere lands on the `absent` outcome below,
|
|
52
|
+
* which writes the page exactly as before.
|
|
53
|
+
*/
|
|
54
|
+
const APP_ROUTES_FILE = "app/shared/routes.ts";
|
|
55
|
+
/**
|
|
56
|
+
* A TOP-LEVEL `router.get("/", ...)` — anchored at column 0 on purpose.
|
|
57
|
+
*
|
|
58
|
+
* Routes nested in a `router.group({ prefix: "/x" }, ...)` are indented by every
|
|
59
|
+
* formatter this codebase runs, and their real path is `/x`, not `/`. Anchoring
|
|
60
|
+
* is what keeps the notifications feature's own `router.get("/", ...)` (inside
|
|
61
|
+
* the `/notifications` group) from reading as a homepage collision.
|
|
62
|
+
*
|
|
63
|
+
* Only the path literal is captured. The handler — a bare identifier in the
|
|
64
|
+
* template, but possibly an inline arrow spanning lines — is never matched, so
|
|
65
|
+
* the rewrite below cannot damage it.
|
|
66
|
+
*/
|
|
67
|
+
const TOP_LEVEL_ROOT_GET = /^router\s*\.\s*get\(\s*(["'`])\/\1/gm;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `/welcome` is already spoken for, so relocating onto it would trade
|
|
70
|
+
* one duplicate-route 500 for another.
|
|
71
|
+
*/
|
|
72
|
+
const TOP_LEVEL_WELCOME_GET = /^router\s*\.\s*get\(\s*(["'`])\/welcome\1/m;
|
|
73
|
+
/**
|
|
74
|
+
* Make room for a page that declares `route = "/"`.
|
|
75
|
+
*
|
|
76
|
+
* The project template registers `router.get("/", homePageController)` and the
|
|
77
|
+
* page stub declares `route = "/"`. Fastify rejects the second registration
|
|
78
|
+
* (`Method 'GET' already declared for route '/'`) and the homepage 500s at
|
|
79
|
+
* request time — so `warlock add web` cannot just write the page and hope.
|
|
80
|
+
*
|
|
81
|
+
* Of the three ways out, this RELOCATES the JSON route to `/welcome` rather than
|
|
82
|
+
* deleting it or refusing to scaffold:
|
|
83
|
+
*
|
|
84
|
+
* - Deleting the controller is what the scaffolder's own `react` feature does,
|
|
85
|
+
* but it may do that: it owns the file it is deleting, seconds after writing
|
|
86
|
+
* it. `warlock add web` runs against a project a human has been living in, and
|
|
87
|
+
* silently unlinking their code is not a thing an `add` command gets to do.
|
|
88
|
+
* - Writing the page anyway and printing a warning ships a project whose
|
|
89
|
+
* homepage 500s. A warning above a broken app is still a broken app.
|
|
90
|
+
* - Relocating keeps BOTH surfaces working: the React homepage takes `/`, the
|
|
91
|
+
* JSON welcome answers at `/welcome`, and no line of user code disappears.
|
|
92
|
+
*
|
|
93
|
+
* Only the exact top-level shape is rewritten, and only the path literal inside
|
|
94
|
+
* it. Anything else that claims `/` is reported and left completely alone — we
|
|
95
|
+
* do not guess at code we cannot recognise.
|
|
96
|
+
*/
|
|
97
|
+
async function relocateConflictingHomeRoute() {
|
|
98
|
+
const routesPath = srcPath(APP_ROUTES_FILE);
|
|
99
|
+
if (!await fileExistsAsync(routesPath)) return { outcome: "absent" };
|
|
100
|
+
let current;
|
|
101
|
+
try {
|
|
102
|
+
current = await getFileAsync(routesPath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
outcome: "failed",
|
|
106
|
+
reason: `could not be read (${error.message})`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const matches = current.match(TOP_LEVEL_ROOT_GET) ?? [];
|
|
110
|
+
if (matches.length === 0) return { outcome: "absent" };
|
|
111
|
+
if (matches.length > 1) return {
|
|
112
|
+
outcome: "conflict",
|
|
113
|
+
reason: `declares ${matches.length} top-level GET "/" routes`
|
|
114
|
+
};
|
|
115
|
+
if (TOP_LEVEL_WELCOME_GET.test(current)) return {
|
|
116
|
+
outcome: "conflict",
|
|
117
|
+
reason: "already declares GET \"/welcome\", so the usual relocation target is taken"
|
|
118
|
+
};
|
|
119
|
+
const next = current.replace(TOP_LEVEL_ROOT_GET, (match, quote) => match.replace(`${quote}/${quote}`, `${quote}/welcome${quote}`));
|
|
120
|
+
if (next === current) return {
|
|
121
|
+
outcome: "conflict",
|
|
122
|
+
reason: "its GET \"/\" route could not be rewritten"
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
await putFileAsync(routesPath, next);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
outcome: "failed",
|
|
129
|
+
reason: `could not be written (${error.message})`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { outcome: "relocated" };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Scaffold the smallest page layer that renders, and register the connector.
|
|
136
|
+
*
|
|
137
|
+
* `src/web/root.tsx` is the sentinel for "already scaffolded" — the framework
|
|
138
|
+
* ships a default root, so its presence means a human has been here.
|
|
139
|
+
*/
|
|
140
|
+
async function completeWebInstallation(_options) {
|
|
141
|
+
const rootFile = srcPath("web/root.tsx");
|
|
142
|
+
if (await fileExistsAsync(rootFile)) console.log(`${colors.yellowBright("src/web")} already scaffolded, skipping...`);
|
|
143
|
+
else {
|
|
144
|
+
await ensureDirectoryAsync(srcPath("web"));
|
|
145
|
+
await putFileAsync(rootFile, webRootStub);
|
|
146
|
+
console.log(`${colors.green("✓")} Created src/web/root.tsx`);
|
|
147
|
+
const collision = await relocateConflictingHomeRoute();
|
|
148
|
+
if (collision.outcome === "relocated") console.log(`${colors.green("✓")} Moved the existing ${colors.yellowBright("GET \"/\"")} route to ${colors.yellowBright("\"/welcome\"")} in ${colors.yellowBright(`src/${APP_ROUTES_FILE}`)} — the new page owns \`/\` now, and the JSON welcome route still answers at /welcome.`);
|
|
149
|
+
if (collision.outcome === "conflict" || collision.outcome === "failed") {
|
|
150
|
+
const verb = collision.outcome === "failed" ? colors.redBright("✗") : colors.yellowBright("!");
|
|
151
|
+
console.log(`${verb} Did not create src/web/home.page.tsx: ${colors.yellowBright(`src/${APP_ROUTES_FILE}`)} ${collision.reason}.\n The page stub declares ${colors.yellowBright("route = \"/\"")}, and two handlers on one path is a 500 at request time, not a startup error.
|
|
152
|
+
Free up ${colors.yellowBright("GET \"/\"")} in that file — move it to a path of its own, or remove it — then create src/web/home.page.tsx yourself. Giving the page a \`route\` other than \`/\` works too.`);
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
} else {
|
|
155
|
+
await putFileAsync(srcPath("web/home.page.tsx"), webHomePageStub);
|
|
156
|
+
console.log(`${colors.green("✓")} Created src/web/home.page.tsx`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await registerWebConnector();
|
|
160
|
+
}
|
|
161
|
+
const webFeature = {
|
|
162
|
+
description: "Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.",
|
|
163
|
+
dependencies: {
|
|
164
|
+
"@warlock.js/web": "~4.0.0",
|
|
165
|
+
react: "^19.2.3",
|
|
166
|
+
"react-dom": "^19.2.3"
|
|
167
|
+
},
|
|
168
|
+
devDependencies: {
|
|
169
|
+
"@types/react": "^19.2.7",
|
|
170
|
+
"@types/react-dom": "^19.2.3",
|
|
171
|
+
vite: "^7.3.5",
|
|
172
|
+
"@vitejs/plugin-react": "^5.2.0"
|
|
173
|
+
},
|
|
174
|
+
onExecuting: completeWebInstallation
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
export { webFeature };
|
|
179
|
+
//# sourceMappingURL=web.feature.mjs.map
|