@hot-updater/cli-tools 1.0.0-rc.43 → 1.0.0-rc.45

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.d.mts CHANGED
@@ -79,7 +79,7 @@ declare const printAppSetup: ({ baseURL, ...setup }: AppSetup & {
79
79
  //#region src/assembleServer.d.ts
80
80
  /**
81
81
  * The server hot-updater.config.ts describes, as the CLI runs it: core and
82
- * the plugins over the config's database and storage.
82
+ * the plugins over the config's database.
83
83
  */
84
84
  interface AssembledServer {
85
85
  /** The config's database: the server's own, or `standaloneRepository(...)`. */
@@ -91,8 +91,6 @@ interface AssembledServer {
91
91
  * `standaloneRepository`, they go through the server's admin API.
92
92
  */
93
93
  readonly core: HotUpdaterCoreApi;
94
- /** Where the CLI uploads bundles; undefined when the config names none. */
95
- readonly storage: StorageAdapter | undefined;
96
94
  /** The plugins as configured, as a frozen copy. */
97
95
  readonly plugins: readonly AnyHotUpdaterPlugin[];
98
96
  /** Each plugin's API by id; undefined over standaloneRepository, whose plugins run on the server. */
@@ -109,6 +107,11 @@ interface AssembledServer {
109
107
  /** What `assembleServer` runs: hot-updater.config.ts's `database`, `storage`, and `plugins`. */
110
108
  interface AssembleServerOptions {
111
109
  readonly database: ConfiguredDatabase;
110
+ /**
111
+ * The config's storage, through which core resolves file URLs and the
112
+ * plugins read stored files. Tooling that does neither, such as init
113
+ * provisioning a credential, leaves it out.
114
+ */
112
115
  readonly storage?: StorageAdapter;
113
116
  readonly plugins?: readonly AnyHotUpdaterPlugin[];
114
117
  }
@@ -194,7 +197,7 @@ declare class ConfigBuilder {
194
197
  setStorage(storageConfig: ProviderConfig): this;
195
198
  /** Sets the server's database and its imports. */
196
199
  setDatabase(databaseConfig: ProviderConfig): this;
197
- /** Sets the plugins the server runs, such as a provider package's `plugins`. */
200
+ /** Sets the server's plugin factory array and its imports. */
198
201
  setPlugins(pluginsConfig: ProviderConfig): this;
199
202
  /** Sets the code between the environment loading and the config, such as a credentials helper. */
200
203
  setIntermediateCode(code: string): this;
@@ -343,7 +346,7 @@ type HotUpdaterConfigScaffold = {
343
346
  initializer: string;
344
347
  callee: string;
345
348
  };
346
- /** The plugins the server runs, such as a provider package's `plugins`. */
349
+ /** The server's plugin factory array. */
347
350
  plugins: {
348
351
  initializer: string;
349
352
  };
@@ -367,7 +370,8 @@ declare const writeHotUpdaterConfig: (scaffold: HotUpdaterConfigScaffold, filePa
367
370
  * Writes hot-updater.config.ts and says what it did. `settings` names the
368
371
  * provider in messages, such as "Supabase". A config it cannot merge is
369
372
  * kept, with what to add to it. The plugins file an older init wrote is
370
- * removed; one the project wrote is named, since nothing reads it.
373
+ * removed only after a successful config write and when the config no longer
374
+ * references it. A file the project wrote is kept with a cleanup notice.
371
375
  */
372
376
  declare const writeHotUpdaterFiles: (scaffold: HotUpdaterConfigScaffold, { cwd, settings }: {
373
377
  cwd?: string;
@@ -573,8 +577,6 @@ interface ServerClientEndpoint {
573
577
  interface ServerDefinition {
574
578
  /** The database as configured, with the tooling `hot-updater db` runs. */
575
579
  readonly database: ToolingDatabase;
576
- /** The storage as configured, in order. */
577
- readonly storage: readonly StorageAdapter[];
578
580
  readonly plugins: readonly AnyHotUpdaterPlugin[];
579
581
  /** The client plugins an app adds for the plugins: what init prints. */
580
582
  readonly clientPlugins: readonly PluginClientPlugin[];
package/dist/index.mjs CHANGED
@@ -16,7 +16,7 @@ import l__default from "node:readline";
16
16
  import tty, { ReadStream } from "node:tty";
17
17
  import Ei, { appendFileSync, createReadStream, createWriteStream, existsSync, lstatSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
18
18
  import path$1, { basename, dirname as dirname$1, join, posix, win32 } from "node:path";
19
- import { createBundleStorageKey, createMemoryAdapter, createStorageRootUriWithPath, getContentType, getManifestAssetDownloadPath, getManifestAssetStoragePath, isContentAddressedAssetFileHash, isRemoteDatabase, resolveManifestAssetStorageUri, toolingTargetOf } from "@hot-updater/plugin-core";
19
+ import { createBundleStorageKey, createMemoryAdapter, createStorageAdapter, createStorageRootUriWithPath, getContentType, getManifestAssetDownloadPath, getManifestAssetStoragePath, isContentAddressedAssetFileHash, isRemoteDatabase, resolveManifestAssetStorageUri, toolingTargetOf } from "@hot-updater/plugin-core";
20
20
  import { createHotUpdater } from "@hot-updater/server";
21
21
  import os, { constants } from "node:os";
22
22
  import crypto$1, { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -22970,6 +22970,11 @@ const printAppSetup = ({ baseURL, ...setup }) => {
22970
22970
  };
22971
22971
  //#endregion
22972
22972
  //#region src/assembleServer.ts
22973
+ /** The storage of an assembly that reads no stored file: it owns no URI. */
22974
+ const noStorage = createStorageAdapter({
22975
+ name: "none",
22976
+ protocol: "none"
22977
+ });
22973
22978
  /** `createHotUpdater`, with public client routes unless a plugin provides clientAuth. */
22974
22979
  const createServer = (database, storage, plugins) => createHotUpdater({
22975
22980
  database,
@@ -22977,27 +22982,25 @@ const createServer = (database, storage, plugins) => createHotUpdater({
22977
22982
  plugins,
22978
22983
  ...plugins.some((plugin) => plugin.provides?.clientAuth) ? {} : { clientAccess: "public" }
22979
22984
  });
22980
- function assembleServer({ database, storage, plugins = [] }) {
22985
+ function assembleServer({ database, storage = noStorage, plugins = [] }) {
22981
22986
  if (isRemoteDatabase(database)) {
22982
22987
  const shape = createServer({
22983
22988
  name: "memory",
22984
22989
  adapter: createMemoryAdapter()
22985
- }, [], plugins);
22990
+ }, storage, plugins);
22986
22991
  return {
22987
22992
  database,
22988
22993
  core: database.core,
22989
- storage,
22990
22994
  plugins: shape.plugins,
22991
22995
  api: void 0,
22992
22996
  clientPlugins: shape.clientPlugins,
22993
22997
  ...shape.clientAuth === void 0 ? {} : { clientAuth: shape.clientAuth }
22994
22998
  };
22995
22999
  }
22996
- const server = createServer(database, storage === void 0 ? [] : [storage], plugins);
23000
+ const server = createServer(database, storage, plugins);
22997
23001
  return {
22998
23002
  database,
22999
23003
  core: server.core,
23000
- storage,
23001
23004
  plugins: server.plugins,
23002
23005
  api: server.api,
23003
23006
  clientPlugins: server.clientPlugins,
@@ -25184,7 +25187,7 @@ var ConfigBuilder = class {
25184
25187
  this.imports.push(...databaseConfig.imports);
25185
25188
  return this;
25186
25189
  }
25187
- /** Sets the plugins the server runs, such as a provider package's `plugins`. */
25190
+ /** Sets the server's plugin factory array and its imports. */
25188
25191
  setPlugins(pluginsConfig) {
25189
25192
  this.pluginsInfo = pluginsConfig;
25190
25193
  this.imports.push(...pluginsConfig.imports);
@@ -44123,6 +44126,7 @@ const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
44123
44126
  "firebase-admin",
44124
44127
  "firebase-admin/app",
44125
44128
  "hot-updater",
44129
+ "hot-updater/plugins",
44126
44130
  "@aws-sdk/credential-provider-sso",
44127
44131
  "@aws-sdk/credential-providers",
44128
44132
  "@hot-updater/aws",
@@ -44131,6 +44135,7 @@ const MANAGED_IMPORT_PACKAGES = /* @__PURE__ */ new Set([
44131
44135
  "@hot-updater/expo",
44132
44136
  "@hot-updater/firebase",
44133
44137
  "@hot-updater/rock",
44138
+ "@hot-updater/server/plugins",
44134
44139
  "@hot-updater/supabase"
44135
44140
  ]);
44136
44141
  const MANAGED_HELPER_NAMES = /* @__PURE__ */ new Set([
@@ -44149,7 +44154,7 @@ const BUILD_IMPORT_PACKAGES = new Set([...KNOWN_BUILD_CALLEES].map((callee) => `
44149
44154
  const importDeclarationsOf = (source) => source.program.body.filter((statement) => statement.type === "ImportDeclaration");
44150
44155
  const parseConfigSource = (text) => {
44151
44156
  const result = parseSync(CONFIG_FILE_NAME, text, {
44152
- astType: "js",
44157
+ astType: "ts",
44153
44158
  lang: "ts",
44154
44159
  preserveParens: false,
44155
44160
  sourceType: "module",
@@ -44316,21 +44321,78 @@ const mergeObjectLiteralText = (existingObject, newObject, replaceIncompatiblePr
44316
44321
  }).map((property) => getPropertyBlockText(newObject.source, property));
44317
44322
  return applyTextEdits(existingText, [...edits, ...appendPropertiesEdits(existingObject.source, existingObject.objectExpression, missingPropertyTexts)]);
44318
44323
  };
44319
- const buildMergedCallInitializer = (existing, next) => {
44320
- const [existingArgument] = existing.callExpression.arguments;
44324
+ /**
44325
+ * The edit that deletes `property` from its object: with its comma, and with
44326
+ * the rest of its line when the property starts one.
44327
+ */
44328
+ const removePropertyEdit = (text, property) => {
44329
+ const comma = /^[ \t]*,/.exec(text.slice(property.end));
44330
+ const end = property.end + (comma?.[0].length ?? 0);
44331
+ if (startsLine(text, property)) {
44332
+ const rest = /^[ \t]*(?:\/\/[^\n]*)?\n/.exec(text.slice(end));
44333
+ return {
44334
+ start: lineStartOf(text, property.start),
44335
+ end: end + (rest?.[0].length ?? 0),
44336
+ text: ""
44337
+ };
44338
+ }
44339
+ const space = /^[ \t]*/.exec(text.slice(end));
44340
+ return {
44341
+ start: property.start,
44342
+ end: end + (space?.[0].length ?? 0),
44343
+ text: ""
44344
+ };
44345
+ };
44346
+ const parseCall = (text) => {
44347
+ const source = parseConfigSource(`(${text});`);
44348
+ const statement = source?.program.body[0];
44349
+ return source && statement?.type === "ExpressionStatement" && statement.expression.type === "CallExpression" ? {
44350
+ callExpression: statement.expression,
44351
+ source
44352
+ } : null;
44353
+ };
44354
+ /**
44355
+ * The project's call without its spreads of the managed helpers the rebuilt
44356
+ * config no longer declares.
44357
+ */
44358
+ const withoutDroppedHelpers = (call, droppedHelpers) => {
44359
+ const [argument] = call.callExpression.arguments;
44360
+ if (call.callExpression.arguments.length !== 1 || argument?.type !== "ObjectExpression") return call;
44361
+ const callStart = call.callExpression.start;
44362
+ const edits = argument.properties.filter((property) => property.type === "SpreadElement" && property.argument.type === "Identifier" && droppedHelpers.has(property.argument.name)).map((property) => {
44363
+ const edit = removePropertyEdit(call.source.text, property);
44364
+ return {
44365
+ ...edit,
44366
+ start: edit.start - callStart,
44367
+ end: edit.end - callStart
44368
+ };
44369
+ });
44370
+ if (edits.length === 0) return call;
44371
+ return parseCall(applyTextEdits(getNodeText(call.source, call.callExpression), edits));
44372
+ };
44373
+ /** Whether `text` uses one of `names`; text that doesn't parse counts as a use. */
44374
+ const usesAny = (text, names) => {
44375
+ if (names.size === 0) return false;
44376
+ const used = usedIdentifiers(`(${text});`);
44377
+ return used === null || [...names].some((name) => used.has(name));
44378
+ };
44379
+ const buildMergedCallInitializer = (existing, next, droppedHelpers) => {
44380
+ const project = withoutDroppedHelpers(existing, droppedHelpers);
44381
+ if (!project) return null;
44382
+ const [existingArgument] = project.callExpression.arguments;
44321
44383
  const [nextArgument] = next.callExpression.arguments;
44322
- if (existing.callExpression.arguments.length === 1 && next.callExpression.arguments.length === 1 && existingArgument?.type === "ObjectExpression" && nextArgument?.type === "ObjectExpression") {
44384
+ if (project.callExpression.arguments.length === 1 && next.callExpression.arguments.length === 1 && existingArgument?.type === "ObjectExpression" && nextArgument?.type === "ObjectExpression") {
44323
44385
  const mergedObjectLiteral = mergeObjectLiteralText({
44324
44386
  objectExpression: existingArgument,
44325
- source: existing.source
44387
+ source: project.source
44326
44388
  }, {
44327
44389
  objectExpression: nextArgument,
44328
44390
  source: next.source
44329
44391
  });
44330
44392
  if (!mergedObjectLiteral) return null;
44331
- return `${getNodeText(existing.source, existing.callExpression.callee)}(${mergedObjectLiteral})`;
44393
+ return `${getNodeText(project.source, project.callExpression.callee)}(${mergedObjectLiteral})`;
44332
44394
  }
44333
- return getNodeText(existing.source, existing.callExpression);
44395
+ return getNodeText(project.source, project.callExpression);
44334
44396
  };
44335
44397
  const findManagedProperty = (objectExpression, propertyName) => {
44336
44398
  const property = objectExpression.properties.find((candidate) => candidate.type === "Property" && candidate.kind === "init" && !candidate.method && !candidate.shorthand && getObjectPropertyName(candidate) === propertyName);
@@ -44369,10 +44431,12 @@ const mergeHelperStatement = (existingStatementText, helper) => {
44369
44431
  * `plugins`: a call to the same adapter keeps the project's arguments and
44370
44432
  * gains the scaffold's missing ones, and `plugins` is the scaffold's. A build
44371
44433
  * that is not a plain build adapter, such as `withSentry(bare())`, stays the
44372
- * project's (`keptBuild`). Null when `build`, `storage`, or `database` is no
44373
- * call.
44434
+ * project's (`keptBuild`). The merged call loses its spreads of the managed
44435
+ * helpers the rebuilt config no longer declares (`droppedHelpers`), and a
44436
+ * call that still uses one takes the scaffold's. Null when `build`,
44437
+ * `storage`, or `database` is no call.
44374
44438
  */
44375
- const updateManagedObject = (existing, next) => {
44439
+ const updateManagedObject = (existing, next, droppedHelpers) => {
44376
44440
  const objectStart = existing.objectExpression.start;
44377
44441
  const objectText = getNodeText(existing.source, existing.objectExpression);
44378
44442
  const propertyEdits = [];
@@ -44408,9 +44472,9 @@ const updateManagedObject = (existing, next) => {
44408
44472
  }, {
44409
44473
  callExpression: nextProperty.value,
44410
44474
  source: next.source
44411
- });
44475
+ }, droppedHelpers);
44412
44476
  if (!mergedInitializer) return null;
44413
- nextInitializerText = mergedInitializer;
44477
+ if (!usesAny(mergedInitializer, droppedHelpers)) nextInitializerText = mergedInitializer;
44414
44478
  }
44415
44479
  propertyEdits.push({
44416
44480
  start: existingProperty.value.start - objectStart,
@@ -44440,8 +44504,25 @@ const getManagedHelperName = (statement) => {
44440
44504
  if (declaration?.id.type !== "Identifier") return null;
44441
44505
  return declaration.id.name;
44442
44506
  };
44443
- /** Whether `text` refers to `name` as an identifier. */
44444
- const usesIdentifier = (text, name) => new RegExp(`(?<![\\w$])${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(text);
44507
+ /** Identifier uses, excluding static property names, strings, and comments. */
44508
+ const usedIdentifiers = (text) => {
44509
+ const source = parseConfigSource(text);
44510
+ if (!source) return null;
44511
+ const names = /* @__PURE__ */ new Set();
44512
+ const propertyNames = /* @__PURE__ */ new Set();
44513
+ new Visitor({
44514
+ Property(property) {
44515
+ if (!property.computed && !property.shorthand) propertyNames.add(property.key.start);
44516
+ },
44517
+ MemberExpression(member) {
44518
+ if (!member.computed) propertyNames.add(member.property.start);
44519
+ },
44520
+ Identifier(identifier) {
44521
+ if (!propertyNames.has(identifier.start)) names.add(identifier.name);
44522
+ }
44523
+ }).visit(source.program);
44524
+ return names;
44525
+ };
44445
44526
  /**
44446
44527
  * What a managed package's existing imports bring that the rebuilt config
44447
44528
  * still uses and the scaffold doesn't import, such as a project's own `cert`
@@ -44453,6 +44534,7 @@ const keptManagedImports = (declarations, scaffoldImports, usedText) => {
44453
44534
  const bound = new Set(scaffoldImports.flatMap((info) => [...(info.named ?? []).map((name) => name.split(/\s+as\s+/).at(-1)), ...info.defaultOrNamespace === void 0 ? [] : [info.defaultOrNamespace.replace(/^\*\s+as\s+/, "")]]));
44454
44535
  const imports = [];
44455
44536
  const texts = [];
44537
+ const references = usedIdentifiers(usedText);
44456
44538
  for (const declaration of declarations) {
44457
44539
  const pkg = declaration.source.value;
44458
44540
  const named = [];
@@ -44460,7 +44542,7 @@ const keptManagedImports = (declarations, scaffoldImports, usedText) => {
44460
44542
  let namespaceName;
44461
44543
  for (const specifier of declaration.specifiers) {
44462
44544
  const local = specifier.local.name;
44463
- if (bound.has(local) || !usesIdentifier(usedText, local)) continue;
44545
+ if (bound.has(local) || references && !references.has(local)) continue;
44464
44546
  if (specifier.type === "ImportDefaultSpecifier") defaultName = local;
44465
44547
  else if (specifier.type === "ImportNamespaceSpecifier") namespaceName = local;
44466
44548
  else {
@@ -44471,7 +44553,9 @@ const keptManagedImports = (declarations, scaffoldImports, usedText) => {
44471
44553
  }
44472
44554
  if (named.length === 0 && !defaultName && !namespaceName) continue;
44473
44555
  if (declaration.importKind === "type") {
44474
- texts.push(`import type { ${named.join(", ")} } from "${pkg}";`);
44556
+ if (named.length > 0) texts.push(`import type { ${named.join(", ")} } from "${pkg}";`);
44557
+ if (namespaceName) texts.push(`import type * as ${namespaceName} from "${pkg}";`);
44558
+ if (defaultName) texts.push(`import type ${defaultName} from "${pkg}";`);
44475
44559
  continue;
44476
44560
  }
44477
44561
  if (named.length > 0) imports.push({
@@ -44553,28 +44637,26 @@ const mergeHotUpdaterConfigText = (existingText, scaffold) => {
44553
44637
  const existingConfig = findDefineConfigObject(existingSource);
44554
44638
  const nextConfig = findDefineConfigObject(nextSource);
44555
44639
  if (!existingConfig || !nextConfig) return { reason: "Existing config is not a supported `export default defineConfig({ ... })` shape." };
44640
+ const existingPlugins = findPluginsProperty(existingConfig.objectExpression);
44641
+ if (existingPlugins && existingConfig.objectExpression.properties.some((property) => property.start > existingPlugins.start && (property.type === "SpreadElement" || property.computed || getObjectPropertyName(property) === "plugins"))) return { reason: "A later spread or property can overwrite plugins. Move plugins after it before running init again." };
44642
+ const scaffoldHelpers = new Set(scaffold.helperStatements.map((helper) => helper.name));
44643
+ const droppedHelpers = new Set(existingSource.program.body.flatMap((statement) => {
44644
+ const name = statement.start < existingConfig.exportDeclaration.start ? getManagedHelperName(statement) : null;
44645
+ return name !== null && MANAGED_HELPER_NAMES.has(name) && !scaffoldHelpers.has(name) ? [name] : [];
44646
+ }));
44556
44647
  const nextObject = updateManagedObject({
44557
44648
  objectExpression: existingConfig.objectExpression,
44558
44649
  source: existingSource
44559
44650
  }, {
44560
44651
  objectExpression: nextConfig.objectExpression,
44561
44652
  source: nextSource
44562
- });
44653
+ }, droppedHelpers);
44563
44654
  if (!nextObject) return { reason: "Existing config uses dynamic build/storage/database expressions that cannot be merged safely." };
44564
44655
  const exportFullStart = getTopLevelFullStart(existingSource, existingConfig.exportDeclaration);
44565
44656
  const firstTriviaContent = existingText.slice(exportFullStart, existingConfig.exportDeclaration.start).search(/\S/);
44566
44657
  const managedBodyEnd = firstTriviaContent === -1 ? existingConfig.exportDeclaration.start : exportFullStart + firstTriviaContent;
44567
44658
  const bodyEdit = rebuildManagedBody(existingSource, managedBodyEnd, scaffold);
44568
44659
  if (!bodyEdit) return { reason: "Existing helper declarations could not be merged safely." };
44569
- const scaffoldNames = new Map(scaffold.imports.flatMap((info) => (info.named ?? []).map((name) => [name.split(/\s+as\s+/).at(-1), info.pkg])));
44570
- for (const declaration of importDeclarationsOf(existingSource)) {
44571
- const pkg = declaration.source.value;
44572
- if (MANAGED_IMPORT_PACKAGES.has(pkg) && !(nextObject.keptBuild && BUILD_IMPORT_PACKAGES.has(pkg))) continue;
44573
- for (const specifier of declaration.specifiers) {
44574
- const scaffoldPackage = scaffoldNames.get(specifier.local.name);
44575
- if (scaffoldPackage !== void 0 && scaffoldPackage !== pkg) return { reason: `The import of ${specifier.local.name} from "${pkg}" takes the name init imports from "${scaffoldPackage}".` };
44576
- }
44577
- }
44578
44660
  const objectEdit = {
44579
44661
  start: existingConfig.objectExpression.start,
44580
44662
  end: existingConfig.objectExpression.end,
@@ -44589,6 +44671,43 @@ const mergeHotUpdaterConfigText = (existingText, scaffold) => {
44589
44671
  text: ""
44590
44672
  }))
44591
44673
  ]);
44674
+ const usedSource = parseConfigSource(usedText);
44675
+ const usedConfig = usedSource && findDefineConfigObject(usedSource);
44676
+ const plugins = usedConfig && findPluginsProperty(usedConfig.objectExpression);
44677
+ const keptReferences = usedIdentifiers(plugins ? applyTextEdits(usedText, [{
44678
+ start: plugins.start,
44679
+ end: plugins.end,
44680
+ text: "plugins: []"
44681
+ }]) : usedText);
44682
+ const scaffoldNames = new Map(scaffold.imports.flatMap((info) => (info.named ?? []).map((name) => {
44683
+ const parts = name.split(/\s+as\s+/);
44684
+ return [parts.at(-1), {
44685
+ pkg: info.pkg,
44686
+ imported: parts[0]
44687
+ }];
44688
+ })));
44689
+ const bindings = [];
44690
+ for (const statement of usedSource?.program.body ?? []) {
44691
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
44692
+ if (declaration?.type === "VariableDeclaration") bindings.push(...declaration.declarations.map(({ id }) => id));
44693
+ else if ((declaration?.type === "FunctionDeclaration" || declaration?.type === "ClassDeclaration") && declaration.id) bindings.push(declaration.id);
44694
+ }
44695
+ for (const binding of bindings) {
44696
+ if (binding.type === "Identifier" && scaffoldNames.has(binding.name)) return { reason: `The declaration of ${binding.name} conflicts with an import init needs.` };
44697
+ if (binding.type === "ObjectPattern") bindings.push(...binding.properties.map((property) => property.type === "RestElement" ? property.argument : property.value));
44698
+ else if (binding.type === "ArrayPattern") bindings.push(...binding.elements.filter((element) => element !== null));
44699
+ else if (binding.type === "AssignmentPattern") bindings.push(binding.left);
44700
+ else if (binding.type === "RestElement") bindings.push(binding.argument);
44701
+ }
44702
+ for (const declaration of importDeclarationsOf(existingSource)) {
44703
+ const pkg = declaration.source.value;
44704
+ for (const specifier of declaration.specifiers) {
44705
+ if (MANAGED_IMPORT_PACKAGES.has(pkg) && !(nextObject.keptBuild && BUILD_IMPORT_PACKAGES.has(pkg)) && keptReferences && !keptReferences.has(specifier.local.name)) continue;
44706
+ const scaffoldImport = scaffoldNames.get(specifier.local.name);
44707
+ const imported = specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier" ? specifier.imported.name : null;
44708
+ if (scaffoldImport !== void 0 && (scaffoldImport.imported !== imported || scaffoldImport.pkg !== pkg && !(scaffoldImport.pkg === "hot-updater/plugins" && pkg === "@hot-updater/server/plugins"))) return { reason: `The import of ${specifier.local.name} from "${pkg}" takes the name init imports from "${scaffoldImport.pkg}".` };
44709
+ }
44710
+ }
44592
44711
  return { text: applyTextEdits(existingText, [
44593
44712
  objectEdit,
44594
44713
  bodyEdit,
@@ -44686,14 +44805,16 @@ const renderServerSettings = (scaffold) => {
44686
44805
  const PLUGINS_FILE_PATH = "hotUpdater.plugins.ts";
44687
44806
  /**
44688
44807
  * Whether `text` is the plugins file an older init wrote: comments and a
44689
- * re-export of a provider package's `plugins`, which the config imports now.
44808
+ * re-export of a provider package's former `plugins`, which the config's
44809
+ * `plugins` list replaces.
44690
44810
  */
44691
44811
  const isGeneratedPluginsFile = (text) => /^export\{plugins\}from(["'])@hot-updater\/[\w-]+\1;?$/u.test(text.replace(/^\s*\/\/.*$/gmu, "").replace(/\s+/gu, ""));
44692
44812
  /**
44693
44813
  * Writes hot-updater.config.ts and says what it did. `settings` names the
44694
44814
  * provider in messages, such as "Supabase". A config it cannot merge is
44695
44815
  * kept, with what to add to it. The plugins file an older init wrote is
44696
- * removed; one the project wrote is named, since nothing reads it.
44816
+ * removed only after a successful config write and when the config no longer
44817
+ * references it. A file the project wrote is kept with a cleanup notice.
44697
44818
  */
44698
44819
  const writeHotUpdaterFiles = async (scaffold, { cwd = process.cwd(), settings }) => {
44699
44820
  const config = await writeHotUpdaterConfig(scaffold, path.join(cwd, HOT_UPDATER_CONFIG_PATH));
@@ -44708,6 +44829,17 @@ const writeHotUpdaterFiles = async (scaffold, { cwd = process.cwd(), settings })
44708
44829
  const pluginsPath = path.join(cwd, PLUGINS_FILE_PATH);
44709
44830
  const pluginsText = await readTextFile(pluginsPath);
44710
44831
  if (pluginsText === null) return { config };
44832
+ if (config.status === "skipped") return {
44833
+ config,
44834
+ pluginsFile: "kept"
44835
+ };
44836
+ if ((await fs$1.readFile(config.path, "utf-8")).includes("./hotUpdater.plugins")) {
44837
+ p.log.warn(`Kept '${PLUGINS_FILE_PATH}': '${HOT_UPDATER_CONFIG_PATH}' still references it.`);
44838
+ return {
44839
+ config,
44840
+ pluginsFile: "kept"
44841
+ };
44842
+ }
44711
44843
  if (isGeneratedPluginsFile(pluginsText)) {
44712
44844
  await fs$1.rm(pluginsPath);
44713
44845
  p.log.success(`Removed '${PLUGINS_FILE_PATH}': \`plugins\` in '${HOT_UPDATER_CONFIG_PATH}' lists the plugins the server runs.`);
@@ -45126,8 +45258,6 @@ const loadPlatformConfigs = async (platforms, { channel }) => {
45126
45258
  return configs;
45127
45259
  };
45128
45260
  const resolveConfig = (config) => {
45129
- for (const key of ["authorityId", "catalogId"]) if (config && Object.hasOwn(config, key)) throw new Error(`Remove ${key} from hot-updater.config. Catalog identity is managed internally.`);
45130
- if (config && Object.hasOwn(config, "compressStrategy")) throw new Error("Remove compressStrategy from hot-updater.config. OTA artifacts use manifest files with per-file Brotli compression.");
45131
45261
  const mergedConfig = mergeConfigSources(config, getDefaultConfig());
45132
45262
  const signing = normalizeSigningConfig(mergedConfig.signing);
45133
45263
  return {
@@ -45143,7 +45273,7 @@ var ServerDefinitionError = class extends Error {
45143
45273
  };
45144
45274
  const isRecord = (value) => typeof value === "object" && value !== null;
45145
45275
  /** Whether `value` has the public properties tooling reads from a server definition. */
45146
- const isServerDefinition = (value) => isRecord(value) && isRecord(value.database) && typeof value.database.name === "string" && Array.isArray(value.storage) && Array.isArray(value.plugins) && Array.isArray(value.clientPlugins) && Array.isArray(value.clientEndpoints) && isRecord(value.core) && isRecord(value.api);
45276
+ const isServerDefinition = (value) => isRecord(value) && isRecord(value.database) && typeof value.database.name === "string" && Array.isArray(value.plugins) && Array.isArray(value.clientPlugins) && Array.isArray(value.clientEndpoints) && isRecord(value.core) && isRecord(value.api);
45147
45277
  /**
45148
45278
  * `value` as a server definition, or a `ServerDefinitionError` that says
45149
45279
  * what `source`, the module it came from, must export instead.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/cli-tools",
3
- "version": "1.0.0-rc.43",
3
+ "version": "1.0.0-rc.45",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
@@ -49,9 +49,9 @@
49
49
  "oxc-parser": "0.141.0",
50
50
  "oxc-transform": "0.141.0",
51
51
  "unconfig": "7.5.0",
52
- "@hot-updater/protocol": "1.0.0-rc.43",
53
- "@hot-updater/server": "1.0.0-rc.43",
54
- "@hot-updater/plugin-core": "1.0.0-rc.43"
52
+ "@hot-updater/protocol": "1.0.0-rc.45",
53
+ "@hot-updater/plugin-core": "1.0.0-rc.45",
54
+ "@hot-updater/server": "1.0.0-rc.45"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@clack/prompts": "1.7.0",
@@ -65,7 +65,7 @@
65
65
  "tar": "^7.5.16",
66
66
  "verkit": "0.4.0",
67
67
  "workspace-tools": "^0.41.7",
68
- "@hot-updater/test-utils": "1.0.0-rc.43"
68
+ "@hot-updater/test-utils": "1.0.0-rc.45"
69
69
  },
70
70
  "inlinedDependencies": {
71
71
  "@babel/code-frame": "7.29.0",