@rebasepro/cli 0.13.1-canary.gcef69d4 → 0.13.1-canary.gef9608c

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/index.es.js CHANGED
@@ -14,7 +14,7 @@ import os from "os";
14
14
  import { createRebaseClient } from "@rebasepro/client";
15
15
  import dotenv from "dotenv";
16
16
  import { createRequire } from "module";
17
- import { BUNDLE_FORMAT_VERSION, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
17
+ import { BUNDLE_FORMAT_VERSION, DEFAULT_DATA_SOURCE_KEY, RUNTIME_CONTRACT_VERSION, computeSchemaVersion, deserializeCollections, findStorageSuffixCollision, getDataSourceCapabilities, normalizeStorageSources, storageEnvSuffix } from "@rebasepro/types";
18
18
  import { generateSDK } from "@rebasepro/codegen";
19
19
  //#region src/utils/package-manager.ts
20
20
  /**
@@ -3130,6 +3130,106 @@ function resolveBackendPaths(app, projectRoot) {
3130
3130
  };
3131
3131
  }
3132
3132
  //#endregion
3133
+ //#region src/utils/collection-drift.ts
3134
+ /**
3135
+ * Which edits under `config/collections` can put the *SQL* schema out of date.
3136
+ *
3137
+ * `rebase dev` watches that directory and, on any change, either tells the
3138
+ * developer to run `rebase schema generate` / `rebase db push` or runs them.
3139
+ * The watcher is recursive and knows nothing about what it is watching, so it
3140
+ * said that about every file under the directory — including a
3141
+ * `collections/firestore/exercises.ts`, whose documents live in Firestore and
3142
+ * for which there is no Drizzle schema to regenerate and no database to push
3143
+ * to. Advice that is wrong on every edit is advice a developer learns to
3144
+ * ignore, which is worse than none: the same box is the only warning for the
3145
+ * Postgres collection next to it, where it is real.
3146
+ *
3147
+ * Two independent reasons a change cannot affect the SQL schema, both checked
3148
+ * here:
3149
+ *
3150
+ * 1. **The loader would never read the file.** `loadCollectionsFromDirectory`
3151
+ * reads the top level of the collections directory only — no recursion, no
3152
+ * `index`, no tests, no declarations. A file it does not read cannot change
3153
+ * what it returns, and the watcher must not claim otherwise.
3154
+ * 2. **Every collection in it is served by another engine.** A Firestore or
3155
+ * MongoDB collection has no table, no migration and no policies.
3156
+ *
3157
+ * The engine check reads the source text rather than importing the module: the
3158
+ * CLI runs as plain Node and cannot evaluate a project's TypeScript, and a
3159
+ * watcher must answer in the time between two keystrokes. Anything it cannot
3160
+ * read confidently counts as SQL-affecting — a spurious warning is a nuisance,
3161
+ * a suppressed one hides real drift.
3162
+ */
3163
+ /**
3164
+ * Would `loadCollectionsFromDirectory` load this file?
3165
+ *
3166
+ * Mirrors that loader's own `isCollectionFile` plus its flat (non-recursive)
3167
+ * scan. `relativePath` is relative to the collections directory, as `fs.watch`
3168
+ * reports it.
3169
+ */
3170
+ function isLoadedCollectionFile(relativePath) {
3171
+ const normalized = relativePath.split(path.sep).join("/");
3172
+ if (normalized.includes("/")) return false;
3173
+ const file = normalized;
3174
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) return false;
3175
+ if (file.startsWith(".")) return false;
3176
+ if (file.includes(".test.")) return false;
3177
+ if (file.endsWith(".d.ts")) return false;
3178
+ if (file === "index.ts" || file === "index.js") return false;
3179
+ return true;
3180
+ }
3181
+ var ENGINE_LITERAL = /\bengine\s*:\s*["'`]([^"'`]+)["'`]/g;
3182
+ var DATA_SOURCE_LITERAL = /\bdataSource\s*:\s*["'`]([^"'`]+)["'`]/g;
3183
+ /**
3184
+ * Drop comments, so a `// engine: "firestore"` in a docblock cannot silence a
3185
+ * warning about the Postgres collection the file actually declares.
3186
+ *
3187
+ * Deliberately naive — it does not understand strings, so a `//` inside one
3188
+ * (a URL in a default value) eats the rest of that line. That only ever removes
3189
+ * text, and removing an engine literal makes this file *more* likely to warn,
3190
+ * which is the side to be wrong on.
3191
+ */
3192
+ function stripComments(source) {
3193
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
3194
+ }
3195
+ function literals(source, pattern) {
3196
+ const found = [];
3197
+ for (const match of source.matchAll(pattern)) found.push(match[1]);
3198
+ return found;
3199
+ }
3200
+ /**
3201
+ * Does this collection source declare anything a SQL toolchain would own?
3202
+ *
3203
+ * The fallback order matches `resolveDataSource`: an explicit `engine` wins,
3204
+ * and a collection that only names a `dataSource` is resolved as if the key
3205
+ * were the engine — which is what that function does when no registry is
3206
+ * available, and the CLI has none. An engine nobody recognises counts as
3207
+ * relational, for the reason `isRelationalCollection` gives.
3208
+ *
3209
+ * A file declaring several collections is SQL-affecting if *any* of them is.
3210
+ */
3211
+ function declaresRelationalCollection(rawSource) {
3212
+ const source = stripComments(rawSource);
3213
+ const engines = literals(source, ENGINE_LITERAL);
3214
+ const declared = engines.length > 0 ? engines : literals(source, DATA_SOURCE_LITERAL).filter((key) => key !== DEFAULT_DATA_SOURCE_KEY);
3215
+ if (declared.length === 0) return true;
3216
+ return declared.some((engine) => getDataSourceCapabilities(engine).supportsRelations);
3217
+ }
3218
+ /**
3219
+ * Can this edit have changed the generated SQL schema?
3220
+ *
3221
+ * Answers `true` when it cannot tell — an unreadable file is a reason to warn,
3222
+ * not a reason to go quiet.
3223
+ */
3224
+ function affectsSqlSchema(collectionsDir, relativePath) {
3225
+ if (!isLoadedCollectionFile(relativePath)) return false;
3226
+ try {
3227
+ return declaresRelationalCollection(fs.readFileSync(path.join(collectionsDir, relativePath), "utf8"));
3228
+ } catch {
3229
+ return true;
3230
+ }
3231
+ }
3232
+ //#endregion
3133
3233
  //#region src/commands/dev.ts
3134
3234
  /**
3135
3235
  * CLI command: rebase dev
@@ -3442,14 +3542,18 @@ async function devCommand(rawArgs) {
3442
3542
  const collectionsDir = path.join(projectRoot, "config", "collections");
3443
3543
  if (fs.existsSync(collectionsDir)) {
3444
3544
  let watchDebounce = null;
3545
+ let sqlSchemaAffected = false;
3445
3546
  fs.watch(collectionsDir, { recursive: true }, (eventType, filename) => {
3446
3547
  if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
3548
+ sqlSchemaAffected = sqlSchemaAffected || affectsSqlSchema(collectionsDir, filename);
3447
3549
  if (watchDebounce) clearTimeout(watchDebounce);
3448
3550
  watchDebounce = setTimeout(async () => {
3449
- console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating schema & SDK...`));
3551
+ const regenerateSchema = sqlSchemaAffected;
3552
+ sqlSchemaAffected = false;
3553
+ console.log(chalk.yellow(`\n 🔄 Collection change detected (${filename}). Regenerating ${regenerateSchema ? "schema & SDK" : "SDK"}...`));
3450
3554
  try {
3451
3555
  const activePlugin = getActiveBackendPlugin(backendDir);
3452
- const pluginCli = activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
3556
+ const pluginCli = regenerateSchema && activePlugin ? resolvePluginCliScript(backendDir, activePlugin) : null;
3453
3557
  if (pluginCli) await execa(tsxBin, [
3454
3558
  pluginCli,
3455
3559
  "schema",
@@ -3465,7 +3569,7 @@ async function devCommand(rawArgs) {
3465
3569
  stdio: "inherit",
3466
3570
  env
3467
3571
  });
3468
- console.log(chalk.green(" ✓ Schema & SDK regenerated successfully. Hono will reload."));
3572
+ console.log(chalk.green(` ✓ ${regenerateSchema ? "Schema & SDK" : "SDK"} regenerated successfully. Hono will reload.`));
3469
3573
  } catch (err) {
3470
3574
  console.error(chalk.red(` ✗ Failed to regenerate schema/SDK: ${err instanceof Error ? err.message : err}`));
3471
3575
  }
@@ -3490,12 +3594,14 @@ async function devCommand(rawArgs) {
3490
3594
  let driftDebounce = null;
3491
3595
  fs.watch(collectionsDir, { recursive: true }, (_eventType, filename) => {
3492
3596
  if (!filename || filename.startsWith(".") || filename.endsWith(".tmp")) return;
3597
+ if (!affectsSqlSchema(collectionsDir, filename)) return;
3493
3598
  if (driftDebounce) clearTimeout(driftDebounce);
3494
3599
  driftDebounce = setTimeout(() => {
3600
+ const shown = filename.length > 31 ? `…${filename.slice(-30)}` : filename.padEnd(31);
3495
3601
  console.log([
3496
3602
  "",
3497
3603
  chalk.yellow(" ┌──────────────────────────────────────────────────────────────┐"),
3498
- chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(filename.padEnd(31)) + chalk.yellow("│"),
3604
+ chalk.yellow(" │ ⚠️ Collection file changed: ") + chalk.white(shown) + chalk.yellow("│"),
3499
3605
  chalk.yellow(" │ │"),
3500
3606
  chalk.yellow(" │ Your schema may be out of sync. Run: │"),
3501
3607
  chalk.yellow(" │ ") + chalk.cyan("rebase schema generate") + chalk.yellow(" regenerate Drizzle schema │"),