create-nextly-app 0.0.2-alpha.72 → 0.0.2-alpha.74

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.
@@ -22855,6 +22855,7 @@ function nextlySource(options) {
22855
22855
  function isNextlyPackage(name) {
22856
22856
  return name === "nextly" || name.startsWith("@nextlyhq/");
22857
22857
  }
22858
+ /** The package.json fields that declare a dependency, in the order read. */
22858
22859
  const DEPENDENCY_FIELDS$1 = [
22859
22860
  "dependencies",
22860
22861
  "devDependencies",
@@ -22930,6 +22931,10 @@ const EXACT_FLAG = {
22930
22931
  yarn: "--exact",
22931
22932
  bun: "--exact"
22932
22933
  };
22934
+ /**
22935
+ * How each package manager adds a dependency. Exported so every message that
22936
+ * tells the person what to install spells the command the same way.
22937
+ */
22933
22938
  const ADD_COMMAND = {
22934
22939
  npm: "npm install",
22935
22940
  pnpm: "pnpm add",
@@ -42499,8 +42504,8 @@ var import_lib = (/* @__PURE__ */ __commonJSMin$1(((exports) => {
42499
42504
  exports.parse = parse;
42500
42505
  })))();
42501
42506
  /**
42502
- * Where Nextly explains why Cache Components is off and when it will be
42503
- * supported: the same page `nextly` links from the error it stops with.
42507
+ * Where Nextly explains the Cache Components settings every app it makes or
42508
+ * joins carries, and why each one is there.
42504
42509
  */
42505
42510
  const CACHE_COMPONENTS_DOCS_URL = "https://nextlyhq.com/docs/getting-started/installation#cache-components";
42506
42511
  /**
@@ -42538,13 +42543,42 @@ function packageListLiteral(database) {
42538
42543
  return JSON.stringify(getServerExternalPackages(database), null, 2).replace(/\n/g, "\n ");
42539
42544
  }
42540
42545
  /**
42541
- * The setting every config this writes carries, with its reason in the file:
42542
- * the developer who finds it is the one deciding whether to change it.
42546
+ * Nextly's cache handlers, as the settings that install them. Under Cache
42547
+ * Components, Next.js's default cache serves a page's previous shell once
42548
+ * after a write expires its tags; these expire it at once.
42549
+ */
42550
+ const HANDLER_SETTINGS = [
42551
+ "cacheHandler",
42552
+ "cacheMaxMemorySize",
42553
+ "cacheHandlers"
42554
+ ];
42555
+ const HANDLER_PROPERTIES = [
42556
+ "cacheHandler: require.resolve(\"nextly/cache-handler\")",
42557
+ "cacheMaxMemorySize: 0",
42558
+ "cacheHandlers: { default: require.resolve(\"nextly/cache-handlers\") }"
42559
+ ];
42560
+ const ON_OPTIONS = ["cacheComponents", "partialPrefetching"];
42561
+ /** Every setting Nextly needs, as lines of a config object. */
42562
+ const ON_LINES = [...ON_OPTIONS.map((option) => `${option}: true`), ...HANDLER_PROPERTIES].map((property) => ` ${property},`);
42563
+ /**
42564
+ * The settings every config this writes carries, with their reason in the
42565
+ * file: the developer who finds them is the one deciding whether to change
42566
+ * them.
42543
42567
  */
42544
- const CACHE_COMPONENTS_OFF = ` // Nextly does not support Cache Components yet: it caches with
42545
- // unstable_cache, and Cache Components refuses \`runtime\` route exports.
42568
+ const CACHE_COMPONENTS_ON = ` // Nextly runs with Cache Components and Partial Prefetching on. Its cache
42569
+ // handlers make the first read after a write show the new version, where
42570
+ // Next.js's default cache would serve the previous page once more:
42546
42571
  // ${CACHE_COMPONENTS_DOCS_URL}
42547
- cacheComponents: false,`;
42572
+ ${ON_LINES.join("\n")}`;
42573
+ /**
42574
+ * How an ES module config gets `require`, which Node defines only in
42575
+ * CommonJS, to resolve the handlers from the app's own node_modules.
42576
+ */
42577
+ const REQUIRE_IMPORT = "import { createRequire } from \"node:module\";";
42578
+ /** The declaration, calling `createRequire` by the name the file has it under. */
42579
+ const requireDeclaration = (createRequireName) => `// Resolves Nextly's cache handlers from this app's node_modules.
42580
+ const require = ${createRequireName}(import.meta.url);`;
42581
+ const REQUIRE_DECLARATION = requireDeclaration("createRequire");
42548
42582
  /**
42549
42583
  * Render a complete next.config.ts file for the selected database.
42550
42584
  */
@@ -42552,7 +42586,7 @@ function buildNextConfigTemplate(database) {
42552
42586
  return `import type { NextConfig } from "next";
42553
42587
 
42554
42588
  const nextConfig: NextConfig = {
42555
- ${CACHE_COMPONENTS_OFF}
42589
+ ${CACHE_COMPONENTS_ON}
42556
42590
  serverExternalPackages: ${packageListLiteral(database)},
42557
42591
  };
42558
42592
 
@@ -42561,9 +42595,13 @@ export default nextConfig;
42561
42595
  }
42562
42596
  /** The same config as a JavaScript module, for an app without TypeScript. */
42563
42597
  function buildNextConfigModule(database) {
42564
- return `/** @type {import("next").NextConfig} */
42598
+ return `${REQUIRE_IMPORT}
42599
+
42600
+ ${REQUIRE_DECLARATION}
42601
+
42602
+ /** @type {import("next").NextConfig} */
42565
42603
  const nextConfig = {
42566
- ${CACHE_COMPONENTS_OFF}
42604
+ ${CACHE_COMPONENTS_ON}
42567
42605
  serverExternalPackages: ${packageListLiteral(database)},
42568
42606
  };
42569
42607
 
@@ -42581,9 +42619,19 @@ const CONFIG_FILES = [
42581
42619
  "next.config.ts",
42582
42620
  "next.config.mts"
42583
42621
  ];
42584
- /** The first Next.js major whose option is the top-level `serverExternalPackages`. */
42585
- const FIRST_TOP_LEVEL_MAJOR = 15;
42586
42622
  const PROPERTY = "serverExternalPackages";
42623
+ /**
42624
+ * The deprecated spelling of the list, under `experimental`. Supported
42625
+ * releases still read it, by copying it over the top-level list, so an app
42626
+ * that has it would lose the packages this adds there.
42627
+ */
42628
+ const LEGACY_PROPERTY = "serverComponentsExternalPackages";
42629
+ /**
42630
+ * Why a config with the deprecated list is left alone. Named, so the advice
42631
+ * printed for it can say what to do with that list rather than the advice
42632
+ * for every other refusal, which says to keep the experimental keys there.
42633
+ */
42634
+ const LEGACY_LIST_REASON = `it lists packages under experimental.${LEGACY_PROPERTY}, which Next.js copies over ${PROPERTY}, replacing the packages added there`;
42587
42635
  /** Where the line holding `offset` starts. */
42588
42636
  const lineStart = (source, offset) => source.lastIndexOf("\n", offset - 1) + 1;
42589
42637
  /** The whitespace a line opens with, for the line holding `offset`. */
@@ -42764,7 +42812,7 @@ const TOP_LEVEL_SETTINGS = [
42764
42812
  "transpilePackages",
42765
42813
  "experimental"
42766
42814
  ];
42767
- const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", "serverComponentsExternalPackages"];
42815
+ const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", LEGACY_PROPERTY];
42768
42816
  /** Why an object might set one of `names` where this cannot read it, or null. */
42769
42817
  function hiddenSettings(object, names, where) {
42770
42818
  return object.properties.some((property) => mayHide(property, names)) ? `${where} spreads another object or has a computed or accessor property, which may set ${names.join(", ")}` : null;
@@ -42822,41 +42870,43 @@ function fillEmptyList(source, array, entries) {
42822
42870
  * so the app's own properties keep their order and their comments.
42823
42871
  */
42824
42872
  function addListProperty(source, object, entries) {
42873
+ return appendProperties(source, object, (indent, ownLines) => [ownLines ? `${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}]` : `${PROPERTY}: [${entries.join(", ")}]`]);
42874
+ }
42875
+ /**
42876
+ * Add properties after an object's last one, in the object's layout: on one
42877
+ * line, as `{ a: 1, b: 2 }`, when it is written that way, and otherwise one
42878
+ * per line at the indentation its properties use, before the closing brace,
42879
+ * with a comma put after the last property first when it has none.
42880
+ * `properties` gets that indentation, and whether each property has a line
42881
+ * of its own, so a property spanning lines can follow the same layout.
42882
+ */
42883
+ function appendProperties(source, object, properties) {
42825
42884
  const open = (object.start ?? 0) + 1;
42826
42885
  const close = (object.end ?? 0) - 1;
42827
- return spansLines(source, open, close) ? listOnItsOwnLines(source, object, entries) : [listOnTheSameLine(object, entries)];
42886
+ if (!spansLines(source, open, close)) return [appendOnTheSameLine(object, properties("", false).join(", "))];
42887
+ const last = object.properties.at(-1);
42888
+ const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
42889
+ const lines = properties(indent, true).map((property) => `${indent}${property},\n`).join("");
42890
+ return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
42891
+ at: last.end ?? 0,
42892
+ remove: 0,
42893
+ text: ","
42894
+ }] : [], beforeClosingBrace(source, close, lines)];
42828
42895
  }
42829
- /** The list added to an object written on one line, as `{ a: 1, serverExternalPackages: [...] }`. */
42830
- function listOnTheSameLine(object, entries) {
42831
- const property = `${PROPERTY}: [${entries.join(", ")}]`;
42896
+ /** Properties added to an object written on one line, as `{ a: 1, b: 2 }`. */
42897
+ function appendOnTheSameLine(object, text) {
42832
42898
  const last = object.properties.at(-1);
42833
42899
  const open = (object.start ?? 0) + 1;
42834
42900
  return last ? {
42835
42901
  at: last.end ?? 0,
42836
42902
  remove: 0,
42837
- text: `, ${property}`
42903
+ text: `, ${text}`
42838
42904
  } : {
42839
42905
  at: open,
42840
42906
  remove: (object.end ?? 0) - 1 - open,
42841
- text: ` ${property} `
42907
+ text: ` ${text} `
42842
42908
  };
42843
42909
  }
42844
- /**
42845
- * The list added as the last property of an object written over several
42846
- * lines, one entry per line at the indentation the object's properties use.
42847
- * A comma goes after the last property first when it has none.
42848
- */
42849
- function listOnItsOwnLines(source, object, entries) {
42850
- const close = (object.end ?? 0) - 1;
42851
- const last = object.properties.at(-1);
42852
- const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
42853
- const list = `${indent}${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}],\n`;
42854
- return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
42855
- at: last.end ?? 0,
42856
- remove: 0,
42857
- text: ","
42858
- }] : [], beforeClosingBrace(source, close, list)];
42859
- }
42860
42910
  /** Text put before an object's closing brace: on the line above it when the brace has a line of its own. */
42861
42911
  function beforeClosingBrace(source, close, text) {
42862
42912
  const closeLine = lineStart(source, close);
@@ -42944,7 +42994,7 @@ function conflictOf(object, required) {
42944
42994
  }
42945
42995
  /** Why the config's lists clash, given an `experimental` object it can read. */
42946
42996
  function experimentalConflict(object, experimental, required) {
42947
- if (propertyValue(experimental, "serverComponentsExternalPackages")) return `it lists packages under experimental.serverComponentsExternalPackages, which Next.js moves to ${PROPERTY}; keep a single list there`;
42997
+ if (propertyValue(experimental, LEGACY_PROPERTY)) return LEGACY_LIST_REASON;
42948
42998
  return clashReason("transpilePackages", object, "transpilePackages", required) ?? clashReason("experimental.optimizePackageImports", experimental, "optimizePackageImports", required);
42949
42999
  }
42950
43000
  /** The object literal the file exports as its config, or why there is none to edit. */
@@ -43014,58 +43064,6 @@ function editConfig(source, file, required) {
43014
43064
  added: plan.added
43015
43065
  };
43016
43066
  }
43017
- /** The string at `keys` inside a parsed JSON file, or null. */
43018
- function stringAt(json, keys) {
43019
- let value = json;
43020
- for (const key of keys) {
43021
- if (typeof value !== "object" || value === null || !(key in value)) return null;
43022
- value = value[key];
43023
- }
43024
- return typeof value === "string" ? value : null;
43025
- }
43026
- /** A JSON file of the app's, or null when it is missing or does not parse. */
43027
- async function readJsonOrNull(file) {
43028
- try {
43029
- return await import_lib$1.default.readJson(file);
43030
- } catch {
43031
- return null;
43032
- }
43033
- }
43034
- /**
43035
- * A version range that admits one major only, such as `^15.1.0`, `~16.0`,
43036
- * `15.x` or `16.0.0-canary.3`, with the major it admits. A range such as
43037
- * `>=14`, `14 || 15` or `latest` admits several, or names none.
43038
- */
43039
- const SINGLE_MAJOR = /^[\^~]?(\d+)(?:\.(?:\d+|[xX*])){0,2}(?:-[\w.]+)?$/;
43040
- /**
43041
- * The major version of the app's Next.js: the installed one, read from its
43042
- * own `node_modules`, or else the one its package.json range admits when
43043
- * that range admits a single major. Null when neither tells, as when
43044
- * dependencies are not installed yet and the range is `latest`.
43045
- */
43046
- async function nextMajor(cwd) {
43047
- const installed = stringAt(await readJsonOrNull(path.default.join(cwd, "node_modules", "next", "package.json")), ["version"]);
43048
- const manifest = await readJsonOrNull(path.default.join(cwd, "package.json"));
43049
- const range = stringAt(manifest, ["dependencies", "next"]) ?? stringAt(manifest, ["devDependencies", "next"]);
43050
- const match = SINGLE_MAJOR.exec((installed ?? range ?? "").trim());
43051
- return match ? Number(match[1]) : null;
43052
- }
43053
- /**
43054
- * Why the option cannot be written for the app's Next.js, and which spelling
43055
- * to print instead, or null when it can. Next.js 14 reads the list under
43056
- * another name, and an unknown version may be 14: writing the top-level list
43057
- * there would report success and externalise nothing.
43058
- */
43059
- function versionBlock(major) {
43060
- if (major === null) return {
43061
- reason: "the version of Next.js the app uses could not be read",
43062
- spelling: "either"
43063
- };
43064
- return major < FIRST_TOP_LEVEL_MAJOR ? {
43065
- reason: `the app runs Next.js ${major}, which reads the list as experimental.serverComponentsExternalPackages and runs instrumentation.ts only with experimental.instrumentationHook`,
43066
- spelling: "legacy"
43067
- } : null;
43068
- }
43069
43067
  /**
43070
43068
  * The Next config file any advice about the app's config names: the one it
43071
43069
  * has, or, when it has none, the one to create. `.mjs` is ESM whatever the
@@ -43117,14 +43115,6 @@ async function createNextConfig(cwd, database) {
43117
43115
  async function patchNextConfig(cwd, database) {
43118
43116
  const file = await findConfigFile(cwd);
43119
43117
  const required = getServerExternalPackages(database);
43120
- const blocked = versionBlock(await nextMajor(cwd));
43121
- if (blocked) return {
43122
- kind: "manual",
43123
- file: file ?? await nextConfigFileToChange(cwd),
43124
- packages: required,
43125
- ...blocked,
43126
- ...file ? {} : { create: true }
43127
- };
43128
43118
  if (!file) return createNextConfig(cwd, database);
43129
43119
  const edited = editConfig(await import_lib$1.default.readFile(path.default.join(cwd, file), "utf-8"), file, required);
43130
43120
  if ("reason" in edited) return {
@@ -43144,119 +43134,281 @@ async function patchNextConfig(cwd, database) {
43144
43134
  added: edited.added
43145
43135
  };
43146
43136
  }
43147
- /** The setting Nextly needs off, and the one Next.js accepts only with it on. */
43148
- const CACHE_COMPONENTS = "cacheComponents";
43149
- const PARTIAL_PREFETCHING = "partialPrefetching";
43150
43137
  /**
43151
- * Remove a property with the comma that separates it: the whole line when it
43152
- * has one of its own, else the property and the comma after it, or before it
43153
- * when it is the last one.
43138
+ * The exported config object, when every setting this edits can be read in
43139
+ * it; else why not. Not through a wrapper call: Next.js receives what the
43140
+ * wrapper returns, which may set an option again, so the literal passed in
43141
+ * says nothing final.
43154
43142
  */
43155
- function removeProperty(source, property) {
43156
- const start = property.start ?? 0;
43157
- const end = property.end ?? 0;
43158
- const ownLine = /^\s*$/.test(source.slice(lineStart(source, start), start));
43159
- const rest = /^[ \t]*,?[ \t]*\n/.exec(source.slice(end));
43160
- if (ownLine && rest) {
43161
- const from = lineStart(source, start);
43162
- return {
43163
- at: from,
43164
- remove: end + rest[0].length - from,
43165
- text: ""
43166
- };
43167
- }
43168
- const after = /^\s*,\s*/.exec(source.slice(end));
43169
- if (after) return {
43170
- at: start,
43171
- remove: end - start + after[0].length,
43172
- text: ""
43173
- };
43174
- const before = /,\s*$/.exec(source.slice(0, start));
43175
- const from = before ? start - before[0].length : start;
43143
+ function cacheComponentsObject(body) {
43144
+ const object = exportedConfig(body, false);
43145
+ if (typeof object === "string") return object;
43146
+ return hiddenSettings(object, [...ON_OPTIONS, ...HANDLER_SETTINGS], "it") ?? object;
43147
+ }
43148
+ /**
43149
+ * How to turn one option on: the `false` to flip, the property to add when the
43150
+ * config does not set it, nothing when it is on, or why the value cannot be
43151
+ * read.
43152
+ */
43153
+ function optionStep(object, name) {
43154
+ const value = propertyValue(object, name);
43155
+ if (!value) return { add: `${name}: true` };
43156
+ if (value.type !== "BooleanLiteral") return `its ${name} is not true or false written in place`;
43157
+ if (value.value) return {};
43158
+ return { flip: {
43159
+ at: value.start ?? 0,
43160
+ remove: (value.end ?? 0) - (value.start ?? 0),
43161
+ text: "true"
43162
+ } };
43163
+ }
43164
+ /**
43165
+ * Whether Node loads the config as an ES module, where `require` is not
43166
+ * defined: `.mjs` and `.mts` always, and `.js` when it imports or exports,
43167
+ * which a config in a package of `"type": "module"` must, and which Node
43168
+ * detects in any other. Next.js compiles `next.config.ts` to CommonJS.
43169
+ */
43170
+ function loadsAsModule(file, body) {
43171
+ if (/\.m[jt]s$/.test(file)) return true;
43172
+ return file.endsWith(".js") && body.some((statement) => statement.type === "ImportDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration");
43173
+ }
43174
+ /**
43175
+ * The name an ES module config imports `createRequire` under from
43176
+ * `node:module`, whatever it renamed it to, or null when it imports none.
43177
+ */
43178
+ function createRequireImport(body) {
43179
+ return body.flatMap((statement) => statement.type === "ImportDeclaration" && (statement.source.value === "node:module" || statement.source.value === "module") ? statement.specifiers : []).find((item) => item.type === "ImportSpecifier" && importedName(item) === "createRequire")?.local.name ?? null;
43180
+ }
43181
+ /** The name an import specifier takes from its module, written plain or quoted. */
43182
+ function importedName(specifier) {
43183
+ const { imported } = specifier;
43184
+ return imported.type === "Identifier" ? imported.name : imported.value;
43185
+ }
43186
+ /** Whether the file binds `require` at its top level, by a declaration or an import. */
43187
+ function declaresRequire(body) {
43188
+ return body.some((statement) => importsRequire(statement) || declaresRequireVariable(statement));
43189
+ }
43190
+ /** Whether `statement` imports a binding named `require`: hoisted, so bound anywhere. */
43191
+ function importsRequire(statement) {
43192
+ return statement.type === "ImportDeclaration" && statement.specifiers.some((specifier) => isNamed(specifier.local, "require"));
43193
+ }
43194
+ /** The declarators in `statement` that name a variable `require`, exported or not. */
43195
+ function requireDeclarators(statement) {
43196
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
43197
+ return declaration?.type === "VariableDeclaration" ? declaration.declarations.filter((declarator) => isNamed(declarator.id, "require")) : [];
43198
+ }
43199
+ /** Whether `statement` declares a variable named `require`, exported or not. */
43200
+ function declaresRequireVariable(statement) {
43201
+ return requireDeclarators(statement).length > 0;
43202
+ }
43203
+ /**
43204
+ * Whether the file binds `require` only by a variable declared after `at`,
43205
+ * where the config object starts. The object's `require.resolve` runs when
43206
+ * the object is evaluated, while that declaration is still in its temporal dead
43207
+ * zone, so it would throw: the lines are printed rather than written.
43208
+ */
43209
+ function declaresRequireAfter(body, at) {
43210
+ if (body.some(importsRequire)) return false;
43211
+ const declarators = body.flatMap(requireDeclarators);
43212
+ return declarators.length > 0 && declarators.every((declarator) => (declarator.start ?? 0) >= at);
43213
+ }
43214
+ /**
43215
+ * What an ES module config needs at its top for the handlers' `require`:
43216
+ * nothing when it binds `require` already, the declaration alone, calling its
43217
+ * own `createRequire` by its name, when it imports one, and otherwise the
43218
+ * import and the declaration.
43219
+ */
43220
+ function requireSetupFor(body) {
43221
+ if (declaresRequire(body)) return null;
43222
+ const imported = createRequireImport(body);
43223
+ return imported ? requireDeclaration(imported) : `${REQUIRE_IMPORT}\n\n${REQUIRE_DECLARATION}`;
43224
+ }
43225
+ /**
43226
+ * The edit that gives an ES module config `require`, before the first
43227
+ * statement that runs, and before the comments that lead it, such as a
43228
+ * `@type` annotation. Imports are hoisted, so one written further down still
43229
+ * loads first; the declaration is not, and a `require.resolve` evaluated
43230
+ * above it would throw.
43231
+ */
43232
+ function requireEdit(source, body, setup) {
43233
+ const first = body.find((statement) => statement.type !== "ImportDeclaration");
43176
43234
  return {
43177
- at: from,
43178
- remove: end - from,
43179
- text: ""
43235
+ at: lineStart(source, first?.leadingComments?.[0]?.start ?? first?.start ?? 0),
43236
+ remove: 0,
43237
+ text: `${setup}\n\n`
43180
43238
  };
43181
43239
  }
43182
- /** The exported config object, when both settings in it can be read; else why not. */
43183
- function cacheComponentsObject(source, file) {
43184
- const body = parseBody(source, file);
43185
- if (!body) return "it could not be parsed";
43186
- const object = exportedConfig(body, false);
43187
- if (typeof object === "string") return object;
43188
- return hiddenSettings(object, [CACHE_COMPONENTS, PARTIAL_PREFETCHING], "it") ?? object;
43240
+ /** Whether `node` is `require.resolve("<specifier>")`. */
43241
+ function resolvesTo(node, specifier) {
43242
+ if (node?.type !== "CallExpression") return false;
43243
+ const { callee, arguments: args } = node;
43244
+ const [only] = args;
43245
+ return callee.type === "MemberExpression" && isNamed(callee.object, "require") && isNamed(callee.property, "resolve") && args.length === 1 && only?.type === "StringLiteral" && only.value === specifier;
43189
43246
  }
43190
- /** The `true` to flip, null when Cache Components is off or unset, or why the value cannot be read. */
43191
- function enabledValue(object) {
43192
- const value = propertyValue(object, CACHE_COMPONENTS);
43193
- if (!value) return null;
43194
- if (value.type !== "BooleanLiteral") return `its ${CACHE_COMPONENTS} is not true or false written in place`;
43195
- return value.value ? value : null;
43247
+ /** Whether the config's cache settings are Nextly's handlers, as this writes them. */
43248
+ function hasNextlysHandlers(object) {
43249
+ const handlers = propertyValue(object, "cacheHandlers");
43250
+ const memory = propertyValue(object, "cacheMaxMemorySize");
43251
+ return resolvesTo(propertyValue(object, "cacheHandler"), "nextly/cache-handler") && handlers?.type === "ObjectExpression" && resolvesTo(propertyValue(handlers, "default"), "nextly/cache-handlers") && memory?.type === "NumericLiteral" && memory.value === 0;
43196
43252
  }
43197
- /** The edits that turn Cache Components off in a config, none when it is off, or why it cannot be changed. */
43198
- function cacheComponentsPlan(source, file) {
43199
- const object = cacheComponentsObject(source, file);
43200
- if (typeof object === "string") return { reason: object };
43201
- const value = enabledValue(object);
43202
- if (typeof value === "string") return { reason: value };
43203
- if (!value) return {
43253
+ /** The settings that make a cache of the app's own: a handler for either cache. */
43254
+ const OWN_CACHE_SETTINGS = ["cacheHandler", "cacheHandlers"];
43255
+ /** Whether the config gives `name` a value: written, and not `undefined`. */
43256
+ function setsValue(object, name) {
43257
+ const value = propertyValue(object, name);
43258
+ return value !== void 0 && !isNamed(value, "undefined");
43259
+ }
43260
+ /**
43261
+ * Why Nextly's handlers cannot go into this config as it stands, or null when
43262
+ * they can. A setting written as `undefined` is a placeholder to fill, not a
43263
+ * value; a handler of the app's own is not replaced; and `cacheMaxMemorySize`
43264
+ * must be 0, so Next.js keeps no second copy of the handlers' entries.
43265
+ */
43266
+ function handlerConflict(object) {
43267
+ const placeholders = HANDLER_SETTINGS.filter((name) => propertyValue(object, name) !== void 0 && !setsValue(object, name));
43268
+ if (placeholders.length > 0) return `it sets ${placeholders.join(", ")} to undefined`;
43269
+ const own = OWN_CACHE_SETTINGS.filter((name) => setsValue(object, name));
43270
+ if (own.length > 0) return `it has cache handlers of its own (${own.join(", ")}), which were kept`;
43271
+ const memory = propertyValue(object, "cacheMaxMemorySize");
43272
+ return memory && !(memory.type === "NumericLiteral" && memory.value === 0) ? "it sets cacheMaxMemorySize to a value other than 0, which Nextly's handlers need so that Next.js keeps no second copy of their entries" : null;
43273
+ }
43274
+ /**
43275
+ * How to install Nextly's cache handlers: the properties to add and any edit
43276
+ * that gives the file `require`, or why they are left out, with the lines to
43277
+ * put in place of the config's own settings.
43278
+ */
43279
+ function handlerStep(source, object, body, esm) {
43280
+ const none = {
43281
+ add: [],
43282
+ edits: []
43283
+ };
43284
+ if (hasNextlysHandlers(object)) return {
43285
+ ...none,
43286
+ outcome: "present"
43287
+ };
43288
+ const setup = esm ? requireSetupFor(body) : null;
43289
+ const conflict = esm && declaresRequireAfter(body, object.start ?? 0) ? "it declares require after its config object, so the handlers' require.resolve would run before require exists; move that declaration above the object" : handlerConflict(object);
43290
+ if (conflict) return {
43291
+ ...none,
43292
+ outcome: {
43293
+ skipped: conflict,
43294
+ lines: HANDLER_PROPERTIES,
43295
+ requireSetup: setup
43296
+ }
43297
+ };
43298
+ const memorySet = propertyValue(object, "cacheMaxMemorySize") !== void 0;
43299
+ const add = HANDLER_PROPERTIES.filter((property) => !(memorySet && property.startsWith("cacheMaxMemorySize")));
43300
+ if (setup === null) return {
43301
+ add,
43204
43302
  edits: [],
43205
- removed: []
43303
+ outcome: "added"
43206
43304
  };
43207
- const flip = {
43208
- at: value.start ?? 0,
43209
- remove: (value.end ?? 0) - (value.start ?? 0),
43210
- text: "false"
43305
+ if (referenceCount("require", body) > 0 || createRequireImport(body) === null && referenceCount("createRequire", body) > 0) return {
43306
+ ...none,
43307
+ outcome: {
43308
+ skipped: "it is an ES module that uses require, or a createRequire not imported from node:module, without binding require, so the lines that define it could clash",
43309
+ lines: add,
43310
+ requireSetup: setup
43311
+ }
43312
+ };
43313
+ return {
43314
+ add,
43315
+ edits: [requireEdit(source, body, setup)],
43316
+ outcome: "added"
43211
43317
  };
43212
- const prefetching = object.properties.filter((property) => isPropertyNamed(property, PARTIAL_PREFETCHING));
43318
+ }
43319
+ /** The edits that turn Cache Components on in a config, or why it cannot be changed. */
43320
+ function cacheComponentsPlan(source, file, esm) {
43321
+ const body = parseBody(source, file);
43322
+ if (!body) return { reason: "it could not be parsed" };
43323
+ const object = cacheComponentsObject(body);
43324
+ if (typeof object === "string") return { reason: object };
43325
+ const options = optionsPlan(object);
43326
+ if ("reason" in options) return options;
43327
+ const handlers = handlerStep(source, object, body, esm);
43328
+ const add = [...options.add, ...handlers.add];
43329
+ const edits = [...options.flips, ...handlers.edits];
43330
+ if (add.length > 0) edits.push(...appendProperties(source, object, () => add));
43213
43331
  return {
43214
- edits: [flip, ...prefetching.map((property) => removeProperty(source, property))],
43215
- removed: prefetching.length > 0 ? [PARTIAL_PREFETCHING] : []
43332
+ edits,
43333
+ turnedOn: options.turnedOn,
43334
+ handlers: handlers.outcome
43216
43335
  };
43217
43336
  }
43218
- /** Whether a config, read as code, still defines `partialPrefetching` anywhere in its exported object. */
43219
- function definesPartialPrefetching(source, file) {
43220
- const object = cacheComponentsObject(source, file);
43221
- return typeof object !== "string" && object.properties.some((property) => isPropertyNamed(property, PARTIAL_PREFETCHING));
43337
+ /** How to turn both options on: the values to flip, the properties to add, and which options change. */
43338
+ function optionsPlan(object) {
43339
+ const plan = {
43340
+ flips: [],
43341
+ add: [],
43342
+ turnedOn: []
43343
+ };
43344
+ for (const option of ON_OPTIONS) {
43345
+ const step = optionStep(object, option);
43346
+ if (typeof step === "string") return { reason: step };
43347
+ if (step.flip) plan.flips.push(step.flip);
43348
+ if (step.add) plan.add.push(step.add);
43349
+ if (step.flip || step.add) plan.turnedOn.push(option);
43350
+ }
43351
+ return plan;
43222
43352
  }
43223
43353
  /**
43224
- * Turn Next.js Cache Components off in an existing app's config, which
43225
- * Nextly does not support yet; `create-next-app` 16.4 turns it on by default.
43354
+ * Turn Next.js Cache Components and Partial Prefetching on in an existing
43355
+ * app's config, with Nextly's cache handlers: Nextly runs with both on.
43356
+ * Apps that an earlier Nextly release turned off are turned on again.
43226
43357
  *
43227
- * Only `cacheComponents: true` written in place is changed, and
43228
- * `partialPrefetching` goes with it, since Next.js refuses that option once
43229
- * Cache Components is off. The edit is trusted only once the result parses
43230
- * and reads as off; anything else is left as it was, with what to change.
43358
+ * Only settings written in place as `true` or `false`, or absent, are
43359
+ * changed; a value this cannot read leaves the file as it was, with what to
43360
+ * add. The edit is trusted only once the result parses and reads as on.
43231
43361
  */
43232
- async function turnOffCacheComponents(cwd) {
43233
- const file = await findConfigFile(cwd);
43234
- if (!file) return { kind: "unchanged" };
43235
- const source = await import_lib$1.default.readFile(path.default.join(cwd, file), "utf-8");
43236
- const plan = cacheComponentsPlan(source, file);
43362
+ async function turnOnCacheComponents(cwd) {
43363
+ const file = await nextConfigFileToChange(cwd);
43364
+ const source = await import_lib$1.default.readFile(path.default.join(cwd, file), "utf-8").catch(() => null);
43365
+ if (source === null) return {
43366
+ kind: "manual",
43367
+ file,
43368
+ reason: "the app has no Next config",
43369
+ requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
43370
+ };
43371
+ const body = parseBody(source, file);
43372
+ if (!body) return {
43373
+ kind: "manual",
43374
+ file,
43375
+ reason: "it could not be parsed",
43376
+ requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
43377
+ };
43378
+ const esm = loadsAsModule(file, body);
43379
+ const requireSetup = esm ? requireSetupFor(body) : null;
43380
+ const plan = cacheComponentsPlan(source, file, esm);
43237
43381
  if ("reason" in plan) return {
43238
43382
  kind: "manual",
43239
43383
  file,
43240
- reason: plan.reason
43384
+ reason: plan.reason,
43385
+ requireSetup
43386
+ };
43387
+ if (plan.edits.length === 0) return {
43388
+ kind: "unchanged",
43389
+ file,
43390
+ handlers: plan.handlers
43241
43391
  };
43242
- if (plan.edits.length === 0) return { kind: "unchanged" };
43243
43392
  const text = applyEdits(source, plan.edits);
43244
- const recheck = cacheComponentsPlan(text, file);
43245
- if ("reason" in recheck || recheck.edits.length > 0 || definesPartialPrefetching(text, file)) return {
43393
+ const recheck = cacheComponentsPlan(text, file, esm);
43394
+ if ("reason" in recheck || recheck.edits.length > 0) return {
43246
43395
  kind: "manual",
43247
43396
  file,
43248
- reason: "the edit could not be confirmed"
43397
+ reason: "the edit could not be confirmed",
43398
+ requireSetup
43249
43399
  };
43250
43400
  await import_lib$1.default.writeFile(path.default.join(cwd, file), text, "utf-8");
43251
43401
  return {
43252
- kind: "turned-off",
43402
+ kind: "turned-on",
43253
43403
  file,
43254
- removed: plan.removed
43404
+ turnedOn: plan.turnedOn,
43405
+ handlers: plan.handlers
43255
43406
  };
43256
43407
  }
43257
43408
  /**
43258
- * What turning Cache Components off did, for the run's report: the change and
43259
- * a notice saying why, or, for a config it could not change, what to change.
43409
+ * What turning Cache Components on did, for the run's report: each change and
43410
+ * a notice saying why, or, for a config it could not change, the exact lines
43411
+ * to add.
43260
43412
  */
43261
43413
  function cacheComponentsFiles(patch) {
43262
43414
  const files = {
@@ -43264,43 +43416,42 @@ function cacheComponentsFiles(patch) {
43264
43416
  changed: [],
43265
43417
  skipped: []
43266
43418
  };
43267
- if (patch.kind === "turned-off") {
43419
+ if (patch.kind === "manual") {
43420
+ files.skipped.push({
43421
+ path: patch.file,
43422
+ why: `Cache Components was not turned on: ${patch.reason}. Nextly runs with Cache Components and Partial Prefetching on, with its cache handlers (${CACHE_COMPONENTS_DOCS_URL}); add these to the config object Next.js receives, in place of any value of theirs`,
43423
+ toAdd: linesToAdd$1(ON_LINES, patch.requireSetup)
43424
+ });
43425
+ return files;
43426
+ }
43427
+ const handlersAdded = patch.handlers === "added";
43428
+ if (patch.kind === "turned-on") {
43268
43429
  files.changed.push({
43269
43430
  path: patch.file,
43270
- added: [`${CACHE_COMPONENTS}: false`]
43431
+ added: [...patch.turnedOn.map((option) => `${option}: true`), ...handlersAdded ? [...HANDLER_SETTINGS] : []]
43271
43432
  });
43272
- const removed = patch.removed.length ? `, and removed ${patch.removed.join(", ")}, which Next.js accepts only with it on` : "";
43273
- files.notices = [`Turned Cache Components off in ${patch.file}: Nextly does not support it yet${removed}. Why, and the plan to support it: ${CACHE_COMPONENTS_DOCS_URL}`];
43274
- } else if (patch.kind === "manual") files.skipped.push({
43433
+ const done = [...patch.turnedOn.length > 0 ? [`turned ${patch.turnedOn.join(" and ")} on`] : [], ...handlersAdded ? ["installed Nextly's cache handlers"] : []].join(" and ");
43434
+ files.notices = [`In ${patch.file}, ${done}: Nextly runs with Cache Components and Partial Prefetching on. What each setting does: ${CACHE_COMPONENTS_DOCS_URL}`];
43435
+ }
43436
+ if (typeof patch.handlers === "object") files.skipped.push({
43275
43437
  path: patch.file,
43276
- why: `Nextly could not confirm Cache Components is off: ${patch.reason}. Nextly does not support it yet (${CACHE_COMPONENTS_DOCS_URL}); make sure the config Next.js receives leaves it unset or sets it to false, with no ${PARTIAL_PREFETCHING}`,
43277
- toAdd: ` ${CACHE_COMPONENTS}: false,`
43438
+ why: `Nextly's cache handlers were not added: ${patch.handlers.skipped}. Without them a page read right after a write can show the previous version once, and in production every cached public read refreshes within a minute of a visit, since a write from another instance, the CLI or a job reaches no other cache (${CACHE_COMPONENTS_DOCS_URL}); to use them, put these in place of the config's own cache settings`,
43439
+ toAdd: linesToAdd$1(patch.handlers.lines.map((property) => ` ${property},`), patch.handlers.requireSetup)
43278
43440
  });
43279
43441
  return files;
43280
43442
  }
43281
43443
  /**
43282
- * What to add by hand, in the spelling the app's Next.js reads.
43283
- *
43284
- * On Next.js 14 the package list and the instrumentation opt-in are keys of
43285
- * one `experimental` object, so they are given as one: two `experimental`
43286
- * properties, pasted as given, leave only the second. The instrumentation
43287
- * hook Nextly starts from needs the opt-in there, and no other step says so.
43288
- * `wrap` turns each spelling into what to paste: the key itself, or a whole
43289
- * config file when the app has none.
43290
- */
43291
- function optionText(packages, spelling, wrap = (option) => option) {
43292
- const list = packages.map((name) => JSON.stringify(name)).join(", ");
43293
- const current = wrap(` ${PROPERTY}: [${list}],`);
43294
- const legacy = wrap(` experimental: { instrumentationHook: true, serverComponentsExternalPackages: [${list}] },`);
43295
- if (spelling === "legacy") return legacy;
43296
- return spelling === "either" ? `On Next.js 15 or later:\n\n${current}\n\nOn Next.js 14:\n\n${legacy}` : current;
43297
- }
43298
- /**
43299
- * A whole config holding `option`, for an app that has none. ESM in `.mjs`,
43300
- * which loads whatever the package's own "type" says.
43444
+ * The config lines to add by hand. An ES module config with no `require`
43445
+ * also gets the lines that define it for the handlers, at the top of the file.
43301
43446
  */
43302
- function wholeConfig(option) {
43303
- return `/** @type {import('next').NextConfig} */\nexport default {\n${option}\n};\n`;
43447
+ function linesToAdd$1(lines, requireSetup) {
43448
+ const config = lines.join("\n");
43449
+ return requireSetup ? `// At the top of the file:\n${requireSetup.replace("\n\n", "\n")}\n\n// In the config object:\n${config}` : config;
43450
+ }
43451
+ /** What to add by hand: the list, as the config's top-level option. */
43452
+ function optionText(packages) {
43453
+ const list = packages.map((name) => JSON.stringify(name)).join(", ");
43454
+ return ` ${PROPERTY}: [${list}],`;
43304
43455
  }
43305
43456
  /**
43306
43457
  * What a patch did, in the shape every existing-app step reports in: the
@@ -43324,109 +43475,14 @@ function nextConfigFiles(patch) {
43324
43475
  files.written.push(patch.file);
43325
43476
  break;
43326
43477
  case "unchanged": break;
43327
- case "manual": files.skipped.push(patch.create ? {
43478
+ case "manual": files.skipped.push({
43328
43479
  path: patch.file,
43329
- why: `Nextly's packages were not added: ${patch.reason}, and the app has no Next.js config. Create ${patch.file} with:`,
43330
- toAdd: optionText(patch.packages, patch.spelling, wholeConfig)
43331
- } : {
43332
- path: patch.file,
43333
- why: `Nextly's packages were not added: ${patch.reason}. Add them to the config object it exports, keeping any packages and experimental keys already there`,
43334
- toAdd: optionText(patch.packages, patch.spelling)
43480
+ why: patch.reason === LEGACY_LIST_REASON ? `Nextly's packages were not added: ${patch.reason}. Move the packages under experimental.${LEGACY_PROPERTY} into a top-level ${PROPERTY} list, add Nextly's to it, and delete experimental.${LEGACY_PROPERTY}, keeping the config's other experimental keys` : `Nextly's packages were not added: ${patch.reason}. Add them to the config object it exports, keeping any packages and experimental keys already there`,
43481
+ toAdd: optionText(patch.packages)
43335
43482
  });
43336
43483
  }
43337
43484
  return files;
43338
43485
  }
43339
- // src/generators/page-extensions.ts
43340
- /**
43341
- * The page extension to name a new module file with, a hook or a route: a
43342
- * TypeScript one first, since Nextly's files are TypeScript, then a
43343
- * JavaScript one; never a content extension such as `mdx`, which Next.js
43344
- * would compile as content, and never `cjs`, since the files are ES modules
43345
- * and a `.cjs` file cannot hold one. Null when the list holds no extension a
43346
- * module can be written in.
43347
- */
43348
- function moduleExtension(extensions) {
43349
- const usable = writable(extensions);
43350
- return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
43351
- }
43352
- /**
43353
- * The extensions a file with code in it can be written under: not a
43354
- * declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
43355
- * types only and refuses code in.
43356
- */
43357
- function writable(extensions) {
43358
- return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
43359
- }
43360
- /**
43361
- * The `pageExtensions` the app's Next.js config sets, as every existing-app
43362
- * step that names a file Next.js routes reads it: the list; `"absent"` when
43363
- * the config sets none, or there is no config; or null when it is set in a
43364
- * way that cannot be read. Read from the config the app exports, as the Next
43365
- * config step reads it, so a commented-out or nested `pageExtensions` sets
43366
- * nothing. `configName` is the file it was read from, for the report.
43367
- */
43368
- async function appPageExtensions(cwd) {
43369
- const nextConfig = await readNextConfig(cwd);
43370
- return {
43371
- configName: nextConfig?.name ?? "The Next.js config",
43372
- extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
43373
- };
43374
- }
43375
- /**
43376
- * The text of the app's Next.js config, or null when it has none, from the
43377
- * file the Next config step reads and extends ({@link findConfigFile}).
43378
- */
43379
- async function readNextConfig(cwd) {
43380
- const name = await findConfigFile(cwd);
43381
- return name === null ? null : {
43382
- name,
43383
- text: await import_lib$1.default.readFile(path.default.join(cwd, name), "utf-8")
43384
- };
43385
- }
43386
- /**
43387
- * The page extension for a file of TypeScript with JSX, such as a page or a
43388
- * layout: one ending in `tsx`. Null when the list holds none.
43389
- */
43390
- function tsxExtension(extensions) {
43391
- return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
43392
- }
43393
- /** A JavaScript or TypeScript module without JSX, such as a route. */
43394
- const MODULE_FILE = {
43395
- pick: moduleExtension,
43396
- holds: "a JavaScript or TypeScript extension"
43397
- };
43398
- /** TypeScript with JSX, such as a page or a layout. */
43399
- const TSX_FILE = {
43400
- pick: tsxExtension,
43401
- holds: "an extension ending in tsx"
43402
- };
43403
- /**
43404
- * The report for a file an app that sets `pageExtensions` gets in place of
43405
- * the file: the name to create it under and its content. Next.js routes a
43406
- * file there only under a name with one of the app's extensions, and the app
43407
- * may already have the file under such a name, which the existing-app steps
43408
- * do not look for, so nothing is written. A list that cannot be read, or that
43409
- * holds no extension the content needs, gives a placeholder name and says
43410
- * which.
43411
- *
43412
- * @param base the file's path without its extension, as reported, such as
43413
- * `app/admin/[[...params]]/page`
43414
- */
43415
- function pageExtensionsSkip(base, text, pages, need) {
43416
- const { configName, extensions } = pages;
43417
- const extension = extensions === null ? null : need.pick(extensions);
43418
- const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
43419
- if (extensions !== null && extension === null) return {
43420
- path: `${base}.<${need.holds} added to pageExtensions>`,
43421
- why: `${sets}, and none of them (${extensions.join(", ")}) is ${need.holds}; add one to pageExtensions, then, unless the app already has this file, create it under that name with this content`,
43422
- toAdd: text
43423
- };
43424
- return {
43425
- path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
43426
- why: `${sets}; unless the app already has this file, create it under that name with this content`,
43427
- toAdd: text
43428
- };
43429
- }
43430
43486
  // src/utils/detect-pm-from-user-agent.ts
43431
43487
  /**
43432
43488
  * Detect the package manager that invoked `create-nextly-app` by reading
@@ -45657,6 +45713,45 @@ function getDefaultApproach(name) {
45657
45713
  function shouldUseBundledTemplate(projectType, options) {
45658
45714
  return BUNDLED_TEMPLATES.some((name) => name === projectType) && !options.localTemplatePath && !options.useYalc && options.branch === void 0;
45659
45715
  }
45716
+ // src/supported-versions.ts
45717
+ /**
45718
+ * The Next.js and React this release of Nextly supports, in one place.
45719
+ *
45720
+ * Two facts, deliberately kept apart:
45721
+ *
45722
+ * - `REVIEWED_VERSIONS` is the newest release of each package this Nextly
45723
+ * release was reviewed against. A new app declares it as a `~` range
45724
+ * ({@link scaffoldRange}): patch releases of that minor, never a newer minor
45725
+ * or major that nobody has checked, and never whatever npm's `latest` names
45726
+ * on the day of the scaffold.
45727
+ * - `SUPPORTED_RANGES` is every release Nextly works with: the `nextly`
45728
+ * package's own peer ranges, inlined at build, so they are declared once.
45729
+ * Every other package declares the same as its peers. Adding Nextly to an
45730
+ * existing app refuses one whose Next.js, React or React DOM falls outside
45731
+ * them, and a scaffolded plugin declares them as its own peers.
45732
+ *
45733
+ * Only a release review moves either: a newer Next.js or React is supported
45734
+ * once a Nextly release has been checked against it, not before.
45735
+ */
45736
+ const REVIEWED_VERSIONS = {
45737
+ next: "16.4.2",
45738
+ "eslint-config-next": "16.4.2",
45739
+ react: "19.3.1",
45740
+ "react-dom": "19.3.1"
45741
+ };
45742
+ const SUPPORTED_RANGES = {
45743
+ "next": "^16.4.1",
45744
+ "react": "^19.3.0",
45745
+ "react-dom": "^19.3.0"
45746
+ };
45747
+ /**
45748
+ * The range a new project declares for `pkg`: patch releases of the reviewed
45749
+ * minor only. Derived here rather than written out, so the reviewed version is
45750
+ * the one thing a release review edits.
45751
+ */
45752
+ function scaffoldRange(pkg) {
45753
+ return `~${REVIEWED_VERSIONS[pkg]}`;
45754
+ }
45660
45755
  // src/utils/package-manager-commands.ts
45661
45756
  /**
45662
45757
  * The prefix that runs a package.json script.
@@ -45684,37 +45779,35 @@ function binaryRunner(packageManager) {
45684
45779
  case "bun": return "bunx";
45685
45780
  }
45686
45781
  }
45687
- // src/utils/plugin-node.ts
45688
- /**
45689
- * The Node versions a scaffolded plugin supports, and the check that refuses
45690
- * to scaffold one on a Node it cannot build on.
45691
- *
45692
- * A plugin builds with tsdown and tests with Vitest, and each declares the
45693
- * Node it runs on; the plugin's floor is where both agree. The repository's
45694
- * packages have a lower floor, because only a plugin author runs these tools.
45695
- * `scaffold-plugin.test.ts` derives this range from the installed tools' own
45696
- * `engines`, so a version bump that moves either one fails there.
45697
- */
45698
- const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
45782
+ // src/utils/version-range.ts
45699
45783
  function parseVersion$1(text) {
45700
- const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text.trim());
45701
- if (!match) throw new Error(`not a Node version: ${text}`);
45702
- return [
45784
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/.exec(text.trim());
45785
+ if (!match) throw new Error(`not a version: ${text}`);
45786
+ return Object.assign([
45703
45787
  Number(match[1]),
45704
45788
  Number(match[2]),
45705
45789
  Number(match[3])
45706
- ];
45790
+ ], { prerelease: match[4] !== void 0 });
45707
45791
  }
45792
+ /** Older first; of two with the same numbers, a prerelease comes before its release, as in semver. */
45708
45793
  function compare(a, b) {
45709
- return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
45794
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || Number(Boolean(b.prerelease)) - Number(Boolean(a.prerelease));
45710
45795
  }
45711
45796
  /**
45712
- * Whether `version` satisfies one comparator. Only the two forms these tools
45713
- * write are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
45714
- * than read approximately, so a tool that starts declaring another form fails
45797
+ * Negative when `a` is older than `b`, positive when newer, 0 when equal.
45798
+ * A prerelease is older than its release: `16.4.1-canary.1` is below `16.4.1`.
45799
+ */
45800
+ function compareVersions(a, b) {
45801
+ return compare(parseVersion$1(a), parseVersion$1(b));
45802
+ }
45803
+ /**
45804
+ * Whether `version` satisfies one comparator. Only the two forms this CLI
45805
+ * writes are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
45806
+ * than read approximately, so a range that starts using another form fails
45715
45807
  * the test that reads it.
45716
45808
  */
45717
45809
  function satisfiesComparator(version, comparator) {
45810
+ if (version.prerelease) return false;
45718
45811
  const caret = /^\^(\d+\.\d+\.\d+)$/.exec(comparator);
45719
45812
  if (caret) {
45720
45813
  const floor = parseVersion$1(caret[1]);
@@ -45722,21 +45815,83 @@ function satisfiesComparator(version, comparator) {
45722
45815
  }
45723
45816
  const atLeast = /^>=(\d+\.\d+\.\d+)$/.exec(comparator);
45724
45817
  if (atLeast) return compare(version, parseVersion$1(atLeast[1])) >= 0;
45725
- throw new Error(`unsupported Node range comparator: ${comparator}`);
45818
+ throw new Error(`unsupported range comparator: ${comparator}`);
45726
45819
  }
45727
45820
  /** Whether `version` satisfies a `||` list of `^X.Y.Z` / `>=X.Y.Z` comparators. */
45728
- function satisfiesNodeRange(version, range) {
45821
+ function satisfiesRange(version, range) {
45729
45822
  const parsed = parseVersion$1(version);
45730
45823
  return range.split("||").map((part) => part.trim()).some((comparator) => satisfiesComparator(parsed, comparator));
45731
45824
  }
45732
45825
  /**
45733
- * Why the plugin template cannot be scaffolded on `nodeVersion`, or
45734
- * `undefined` when it can. Checked before anything is written, since a plugin
45735
- * scaffolded on an older Node would install and then fail its first build.
45826
+ * The lowest version a package.json dependency spec admits, as `X.Y.Z`, or
45827
+ * `undefined` when the spec names no lower bound this can read: a dist-tag
45828
+ * (`latest`, `canary`), a protocol (`workspace:*`, `file:`, a URL), `*`, or an
45829
+ * upper bound alone (`<17`). An exact version is its own lowest; `^`, `~`, `=`
45830
+ * and `>=` start at the version they name; a missing or wildcard minor or
45831
+ * patch (`15`, `15.x`) counts as 0; across `||` the lowest alternative wins.
45736
45832
  */
45737
- function pluginNodeRefusal(nodeVersion) {
45738
- if (satisfiesNodeRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
45739
- return `The plugin template builds with tools that need Node ${PLUGIN_NODE_ENGINES}, and this is Node ${nodeVersion}. Install a supported Node (for example \`nvm install 24\`) and run create-nextly-app again.`;
45833
+ function lowestVersionIn(spec) {
45834
+ let lowest;
45835
+ for (const part of spec.split("||")) {
45836
+ const match = /^\s*(?:\^|~|>=|=)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?/.exec(part);
45837
+ if (!match) continue;
45838
+ const number = (text) => text === void 0 || !/^\d+$/.test(text) ? 0 : Number(text);
45839
+ const found = [
45840
+ Number(match[1]),
45841
+ number(match[2]),
45842
+ number(match[3])
45843
+ ];
45844
+ if (!lowest || compare(found, lowest) < 0) lowest = found;
45845
+ }
45846
+ return lowest ? lowest.join(".") : void 0;
45847
+ }
45848
+ /**
45849
+ * The highest major a package.json dependency spec admits, `Infinity` when it
45850
+ * has no upper bound (`>=16.4.1`, `*`, `x`), or `undefined` when no
45851
+ * alternative names one this can read (a dist-tag, a protocol). Across `||`
45852
+ * the highest alternative wins, since an install resolves the newest version
45853
+ * any of them admits. Within one alternative an upper bound (`<17`, `<=17.2`)
45854
+ * decides; otherwise the version a `^`, `~`, `=` or bare spec names.
45855
+ */
45856
+ function highestMajorIn(spec) {
45857
+ let highest;
45858
+ for (const part of spec.split("||")) {
45859
+ const found = majorReachedBy(part.trim());
45860
+ if (found !== void 0 && (highest === void 0 || found > highest)) highest = found;
45861
+ }
45862
+ return highest;
45863
+ }
45864
+ /** The highest major one `||` alternative reaches, as `highestMajorIn` reads it. */
45865
+ function majorReachedBy(part) {
45866
+ const upper = /<(=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(part);
45867
+ if (upper) {
45868
+ const [, inclusive, major, minor, patch] = upper;
45869
+ return inclusive !== void 0 || Number(minor ?? 0) > 0 || Number(patch ?? 0) > 0 ? Number(major) : Number(major) - 1;
45870
+ }
45871
+ if (part === "" || /^(\*|[xX])$/.test(part) || part.startsWith(">")) return Infinity;
45872
+ const named = /^(?:\^|~|=)?\s*v?(\d+)/.exec(part);
45873
+ return named ? Number(named[1]) : void 0;
45874
+ }
45875
+ // src/utils/plugin-node.ts
45876
+ /**
45877
+ * The Node versions a scaffolded plugin supports, and the check that refuses
45878
+ * to scaffold one on a Node it cannot build on.
45879
+ *
45880
+ * A plugin builds with tsdown and tests with Vitest, and each declares the
45881
+ * Node it runs on; the plugin's floor is where both agree. The repository's
45882
+ * packages have a lower floor, because only a plugin author runs these tools.
45883
+ * `scaffold-plugin.test.ts` derives this range from the installed tools' own
45884
+ * `engines`, so a version bump that moves either one fails there.
45885
+ */
45886
+ const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
45887
+ /**
45888
+ * Why the plugin template cannot be scaffolded on `nodeVersion`, or
45889
+ * `undefined` when it can. Checked before anything is written, since a plugin
45890
+ * scaffolded on an older Node would install and then fail its first build.
45891
+ */
45892
+ function pluginNodeRefusal(nodeVersion) {
45893
+ if (satisfiesRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
45894
+ return `The plugin template builds with tools that need Node ${PLUGIN_NODE_ENGINES}, and this is Node ${nodeVersion}. Install a supported Node (for example \`nvm install 24\`) and run create-nextly-app again.`;
45740
45895
  }
45741
45896
  // src/utils/template.ts
45742
45897
  /**
@@ -45831,41 +45986,56 @@ const SEARCH_INDEX_SCRIPT = path.default.join("scripts", "build-search-index.mjs
45831
45986
  async function projectHasSearchIndexScript(targetDir) {
45832
45987
  return import_lib$1.default.pathExists(path.default.join(targetDir, SEARCH_INDEX_SCRIPT));
45833
45988
  }
45834
- /**
45835
- * The plugins a Nextly config registers, by package name: each binding it
45836
- * imports from a `@nextlyhq/plugin-*` package (the SDK aside) that a
45837
- * `plugins: [...]` list in it names, however deep in an element. Read with
45838
- * the parser the Next config step uses, so nested brackets, either quote and
45839
- * default or aliased imports are read as the compiler reads them. First-party
45840
- * plugins register under their package name, which is what
45841
- * `nextly plugins install` takes. Throws for a config that does not parse,
45842
- * since every template's must.
45843
- *
45844
- * Exported for the test that holds every template's config to its build.
45845
- */
45846
- function configuredPlugins(configText, file = "nextly.config.ts") {
45847
- let program;
45989
+ /** Parses a config, naming the file when it does not parse. */
45990
+ function parseConfigProgram(configText, file) {
45848
45991
  try {
45849
- program = (0, import_lib.parse)(configText, {
45992
+ return (0, import_lib.parse)(configText, {
45850
45993
  sourceType: "module",
45851
45994
  plugins: ["typescript", "jsx"]
45852
45995
  }).program;
45853
45996
  } catch (error) {
45854
45997
  throw new Error(`${file} cannot be parsed (${error.message}), so the plugins its build must install cannot be read.`);
45855
45998
  }
45856
- const fromPackage = /* @__PURE__ */ new Map();
45999
+ }
46000
+ /** Each binding imported from a module `matches` accepts, to that module's name. */
46001
+ function bindingsImportedFrom(program, matches) {
46002
+ const bindings = /* @__PURE__ */ new Map();
45857
46003
  for (const statement of program.body) {
45858
46004
  if (statement.type !== "ImportDeclaration") continue;
45859
- const pkg = statement.source.value;
45860
- if (!pkg.startsWith("@nextlyhq/plugin-") || pkg === "@nextlyhq/plugin-sdk") continue;
45861
- for (const specifier of statement.specifiers) fromPackage.set(specifier.local.name, pkg);
46005
+ if (!matches(statement.source.value)) continue;
46006
+ for (const specifier of statement.specifiers) bindings.set(specifier.local.name, statement.source.value);
45862
46007
  }
45863
- const found = [];
46008
+ return bindings;
46009
+ }
46010
+ const isPluginPackage = (source) => source.startsWith("@nextlyhq/plugin-") && source !== "@nextlyhq/plugin-sdk";
46011
+ /** What a config's `plugins` lists hold: the packages, and whether a binding from its own source is among them. */
46012
+ function readPluginLists(configText, file) {
46013
+ const program = parseConfigProgram(configText, file);
46014
+ const fromPackage = bindingsImportedFrom(program, isPluginPackage);
46015
+ const packages = /* @__PURE__ */ new Set();
45864
46016
  for (const name of identifiersInPluginLists(program, file)) {
45865
46017
  const pkg = fromPackage.get(name);
45866
- if (pkg !== void 0 && !found.includes(pkg)) found.push(pkg);
46018
+ if (pkg !== void 0) packages.add(pkg);
45867
46019
  }
45868
- return found;
46020
+ const local = bindingsImportedFrom(program, (source) => source.startsWith("."));
46021
+ const ownPlugin = namesInPluginEntries(program, file).some((name) => local.has(name) && !fromPackage.has(name));
46022
+ return {
46023
+ packages: [...packages],
46024
+ ownPlugin
46025
+ };
46026
+ }
46027
+ /**
46028
+ * The `nextly plugins install` steps a project's build runs, read from its
46029
+ * config. A plugin package is installed by name, and the build says which. A
46030
+ * plugin the site wrote itself has no package name to give, and a production
46031
+ * boot refuses it like any other when nothing has installed it, so a config
46032
+ * that lists one installs with `--all`, which reads the names from the config.
46033
+ * Exported for the test that holds every template's config to its build.
46034
+ */
46035
+ function pluginInstallSteps(configText, file = "nextly.config.ts") {
46036
+ const { packages, ownPlugin } = readPluginLists(configText, file);
46037
+ if (ownPlugin) return ["nextly plugins install --all"];
46038
+ return packages.map((pkg) => `nextly plugins install ${pkg}`);
45869
46039
  }
45870
46040
  /** Every identifier inside the value of each `plugins: [...]` property, in source order. */
45871
46041
  function identifiersInPluginLists(program, file) {
@@ -45879,6 +46049,20 @@ function identifiersInPluginLists(program, file) {
45879
46049
  return names;
45880
46050
  }
45881
46051
  /**
46052
+ * The binding each entry of a `plugins: [...]` list is, as written: `seo` for
46053
+ * `seo` or `seo({ ... })`. Narrower than {@link identifiersInPluginLists},
46054
+ * which also sees the identifiers inside an entry's options.
46055
+ */
46056
+ function namesInPluginEntries(program, file) {
46057
+ const names = [];
46058
+ walkNodes(program, (node) => {
46059
+ if (!isPluginList(node, file) || node.value.type !== "ArrayExpression") return;
46060
+ for (const element of node.value.elements) if (element?.type === "Identifier") names.push(element.name);
46061
+ else if (element?.type === "CallExpression" && element.callee.type === "Identifier") names.push(element.callee.name);
46062
+ });
46063
+ return names;
46064
+ }
46065
+ /**
45882
46066
  * A `plugins: [...]` property, by an identifier or a string key. A `plugins`
45883
46067
  * property written any other way, such as a variable holding the list, a call
45884
46068
  * or a spread inside it, is
@@ -45910,16 +46094,15 @@ function isNode(value) {
45910
46094
  return typeof value === "object" && value !== null && typeof value.type === "string";
45911
46095
  }
45912
46096
  /**
45913
- * The plugins the scaffolded PROJECT's config registers, read from the
45914
- * finished project as {@link projectHasSearchIndexScript} is: a template
45915
- * offers more than one config (the blog's visual one registers none), so
45916
- * only the file the copy wrote can say. None when there is no config, or
45917
- * no project on disk to ask.
46097
+ * The plugin install steps the scaffolded PROJECT's build needs, read from
46098
+ * the finished project as {@link projectHasSearchIndexScript} is: a template
46099
+ * offers more than one config, so only the file the copy wrote can say. None
46100
+ * when there is no config, or no project on disk to ask.
45918
46101
  */
45919
- async function projectPlugins(targetDir) {
46102
+ async function projectPluginInstalls(targetDir) {
45920
46103
  if (targetDir === void 0) return [];
45921
46104
  const config = path.default.join(targetDir, "nextly.config.ts");
45922
- return await import_lib$1.default.pathExists(config) ? configuredPlugins(await import_lib$1.default.readFile(config, "utf-8"), config) : [];
46105
+ return await import_lib$1.default.pathExists(config) ? pluginInstallSteps(await import_lib$1.default.readFile(config, "utf-8"), config) : [];
45923
46106
  }
45924
46107
  /**
45925
46108
  * Every file under `root` that could carry an import specifier.
@@ -46380,14 +46563,12 @@ async function replacePlaceholders(dir, placeholders, skip = /* @__PURE__ */ new
46380
46563
  /**
46381
46564
  * Pinned dependency versions for generated projects.
46382
46565
  *
46383
- * `next` and `eslint-config-next` are resolved at runtime from the npm
46384
- * registry so that fresh projects always get the latest release without
46385
- * needing to republish create-nextly-app. The remaining versions use
46386
- * wide semver ranges that rarely need updating.
46566
+ * `next`, `eslint-config-next`, `react` and `react-dom` are not here: they
46567
+ * come from `supported-versions`, the reviewed release of each, so a project
46568
+ * never declares a Next.js or React no Nextly release was checked against.
46569
+ * The remaining versions use wide semver ranges that rarely need updating.
46387
46570
  */
46388
46571
  const PINNED_VERSIONS = {
46389
- react: "^19.3.1",
46390
- "react-dom": "^19.3.1",
46391
46572
  typescript: "^5",
46392
46573
  "@types/node": "^22",
46393
46574
  "@types/react": "^19",
@@ -46405,11 +46586,6 @@ const PINNED_VERSIONS = {
46405
46586
  */
46406
46587
  const CONFIG_TOOLING_DEV_DEPENDENCIES = [`esbuild@${PINNED_VERSIONS.esbuild}`];
46407
46588
  /**
46408
- * Packages whose latest version is fetched from the npm registry at
46409
- * runtime so the CLI always scaffolds with the newest release.
46410
- */
46411
- const RUNTIME_RESOLVED_PACKAGES = ["next", "eslint-config-next"];
46412
- /**
46413
46589
  * @nextlyhq packages whose latest version is fetched from npm at runtime.
46414
46590
  * This avoids having to republish create-nextly-app every time a
46415
46591
  * dependency package is updated.
@@ -46433,9 +46609,10 @@ const resolvedNextlyVersions = /* @__PURE__ */ new Map();
46433
46609
  * On any failure (non-OK response, timeout, missing tag, thrown error) it
46434
46610
  * returns the requested dist-tag NAME itself (e.g. "alpha") — a valid npm
46435
46611
  * install spec — so a project on the alpha line never silently drops to
46436
- * `latest`.
46612
+ * `latest`. The channel has no default: only the Nextly packages are read
46613
+ * from the registry, always on a tag their caller names.
46437
46614
  */
46438
- async function fetchLatestVersion(pkg, channel = "latest") {
46615
+ async function fetchLatestVersion(pkg, channel) {
46439
46616
  try {
46440
46617
  const res = await fetch(`https://registry.npmjs.org/-/package/${encodeURIComponent(pkg)}/dist-tags`, { signal: AbortSignal.timeout(5e3) });
46441
46618
  if (!res.ok) return channel;
@@ -46469,26 +46646,6 @@ async function nextlyVersions(source) {
46469
46646
  if (source.kind === "release") return Object.fromEntries(NEXTLY_PACKAGES.map((pkg) => [pkg, source.cliVersion]));
46470
46647
  return resolveNextlyVersions("alpha");
46471
46648
  }
46472
- /** Cache for runtime-resolved package versions (next, eslint-config-next). */
46473
- let resolvedRuntimeVersions = null;
46474
- /**
46475
- * Resolve latest versions for Next.js ecosystem packages in parallel.
46476
- * Falls back to a safe semver range if the registry is unreachable.
46477
- */
46478
- async function resolveRuntimeVersions() {
46479
- if (resolvedRuntimeVersions) return resolvedRuntimeVersions;
46480
- const NEXT_FLOOR = "^16.4.1";
46481
- const FALLBACKS = {
46482
- next: NEXT_FLOOR,
46483
- "eslint-config-next": NEXT_FLOOR
46484
- };
46485
- const entries = await Promise.all(RUNTIME_RESOLVED_PACKAGES.map(async (pkg) => {
46486
- const version = await fetchLatestVersion(pkg);
46487
- return [pkg, version === "latest" ? FALLBACKS[pkg] : version];
46488
- }));
46489
- resolvedRuntimeVersions = Object.fromEntries(entries);
46490
- return resolvedRuntimeVersions;
46491
- }
46492
46649
  /**
46493
46650
  * Generate a `package.json` string for a fresh Nextly project.
46494
46651
  *
@@ -46507,11 +46664,10 @@ async function resolveRuntimeVersions() {
46507
46664
  */
46508
46665
  async function generatePackageJson(projectName, database, source = nextlySource({ cliVersion: packageVersion() }), projectType = "blank", templateDirs = [], targetDir) {
46509
46666
  if (projectType === "plugin") return generatePluginPackageJson(projectName, source);
46510
- const runtimeVersions = await resolveRuntimeVersions();
46511
46667
  const dependencies = {
46512
- next: runtimeVersions.next,
46513
- react: PINNED_VERSIONS.react,
46514
- "react-dom": PINNED_VERSIONS["react-dom"]
46668
+ next: scaffoldRange("next"),
46669
+ react: scaffoldRange("react"),
46670
+ "react-dom": scaffoldRange("react-dom")
46515
46671
  };
46516
46672
  dependencies["@tanstack/react-query"] = "^5.62.0";
46517
46673
  dependencies["lucide-react"] = "^0.544.0";
@@ -46535,11 +46691,11 @@ async function generatePackageJson(projectName, database, source = nextlySource(
46535
46691
  "@tailwindcss/postcss": PINNED_VERSIONS["@tailwindcss/postcss"],
46536
46692
  tailwindcss: PINNED_VERSIONS.tailwindcss,
46537
46693
  eslint: PINNED_VERSIONS.eslint,
46538
- "eslint-config-next": runtimeVersions["eslint-config-next"],
46694
+ "eslint-config-next": scaffoldRange("eslint-config-next"),
46539
46695
  esbuild: PINNED_VERSIONS.esbuild
46540
46696
  };
46541
46697
  const shipsSearchIndex = targetDir ? await projectHasSearchIndexScript(targetDir) : false;
46542
- const plugins = await projectPlugins(targetDir);
46698
+ const pluginInstalls = await projectPluginInstalls(targetDir);
46543
46699
  if (shipsSearchIndex) devDependencies.pagefind = "^1.1.0";
46544
46700
  const pkg = {
46545
46701
  name: projectName,
@@ -46549,7 +46705,7 @@ async function generatePackageJson(projectName, database, source = nextlySource(
46549
46705
  dev: "next dev --turbopack",
46550
46706
  build: [
46551
46707
  "nextly migrate",
46552
- ...plugins.map((plugin) => `nextly plugins install ${plugin}`),
46708
+ ...pluginInstalls,
46553
46709
  "next build",
46554
46710
  ...shipsSearchIndex ? ["node scripts/build-search-index.mjs"] : []
46555
46711
  ].join(" && "),
@@ -46611,7 +46767,6 @@ async function resolvePluginNextlyRange(source) {
46611
46767
  */
46612
46768
  async function generatePluginPackageJson(projectName, source) {
46613
46769
  const versions = await nextlyVersions(source);
46614
- const runtimeVersions = await resolveRuntimeVersions();
46615
46770
  const range = (pkg) => versions[pkg] ?? "alpha";
46616
46771
  const peerRange = (pkg) => pluginPeerRange(range(pkg), source.cliVersion);
46617
46772
  const peerDependencies = {
@@ -46619,8 +46774,8 @@ async function generatePluginPackageJson(projectName, source) {
46619
46774
  "@nextlyhq/admin": peerRange("@nextlyhq/admin"),
46620
46775
  "@nextlyhq/plugin-sdk": peerRange("@nextlyhq/plugin-sdk"),
46621
46776
  "@nextlyhq/ui": peerRange("@nextlyhq/ui"),
46622
- react: PINNED_VERSIONS.react,
46623
- "react-dom": PINNED_VERSIONS["react-dom"]
46777
+ react: SUPPORTED_RANGES.react,
46778
+ "react-dom": SUPPORTED_RANGES["react-dom"]
46624
46779
  };
46625
46780
  const devDependencies = {
46626
46781
  nextly: range("nextly"),
@@ -46629,9 +46784,9 @@ async function generatePluginPackageJson(projectName, source) {
46629
46784
  "@nextlyhq/plugin-sdk": range("@nextlyhq/plugin-sdk"),
46630
46785
  "@nextlyhq/adapter-drizzle": range("@nextlyhq/adapter-drizzle"),
46631
46786
  "@nextlyhq/adapter-sqlite": range("@nextlyhq/adapter-sqlite"),
46632
- next: runtimeVersions.next,
46633
- react: PINNED_VERSIONS.react,
46634
- "react-dom": PINNED_VERSIONS["react-dom"],
46787
+ next: scaffoldRange("next"),
46788
+ react: scaffoldRange("react"),
46789
+ "react-dom": scaffoldRange("react-dom"),
46635
46790
  "better-sqlite3": "^12.0.0",
46636
46791
  "@types/node": PINNED_VERSIONS["@types/node"],
46637
46792
  "@types/react": PINNED_VERSIONS["@types/react"],
@@ -46949,6 +47104,143 @@ async function copyPluginTemplate(opts) {
46949
47104
  await writeScaffoldNpmrc(targetDir, packageManager);
46950
47105
  await replacePlaceholders(targetDir, placeholders, alreadyRendered);
46951
47106
  }
47107
+ // src/generators/page-extensions.ts
47108
+ /**
47109
+ * The page extension to name a new module file with, a hook or a route: a
47110
+ * TypeScript one first, since Nextly's files are TypeScript, then a
47111
+ * JavaScript one; never a content extension such as `mdx`, which Next.js
47112
+ * would compile as content, and never `cjs`, since the files are ES modules
47113
+ * and a `.cjs` file cannot hold one. Null when the list holds no extension a
47114
+ * module can be written in.
47115
+ */
47116
+ function moduleExtension(extensions) {
47117
+ const usable = writable(extensions);
47118
+ return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
47119
+ }
47120
+ /**
47121
+ * The extensions a file with code in it can be written under: not a
47122
+ * declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
47123
+ * types only and refuses code in.
47124
+ */
47125
+ function writable(extensions) {
47126
+ return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
47127
+ }
47128
+ /**
47129
+ * The `pageExtensions` the app's Next.js config sets, as every existing-app
47130
+ * step that names a file Next.js routes reads it: the list; `"absent"` when
47131
+ * the config sets none, or there is no config; or null when it is set in a
47132
+ * way that cannot be read. Read from the config the app exports, as the Next
47133
+ * config step reads it, so a commented-out or nested `pageExtensions` sets
47134
+ * nothing. `configName` is the file it was read from, for the report.
47135
+ */
47136
+ async function appPageExtensions(cwd) {
47137
+ const nextConfig = await readNextConfig(cwd);
47138
+ return {
47139
+ configName: nextConfig?.name ?? "The Next.js config",
47140
+ extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
47141
+ };
47142
+ }
47143
+ /**
47144
+ * The text of the app's Next.js config, or null when it has none, from the
47145
+ * file the Next config step reads and extends ({@link findConfigFile}).
47146
+ */
47147
+ async function readNextConfig(cwd) {
47148
+ const name = await findConfigFile(cwd);
47149
+ return name === null ? null : {
47150
+ name,
47151
+ text: await import_lib$1.default.readFile(path.default.join(cwd, name), "utf-8")
47152
+ };
47153
+ }
47154
+ /**
47155
+ * The page extension for a file of TypeScript with JSX, such as a page or a
47156
+ * layout: one ending in `tsx`. Null when the list holds none.
47157
+ */
47158
+ function tsxExtension(extensions) {
47159
+ return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
47160
+ }
47161
+ /** A JavaScript or TypeScript module without JSX, such as a route. */
47162
+ const MODULE_FILE = {
47163
+ pick: moduleExtension,
47164
+ holds: "a JavaScript or TypeScript extension"
47165
+ };
47166
+ /** TypeScript with JSX, such as a page or a layout. */
47167
+ const TSX_FILE = {
47168
+ pick: tsxExtension,
47169
+ holds: "an extension ending in tsx"
47170
+ };
47171
+ /**
47172
+ * The report for a file an app that sets `pageExtensions` gets in place of
47173
+ * the file: the name to create it under and its content. Next.js routes a
47174
+ * file there only under a name with one of the app's extensions, and the app
47175
+ * may already have the file under such a name, which the existing-app steps
47176
+ * do not look for, so nothing is written. A list that cannot be read, or that
47177
+ * holds no extension the content needs, gives a placeholder name and says
47178
+ * which.
47179
+ *
47180
+ * @param base the file's path without its extension, as reported, such as
47181
+ * `app/admin/[[...params]]/page`
47182
+ */
47183
+ function pageExtensionsSkip(base, text, pages, need) {
47184
+ const { configName, extensions } = pages;
47185
+ const extension = extensions === null ? null : need.pick(extensions);
47186
+ const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
47187
+ if (extensions !== null && extension === null) return {
47188
+ path: `${base}.<${need.holds} added to pageExtensions>`,
47189
+ why: `${sets}, and none of them (${extensions.join(", ")}) is ${need.holds}; add one to pageExtensions, then, unless the app already has this file, create it under that name with this content`,
47190
+ toAdd: text
47191
+ };
47192
+ return {
47193
+ path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
47194
+ why: `${sets}; unless the app already has this file, create it under that name with this content`,
47195
+ toAdd: text
47196
+ };
47197
+ }
47198
+ // src/generators/write-new.ts
47199
+ /**
47200
+ * Writes `text` to `relative` (a project-relative path, written with `/`)
47201
+ * only where nothing is, and records which it was.
47202
+ *
47203
+ * The file is created exclusively (`wx`), so a file the app already has, a
47204
+ * link included, is never replaced, whatever appeared since any earlier
47205
+ * check. One that is there is reported as skipped, with why and, when given,
47206
+ * what to add to it by hand. `wx` guards only the file's own name, so a
47207
+ * folder on its path that is a symbolic link, which would take the write
47208
+ * wherever it points, is found first, as the media routes find it, and
47209
+ * nothing is written through it.
47210
+ */
47211
+ async function writeNew(cwd, relative, text, files, ifThere) {
47212
+ const file = path.default.join(cwd, ...relative.split("/"));
47213
+ const link = await linkedFolder(cwd, file);
47214
+ if (link !== null) {
47215
+ const folder = path.default.relative(cwd, link.folder).split(path.default.sep).join("/");
47216
+ files.skipped.push({
47217
+ path: relative,
47218
+ why: link.unreadable ? `${folder} cannot be read, so whether a folder in it is a symbolic link is unknown; nothing was written there` : `${folder} ${THROUGH_A_LINK}`,
47219
+ toAdd: ifThere.toAdd
47220
+ });
47221
+ return;
47222
+ }
47223
+ await import_lib$1.default.ensureDir(path.default.dirname(file));
47224
+ try {
47225
+ await import_lib$1.default.writeFile(file, text, {
47226
+ encoding: "utf-8",
47227
+ flag: "wx"
47228
+ });
47229
+ files.written.push(relative);
47230
+ } catch (error) {
47231
+ if (error.code !== "EEXIST") throw error;
47232
+ files.skipped.push({
47233
+ path: relative,
47234
+ ...ifThere
47235
+ });
47236
+ }
47237
+ }
47238
+ /** A step's report before it has done anything. */
47239
+ const nothingYet = () => ({
47240
+ written: [],
47241
+ changed: [],
47242
+ skipped: []
47243
+ });
46952
47244
  // src/generators/routes.ts
46953
47245
  const API_ROUTE_TEMPLATE = `// Imported from \`nextly/runtime\` (not the package root) so the
46954
47246
  // Next.js-coupled handler factory stays out of Node-only code paths
@@ -47059,8 +47351,31 @@ const UPLOADS_ROUTE = {
47059
47351
  importsConfig: true,
47060
47352
  role: (file) => `Nextly's uploads route at ${file} serves files stored on local disk at /uploads, which a production server otherwise answers 404 for until it restarts`
47061
47353
  };
47062
- /** Every route the existing-app path writes, read from the templates together. */
47063
- const ALL_ROUTES = [...ROUTES, UPLOADS_ROUTE];
47354
+ /**
47355
+ * The admin's layout, which `generateAdminPage` writes beside the admin page.
47356
+ * Read with the routes, from the template a new project gets, so an app Nextly
47357
+ * is added to renders its admin the same way: its shell prerendered, and its
47358
+ * request work beneath a Suspense boundary.
47359
+ */
47360
+ const ADMIN_LAYOUT = {
47361
+ path: "admin/[[...params]]/layout.tsx",
47362
+ importsConfig: true
47363
+ };
47364
+ /**
47365
+ * The admin component map's two generated files, under the app's source root,
47366
+ * as the base template commits them: the admin layout imports the list, and
47367
+ * the admin page imports the map that registers the components. Read with the
47368
+ * routes, so an app Nextly is added to starts from the template's own empty
47369
+ * map rather than a second one written out here, and `next dev` rewrites them
47370
+ * as plugins are added.
47371
+ */
47372
+ const ADMIN_COMPONENT_MAP_FILES = ["types/generated/plugin-admin-components.generated.ts", "types/generated/plugin-admin-imports.generated.ts"];
47373
+ /** Every file the existing-app path writes from the templates, read together. */
47374
+ const ALL_ROUTES = [
47375
+ ...ROUTES,
47376
+ UPLOADS_ROUTE,
47377
+ ADMIN_LAYOUT
47378
+ ];
47064
47379
  /**
47065
47380
  * The import that brings in the Nextly config: the template spells it through
47066
47381
  * the `@nextly-config` alias or a path relative to its own layout, neither of
@@ -47160,6 +47475,15 @@ async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
47160
47475
  if (route.importsConfig && !CONFIG_IMPORT.test(text)) throw new Error(`The template's ${route.path} does not import the Nextly config, so no route that finds it can be written. Nothing was installed or written.`);
47161
47476
  texts.set(route.path, text);
47162
47477
  }
47478
+ for (const file of ADMIN_COMPONENT_MAP_FILES) {
47479
+ const relative = `base/src/${file}`;
47480
+ try {
47481
+ texts.set(file, await import_lib$1.default.readFile(path.default.join(templatesDir, relative), "utf-8"));
47482
+ } catch (error) {
47483
+ const code = error.code ?? "unreadable";
47484
+ throw new Error(`The templates at ${templatesDir} have no readable ${relative} (${code}), so Nextly's admin component map cannot be added. Nothing was installed or written; check the --local-template path if one was given.`);
47485
+ }
47486
+ }
47163
47487
  return texts;
47164
47488
  }
47165
47489
  /**
@@ -47610,56 +47934,71 @@ function skipped(cwd, left, target, text, role, reason) {
47610
47934
  function toPosix(p) {
47611
47935
  return p.split(path.default.sep).join("/");
47612
47936
  }
47613
- // src/generators/write-new.ts
47937
+ // src/generators/types.ts
47614
47938
  /**
47615
- * Writes `text` to `relative` (a project-relative path, written with `/`)
47616
- * only where nothing is, and records which it was.
47617
- *
47618
- * The file is created exclusively (`wx`), so a file the app already has, a
47619
- * link included, is never replaced, whatever appeared since any earlier
47620
- * check. One that is there is reported as skipped, with why and, when given,
47621
- * what to add to it by hand. `wx` guards only the file's own name, so a
47622
- * folder on its path that is a symbolic link, which would take the write
47623
- * wherever it points, is found first, as the media routes find it, and
47624
- * nothing is written through it.
47939
+ * Where the generated types file goes, relative to the project root: under
47940
+ * src/ when the app keeps its code there, at the root when it does not. The
47941
+ * config names this path and the types step creates it, so both read it here.
47625
47942
  */
47626
- async function writeNew(cwd, relative, text, files, ifThere) {
47627
- const file = path.default.join(cwd, ...relative.split("/"));
47628
- const link = await linkedFolder(cwd, file);
47629
- if (link !== null) {
47630
- const folder = path.default.relative(cwd, link.folder).split(path.default.sep).join("/");
47631
- files.skipped.push({
47632
- path: relative,
47633
- why: link.unreadable ? `${folder} cannot be read, so whether a folder in it is a symbolic link is unknown; nothing was written there` : `${folder} ${THROUGH_A_LINK}`,
47634
- toAdd: ifThere.toAdd
47635
- });
47636
- return;
47637
- }
47638
- await import_lib$1.default.ensureDir(path.default.dirname(file));
47639
- try {
47640
- await import_lib$1.default.writeFile(file, text, {
47641
- encoding: "utf-8",
47642
- flag: "wx"
47643
- });
47644
- files.written.push(relative);
47645
- } catch (error) {
47646
- if (error.code !== "EEXIST") throw error;
47647
- files.skipped.push({
47648
- path: relative,
47649
- ...ifThere
47650
- });
47943
+ function typesOutputFile(projectInfo) {
47944
+ return `${projectInfo.srcDir ? "./src" : "."}/types/generated/nextly-types.ts`;
47945
+ }
47946
+ /**
47947
+ * The folder the generated files go in, relative to the project root, as
47948
+ * {@link typesOutputFile} names it: the admin layout and page import the
47949
+ * component map from here.
47950
+ */
47951
+ function typesOutputFolder(projectInfo) {
47952
+ return path.default.posix.dirname(path.default.posix.normalize(typesOutputFile(projectInfo)));
47953
+ }
47954
+ /**
47955
+ * Create the types directory structure for generated TypeScript types.
47956
+ * This mirrors what `nextly init` does - creates the directory and a placeholder file.
47957
+ * The actual types are generated when running `next dev` (which boots Nextly
47958
+ * and emits types lazily) or `npx nextly generate:types`.
47959
+ */
47960
+ async function generateTypesDirectory(cwd, projectInfo, templates) {
47961
+ const files = nothingYet();
47962
+ const typesFile = path.default.posix.normalize(typesOutputFile(projectInfo));
47963
+ const typesDir = typesOutputFolder(projectInfo);
47964
+ await writeNew(cwd, `${typesDir}/.gitkeep`, "", files, { why: "it is already there" });
47965
+ await writeNew(cwd, typesFile, `/**
47966
+ * Nextly Generated Types
47967
+ *
47968
+ * This file will be auto-generated when you run:
47969
+ * next dev (Nextly emits types on first boot)
47970
+ * npx nextly generate:types
47971
+ *
47972
+ * Do not edit this file manually - it will be overwritten.
47973
+ */
47974
+
47975
+ // Types will be generated here after running next dev
47976
+ export {};
47977
+ `, files, { why: "it is already there; Nextly writes the types into it when they are generated" });
47978
+ for (const file of ADMIN_COMPONENT_MAP_FILES) {
47979
+ const text = templates.get(file);
47980
+ if (text === void 0) throw new Error(`The templates read for this app hold no ${file}, so Nextly's admin component map cannot be written.`);
47981
+ await writeNew(cwd, `${typesDir}/${path.default.posix.basename(file)}`, text, files, { why: "it is already there; `next dev` and `nextly generate:types` keep it current" });
47651
47982
  }
47983
+ return files;
47652
47984
  }
47653
- /** A step's report before it has done anything. */
47654
- const nothingYet = () => ({
47655
- written: [],
47656
- changed: [],
47657
- skipped: []
47658
- });
47659
47985
  // src/generators/admin.ts
47660
- const ADMIN_PAGE_TEMPLATE = `"use client";
47986
+ /**
47987
+ * The generated component map's two files, by the name a module imports them
47988
+ * as: the list the layout reads, and the map the page loads to register the
47989
+ * components.
47990
+ */
47991
+ const COMPONENT_LIST = "plugin-admin-components.generated";
47992
+ const COMPONENT_MAP = "plugin-admin-imports.generated";
47993
+ /** How the file at `target` imports `name` from the app's generated types folder. */
47994
+ function generatedImport(cwd, target, projectInfo, name) {
47995
+ const relative = path.default.relative(path.default.dirname(target), path.default.join(cwd, typesOutputFolder(projectInfo), name)).split(path.default.sep).join("/");
47996
+ return relative.startsWith(".") ? relative : `./${relative}`;
47997
+ }
47998
+ const adminPageTemplate = (componentMap) => `"use client";
47661
47999
 
47662
48000
  import "@nextlyhq/admin/style.css";
48001
+ import "${componentMap}";
47663
48002
  import { RootLayout, QueryProvider, ErrorBoundary } from "@nextlyhq/admin";
47664
48003
 
47665
48004
  export default function AdminPage() {
@@ -47677,92 +48016,42 @@ export default function AdminPage() {
47677
48016
  }
47678
48017
  `;
47679
48018
  /**
47680
- * Generate the layout template with the correct relative import path
47681
- * to nextly.config based on whether the project uses a src/ directory.
47682
- *
47683
- * With src/: src/app/admin/[[...params]]/layout.tsx → ../../../../nextly.config
47684
- * Without src: app/admin/[[...params]]/layout.tsx → ../../../nextly.config
48019
+ * The admin layout, as the base template holds it, with its config import
48020
+ * pointing at the project's `nextly.config`, and its component-list import at
48021
+ * the app's generated types folder, from where the layout is written. One
48022
+ * source with a new project's layout, so an app Nextly is added to gets the
48023
+ * same per-request admin.
47685
48024
  */
47686
- function getAdminLayoutTemplate(srcDir) {
47687
- return `import { getBrandingCss } from "nextly/config";
47688
-
47689
- import config from "${srcDir ? "../../../../nextly.config" : "../../../nextly.config"}";
47690
-
47691
- const brandingCss = getBrandingCss(config.admin?.branding);
47692
-
47693
- export default function AdminLayout({
47694
- children,
47695
- }: {
47696
- children: React.ReactNode;
47697
- }) {
47698
- return (
47699
- <>
47700
- {brandingCss && (
47701
- <style dangerouslySetInnerHTML={{ __html: brandingCss }} />
47702
- )}
47703
- {children}
47704
- </>
47705
- );
47706
- }
47707
- `;
48025
+ function adminLayout(cwd, adminDir, projectInfo, templates) {
48026
+ const text = templates.get(ADMIN_LAYOUT.path);
48027
+ if (text === void 0) throw new Error(`The templates read for this app hold no ${ADMIN_LAYOUT.path}, so Nextly's admin layout cannot be written.`);
48028
+ const target = path.default.join(cwd, adminDir, "layout.tsx");
48029
+ return withRelativeConfigImport(text, cwd, target, ADMIN_LAYOUT.path).replace(new RegExp(`"[^"]*/${COMPONENT_LIST.replaceAll(".", "\\.")}"`), `"${generatedImport(cwd, target, projectInfo, COMPONENT_LIST)}"`);
47708
48030
  }
47709
48031
  /**
47710
48032
  * Generate the admin panel page and layout.
47711
48033
  * Uses [[...params]] to match the expected route pattern.
47712
48034
  */
47713
- async function generateAdminPage(cwd, projectInfo) {
48035
+ async function generateAdminPage(cwd, projectInfo, templates) {
47714
48036
  const files = nothingYet();
47715
48037
  const adminDir = `${projectInfo.appDir}/admin/[[...params]]`;
47716
- const layout = getAdminLayoutTemplate(projectInfo.srcDir);
48038
+ const layout = adminLayout(cwd, adminDir, projectInfo, templates);
48039
+ const page = adminPageTemplate(generatedImport(cwd, path.default.join(cwd, adminDir, "page.tsx"), projectInfo, COMPONENT_MAP));
47717
48040
  const pages = await appPageExtensions(cwd);
47718
48041
  if (pages.extensions !== "absent") {
47719
48042
  const { configName, extensions } = pages;
47720
- for (const [name, text] of [["page", ADMIN_PAGE_TEMPLATE], ["layout", layout]]) files.skipped.push(pageExtensionsSkip(`${adminDir}/${name}`, text, {
48043
+ for (const [name, text] of [["page", page], ["layout", layout]]) files.skipped.push(pageExtensionsSkip(`${adminDir}/${name}`, text, {
47721
48044
  configName,
47722
48045
  extensions
47723
48046
  }, TSX_FILE));
47724
48047
  return files;
47725
48048
  }
47726
- for (const [name, text] of [["page.tsx", ADMIN_PAGE_TEMPLATE], ["layout.tsx", layout]]) await writeNew(cwd, `${adminDir}/${name}`, text, files, {
48049
+ for (const [name, text] of [["page.tsx", page], ["layout.tsx", layout]]) await writeNew(cwd, `${adminDir}/${name}`, text, files, {
47727
48050
  why: `the app already has one; Nextly's admin needs its own ${name}, which is`,
47728
48051
  toAdd: text
47729
48052
  });
47730
48053
  return files;
47731
48054
  }
47732
- // src/generators/types.ts
47733
- /**
47734
- * Where the generated types file goes, relative to the project root: under
47735
- * src/ when the app keeps its code there, at the root when it does not. The
47736
- * config names this path and the types step creates it, so both read it here.
47737
- */
47738
- function typesOutputFile(projectInfo) {
47739
- return `${projectInfo.srcDir ? "./src" : "."}/types/generated/nextly-types.ts`;
47740
- }
47741
- /**
47742
- * Create the types directory structure for generated TypeScript types.
47743
- * This mirrors what `nextly init` does - creates the directory and a placeholder file.
47744
- * The actual types are generated when running `next dev` (which boots Nextly
47745
- * and emits types lazily) or `npx nextly generate:types`.
47746
- */
47747
- async function generateTypesDirectory(cwd, projectInfo) {
47748
- const files = nothingYet();
47749
- const typesFile = path.default.posix.normalize(typesOutputFile(projectInfo));
47750
- await writeNew(cwd, `${path.default.posix.dirname(typesFile)}/.gitkeep`, "", files, { why: "it is already there" });
47751
- await writeNew(cwd, typesFile, `/**
47752
- * Nextly Generated Types
47753
- *
47754
- * This file will be auto-generated when you run:
47755
- * next dev (Nextly emits types on first boot)
47756
- * npx nextly generate:types
47757
- *
47758
- * Do not edit this file manually - it will be overwritten.
47759
- */
47760
-
47761
- // Types will be generated here after running next dev
47762
- export {};
47763
- `, files, { why: "it is already there; Nextly writes the types into it when they are generated" });
47764
- return files;
47765
- }
47766
48055
  // src/generators/config.ts
47767
48056
  /**
47768
48057
  * Base templates for project types that use inline config generation.
@@ -47843,8 +48132,9 @@ function startNextly(configImport) {
47843
48132
  const { createRegister } = await import("nextly");
47844
48133
  const { default: config } = await import("${configImport}");
47845
48134
  // Next.js writes its Cache Components setting into this file as it builds
47846
- // it. Nextly, loaded from node_modules, cannot read that for itself, and
47847
- // warns when Cache Components is on, which it does not support yet.
48135
+ // it. Nextly, loaded from node_modules, cannot read that for itself. With
48136
+ // it off, Nextly warns at start; with it on, a cached read that finds no
48137
+ // "use cache" read function says so.
47848
48138
  await createRegister(config, {
47849
48139
  cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
47850
48140
  })();`;
@@ -47942,10 +48232,7 @@ function pageExtensionsAdvice(dir, configImport, configName, extensions) {
47942
48232
  * never rewritten; it is reported as skipped with what to add. So is the case
47943
48233
  * where the app sets \`pageExtensions\`: Next.js then reads the hook only under a
47944
48234
  * name with one of those extensions, which this does not guess. The config file
47945
- * itself is never edited here. Next.js 14 runs the hook only with
47946
- * `experimental.instrumentationHook`, which the Next config step gives in the
47947
- * same `experimental` object as Nextly's packages (`patchNextConfig`), so two
47948
- * steps never hand the developer two objects for one key.
48235
+ * itself is never edited here.
47949
48236
  */
47950
48237
  async function generateInstrumentation(cwd, projectInfo) {
47951
48238
  const dir = projectInfo.srcDir ? "src" : "";
@@ -47993,13 +48280,13 @@ async function generateIntoExistingApp(cwd, projectType, projectInfo, database,
47993
48280
  return [
47994
48281
  await generateConfig(cwd, projectType, projectInfo),
47995
48282
  await generateRoutes(cwd, projectInfo),
47996
- await generateAdminPage(cwd, projectInfo),
48283
+ await generateAdminPage(cwd, projectInfo, templates),
47997
48284
  await generateMediaRoutes(cwd, projectInfo, templates),
47998
48285
  await generateUploadsRoute(cwd, projectInfo, templates),
47999
48286
  await generateInstrumentation(cwd, projectInfo),
48000
- await generateTypesDirectory(cwd, projectInfo),
48287
+ await generateTypesDirectory(cwd, projectInfo, templates),
48001
48288
  nextConfigFiles(await patchNextConfig(cwd, database)),
48002
- cacheComponentsFiles(await turnOffCacheComponents(cwd))
48289
+ cacheComponentsFiles(await turnOnCacheComponents(cwd))
48003
48290
  ];
48004
48291
  }
48005
48292
  // src/generators/report.ts
@@ -50656,7 +50943,7 @@ var hr = Symbol("entry");
50656
50943
  var cs = Symbol("entryOpt");
50657
50944
  var ui = Symbol("writeEntryClass");
50658
50945
  var lr = Symbol("write");
50659
- var fs$6 = Symbol("ondrain");
50946
+ var fs$7 = Symbol("ondrain");
50660
50947
  var wt = class extends A {
50661
50948
  sync = !1;
50662
50949
  opt;
@@ -50690,8 +50977,8 @@ var wt = class extends A {
50690
50977
  if ((t.gzip ? 1 : 0) + (t.brotli ? 1 : 0) + (t.zstd ? 1 : 0) > 1) throw new TypeError("gzip, brotli, zstd are mutually exclusive");
50691
50978
  if (t.gzip && (typeof t.gzip != "object" && (t.gzip = {}), this.portable && (t.gzip.portable = !0), this.zip = new ze(t.gzip)), t.brotli && (typeof t.brotli != "object" && (t.brotli = {}), this.zip = new We(t.brotli)), t.zstd && (typeof t.zstd != "object" && (t.zstd = {}), this.zip = new Ye(t.zstd)), !this.zip) throw new Error("impossible");
50692
50979
  let e = this.zip;
50693
- e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$6]()), this.on("resume", () => e.resume());
50694
- } else this.on("drain", this[fs$6]);
50980
+ e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$7]()), this.on("resume", () => e.resume());
50981
+ } else this.on("drain", this[fs$7]);
50695
50982
  this.noDirRecurse = !!t.noDirRecurse, this.follow = !!t.follow, this.noMtime = !!t.noMtime, t.mtime && (this.mtime = t.mtime), this.filter = typeof t.filter == "function" ? t.filter : () => !0, this[W] = new hi(), this[G] = 0, this.jobs = Number(t.jobs) || 4, this[Ee] = !1, this[me] = !1;
50696
50983
  }
50697
50984
  [lr](t) {
@@ -50819,7 +51106,7 @@ var wt = class extends A {
50819
51106
  this.emit("error", e);
50820
51107
  }
50821
51108
  }
50822
- [fs$6]() {
51109
+ [fs$7]() {
50823
51110
  this[Et] && this[Et].entry && this[Et].entry.resume();
50824
51111
  }
50825
51112
  [di](t) {
@@ -51886,6 +52173,126 @@ async function cleanupDownload(source) {
51886
52173
  await import_lib$1.default.remove(extractionRoot).catch(() => {});
51887
52174
  }
51888
52175
  }
52176
+ // src/lib/framework-versions.ts
52177
+ /**
52178
+ * Whether an existing app's Next.js, React and React DOM are ones this
52179
+ * release of Nextly supports, and what to change when they are not.
52180
+ *
52181
+ * Nextly is reviewed against one Next.js major and one React major
52182
+ * (`SUPPORTED_RANGES`). An app below the floor or on a newer major would
52183
+ * install Nextly and then fail at build or at run time, so adding Nextly to
52184
+ * it stops before anything is written, saying what was found and the way on.
52185
+ */
52186
+ /** The first spec the app's package.json gives `name`, in field order. */
52187
+ function declaredSpec(manifest, name) {
52188
+ const fields = ["optionalDependencies", ...DEPENDENCY_FIELDS$1.filter((field) => field !== "optionalDependencies")];
52189
+ for (const field of fields) {
52190
+ const deps = manifest[field];
52191
+ if (!deps || typeof deps !== "object") continue;
52192
+ const spec = deps[name];
52193
+ if (typeof spec === "string") return spec;
52194
+ }
52195
+ }
52196
+ /**
52197
+ * The version an app is headed for, the one judged against the range: the
52198
+ * installed copy, unless the app's declaration points past it.
52199
+ */
52200
+ function judgedVersion(installed, declared, supported) {
52201
+ const lowestDeclared = declared !== void 0 ? lowestVersionIn(declared) : void 0;
52202
+ const floor = lowestVersionIn(supported);
52203
+ if (floor === void 0) return installed ?? lowestDeclared;
52204
+ if (lowestDeclared !== void 0 && compareVersions(lowestDeclared, floor) > 0 && !satisfiesRange(lowestDeclared, supported)) return lowestDeclared;
52205
+ const highest = declared !== void 0 ? highestMajorIn(declared) : void 0;
52206
+ if (installed === void 0 && highest !== void 0 && highest > major(floor)) return `${major(floor) + 1}.0.0`;
52207
+ return installed ?? lowestDeclared;
52208
+ }
52209
+ /**
52210
+ * Too new only on a later major than the range covers; anything else outside
52211
+ * it (below the floor, or a prerelease within the major) needs a supported
52212
+ * release of the same major. A range without a floor leaves nothing to
52213
+ * compare, and the version is then reported as too new.
52214
+ */
52215
+ function directionOf(judged, supported) {
52216
+ const floor = lowestVersionIn(supported);
52217
+ return floor === void 0 || major(judged) > major(floor) ? "too-new" : "too-old";
52218
+ }
52219
+ /**
52220
+ * Every supported framework package the app at `cwd` has outside its range.
52221
+ *
52222
+ * The version judged is the one the app is headed for (`judgedVersion`): the
52223
+ * installed copy, read from the `node_modules` Node would load it from,
52224
+ * unless the app's package.json declaration points past it; with none
52225
+ * installed, what an install of that declaration could resolve to. A package
52226
+ * the app neither has installed nor declares in a form that names a version
52227
+ * (a dist-tag, `workspace:*`) is not judged.
52228
+ */
52229
+ async function frameworkVersionProblems(cwd) {
52230
+ const manifest = await import_lib$1.default.readJson(path.default.join(cwd, "package.json")).catch(() => ({}));
52231
+ const problems = [];
52232
+ for (const name of Object.keys(SUPPORTED_RANGES)) {
52233
+ const supported = SUPPORTED_RANGES[name];
52234
+ const installed = await installedVersion(cwd, name);
52235
+ const declared = declaredSpec(manifest, name);
52236
+ const judged = judgedVersion(installed, declared, supported);
52237
+ if (judged === void 0 || satisfiesRange(judged, supported)) continue;
52238
+ const direction = directionOf(judged, supported);
52239
+ problems.push({
52240
+ name,
52241
+ installed,
52242
+ declared,
52243
+ supported,
52244
+ judged,
52245
+ direction
52246
+ });
52247
+ }
52248
+ return problems;
52249
+ }
52250
+ const LABEL = {
52251
+ next: "Next.js",
52252
+ react: "React",
52253
+ "react-dom": "React"
52254
+ };
52255
+ const major = (version) => Number(version.split(".")[0]);
52256
+ /** What `fix` reads from a problem: the supported major and the version found. */
52257
+ function majorsOf(problem) {
52258
+ const floor = lowestVersionIn(problem.supported) ?? problem.supported;
52259
+ return {
52260
+ supportedMajor: major(floor),
52261
+ foundMajor: major(problem.judged)
52262
+ };
52263
+ }
52264
+ /** A version past the supported major: install that major, or wait for Nextly. */
52265
+ function tooNewFix(problem, add) {
52266
+ const { supportedMajor, foundMajor } = majorsOf(problem);
52267
+ const label = LABEL[problem.name];
52268
+ return `This release of Nextly supports ${label} up to the ${supportedMajor}.x line. Install ${label} ${supportedMajor}, or wait for a Nextly release that supports ${label} ${foundMajor}:\n ${add} ${problem.name === "next" ? `next@${supportedMajor}` : `react@${supportedMajor} react-dom@${supportedMajor}`}`;
52269
+ }
52270
+ /** A version below the supported floor: the codemod across a Next.js major, an install otherwise. */
52271
+ function tooOldFix(problem, add) {
52272
+ const { supportedMajor, foundMajor } = majorsOf(problem);
52273
+ if (problem.name === "next" && foundMajor < supportedMajor) return `Upgrade Next.js to ${REVIEWED_VERSIONS.next} with its codemod:\n npx @next/codemod@latest upgrade ${REVIEWED_VERSIONS.next}`;
52274
+ const install = problem.name === "next" ? `next@${problem.supported}` : `react@${SUPPORTED_RANGES.react} react-dom@${SUPPORTED_RANGES["react-dom"]}`;
52275
+ return `Upgrade ${LABEL[problem.name]} to ${problem.supported}:\n ${add} ${install}`;
52276
+ }
52277
+ /** The one step that brings `problem` into range, as a sentence and a command. */
52278
+ function fix(problem, pm) {
52279
+ const add = ADD_COMMAND[pm];
52280
+ return problem.direction === "too-new" ? tooNewFix(problem, add) : tooOldFix(problem, add);
52281
+ }
52282
+ /**
52283
+ * Why the run stopped: each package found, the range it falls outside, and
52284
+ * the change that brings it in. React and React DOM move together, so a
52285
+ * problem with both gives one command.
52286
+ */
52287
+ function frameworkVersionMessage(problems, pm) {
52288
+ const lines = problems.map(({ name, installed, declared, supported }) => {
52289
+ return ` ${name}: ${[installed !== void 0 ? `installed ${installed}` : void 0, declared !== void 0 ? `declared ${declared}` : void 0].filter(Boolean).join(", ")}; supported ${supported}`;
52290
+ });
52291
+ const fixes = [...new Set(problems.map((problem) => fix(problem, pm)))];
52292
+ return `This app's Next.js or React is outside what this release of Nextly supports:
52293
+ ${lines.join("\n")}\n\nNothing was changed. Change the app as follows, then run create-nextly-app again:
52294
+ ` + fixes.map((line) => ` ${line}`).join("\n");
52295
+ }
51889
52296
  // src/prompts/approach.ts
51890
52297
  /**
51891
52298
  * Labels shown in the CLI select prompt for each approach.
@@ -52326,6 +52733,16 @@ async function createNextly(options = {}) {
52326
52733
  const existingProject = options.projectType !== "plugin" && await isExistingNextProject(cwd);
52327
52734
  if (existingProject) {
52328
52735
  R$1.success(`${import_picocolors.default.green("Next.js")} project detected`);
52736
+ const frameworkProblems = await frameworkVersionProblems(cwd);
52737
+ if (frameworkProblems.length > 0) {
52738
+ capture("scaffold_failed", {
52739
+ stage: "detect",
52740
+ error_code: "unknown",
52741
+ duration_ms: Date.now() - overallStartedAt
52742
+ });
52743
+ Nt$1(frameworkVersionMessage(frameworkProblems, await detectAppPackageManager(cwd)));
52744
+ process.exit(1);
52745
+ }
52329
52746
  if (!defaults) {
52330
52747
  const shouldInstall = await Rt$1({
52331
52748
  message: "Install Nextly in this project?",
@@ -52733,4 +53150,4 @@ Object.defineProperty(exports, "validateTemplateName", {
52733
53150
  }
52734
53151
  });
52735
53152
 
52736
- //# sourceMappingURL=create-nextly-BGu1tXYs.cjs.map
53153
+ //# sourceMappingURL=create-nextly-Decg8dye.cjs.map