@forinda/kickjs-cli 8.8.0 → 8.9.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/dist/{agent-docs-BsXKM5QH.mjs → agent-docs-DUB-1Z6Q.mjs} +34 -13
- package/dist/{agent-docs-BsXKM5QH.mjs.map → agent-docs-DUB-1Z6Q.mjs.map} +1 -1
- package/dist/{agent-docs-DQksWOe5.mjs → agent-docs-bVMPt2m5.mjs} +33 -12
- package/dist/{build-Bnd_mk4h.mjs → build-C30mBgw6.mjs} +3 -3
- package/dist/{build-Bnd_mk4h.mjs.map → build-C30mBgw6.mjs.map} +1 -1
- package/dist/{build-DPUojF2G.mjs → build-xQXbdDpw.mjs} +2 -2
- package/dist/{builtins-BEBO5upe.mjs → builtins-CtpLZ4FY.mjs} +2 -2
- package/dist/{builtins-CJTP3xQj.mjs → builtins-DiWfXc-D.mjs} +115 -101
- package/dist/cli.mjs +2 -2
- package/dist/{config-ty1Nc3PN.mjs → config-YvIngROH.mjs} +2 -2
- package/dist/{config-F9g6CNDA.mjs → config-ZHuFjsKI.mjs} +3 -3
- package/dist/config-ZHuFjsKI.mjs.map +1 -0
- package/dist/{doctor-C8TAKn1d.mjs → doctor-CnsnRW7T.mjs} +4 -4
- package/dist/{doctor-C8TAKn1d.mjs.map → doctor-CnsnRW7T.mjs.map} +1 -1
- package/dist/{fs-BGHl3dHm.mjs → fs-Bm2St4gk.mjs} +1 -1
- package/dist/{fs-BB3QxPj8.mjs → fs-BvGbbHY0.mjs} +2 -2
- package/dist/{fs-BB3QxPj8.mjs.map → fs-BvGbbHY0.mjs.map} +1 -1
- package/dist/fullstack-B0g06sSQ.mjs +96 -0
- package/dist/fullstack-B0g06sSQ.mjs.map +1 -0
- package/dist/fullstack-CZ9XQYk-.mjs +95 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{plugin-C4Dl_s4-.mjs → plugin-BfdvW6_r.mjs} +2 -2
- package/dist/{plugin-Y7BoYk1K.mjs → plugin-Bs0soxrU.mjs} +2 -2
- package/dist/{project-3LsfnPLE.mjs → project-B_qDdkYQ.mjs} +89 -23
- package/dist/project-B_qDdkYQ.mjs.map +1 -0
- package/dist/{project-BIq7_Fzk.mjs → project-Bp-KfBNb.mjs} +88 -22
- package/dist/{project-root-CFrn6ftT.mjs → project-root-B9BrO1na.mjs} +3 -3
- package/dist/{project-root-CFrn6ftT.mjs.map → project-root-B9BrO1na.mjs.map} +1 -1
- package/dist/{project-root-BDRPMlb9.mjs → project-root-LYOxQHXp.mjs} +2 -2
- package/dist/{prompts-BJTp1w0l.mjs → prompts-BEAqcbPT.mjs} +2 -2
- package/dist/{prompts-BJTp1w0l.mjs.map → prompts-BEAqcbPT.mjs.map} +1 -1
- package/dist/{rolldown-runtime-OACgOwtl.mjs → rolldown-runtime-Da-NDvKz.mjs} +1 -1
- package/dist/{run-plugins-2DJy76gx.mjs → run-plugins-0AfIgIsZ.mjs} +89 -75
- package/dist/run-plugins-0AfIgIsZ.mjs.map +1 -0
- package/dist/{typegen-BBzX2LDz.mjs → typegen-BzgWveE_.mjs} +5 -5
- package/dist/{typegen-BBzX2LDz.mjs.map → typegen-BzgWveE_.mjs.map} +1 -1
- package/dist/{typegen-COAIhZZ4.mjs → typegen-up-UkaNc.mjs} +4 -4
- package/dist/{types-gy6PkRFW.mjs → types-ChFFRT8E.mjs} +1 -1
- package/package.json +3 -3
- package/templates/base/files/src/serverless.ts +6 -0
- package/templates/host-config/files/netlify.toml +1 -1
- package/templates/template-inertia/files/src/index.ts +4 -2
- package/templates/template-inertia/files/vite.client.config.ts +14 -3
- package/templates/template-inertia/files/vite.config.ts +9 -5
- package/dist/config-F9g6CNDA.mjs.map +0 -1
- package/dist/fullstack-D25STbVk.mjs +0 -95
- package/dist/fullstack-DWcfo-zl.mjs +0 -96
- package/dist/fullstack-DWcfo-zl.mjs.map +0 -1
- package/dist/project-3LsfnPLE.mjs.map +0 -1
- package/dist/run-plugins-2DJy76gx.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v8.
|
|
2
|
+
* @forinda/kickjs-cli v8.9.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{t as e}from"./rolldown-runtime-
|
|
11
|
+
import{t as e}from"./rolldown-runtime-Da-NDvKz.mjs";import{a as t}from"./config-YvIngROH.mjs";import{r as n}from"./fs-Bm2St4gk.mjs";import{existsSync as r,readFileSync as i}from"node:fs";import{join as a}from"node:path";import*as o from"@clack/prompts";import s from"picocolors";const c={GET:s.green,POST:s.cyan,PUT:s.yellow,PATCH:s.magenta,DELETE:s.red};function l(e){return(c[e]??s.dim)(e.padEnd(7))}function u(e){let t=`[${e}]`.padEnd(10);switch(e){case`CRITICAL`:return s.red(t);case`WARNING`:return s.yellow(t);case`INFO`:return s.blue(s.dim(t));default:return t}}s.green(`✓`),s.red(`✖`),s.yellow(`⚠`),s.blue(`ℹ`);function d(e){o.intro(s.bgCyan(s.black(` ${e} `)))}function f(e){o.outro(e)}function p(e){o.isCancel(e)&&(o.cancel(`Operation cancelled.`),process.exit(0))}async function m(e){let t=await o.text(e);return p(t),t}async function h(e){let t=await o.select(e);return p(t),t}async function g(e){let t=await o.multiselect(e);return p(t),t}async function _(e){let t=await o.confirm(e);return p(t),t}function v(){return o.spinner()}const y=o.log;function b(e,t,n){return`# CLAUDE.md — ${e}
|
|
12
12
|
|
|
13
13
|
**Read \`./.agents/AGENTS.md\` first.** It is the canonical, multi-agent
|
|
14
14
|
reference for this project (Claude, Copilot, Codex, Gemini, etc.) —
|
|
@@ -675,27 +675,45 @@ kick g config --force --repo postgres # Drop a kick.config.ts i
|
|
|
675
675
|
- Writing \`"name:type?"\` for optional scaffold fields — \`?\` is a shell glob in bash/zsh; use \`name:type:optional\`.
|
|
676
676
|
- Running \`kick new <name> --yes\` in a non-empty directory expecting it to wipe — \`--yes\` aborts without \`--force\`; pair them when destruction is intended.
|
|
677
677
|
- Skipping \`kick g config\` on a legacy project then wondering why generators ignore \`modules.dir\` / \`modules.repo\`.
|
|
678
|
-
- Editing \`kick.config.ts\` with deprecated top-level \`modulesDir\` / \`defaultRepo\` / \`schemaDir\` / \`pluralize\` instead of the nested \`modules\` block.`},{slug:`route-flags`,frontmatterName:`kickjs-route-flags`,description:`Use when a fact about a route (public, CSRF-exempt, unmetered, expose as a tool) has to be read by auth, CSRF, rate limiting, OpenAPI or MCP — instead of repeating it as paths in each consumer.`,body:"A route flag is a named, inheritable fact about a route. It carries no\nbehaviour: it records something every consumer can read, so \"this endpoint is\nopen\" is declared once instead of restated as an auth contributor, a\n`csrf ignorePaths` entry and a `rateLimit skipPaths` entry — two of which are\npathname strings that cannot express `/users/:id`.\n\n**Steps**:\n1. Declare it once, in its own module:\n ```ts\n import { defineRouteFlag } from '@forinda/kickjs'\n\n export const Public = defineRouteFlag('auth.public')\n export const Limit = defineRouteFlag<{ rpm: number }>('rate.limit')\n ```\n2. Apply it. A class flag is inherited by every route under it; `.off` on a\n method removes it:\n ```ts\n @Public // every route below inherits it\n @Controller()\n export class WebhooksController {\n @Get('/health')\n health(ctx: RequestContext) {} // public\n\n @Public.off // the method wins\n @Post('/admin')\n admin(ctx: RequestContext) {} // not public\n\n @Limit({ rpm: 10 }) // flags can carry a value\n @Post('/login')\n login(ctx: RequestContext) {}\n }\n ```\n3. Read it from the consumer that already exists, rather than a new one.\n **Where a consumer is mounted decides how it reads flags** — everything\n after route matching gets `ctx.route`; anything before it does not:\n\n | Consumer | Mounted at | Reads flags by |\n | --- | --- | --- |\n | Handler | the route itself | `ctx.route?.flags.has('auth.public')` |\n | Guard / `@Middleware()` | `@Middleware(fn)` on a method or controller class | `ctx.route`, or the built-ins' `exemptWhen` |\n | `csrfGuard()` / `rateLimitGuard()` | same — `@Middleware()`, class or method | `exemptWhen: 'csrf.exempt'` |\n | Contributor | method > class > module `contributors()` > adapter `contributors()` > `bootstrap({ contributors })` | `skipWhen` / `onlyWhen` on the registration |\n | Global middleware (`rateLimit()`) | `bootstrap({ middlewares: [...] })` — runs **before** matching | the route policy table (`bindRoutePolicy`); `ctx.route` is `undefined` here |\n | Adapter middleware | `AppAdapter.middleware()`, phase `beforeGlobal` … `afterRoutes` | the policy table — the handler is Express-style `(req, res, next)`, so there is no `ctx` in any phase |\n | OpenAPI | `SwaggerAdapter({ publicFlag: 'auth.public' })` | read once at startup, not per request |\n | MCP / AI tools | `McpAdapter({ exposeWhen: 'mcp.tool', hideWhen: 'mcp.hidden' })` | read once at startup |\n\n The contributor row is the one that buys the most: those five levels mean a\n plugin's contributor can be skipped on one route (`skipWhen`) without\n forking it, and a method-level registration beats the adapter's.\n\n4. Run `kick typegen`. It writes the `KickRouteFlags` augmentation, so flag\n names autocomplete and a typo fails `tsc` instead of silently never matching.\n\n**Red flags**:\n- Writing `@Public(false)` to remove a flag — removal is `@Public.off`. A\n resolved flag is present or absent; `false` is a legitimate *value* for a\n flag declared `boolean`, and a presence check would still answer `true`.\n- A new consumer growing its own `skipPaths` / `ignorePaths` list — that is\n the duplication flags exist to remove, and path strings break on params and\n on any `apiPrefix` change.\n- `@Public.off` on a route that never inherited the flag — a no-op, and\n usually a sign the expected class-level flag is not there.\n- Reading `ctx.route` in global middleware — it is `undefined` before a route\n is matched (steps 1-6 of the pipeline). Move the check into a guard mounted\n with `@Middleware()`, or read the policy table.\n- Inventing a flag name per consumer (`auth.open`, `public`, `isPublic`). The\n framework names none of them; pick one name per fact and reuse it."},{slug:`cli-plugin`,frontmatterName:`kickjs-cli-plugin`,description:`Use when adding a kick <name> command, a kick g <name> generator, or a typegen emitter to this project or a package it ships.`,body:"A CLI plugin contributes commands, generators and typegen emitters to the\n`kick` CLI. Every built-in command ships as one, so the contract is the same\none the framework uses.\n\n**Steps**:\n1. Write the plugin. All four contribution kinds are optional:\n ```ts\n // kick-tool.ts\n import { defineCliPlugin } from '@forinda/kickjs-cli'\n\n export const toolPlugin = (options: { watch?: boolean } = {}) =>\n defineCliPlugin({\n // Stable id, unique across plugins — it names the plugin in conflict errors.\n name: 'my-tool',\n // Declarative: same shape as kick.config.ts `commands`.\n commands: [{ name: 'db:migrate', description: 'Apply migrations', steps: 'node ./scripts/migrate.js' }],\n // Programmatic: full commander API, when you need options or subcommands.\n register(program, ctx) {\n program\n .command('my-tool')\n .description('Do the thing')\n .option('--watch', 'Watch mode')\n .action(async () => {\n ctx.log(`running in ${ctx.projectRoot}`)\n // ctx.config is the loaded kick.config.ts\n })\n },\n })\n ```\n2. Register it in `kick.config.ts`:\n ```ts\n import { toolPlugin } from './kick-tool'\n\n export default defineConfig({ plugins: [toolPlugin()] })\n ```\n3. Check it is loaded, per contribution kind: `kick --help` lists a command,\n `kick g --list` lists a generator, `kick typegen` runs an emitter. A\n plugin that fails to load takes the whole CLI down with a conflict error,\n so silence here means the config never referenced it.\n\n**Which kind to use**: `commands` for a shell one-liner, `register` for\noptions / subcommands / async work, `generators` (`defineGenerator`) for\n`kick g <name>` scaffolders, `typegens` for a file under `.kickjs/types/`.\n\n**Red flags**:\n- Confusing this with a runtime plugin: `defineCliPlugin` (from\n `@forinda/kickjs-cli`) extends the CLI; `definePlugin` (from\n `@forinda/kickjs`) hooks the running app. They share nothing.\n- Claiming a command name a built-in owns (`build:netlify`, `typegen`,\n `generate`) — duplicate declarative names are a startup conflict, and\n `register` has no conflict detection at all, so it silently loses. Namespace\n your own (`my-tool:build`).\n- Doing real work inside `register` — it runs at CLI startup for every\n command, so it must only register. Put the work in the action.\n- Importing heavy dependencies at module scope. `await import()` them inside\n the action, so an unrelated `kick dev` does not pay for them."},{slug:`deploy`,frontmatterName:`kickjs-deploy`,description:`Use when deploying this app — Netlify, Vercel or Cloudflare Workers — or when an API route returns the SPA shell, the platform build fails, or the function is missing from a deploy.`,body:`**
|
|
678
|
+
- Editing \`kick.config.ts\` with deprecated top-level \`modulesDir\` / \`defaultRepo\` / \`schemaDir\` / \`pluralize\` instead of the nested \`modules\` block.`},{slug:`route-flags`,frontmatterName:`kickjs-route-flags`,description:`Use when a fact about a route (public, CSRF-exempt, unmetered, expose as a tool) has to be read by auth, CSRF, rate limiting, OpenAPI or MCP — instead of repeating it as paths in each consumer.`,body:"A route flag is a named, inheritable fact about a route. It carries no\nbehaviour: it records something every consumer can read, so \"this endpoint is\nopen\" is declared once instead of restated as an auth contributor, a\n`csrf ignorePaths` entry and a `rateLimit skipPaths` entry — two of which are\npathname strings that cannot express `/users/:id`.\n\n**Steps**:\n1. Declare it once, in its own module:\n ```ts\n import { defineRouteFlag } from '@forinda/kickjs'\n\n export const Public = defineRouteFlag('auth.public')\n export const Limit = defineRouteFlag<{ rpm: number }>('rate.limit')\n ```\n2. Apply it. A class flag is inherited by every route under it; `.off` on a\n method removes it:\n ```ts\n @Public // every route below inherits it\n @Controller()\n export class WebhooksController {\n @Get('/health')\n health(ctx: RequestContext) {} // public\n\n @Public.off // the method wins\n @Post('/admin')\n admin(ctx: RequestContext) {} // not public\n\n @Limit({ rpm: 10 }) // flags can carry a value\n @Post('/login')\n login(ctx: RequestContext) {}\n }\n ```\n3. Read it from the consumer that already exists, rather than a new one.\n **Where a consumer is mounted decides how it reads flags** — everything\n after route matching gets `ctx.route`; anything before it does not:\n\n | Consumer | Mounted at | Reads flags by |\n | --- | --- | --- |\n | Handler | the route itself | `ctx.route?.flags.has('auth.public')` |\n | Guard / `@Middleware()` | `@Middleware(fn)` on a method or controller class | `ctx.route`, or the built-ins' `exemptWhen` |\n | `csrfGuard()` / `rateLimitGuard()` | same — `@Middleware()`, class or method | `exemptWhen: 'csrf.exempt'` |\n | Contributor | method > class > module `contributors()` > adapter `contributors()` > `bootstrap({ contributors })` | `skipWhen` / `onlyWhen` on the registration |\n | Global middleware (`rateLimit()`) | `bootstrap({ middlewares: [...] })` — runs **before** matching | the route policy table (`bindRoutePolicy`); `ctx.route` is `undefined` here |\n | Adapter middleware | `AppAdapter.middleware()`, phase `beforeGlobal` … `afterRoutes` | the policy table — the handler is Express-style `(req, res, next)`, so there is no `ctx` in any phase |\n | OpenAPI | `SwaggerAdapter({ publicFlag: 'auth.public' })` | read once at startup, not per request |\n | MCP / AI tools | `McpAdapter({ exposeWhen: 'mcp.tool', hideWhen: 'mcp.hidden' })` | read once at startup |\n\n The contributor row is the one that buys the most: those five levels mean a\n plugin's contributor can be skipped on one route (`skipWhen`) without\n forking it, and a method-level registration beats the adapter's.\n\n4. Run `kick typegen`. It writes the `KickRouteFlags` augmentation, so flag\n names autocomplete and a typo fails `tsc` instead of silently never matching.\n\n**Red flags**:\n- Writing `@Public(false)` to remove a flag — removal is `@Public.off`. A\n resolved flag is present or absent; `false` is a legitimate *value* for a\n flag declared `boolean`, and a presence check would still answer `true`.\n- A new consumer growing its own `skipPaths` / `ignorePaths` list — that is\n the duplication flags exist to remove, and path strings break on params and\n on any `apiPrefix` change.\n- `@Public.off` on a route that never inherited the flag — a no-op, and\n usually a sign the expected class-level flag is not there.\n- Reading `ctx.route` in global middleware — it is `undefined` before a route\n is matched (steps 1-6 of the pipeline). Move the check into a guard mounted\n with `@Middleware()`, or read the policy table.\n- Inventing a flag name per consumer (`auth.open`, `public`, `isPublic`). The\n framework names none of them; pick one name per fact and reuse it."},{slug:`cli-plugin`,frontmatterName:`kickjs-cli-plugin`,description:`Use when adding a kick <name> command, a kick g <name> generator, or a typegen emitter to this project or a package it ships.`,body:"A CLI plugin contributes commands, generators and typegen emitters to the\n`kick` CLI. Every built-in command ships as one, so the contract is the same\none the framework uses.\n\n**Steps**:\n1. Write the plugin. All four contribution kinds are optional:\n ```ts\n // kick-tool.ts\n import { defineCliPlugin } from '@forinda/kickjs-cli'\n\n export const toolPlugin = (options: { watch?: boolean } = {}) =>\n defineCliPlugin({\n // Stable id, unique across plugins — it names the plugin in conflict errors.\n name: 'my-tool',\n // Declarative: same shape as kick.config.ts `commands`.\n commands: [{ name: 'db:migrate', description: 'Apply migrations', steps: 'node ./scripts/migrate.js' }],\n // Programmatic: full commander API, when you need options or subcommands.\n register(program, ctx) {\n program\n .command('my-tool')\n .description('Do the thing')\n .option('--watch', 'Watch mode')\n .action(async () => {\n ctx.log(`running in ${ctx.projectRoot}`)\n // ctx.config is the loaded kick.config.ts\n })\n },\n })\n ```\n2. Register it in `kick.config.ts`:\n ```ts\n import { toolPlugin } from './kick-tool'\n\n export default defineConfig({ plugins: [toolPlugin()] })\n ```\n3. Check it is loaded, per contribution kind: `kick --help` lists a command,\n `kick g --list` lists a generator, `kick typegen` runs an emitter. A\n plugin that fails to load takes the whole CLI down with a conflict error,\n so silence here means the config never referenced it.\n\n**Which kind to use**: `commands` for a shell one-liner, `register` for\noptions / subcommands / async work, `generators` (`defineGenerator`) for\n`kick g <name>` scaffolders, `typegens` for a file under `.kickjs/types/`.\n\n**Red flags**:\n- Confusing this with a runtime plugin: `defineCliPlugin` (from\n `@forinda/kickjs-cli`) extends the CLI; `definePlugin` (from\n `@forinda/kickjs`) hooks the running app. They share nothing.\n- Claiming a command name a built-in owns (`build:netlify`, `typegen`,\n `generate`) — duplicate declarative names are a startup conflict, and\n `register` has no conflict detection at all, so it silently loses. Namespace\n your own (`my-tool:build`).\n- Doing real work inside `register` — it runs at CLI startup for every\n command, so it must only register. Put the work in the action.\n- Importing heavy dependencies at module scope. `await import()` them inside\n the action, so an unrelated `kick dev` does not pay for them."},{slug:`deploy`,frontmatterName:`kickjs-deploy`,description:`Use when deploying this app — a Node host or Docker, Netlify, Vercel or Cloudflare Workers — or when an API route returns the SPA shell, the platform build fails, or the function is missing from a deploy.`,body:`**A Node host or a container** runs the long-running server. The project
|
|
679
|
+
ships a \`Dockerfile\`; without Docker:
|
|
680
|
+
|
|
681
|
+
\`\`\`bash
|
|
682
|
+
${e} run build && NODE_ENV=production node dist/index.js
|
|
683
|
+
\`\`\`
|
|
684
|
+
|
|
685
|
+
Start it with \`node\`, not \`kick start\`: the CLI is a dev dependency, gone
|
|
686
|
+
after a production install. The image installs from the lockfile, so commit it.
|
|
687
|
+
|
|
688
|
+
**Netlify and Vercel** run the app as a Node function; the CLI builds both:
|
|
679
689
|
|
|
680
690
|
\`\`\`bash
|
|
681
691
|
${e} run build:netlify # → .netlify/v1/functions/api.mjs
|
|
682
692
|
${e} run build:vercel # → .vercel/output (Build Output API v3)
|
|
683
693
|
\`\`\`
|
|
684
694
|
|
|
685
|
-
Both bundle \`src/serverless.ts
|
|
686
|
-
|
|
687
|
-
|
|
695
|
+
Both bundle \`src/serverless.ts\`:
|
|
696
|
+
\`createHandler(() => import('./index'))\` serves the app \`src/index.ts\`
|
|
697
|
+
describes — its \`bootstrap()\` call builds the app without listening — so
|
|
698
|
+
there is one set of options. Add middleware and adapters to \`src/index.ts\`
|
|
699
|
+
as usual.
|
|
688
700
|
|
|
689
701
|
Settings are detected from the layout: a workspace member beside a \`web\`
|
|
690
|
-
package publishes \`web/dist\` and routes \`/api/*\`;
|
|
691
|
-
|
|
702
|
+
package publishes \`web/dist\` and routes \`/api/*\`; an Inertia app publishes
|
|
703
|
+
\`dist/client\`, packages the asset manifest and SSR bundle with the function,
|
|
704
|
+
and lets it take every other path; a standalone API takes every path.
|
|
705
|
+
Override in \`kick.config.ts\`:
|
|
692
706
|
|
|
693
707
|
\`\`\`ts
|
|
694
708
|
export default defineConfig({
|
|
695
|
-
deploy: { apiPath: '/api', staticDir: '../web/dist' },
|
|
709
|
+
deploy: { apiPath: '/api', staticDir: '../web/dist', include: ['data/geo.json'] },
|
|
696
710
|
})
|
|
697
711
|
\`\`\`
|
|
698
712
|
|
|
713
|
+
\`include\` lists files the app reads at runtime; they are packaged with the
|
|
714
|
+
function. Memory does not survive between function instances: keep sessions in
|
|
715
|
+
\`session({ store: 'cookie' })\` or a shared store, and data in a database.
|
|
716
|
+
|
|
699
717
|
**Cloudflare Workers** is a different entry, not a third flag on this one.
|
|
700
718
|
Workers have no \`node:http\`, so \`createHandler\` cannot run there; use the
|
|
701
719
|
web entry over h3 v2:
|
|
@@ -734,14 +752,17 @@ plugins. Serve static files from Workers Assets or a separate deploy.
|
|
|
734
752
|
**Red flags**:
|
|
735
753
|
- An \`/api/*\` route returning \`index.html\` on Netlify — the function is not
|
|
736
754
|
routed. Check the deploy summary says a function was deployed, keep
|
|
737
|
-
\`preferStatic\` out of
|
|
738
|
-
unset so it reads the function at the repo root.
|
|
755
|
+
\`preferStatic\` out of a fullstack function's config, and leave Netlify's
|
|
756
|
+
**Package directory** unset so it reads the function at the repo root.
|
|
757
|
+
- Validation errors or flash messages that vanish on Netlify / Vercel — the
|
|
758
|
+
session lives in one instance's memory. Use \`session({ store: 'cookie' })\`.
|
|
739
759
|
- Committing \`.netlify/\` or \`.vercel/\` — both are build output.
|
|
740
760
|
- A frontend that builds somewhere other than \`web/dist\` and only one of
|
|
741
761
|
\`deploy.staticDir\`, \`SpaAdapter({ clientDir })\` and netlify.toml's
|
|
742
762
|
\`publish\` updated. All three name that directory.
|
|
743
763
|
- Pointing a platform at \`src/index.ts\` — it calls \`bootstrap()\`, which
|
|
744
|
-
listens on a port and registers signal handlers inside the function.
|
|
764
|
+
listens on a port and registers signal handlers inside the function. The
|
|
765
|
+
platform entry is \`src/serverless.ts\`.
|
|
745
766
|
- Expecting \`kick build\` alone to produce a deployable function; it builds
|
|
746
767
|
the server. Run the target command after it.`},{slug:`docs-lookup`,frontmatterName:`kickjs-docs-lookup`,description:`Use FIRST when unsure about any KickJS API, option, or behaviour — before guessing from memory or inferring from surrounding code.`,body:`These skills are deliberately short — they cover the traps, not the whole API
|
|
747
768
|
surface. When the answer is not in one of them, read the docs rather than
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v8.
|
|
2
|
+
* @forinda/kickjs-cli v8.9.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{t as e}from"./rolldown-runtime-
|
|
11
|
+
import{t as e}from"./rolldown-runtime-Da-NDvKz.mjs";import{dirname as t,isAbsolute as n,join as r,relative as i,resolve as a}from"node:path";import{cpSync as o,existsSync as s,mkdirSync as c,statSync as l,writeFileSync as u}from"node:fs";import{glob as d}from"glob";import{groupAssetKeys as f}from"@forinda/kickjs";var p=/* @__PURE__ */ e({ASSET_MANIFEST_VERSION:()=>1,buildAssets:()=>m});async function m(e,t){let{cwd:n,silent:o=!1}=t,s=t.distDir??e?.build?.outDir??`dist`,l=e?.assetMap;if(!l||Object.keys(l).length===0)return null;let d=o?()=>{}:console.log,f=a(n,s);c(f,{recursive:!0});let p=[],m={};for(let[e,t]of Object.entries(l)){let r=await h(e,t,n,f);p.push(r.entrySummary),Object.assign(m,r.manifestSlice),d(` ✓ ${e}: ${r.entrySummary.filesCopied} file(s) → ${r.entrySummary.dest}`)}let g={version:1,entries:m},_=r(f,`.kickjs-assets.json`);return u(_,JSON.stringify(g,null,2)+`
|
|
12
12
|
`,`utf-8`),d(` ✓ wrote manifest → ${i(n,_)} (${Object.keys(m).length} entries)`),{manifestPath:_,entries:p,manifest:g}}async function h(e,n,l,u){let p=a(l,n.src),m=n.dest?a(l,n.dest):r(u,e);if(v(m,l))return console.warn(` ⚠ assetMap.${e}.dest ('${n.dest}') resolves outside the project root — skipping copy`),{entrySummary:{namespace:e,src:n.src,dest:i(l,m),filesCopied:0},manifestSlice:{}};if(!s(p)||!y(p))return{entrySummary:{namespace:e,src:n.src,dest:i(l,m),filesCopied:0},manifestSlice:{}};let h=n.glob??`**/*`,b=await d(h,{cwd:p,nodir:!0,dot:!1,posix:!0});c(m,{recursive:!0});let x={},S=[...b].toSorted(),{pairs:C,collisionGroupsResolved:w}=f(e,S,{strategy:n.keys??`auto`}),T=0;for(let{rel:e,key:n}of C){let i=r(p,e),a=r(m,e);x[n]=_(u,a),!g(i,a)&&(c(t(a),{recursive:!0}),o(i,a),T++)}return w>0&&console.log(` ℹ assetMap.${e}: auto-resolved ${w} basename collision(s) by keeping extensions (set 'keys: "strip"' to opt back into legacy last-write-wins behaviour, or 'keys: "with-extension"' to keep all keys verbose).`),{entrySummary:{namespace:e,src:n.src,dest:i(l,m),filesCopied:T},manifestSlice:x}}function g(e,t){if(!s(t))return!1;try{let n=l(e),r=l(t);return r.size===n.size&&r.mtimeMs>=n.mtimeMs}catch{return!1}}function _(e,t){return i(e,t).split(/[\\/]/).filter(Boolean).join(`/`)}function v(e,t){let r=i(t,e);return r===``?!1:r.startsWith(`..`)||n(r)}function y(e){try{return l(e).isDirectory()}catch{return!1}}export{p as n,m as t};
|
|
13
|
-
//# sourceMappingURL=build-
|
|
13
|
+
//# sourceMappingURL=build-C30mBgw6.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-Bnd_mk4h.mjs","names":["__exportAll","build_exports","buildAssets","opts","distDir","config","map","log","silent","distAbs","resolve","cwd","mkdirSync","summary","manifestEntries","namespace","entry","result","processEntry","manifest","manifestPath","join","writeFileSync","relative","srcAbs","destAbs","escapesRoot","existsSync","isDirectorySync","pattern","matches","glob","manifestSlice","sorted","groupAssetKeys","filesCopied","relPath","pairs","srcFile","destFile","key","toManifestRelative","dirname","cpSync","collisionGroupsResolved","isUpToDate","s","statSync","d","manifestDir","rel","root","path","isAbsolute"],"sources":["../src/asset-manager/build.ts"],"sourcesContent":["/**\n * Build pipeline for `assetMap` entries (asset-manager PR 2).\n *\n * For each entry, walks `src/<...>` matching the configured `glob`,\n * copies matches into `dest` (default `dist/<name>/`), then emits a\n * `dist/.kickjs-assets.json` manifest mapping logical\n * `<namespace>/<key>` keys to repo-relative paths inside `dist/`.\n *\n * Pure function on top of `node:fs` + `glob` — no shell, no side\n * effects beyond the configured directory writes. The build entry-\n * point in `commands/run.ts` calls `buildAssets` after the existing\n * `copyDirs` step.\n *\n * @module @forinda/kickjs-cli/asset-manager/build\n */\n\nimport { cpSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path'\nimport { glob } from 'glob'\nimport { groupAssetKeys } from '@forinda/kickjs'\nimport type { AssetMapEntry, KickConfig } from '../config'\n\n/** Wire-format version for `dist/.kickjs-assets.json`. Bump on shape change. */\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\n/** On-disk manifest format (`dist/.kickjs-assets.json`). */\nexport interface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n /**\n * Logical key → manifest-relative path. Logical key is\n * `<namespace>/<key>` where `<key>` is the file path under `src`\n * with the extension stripped + path separators normalised.\n *\n * Path values are relative to the manifest file's directory so the\n * runtime can resolve them with a single `path.resolve(manifestDir,\n * entry)` regardless of where dist/ lives.\n */\n entries: Record<string, string>\n}\n\nexport interface BuildAssetsOptions {\n /** Project root — resolved for every relative path in the entry. */\n cwd: string\n /**\n * Output dir for the manifest + per-namespace asset copies. When\n * omitted, falls back to `config.build?.outDir` (resolved against\n * `cwd`), then to `dist/` under cwd. Adopters with a custom Vite\n * `build.outDir` should set `kick.config.ts.build.outDir` to match.\n */\n distDir?: string\n /** Suppress per-entry log lines. Default: false. */\n silent?: boolean\n}\n\n/** One entry in the per-build summary returned by `buildAssets`. */\nexport interface BuildAssetsEntryResult {\n namespace: string\n src: string\n dest: string\n /**\n * Number of files actually written this run. On an incremental\n * rebuild where nothing changed this is 0 even though the manifest\n * still lists every matched file.\n */\n filesCopied: number\n}\n\n/** Aggregated outcome of `buildAssets`. */\nexport interface BuildAssetsResult {\n manifestPath: string\n entries: BuildAssetsEntryResult[]\n /** `entries` merged into a single record — useful for tests + tooling. */\n manifest: AssetManifest\n}\n\n/**\n * Run the full asset build for a loaded config:\n *\n * 1. For each `assetMap` entry, glob → copy → manifest stub.\n * 2. Write `dist/.kickjs-assets.json`.\n *\n * Returns a summary including the manifest contents. No-op (and no\n * manifest written) when `assetMap` is empty / missing — the build\n * pipeline shouldn't litter `dist/` with empty manifests for\n * adopters who don't use the feature.\n */\nexport async function buildAssets(\n config: KickConfig | null,\n opts: BuildAssetsOptions,\n): Promise<BuildAssetsResult | null> {\n const { cwd, silent = false } = opts\n // Resolution order: explicit opts.distDir → config.build.outDir → 'dist'.\n // The CLI build command passes nothing explicit, so adopters control\n // the output via kick.config.ts.build.outDir alone.\n const distDir = opts.distDir ?? config?.build?.outDir ?? 'dist'\n const map = config?.assetMap\n if (!map || Object.keys(map).length === 0) return null\n\n const log = silent ? () => {} : console.log\n\n const distAbs = resolve(cwd, distDir)\n mkdirSync(distAbs, { recursive: true })\n\n const summary: BuildAssetsEntryResult[] = []\n const manifestEntries: Record<string, string> = {}\n\n for (const [namespace, entry] of Object.entries(map)) {\n const result = await processEntry(namespace, entry, cwd, distAbs)\n summary.push(result.entrySummary)\n Object.assign(manifestEntries, result.manifestSlice)\n log(\n ` ✓ ${namespace}: ${result.entrySummary.filesCopied} file(s) → ${result.entrySummary.dest}`,\n )\n }\n\n const manifest: AssetManifest = {\n version: ASSET_MANIFEST_VERSION,\n entries: manifestEntries,\n }\n const manifestPath = join(distAbs, '.kickjs-assets.json')\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n log(\n ` ✓ wrote manifest → ${relative(cwd, manifestPath)} (${Object.keys(manifestEntries).length} entries)`,\n )\n\n return { manifestPath, entries: summary, manifest }\n}\n\n/** Per-entry inner pipeline — extracted for unit-test reuse. */\nasync function processEntry(\n namespace: string,\n entry: AssetMapEntry,\n cwd: string,\n distAbs: string,\n): Promise<{\n entrySummary: BuildAssetsEntryResult\n manifestSlice: Record<string, string>\n}> {\n const srcAbs = resolve(cwd, entry.src)\n const destAbs = entry.dest ? resolve(cwd, entry.dest) : join(distAbs, namespace)\n\n // Defensive: refuse to write outside the project root (cwd) even\n // though validateAssetMap warned about it at config-load time. The\n // build step shouldn't trust upstream warnings — a typo like\n // `dest: '../../'` would otherwise sprinkle files outside the\n // workspace despite the warning being printed.\n if (escapesRoot(destAbs, cwd)) {\n console.warn(\n ` ⚠ assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — skipping copy`,\n )\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n // Treat src-not-a-directory the same as src-missing — `glob` would\n // throw if pointed at a file, surfacing as a generic build failure\n // instead of a clean 0-files entry. Matches the validator's warning\n // shape (already emitted at config-load time for the missing case).\n if (!existsSync(srcAbs) || !isDirectorySync(srcAbs)) {\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n const pattern = entry.glob ?? '**/*'\n // `glob` returns paths relative to `cwd` when `cwd` is set —\n // exactly the slugs we want for the manifest keys.\n const matches = await glob(pattern, {\n cwd: srcAbs,\n nodir: true,\n dot: false,\n posix: true,\n })\n\n mkdirSync(destAbs, { recursive: true })\n\n const manifestSlice: Record<string, string> = {}\n // Sort the walk so the manifest is byte-stable across platforms.\n // The `groupAssetKeys` helper preserves input order, so a stable\n // sort here makes the resulting manifest stable too.\n const sorted = [...matches].toSorted()\n const { pairs, collisionGroupsResolved } = groupAssetKeys(namespace, sorted, {\n strategy: entry.keys ?? 'auto',\n })\n\n // Copy the files and write the keyed manifest slice. Both come from\n // the same `pairs` order so the on-disk layout matches the\n // manifest's iteration order — easier to grep in cold-start\n // debugging.\n //\n // Incremental: skip the copy when the destination is already\n // up-to-date (exists, same size, mtime ≥ source). `cpSync` stamps the\n // copy with the current time, so an unchanged source always satisfies\n // `dest.mtime ≥ src.mtime` on the next run — turning a re-build into a\n // pure stat sweep instead of re-copying every asset. The manifest\n // slice is written unconditionally so the manifest stays complete.\n let filesCopied = 0\n for (const { rel: relPath, key } of pairs) {\n const srcFile = join(srcAbs, relPath)\n const destFile = join(destAbs, relPath)\n manifestSlice[key] = toManifestRelative(distAbs, destFile)\n if (isUpToDate(srcFile, destFile)) continue\n mkdirSync(dirname(destFile), { recursive: true })\n cpSync(srcFile, destFile)\n filesCopied++\n }\n\n if (collisionGroupsResolved > 0) {\n console.log(\n ` ℹ assetMap.${namespace}: auto-resolved ${collisionGroupsResolved} basename collision(s) by keeping extensions ` +\n `(set 'keys: \"strip\"' to opt back into legacy last-write-wins behaviour, or 'keys: \"with-extension\"' to keep all keys verbose).`,\n )\n }\n\n return {\n entrySummary: {\n namespace,\n src: entry.src,\n dest: relative(cwd, destAbs),\n filesCopied,\n },\n manifestSlice,\n }\n}\n\n/**\n * Is `destFile` already a current copy of `srcFile`? True when the\n * destination exists with the same byte size and an mtime no older than\n * the source. Used to skip redundant copies on an incremental rebuild.\n */\nfunction isUpToDate(srcFile: string, destFile: string): boolean {\n if (!existsSync(destFile)) return false\n try {\n const s = statSync(srcFile)\n const d = statSync(destFile)\n return d.size === s.size && d.mtimeMs >= s.mtimeMs\n } catch {\n return false\n }\n}\n\n/**\n * Make `destFile` relative to the manifest's directory + force POSIX\n * separators so the manifest is byte-stable across platforms.\n */\nfunction toManifestRelative(manifestDir: string, destFile: string): string {\n const rel = relative(manifestDir, destFile)\n // path.relative returns OS-native separators; the manifest is JSON\n // and the runtime uses path.resolve which handles either, but a\n // forward-slash manifest is grep-friendly + diff-stable.\n return rel.split(/[\\\\/]/).filter(Boolean).join('/')\n}\n\n/**\n * Pure manifest writer — handy for tests that want to assert against\n * a hand-crafted manifest without exercising the full pipeline.\n */\nexport function writeAssetManifest(distDir: string, manifest: AssetManifest): string {\n const path = join(distDir, '.kickjs-assets.json')\n mkdirSync(distDir, { recursive: true })\n writeFileSync(path, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n return path\n}\n\n/**\n * Read + parse a manifest from disk. Returns `null` on missing or\n * malformed file rather than throwing — the runtime resolver wants\n * to fall through to dev-mode lookup in that case.\n */\nexport function readAssetManifest(distDir: string): AssetManifest | null {\n const path = join(distDir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require('node:fs') as typeof import('node:fs')\n const raw = fs.readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return parsed as AssetManifest\n } catch {\n return null\n }\n}\n\n/**\n * Project-root escape check that's safe across symlinks + drive letters.\n * `path.relative` returns `..` segments when the target sits above root,\n * and an absolute path when the two live on different roots (Windows).\n * `startsWith(root)` would miss both cases.\n */\nfunction escapesRoot(path: string, root: string): boolean {\n const rel = relative(root, path)\n if (rel === '') return false\n return rel.startsWith('..') || isAbsolute(rel)\n}\n\n/** Pure helper — `false` for missing, non-dir, or unreadable paths. */\nfunction isDirectorySync(path: string): boolean {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"build-C30mBgw6.mjs","names":["__exportAll","build_exports","buildAssets","opts","distDir","config","map","log","silent","distAbs","resolve","cwd","mkdirSync","summary","manifestEntries","namespace","entry","result","processEntry","manifest","manifestPath","join","writeFileSync","relative","srcAbs","destAbs","escapesRoot","existsSync","isDirectorySync","pattern","matches","glob","manifestSlice","sorted","groupAssetKeys","filesCopied","relPath","pairs","srcFile","destFile","key","toManifestRelative","dirname","cpSync","collisionGroupsResolved","isUpToDate","s","statSync","d","manifestDir","rel","root","path","isAbsolute"],"sources":["../src/asset-manager/build.ts"],"sourcesContent":["/**\n * Build pipeline for `assetMap` entries (asset-manager PR 2).\n *\n * For each entry, walks `src/<...>` matching the configured `glob`,\n * copies matches into `dest` (default `dist/<name>/`), then emits a\n * `dist/.kickjs-assets.json` manifest mapping logical\n * `<namespace>/<key>` keys to repo-relative paths inside `dist/`.\n *\n * Pure function on top of `node:fs` + `glob` — no shell, no side\n * effects beyond the configured directory writes. The build entry-\n * point in `commands/run.ts` calls `buildAssets` after the existing\n * `copyDirs` step.\n *\n * @module @forinda/kickjs-cli/asset-manager/build\n */\n\nimport { cpSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname, isAbsolute, join, relative, resolve } from 'node:path'\nimport { glob } from 'glob'\nimport { groupAssetKeys } from '@forinda/kickjs'\nimport type { AssetMapEntry, KickConfig } from '../config'\n\n/** Wire-format version for `dist/.kickjs-assets.json`. Bump on shape change. */\nexport const ASSET_MANIFEST_VERSION = 1 as const\n\n/** On-disk manifest format (`dist/.kickjs-assets.json`). */\nexport interface AssetManifest {\n version: typeof ASSET_MANIFEST_VERSION\n /**\n * Logical key → manifest-relative path. Logical key is\n * `<namespace>/<key>` where `<key>` is the file path under `src`\n * with the extension stripped + path separators normalised.\n *\n * Path values are relative to the manifest file's directory so the\n * runtime can resolve them with a single `path.resolve(manifestDir,\n * entry)` regardless of where dist/ lives.\n */\n entries: Record<string, string>\n}\n\nexport interface BuildAssetsOptions {\n /** Project root — resolved for every relative path in the entry. */\n cwd: string\n /**\n * Output dir for the manifest + per-namespace asset copies. When\n * omitted, falls back to `config.build?.outDir` (resolved against\n * `cwd`), then to `dist/` under cwd. Adopters with a custom Vite\n * `build.outDir` should set `kick.config.ts.build.outDir` to match.\n */\n distDir?: string\n /** Suppress per-entry log lines. Default: false. */\n silent?: boolean\n}\n\n/** One entry in the per-build summary returned by `buildAssets`. */\nexport interface BuildAssetsEntryResult {\n namespace: string\n src: string\n dest: string\n /**\n * Number of files actually written this run. On an incremental\n * rebuild where nothing changed this is 0 even though the manifest\n * still lists every matched file.\n */\n filesCopied: number\n}\n\n/** Aggregated outcome of `buildAssets`. */\nexport interface BuildAssetsResult {\n manifestPath: string\n entries: BuildAssetsEntryResult[]\n /** `entries` merged into a single record — useful for tests + tooling. */\n manifest: AssetManifest\n}\n\n/**\n * Run the full asset build for a loaded config:\n *\n * 1. For each `assetMap` entry, glob → copy → manifest stub.\n * 2. Write `dist/.kickjs-assets.json`.\n *\n * Returns a summary including the manifest contents. No-op (and no\n * manifest written) when `assetMap` is empty / missing — the build\n * pipeline shouldn't litter `dist/` with empty manifests for\n * adopters who don't use the feature.\n */\nexport async function buildAssets(\n config: KickConfig | null,\n opts: BuildAssetsOptions,\n): Promise<BuildAssetsResult | null> {\n const { cwd, silent = false } = opts\n // Resolution order: explicit opts.distDir → config.build.outDir → 'dist'.\n // The CLI build command passes nothing explicit, so adopters control\n // the output via kick.config.ts.build.outDir alone.\n const distDir = opts.distDir ?? config?.build?.outDir ?? 'dist'\n const map = config?.assetMap\n if (!map || Object.keys(map).length === 0) return null\n\n const log = silent ? () => {} : console.log\n\n const distAbs = resolve(cwd, distDir)\n mkdirSync(distAbs, { recursive: true })\n\n const summary: BuildAssetsEntryResult[] = []\n const manifestEntries: Record<string, string> = {}\n\n for (const [namespace, entry] of Object.entries(map)) {\n const result = await processEntry(namespace, entry, cwd, distAbs)\n summary.push(result.entrySummary)\n Object.assign(manifestEntries, result.manifestSlice)\n log(\n ` ✓ ${namespace}: ${result.entrySummary.filesCopied} file(s) → ${result.entrySummary.dest}`,\n )\n }\n\n const manifest: AssetManifest = {\n version: ASSET_MANIFEST_VERSION,\n entries: manifestEntries,\n }\n const manifestPath = join(distAbs, '.kickjs-assets.json')\n writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n log(\n ` ✓ wrote manifest → ${relative(cwd, manifestPath)} (${Object.keys(manifestEntries).length} entries)`,\n )\n\n return { manifestPath, entries: summary, manifest }\n}\n\n/** Per-entry inner pipeline — extracted for unit-test reuse. */\nasync function processEntry(\n namespace: string,\n entry: AssetMapEntry,\n cwd: string,\n distAbs: string,\n): Promise<{\n entrySummary: BuildAssetsEntryResult\n manifestSlice: Record<string, string>\n}> {\n const srcAbs = resolve(cwd, entry.src)\n const destAbs = entry.dest ? resolve(cwd, entry.dest) : join(distAbs, namespace)\n\n // Defensive: refuse to write outside the project root (cwd) even\n // though validateAssetMap warned about it at config-load time. The\n // build step shouldn't trust upstream warnings — a typo like\n // `dest: '../../'` would otherwise sprinkle files outside the\n // workspace despite the warning being printed.\n if (escapesRoot(destAbs, cwd)) {\n console.warn(\n ` ⚠ assetMap.${namespace}.dest ('${entry.dest}') resolves outside the project root — skipping copy`,\n )\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n // Treat src-not-a-directory the same as src-missing — `glob` would\n // throw if pointed at a file, surfacing as a generic build failure\n // instead of a clean 0-files entry. Matches the validator's warning\n // shape (already emitted at config-load time for the missing case).\n if (!existsSync(srcAbs) || !isDirectorySync(srcAbs)) {\n return {\n entrySummary: { namespace, src: entry.src, dest: relative(cwd, destAbs), filesCopied: 0 },\n manifestSlice: {},\n }\n }\n\n const pattern = entry.glob ?? '**/*'\n // `glob` returns paths relative to `cwd` when `cwd` is set —\n // exactly the slugs we want for the manifest keys.\n const matches = await glob(pattern, {\n cwd: srcAbs,\n nodir: true,\n dot: false,\n posix: true,\n })\n\n mkdirSync(destAbs, { recursive: true })\n\n const manifestSlice: Record<string, string> = {}\n // Sort the walk so the manifest is byte-stable across platforms.\n // The `groupAssetKeys` helper preserves input order, so a stable\n // sort here makes the resulting manifest stable too.\n const sorted = [...matches].toSorted()\n const { pairs, collisionGroupsResolved } = groupAssetKeys(namespace, sorted, {\n strategy: entry.keys ?? 'auto',\n })\n\n // Copy the files and write the keyed manifest slice. Both come from\n // the same `pairs` order so the on-disk layout matches the\n // manifest's iteration order — easier to grep in cold-start\n // debugging.\n //\n // Incremental: skip the copy when the destination is already\n // up-to-date (exists, same size, mtime ≥ source). `cpSync` stamps the\n // copy with the current time, so an unchanged source always satisfies\n // `dest.mtime ≥ src.mtime` on the next run — turning a re-build into a\n // pure stat sweep instead of re-copying every asset. The manifest\n // slice is written unconditionally so the manifest stays complete.\n let filesCopied = 0\n for (const { rel: relPath, key } of pairs) {\n const srcFile = join(srcAbs, relPath)\n const destFile = join(destAbs, relPath)\n manifestSlice[key] = toManifestRelative(distAbs, destFile)\n if (isUpToDate(srcFile, destFile)) continue\n mkdirSync(dirname(destFile), { recursive: true })\n cpSync(srcFile, destFile)\n filesCopied++\n }\n\n if (collisionGroupsResolved > 0) {\n console.log(\n ` ℹ assetMap.${namespace}: auto-resolved ${collisionGroupsResolved} basename collision(s) by keeping extensions ` +\n `(set 'keys: \"strip\"' to opt back into legacy last-write-wins behaviour, or 'keys: \"with-extension\"' to keep all keys verbose).`,\n )\n }\n\n return {\n entrySummary: {\n namespace,\n src: entry.src,\n dest: relative(cwd, destAbs),\n filesCopied,\n },\n manifestSlice,\n }\n}\n\n/**\n * Is `destFile` already a current copy of `srcFile`? True when the\n * destination exists with the same byte size and an mtime no older than\n * the source. Used to skip redundant copies on an incremental rebuild.\n */\nfunction isUpToDate(srcFile: string, destFile: string): boolean {\n if (!existsSync(destFile)) return false\n try {\n const s = statSync(srcFile)\n const d = statSync(destFile)\n return d.size === s.size && d.mtimeMs >= s.mtimeMs\n } catch {\n return false\n }\n}\n\n/**\n * Make `destFile` relative to the manifest's directory + force POSIX\n * separators so the manifest is byte-stable across platforms.\n */\nfunction toManifestRelative(manifestDir: string, destFile: string): string {\n const rel = relative(manifestDir, destFile)\n // path.relative returns OS-native separators; the manifest is JSON\n // and the runtime uses path.resolve which handles either, but a\n // forward-slash manifest is grep-friendly + diff-stable.\n return rel.split(/[\\\\/]/).filter(Boolean).join('/')\n}\n\n/**\n * Pure manifest writer — handy for tests that want to assert against\n * a hand-crafted manifest without exercising the full pipeline.\n */\nexport function writeAssetManifest(distDir: string, manifest: AssetManifest): string {\n const path = join(distDir, '.kickjs-assets.json')\n mkdirSync(distDir, { recursive: true })\n writeFileSync(path, JSON.stringify(manifest, null, 2) + '\\n', 'utf-8')\n return path\n}\n\n/**\n * Read + parse a manifest from disk. Returns `null` on missing or\n * malformed file rather than throwing — the runtime resolver wants\n * to fall through to dev-mode lookup in that case.\n */\nexport function readAssetManifest(distDir: string): AssetManifest | null {\n const path = join(distDir, '.kickjs-assets.json')\n if (!existsSync(path)) return null\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require('node:fs') as typeof import('node:fs')\n const raw = fs.readFileSync(path, 'utf-8')\n const parsed = JSON.parse(raw) as Partial<AssetManifest>\n if (parsed.version !== ASSET_MANIFEST_VERSION) return null\n if (!parsed.entries || typeof parsed.entries !== 'object') return null\n return parsed as AssetManifest\n } catch {\n return null\n }\n}\n\n/**\n * Project-root escape check that's safe across symlinks + drive letters.\n * `path.relative` returns `..` segments when the target sits above root,\n * and an absolute path when the two live on different roots (Windows).\n * `startsWith(root)` would miss both cases.\n */\nfunction escapesRoot(path: string, root: string): boolean {\n const rel = relative(root, path)\n if (rel === '') return false\n return rel.startsWith('..') || isAbsolute(rel)\n}\n\n/** Pure helper — `false` for missing, non-dir, or unreadable paths. */\nfunction isDirectorySync(path: string): boolean {\n try {\n return statSync(path).isDirectory()\n } catch {\n return false\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v8.
|
|
2
|
+
* @forinda/kickjs-cli v8.9.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{t as e}from"./rolldown-runtime-
|
|
11
|
+
import{t as e}from"./rolldown-runtime-Da-NDvKz.mjs";import{cpSync as t,existsSync as n,mkdirSync as r,statSync as i,writeFileSync as a}from"node:fs";import{dirname as o,isAbsolute as s,join as c,relative as l,resolve as u}from"node:path";import{glob as d}from"glob";import{groupAssetKeys as f}from"@forinda/kickjs";var p=/* @__PURE__ */ e({ASSET_MANIFEST_VERSION:()=>1,buildAssets:()=>m});async function m(e,t){let{cwd:n,silent:i=!1}=t,o=t.distDir??e?.build?.outDir??`dist`,s=e?.assetMap;if(!s||Object.keys(s).length===0)return null;let d=i?()=>{}:console.log,f=u(n,o);r(f,{recursive:!0});let p=[],m={};for(let[e,t]of Object.entries(s)){let r=await h(e,t,n,f);p.push(r.entrySummary),Object.assign(m,r.manifestSlice),d(` ✓ ${e}: ${r.entrySummary.filesCopied} file(s) → ${r.entrySummary.dest}`)}let g={version:1,entries:m},_=c(f,`.kickjs-assets.json`);return a(_,JSON.stringify(g,null,2)+`
|
|
12
12
|
`,`utf-8`),d(` ✓ wrote manifest → ${l(n,_)} (${Object.keys(m).length} entries)`),{manifestPath:_,entries:p,manifest:g}}async function h(e,i,a,s){let p=u(a,i.src),m=i.dest?u(a,i.dest):c(s,e);if(v(m,a))return console.warn(` ⚠ assetMap.${e}.dest ('${i.dest}') resolves outside the project root — skipping copy`),{entrySummary:{namespace:e,src:i.src,dest:l(a,m),filesCopied:0},manifestSlice:{}};if(!n(p)||!y(p))return{entrySummary:{namespace:e,src:i.src,dest:l(a,m),filesCopied:0},manifestSlice:{}};let h=i.glob??`**/*`,b=await d(h,{cwd:p,nodir:!0,dot:!1,posix:!0});r(m,{recursive:!0});let x={},S=[...b].toSorted(),{pairs:C,collisionGroupsResolved:w}=f(e,S,{strategy:i.keys??`auto`}),T=0;for(let{rel:e,key:n}of C){let i=c(p,e),a=c(m,e);x[n]=_(s,a),!g(i,a)&&(r(o(a),{recursive:!0}),t(i,a),T++)}return w>0&&console.log(` ℹ assetMap.${e}: auto-resolved ${w} basename collision(s) by keeping extensions (set 'keys: "strip"' to opt back into legacy last-write-wins behaviour, or 'keys: "with-extension"' to keep all keys verbose).`),{entrySummary:{namespace:e,src:i.src,dest:l(a,m),filesCopied:T},manifestSlice:x}}function g(e,t){if(!n(t))return!1;try{let n=i(e),r=i(t);return r.size===n.size&&r.mtimeMs>=n.mtimeMs}catch{return!1}}function _(e,t){return l(e,t).split(/[\\/]/).filter(Boolean).join(`/`)}function v(e,t){let n=l(t,e);return n===``?!1:n.startsWith(`..`)||s(n)}function y(e){try{return i(e).isDirectory()}catch{return!1}}export{p as n,m as t};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs-cli v8.
|
|
2
|
+
* @forinda/kickjs-cli v8.9.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
export{t as builtinCliPlugins}from"./run-plugins-
|
|
11
|
+
export{t as builtinCliPlugins}from"./run-plugins-0AfIgIsZ.mjs";
|