@rebasepro/cli 0.13.1-canary.gf57a27e → 0.14.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.
@@ -54,7 +54,22 @@ export declare function manifestExists(projectRoot: string): boolean;
54
54
  * available behaviour.
55
55
  */
56
56
  export declare function loadManifest(projectRoot: string): LoadedManifest;
57
- /** Write a manifest, with a trailing newline so it plays well with other tools. */
57
+ /**
58
+ * Write a manifest, with a trailing newline so it plays well with other tools.
59
+ *
60
+ * **Every key on disk survives.** This used to emit exactly `$schema`, `rebase`
61
+ * and `apps`, so a rewrite deleted the rest of the file — and two commands with
62
+ * no visible relationship to either key rewrite it: `rebase eject` and
63
+ * `rebase apps init --force`. A repository that had committed
64
+ * `"telemetry": false` lost its opt-out, and a multi-bucket project lost its
65
+ * whole `storage` block, in a commit whose stated change was `runtime: custom`.
66
+ *
67
+ * So: the caller's manifest wins for what it models, the file supplies the rest.
68
+ * `storage` and `telemetry` fall back to the file because the callers that
69
+ * synthesize a manifest (`apps init --force`) cannot know them — they are
70
+ * authored, not inferred — and unknown top-level keys are copied verbatim
71
+ * rather than listed, since a hand-listed set loses the next key too.
72
+ */
58
73
  export declare function writeManifest(projectRoot: string, manifest: RebaseProjectManifest): string;
59
74
  /** Find the single backend app, if this repository declares one. */
60
75
  export declare function findBackendApp(manifest: RebaseProjectManifest): {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Argument parsing for the commands that take positional arguments.
3
+ *
4
+ * Every command in the small group used to parse its own line the same way:
5
+ * `arg(spec, { argv: rawArgs.slice(4), permissive: true })`, then read `_[0]`
6
+ * and `_[1]`. Both halves of that are wrong in a way that costs data.
7
+ *
8
+ * - **`permissive: true` turns an unknown flag into a positional.** `arg`
9
+ * pushes an undeclared flag into `_` as a bare token, so `_[1]` is whatever
10
+ * came second on the line, flag or not. `rebase auth reset-password
11
+ * bob@example.com --debug` set Bob's password to the literal `--debug` — and
12
+ * `--debug` is what `bin/rebase.js` prints after *every* failure as the thing
13
+ * to re-run with, so the single most likely next keystroke after a failed
14
+ * reset was the one that reset the account to a two-word string.
15
+ * - **`slice(4)` assumes the command words are at fixed indices.** They are
16
+ * not: a flag before the command shifts everything, so `rebase --debug auth
17
+ * reset-password bob@example.com NewPass1!` read the email as
18
+ * `reset-password` and the password as `bob@example.com`.
19
+ *
20
+ * So: parse the *whole* line — `rawArgs` is `process.argv` — against a spec
21
+ * strictly, with no permissive mode. `arg` then consumes every flag wherever it
22
+ * appears and rejects the ones nobody declared, which leaves `_` holding the
23
+ * command words followed by the real positionals, in order and at a known
24
+ * offset. An unrecognised flag becomes an error naming the command's help,
25
+ * which is the only safe answer: the alternative is guessing that it was meant
26
+ * as a value.
27
+ *
28
+ * `commands/cloud/index.ts` resolves its positionals against its own spec for
29
+ * the same reason; this is that idea for the commands whose positionals are
30
+ * credentials rather than resource names.
31
+ */
32
+ import arg from "arg";
33
+ /**
34
+ * Flags accepted on top of whatever a command declares.
35
+ *
36
+ * `--debug` is read by `bin/rebase.js` off `process.argv` and never by a
37
+ * command, but it has to be *declared* somewhere or strict parsing rejects the
38
+ * exact flag the CLI tells people to add. `--help`/`-h` are answered by each
39
+ * command's dispatcher before any work happens.
40
+ */
41
+ export declare const GLOBAL_COMMAND_FLAGS: {
42
+ readonly "--debug": BooleanConstructor;
43
+ readonly "--help": BooleanConstructor;
44
+ readonly "-h": "--help";
45
+ };
46
+ export interface ParsedCommand<S extends arg.Spec> {
47
+ /** The declared flags, as `arg` resolved them. */
48
+ flags: arg.Result<S>;
49
+ /** What is left after the command words — never a flag. */
50
+ positionals: string[];
51
+ /** `--help` or `-h` appeared anywhere on the line. */
52
+ help: boolean;
53
+ }
54
+ /** Did the line ask for help? Answered before dispatch, never by a handler. */
55
+ export declare function wantsHelp(rawArgs: string[]): boolean;
56
+ /**
57
+ * Resolve a command's flags and positionals from the full `process.argv`.
58
+ *
59
+ * `commandWords` is how many words name the command itself — 2 for
60
+ * `auth reset-password`, 1 for `start` — and is applied to the *parsed*
61
+ * positionals rather than to `argv`, so a flag placed before the command no
62
+ * longer shifts them.
63
+ *
64
+ * `command` names the command in error messages, e.g. `auth reset-password`.
65
+ *
66
+ * Throws on an unknown flag, on a positional that looks like a flag, and on
67
+ * more positionals than the command takes. `bin/rebase.js` turns each into a
68
+ * one-line `✗ …` and exit 1.
69
+ */
70
+ export declare function parseCommandArgs<S extends arg.Spec>({ spec, rawArgs, commandWords, command, maxPositionals }: {
71
+ spec: S;
72
+ rawArgs: string[];
73
+ commandWords: number;
74
+ command: string;
75
+ maxPositionals?: number;
76
+ }): ParsedCommand<S>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.13.1-canary.gf57a27e",
3
+ "version": "0.14.0",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
@@ -31,12 +31,12 @@
31
31
  "execa": "^9.6.1",
32
32
  "inquirer": "14.0.2",
33
33
  "jiti": "^2.7.0",
34
- "@rebasepro/agent-skills": "0.13.1-canary.gf57a27e",
35
- "@rebasepro/client": "0.13.1-canary.gf57a27e",
36
- "@rebasepro/codegen": "0.13.1-canary.gf57a27e",
37
- "@rebasepro/server": "0.13.1-canary.gf57a27e",
38
- "@rebasepro/server-postgres": "0.13.1-canary.gf57a27e",
39
- "@rebasepro/types": "0.13.1-canary.gf57a27e"
34
+ "@rebasepro/agent-skills": "0.14.0",
35
+ "@rebasepro/server": "0.14.0",
36
+ "@rebasepro/codegen": "0.14.0",
37
+ "@rebasepro/client": "0.14.0",
38
+ "@rebasepro/server-postgres": "0.14.0",
39
+ "@rebasepro/types": "0.14.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^26.1.2",
@@ -7,7 +7,12 @@
7
7
  # not reach this project.
8
8
  #
9
9
  # Assumes pnpm, which is what `rebase init` scaffolds a workspace for. On npm,
10
- # swap the three pnpm lines below for `npm ci` and `npm run build --workspace`.
10
+ # every line below that mentions pnpm has to change — the `pnpm install` and
11
+ # `pnpm --filter … run build` lines become `npm ci` and
12
+ # `npm run build --workspace <package>`, `CMD ["pnpm", "start"]` becomes
13
+ # `CMD ["npm", "start"]`, and the two `COPY` lines that name `pnpm-lock.yaml`
14
+ # and `pnpm-workspace.yaml` have to name `package-lock.json` instead. Those
15
+ # COPYs fail first, before any of the build lines runs.
11
16
  #
12
17
  # Build context: the project root (where pnpm-workspace.yaml lives)
13
18
  # Usage:
@@ -33,15 +38,31 @@ WORKDIR /app
33
38
  # Copy workspace root files first (cache-friendly layer)
34
39
  COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
35
40
 
36
- # Copy workspace packages
41
+ # Copy workspace packages. Every workspace the lockfile has an importer for,
42
+ # so `--frozen-lockfile` below sees the same set of packages it was written from.
37
43
  COPY backend ./backend
38
44
  COPY config ./config
45
+ # {{#frontend}}
46
+ COPY frontend ./frontend
47
+ # {{/frontend}}
48
+
49
+ # The entrypoint reads rebase.json at boot — it is where this project declares
50
+ # its storage buckets, so that the platform, the console and this process all
51
+ # read one list. An image without it boots believing nothing was declared, and
52
+ # every upload lands in the wrong bucket or 501s.
53
+ COPY rebase.json ./
39
54
 
40
55
  # Install all deps (including devDependencies for build)
41
56
  RUN pnpm install --frozen-lockfile
42
57
 
43
- # Build config first, then backend
58
+ # Build config first, then the rest: both the frontend and the backend import it.
44
59
  RUN pnpm --filter "*-config" run build
60
+ # {{#frontend}}
61
+ # The image serves the built site itself (see the `serveSPA` call in
62
+ # backend/src/index.ts), so the frontend is built here rather than deployed
63
+ # separately. Vite reads no API URL: same origin, same container.
64
+ RUN pnpm --filter "*-frontend" run build
65
+ # {{/frontend}}
45
66
  RUN pnpm --filter "*-backend" run build
46
67
 
47
68
  # Prune dev dependencies for a smaller runtime
@@ -63,10 +84,14 @@ RUN addgroup -g 1001 rebase && adduser -u 1001 -G rebase -s /bin/sh -D rebase
63
84
  WORKDIR /app
64
85
 
65
86
  # Copy only production artifacts
66
- COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/.npmrc ./
87
+ COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml /app/.npmrc /app/rebase.json ./
67
88
  COPY --from=builder /app/node_modules ./node_modules
68
89
  COPY --from=builder /app/backend ./backend
69
90
  COPY --from=builder /app/config ./config
91
+ # {{#frontend}}
92
+ # The built site only — its sources and devDependencies stay in the builder.
93
+ COPY --from=builder /app/frontend/dist ./frontend/dist
94
+ # {{/frontend}}
70
95
 
71
96
  # Create uploads directory
72
97
  RUN mkdir -p /app/backend/uploads && chown -R rebase:rebase /app
@@ -1,14 +1,17 @@
1
- import { Hono } from "hono";
1
+ import { Hono, type Context } from "hono";
2
2
  import { cors } from "hono/cors";
3
3
  import { secureHeaders } from "hono/secure-headers";
4
4
  import { getRequestListener } from "@hono/node-server";
5
5
  import { createServer } from "http";
6
+ import fs from "fs";
6
7
  import path from "path";
7
8
  import { fileURLToPath } from "url";
8
9
  import {
9
10
  initializeRebaseBackend,
10
11
  installShutdownHandlers,
12
+ // {{#frontend}}
11
13
  serveSPA,
14
+ // {{/frontend}}
12
15
  HonoEnv,
13
16
  listenWithPortRetry,
14
17
  cleanupDevPortFile,
@@ -17,10 +20,14 @@ import {
17
20
  logger
18
21
  } from "@rebasepro/server";
19
22
  import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
23
+ // {{#collections}}
20
24
  import { enums, relations, tables } from "./schema.generated.js";
25
+ // {{/collections}}
21
26
  import { storageAuthorize } from "../../config/storage.js";
22
27
  import { env } from "./env.js";
28
+ // {{#collections}}
23
29
  import usersCollection from "../../config/collections/users.js";
30
+ // {{/collections}}
24
31
 
25
32
  const __filename = fileURLToPath(import.meta.url);
26
33
  const __dirname = path.dirname(__filename);
@@ -86,21 +93,44 @@ async function startServer() {
86
93
  const PORT = env.PORT;
87
94
  const server = createServer(getRequestListener(app.fetch));
88
95
 
96
+ // `backend/crons` holds the scheduled jobs — nightly backups among them.
97
+ // Passed only when the directory exists, because a configured `cronsDir`
98
+ // with nothing in it mounts the cron routes and warns at every boot.
99
+ // Sibling-relative like `functionsDir`: both compile alongside this file, so
100
+ // the same path is right from source and from `backend/dist/backend/src`.
101
+ const cronsDir = path.resolve(__dirname, "../crons");
102
+
89
103
  const backend = await initializeRebaseBackend({
104
+ // {{#collections}}
90
105
  collectionsDir: path.resolve(__dirname, "../../config/collections"),
106
+ // {{/collections}}
107
+ // {{^collections}}
108
+ // No `collectionsDir`: this project declares no collections in code, so
109
+ // the server derives them from the live database schema at boot —
110
+ // exactly what the managed runtime did for it.
111
+ // {{/collections}}
91
112
  functionsDir: path.resolve(__dirname, "../functions"),
113
+ cronsDir: fs.existsSync(cronsDir) ? cronsDir : undefined,
92
114
  server,
93
115
  app,
94
116
  database: createPostgresAdapter({
95
117
  connection: db,
118
+ // {{#collections}}
96
119
  schema: { tables,
97
120
  enums,
98
121
  relations },
122
+ // {{/collections}}
99
123
  adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
100
124
  connectionString
101
125
  }),
102
126
  auth: {
127
+ // {{#collections}}
103
128
  collection: usersCollection,
129
+ // {{/collections}}
130
+ // {{^collections}}
131
+ // No users collection is declared here, so auth falls back to its
132
+ // own default users table — the same fallback a headless bundle gets.
133
+ // {{/collections}}
104
134
  jwtSecret,
105
135
  accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
106
136
  refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
@@ -173,13 +203,23 @@ pass: env.SMTP_PASS! }
173
203
  //
174
204
  // `requireAuth` answers 401 without a valid token; `requireAdmin` answers
175
205
  // 403 without the `admin` role and must follow `requireAuth`. Note that
176
- // `c.get("driver")` — the RLS-scoped driver is only set inside the Rebase
177
- // routers, so out here reach for `rebase.dataAsAdmin`, which **bypasses
178
- // RLS** and therefore belongs behind one of those guards.
206
+ // `c.get("driver")` — the driver carrying the caller's identity is only
207
+ // set inside the Rebase routers, so out here reach for
208
+ // `rebase.dataAsAdmin`. That one is **admin-scoped, not an RLS bypass**: it
209
+ // runs as `{ uid: "service", roles: ["admin"] }` and your policies still
210
+ // apply, evaluated against that identity — which is exactly an admin's
211
+ // reach, and therefore belongs behind one of those guards. (`rebase.sql()`
212
+ // is the real bypass: owner connection, no policies.)
179
213
 
180
214
  // ─── Health check ─────────────────────────────────────────────
181
215
  // Deliberately public: an orchestrator's probe has no token to send.
182
- app.get("/health", async (c) => {
216
+ //
217
+ // Answered on both paths. `/health` is what an orchestrator probes and what
218
+ // the generated `docker-compose.yml` already points at, so it stays.
219
+ // `/api/health` is where a developer looks first, because every other route
220
+ // this server has is under `/api` — and a reverse proxy that forwards only
221
+ // `/api` to the backend can reach nothing else.
222
+ const healthCheck = async (c: Context<HonoEnv>) => {
183
223
  const result = await backend.healthCheck();
184
224
  const status = result.healthy ? 200 : 503;
185
225
  return c.json({
@@ -187,12 +227,24 @@ pass: env.SMTP_PASS! }
187
227
  latencyMs: result.latencyMs,
188
228
  ...(result.details ? { details: result.details } : {})
189
229
  }, status);
190
- });
230
+ };
231
+ app.get("/health", healthCheck);
232
+ app.get("/api/health", healthCheck);
191
233
 
192
- // Serve the frontend in production
234
+ // {{#frontend}}
235
+ // Serve the frontend in production.
236
+ //
237
+ // Four levels up, not two: in production this file runs compiled, from
238
+ // `backend/dist/backend/src`. The paths above are the same in both modes
239
+ // because `config/` is compiled alongside this file; `frontend/dist` is not
240
+ // in the compiled tree at all, so it stays where the repository put it.
241
+ // `serveSPA` only warns when the path is wrong, which is why the Dockerfile
242
+ // that builds this image also copies `frontend` — verify a mount by
243
+ // fetching `/`, never by reading the log.
193
244
  if (isProduction) {
194
- serveSPA(app, { frontendPath: path.join(__dirname, "../../frontend/dist") });
245
+ serveSPA(app, { frontendPath: path.resolve(__dirname, "../../../../frontend/dist") });
195
246
  }
247
+ // {{/frontend}}
196
248
 
197
249
  if (!isProduction) {
198
250
  // Dev mode: retry the next port if the current one is in use
@@ -52,8 +52,16 @@ services:
52
52
  ADMIN_CONNECTION_STRING: postgresql://rebase_app:${DATABASE_PASSWORD:-changeme}@db:5432/rebase?options=-c%20search_path=public
53
53
  NODE_ENV: production
54
54
  PORT: "3001"
55
+ # {{#frontend}}
55
56
  # Your entrypoint serves the built frontend itself (see the `serveSPA`
56
- # call in backend/src/index.ts), so this is one container, same origin.
57
+ # call in backend/src/index.ts, and the frontend build stage in the
58
+ # Dockerfile), so this is one container, same origin.
59
+ # {{/frontend}}
60
+ # {{^frontend}}
61
+ # This image serves the API only — there is no frontend workspace in this
62
+ # project — so CORS_ORIGINS has to list the origins your clients are
63
+ # served from.
64
+ # {{/frontend}}
57
65
  CORS_ORIGINS: ${CORS_ORIGINS:?set CORS_ORIGINS to the origin you browse to}
58
66
  # A durable named volume, which is the case this flag acknowledges.
59
67
  STORAGE_PATH: /uploads
@@ -21,5 +21,10 @@
21
21
  //
22
22
  // No `rootDir`: it would have to be `.` for both, and letting tsc infer the
23
23
  // common root gets there without a second place to keep in sync.
24
- "include": ["src/**/*", "functions/**/*"]
24
+ //
25
+ // `crons/` too: `rebase build` compiles it through a generated tsconfig, but
26
+ // an ejected project builds with *this* file, so leaving it out meant an
27
+ // ejected server had no compiled jobs to load and silently stopped running
28
+ // every schedule.
29
+ "include": ["src/**/*", "functions/**/*", "crons/**/*"]
25
30
  }
@@ -10,6 +10,15 @@
10
10
  * a deployment with file storage enabled serves every user's files to every
11
11
  * signed-in user. The server refuses to boot in that state, and this is the
12
12
  * export it looks for.
13
+ *
14
+ * It depends on `@rebasepro/common` for one thing: `defineCollection`, which
15
+ * `rebase schema introspect` writes its output against. A plain
16
+ * `const x: PostgresCollectionConfig = { … }` annotation widens the property
17
+ * keys to `string`, so nothing checks the keys named elsewhere in the config —
18
+ * and the keys an introspected collection has are precisely the ones nobody
19
+ * typed and nobody remembers. `common` is the headless half of the pair: the
20
+ * same key inference the admin layer's builder gives a CMS project, with no
21
+ * admin surface and no React.
13
22
  */
14
23
 
15
24
  export { storageAuthorize } from "./storage.js";
@@ -12,6 +12,7 @@
12
12
  "clean": "rm -rf dist"
13
13
  },
14
14
  "dependencies": {
15
+ "@rebasepro/common": "workspace:*",
15
16
  "@rebasepro/types": "workspace:*"
16
17
  },
17
18
  "devDependencies": {
@@ -30,10 +30,14 @@ import { defineFunction, requireAuth, requireAdmin } from "@rebasepro/server";
30
30
  * slot over `app.use("/*", requireAuth)`: `use()` only covers routes declared
31
31
  * *below* it, so a route appended later above it is silently unprotected.
32
32
  *
33
- * `rebase.dataAsAdmin` gives you admin-level access to your data and
34
- * **bypasses RLS** use it only for trusted admin work. For request-scoped /
35
- * RLS-scoped data access, use c.get("user") and c.get("driver"), which carry
36
- * the caller's identity. (`rebase` also exposes auth, storage, email.)
33
+ * `rebase.dataAsAdmin` gives you admin-level access to your data: it runs as
34
+ * `{ uid: "service", roles: ["admin"] }`, which is **admin-scoped, not an RLS
35
+ * bypass** your policies are still evaluated, just against that identity. So
36
+ * `policy.serverContext()` is false for it (that arm means "no uid at all"),
37
+ * and anything an `admin` user can reach, it can reach too. For request-scoped
38
+ * data access use c.get("user") and c.get("driver"), which carry the caller's
39
+ * identity. `rebase.sql()` is the real bypass — owner connection, no policies.
40
+ * (`rebase` also exposes auth, storage, email.)
37
41
  */
38
42
  export default defineFunction((app, { rebase }) => {
39
43
  void rebase; // available for dataAsAdmin/auth/storage/email — see commented usage below
@@ -36,5 +36,10 @@
36
36
  // unnoticed until runtime. The inferred common root is already the project
37
37
  // root because of `../config`, so this adds no `rootDir` and moves nothing
38
38
  // that dist/ already contains.
39
- "include": ["src/**/*", "functions/**/*", "../config/**/*", "drizzle.config.ts"]
39
+ //
40
+ // `crons/` for the same reason, and for one more: `rebase build` compiles it
41
+ // through a generated tsconfig, but an ejected project builds with *this*
42
+ // file — so leaving crons out meant an ejected server had no compiled jobs to
43
+ // load and stopped running every schedule, nightly backups included.
44
+ "include": ["src/**/*", "functions/**/*", "crons/**/*", "../config/**/*", "drizzle.config.ts"]
40
45
  }
@@ -36,7 +36,28 @@ export default defineConfig({
36
36
  rollupOptions: {
37
37
  output: {
38
38
  manualChunks(id) {
39
- // Heavy vendor libraries — split into individually cached chunks
39
+ // Heavy vendor libraries — split into individually cached chunks.
40
+ //
41
+ // A name here says these modules travel TOGETHER. It does not
42
+ // say they travel late: a chunk becomes a static dependency of
43
+ // the entry — and so a `modulepreload` in index.html — the
44
+ // moment any one module in it is statically reachable. Naming
45
+ // a library that is only partly lazy therefore drags the lazy
46
+ // part onto the critical path. Read the two exceptions below
47
+ // before adding a line.
48
+
49
+ // @rollup/plugin-commonjs emits its shared helpers as two
50
+ // virtual modules ("\0commonjsHelpers.js" and
51
+ // "\0commonjs-dynamic-modules") that every CommonJS
52
+ // dependency reaches, the entry's included. They match no
53
+ // rule below, so Rollup parks them in one of the chunks
54
+ // that use them — and it chose `vendor-exceljs`, which
55
+ // meant the entry statically imported 940 kB of
56
+ // spreadsheet reader to get a ten-line `require` shim.
57
+ // Give the helpers a chunk of their own so they can never
58
+ // anchor a heavy one to the critical path.
59
+ if (id.includes("commonjsHelpers") || id.includes("commonjs-dynamic-modules")) return "vendor-commonjs-helpers";
60
+
40
61
  if (id.includes("exceljs")) return "vendor-exceljs";
41
62
  if (id.includes("prosemirror")) return "vendor-prosemirror";
42
63
  if (id.includes("monaco-editor") || id.includes("@monaco-editor")) return "vendor-monaco";
@@ -45,6 +66,10 @@ export default defineConfig({
45
66
  if (id.includes("prism-react-renderer")) return "vendor-prism";
46
67
  if (id.includes("markdown-it")) return "vendor-markdown";
47
68
  if (id.includes("react-dropzone")) return "vendor-dropzone";
69
+ // date-fns core only. The ~77 locales are imported one at a
70
+ // time by the admin's date preview; sharing a chunk name with
71
+ // the core would make all of them eager again.
72
+ if (id.includes("date-fns/locale")) return undefined;
48
73
  if (id.includes("date-fns")) return "vendor-datefns";
49
74
  if (id.includes("fuse.js")) return "vendor-fuse";
50
75
  if (id.includes("node_modules/react-dom/")) return "vendor-react-dom";
@@ -56,7 +81,13 @@ export default defineConfig({
56
81
  if (id.includes("node_modules/@floating-ui/")) return "vendor-floating-ui";
57
82
  if (id.includes("node_modules/tailwind-merge/")) return "vendor-tailwind-merge";
58
83
  if (id.includes("node_modules/notistack/")) return "vendor-notistack";
59
- if (id.includes("node_modules/lucide-react/")) return "vendor-lucide-react";
84
+
85
+ // lucide-react has no line on purpose. The ~130 icons the
86
+ // chrome imports by name are static; the by-name lookup map
87
+ // is fetched on demand. One chunk name cannot hold both apart,
88
+ // and naming it welded 822 kB of icons into the preload set.
89
+ // Left to Rollup, the named icons land in the entry and the
90
+ // map gets its own async chunk.
60
91
 
61
92
  return undefined;
62
93
  }