create-nextly-app 0.0.2-alpha.71 → 0.0.2-alpha.73

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.
@@ -8053,6 +8053,38 @@ var require_utils$3 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8053
8053
  return output.join("");
8054
8054
  }
8055
8055
  /**
8056
+ * Removes dot segments from the path of a relative reference (a path that does
8057
+ * not start with "/") without changing what it resolves to against any base.
8058
+ * Unlike `removeDotSegments`, leading ".." segments that cannot be collapsed
8059
+ * are kept, the result never becomes root-relative, and a result that would be
8060
+ * empty or start with an empty segment is prefixed with "." (an empty path
8061
+ * means "same document" and a leading "//" would introduce an authority).
8062
+ *
8063
+ * @param {string} path
8064
+ * @returns {string}
8065
+ */
8066
+ function removeDotSegmentsRelative(path) {
8067
+ if (path === "" || path[0] === "/") return removeDotSegments(path);
8068
+ const segments = path.split("/");
8069
+ const last = segments.length - 1;
8070
+ /** @type {string[]} */
8071
+ const output = [];
8072
+ let trailingDirectory = false;
8073
+ for (let i = 0; i <= last; i++) {
8074
+ const segment = segments[i];
8075
+ if (segment === "." || segment === "..") {
8076
+ if (segment === "..") {
8077
+ if (output.length !== 0 && output[output.length - 1] !== "..") output.pop();
8078
+ else output.push("..");
8079
+ }
8080
+ trailingDirectory = i === last;
8081
+ } else output.push(segment);
8082
+ }
8083
+ if (trailingDirectory && output[output.length - 1] !== "..") output.push("");
8084
+ if (output.length === 0 || output[0] === "" || output[0].includes(":")) output.unshift(".");
8085
+ return output.join("/");
8086
+ }
8087
+ /**
8056
8088
  * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
8057
8089
  * After the URI regex parses, these characters cannot be literal in the host
8058
8090
  * field, so any that appear after decoding came from percent-encoding and
@@ -8356,6 +8388,7 @@ var require_utils$3 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8356
8388
  encodeFragment,
8357
8389
  escapePreservingEscapes,
8358
8390
  removeDotSegments,
8391
+ removeDotSegmentsRelative,
8359
8392
  isIPv4,
8360
8393
  isUUID,
8361
8394
  normalizeIPv6,
@@ -8545,18 +8578,24 @@ var require_schemes = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8545
8578
  };
8546
8579
  }));
8547
8580
  var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8548
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils$3();
8581
+ const { normalizeIPv6, removeDotSegments, removeDotSegmentsRelative, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils$3();
8549
8582
  const { SCHEMES, getSchemeHandler } = require_schemes();
8550
8583
  const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
8551
8584
  const MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
8585
+ const MALFORMED_HOST_ERROR = "URI host is malformed.";
8586
+ const FORBIDDEN_DOMAIN_HOST_CHARACTER = /[\0-\x20"#/:<>?@[\\\]^`{|}\x7F]/u;
8552
8587
  /**
8588
+ * RFC 3986 section 3.1 does not allow percent-encoding in a scheme, so the
8589
+ * scheme is validated as-is and never decoded. Decoding would let inert input
8590
+ * such as `%6Aavascript:` become a live `javascript:` scheme.
8591
+ *
8553
8592
  * @param {string} scheme
8554
8593
  * @returns {string}
8555
8594
  */
8556
- function decodeValidScheme(scheme) {
8557
- const decodedScheme = unescape(String(scheme));
8558
- if (!VALID_SCHEME.test(decodedScheme)) throw new TypeError(MALFORMED_SCHEME_ERROR);
8559
- return decodedScheme;
8595
+ function validateScheme(scheme) {
8596
+ const stringScheme = String(scheme);
8597
+ if (!VALID_SCHEME.test(stringScheme)) throw new TypeError(MALFORMED_SCHEME_ERROR);
8598
+ return stringScheme;
8560
8599
  }
8561
8600
  /**
8562
8601
  * @template {import('./types/index').URIComponent|string} T
@@ -8584,6 +8623,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8584
8623
  const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
8585
8624
  const resolvedHost = resolved.host;
8586
8625
  const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
8626
+ if (hasForbiddenDomainHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)) throw new Error(MALFORMED_HOST_ERROR);
8587
8627
  canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
8588
8628
  const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !/\P{ASCII}/u.test(resolvedHost);
8589
8629
  if (resolved.error && !encodedASCIIHost) throw new Error(resolved.error);
@@ -8627,10 +8667,11 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8627
8667
  } else {
8628
8668
  if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
8629
8669
  else {
8630
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
8670
+ const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
8671
+ if (baseHasAuthority && !base.path) target.path = "/" + relative.path;
8631
8672
  else if (!base.path) target.path = relative.path;
8632
8673
  else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
8633
- target.path = removeDotSegments(target.path);
8674
+ target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
8634
8675
  }
8635
8676
  target.query = relative.query;
8636
8677
  }
@@ -8678,17 +8719,18 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8678
8719
  };
8679
8720
  const options = Object.assign({}, opts);
8680
8721
  const uriTokens = [];
8681
- if (component.scheme) component.scheme = decodeValidScheme(component.scheme);
8722
+ if (component.scheme) component.scheme = validateScheme(component.scheme);
8682
8723
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
8683
8724
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
8684
8725
  const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
8685
- const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
8726
+ const schemelessReference = (component.scheme === void 0 || options.reference === "suffix") && !hasAuthority;
8727
+ const pathNoScheme = !options.skipEscape && schemelessReference;
8686
8728
  if (component.path !== void 0) {
8687
8729
  if (!options.skipEscape) component.path = serializePathEncoding(component.path, pathNoScheme);
8688
8730
  else component.path = normalizePercentEncoding(component.path);
8689
8731
  }
8690
8732
  if (options.reference !== "suffix" && component.scheme) {
8691
- component.scheme = decodeValidScheme(component.scheme);
8733
+ component.scheme = validateScheme(component.scheme);
8692
8734
  uriTokens.push(component.scheme, ":");
8693
8735
  }
8694
8736
  const authority = recomposeAuthority(component);
@@ -8699,8 +8741,8 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8699
8741
  }
8700
8742
  if (component.path !== void 0) {
8701
8743
  let s = component.path;
8702
- if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) s = removeDotSegments(s);
8703
- if (pathNoScheme) s = serializePathEncoding(s, true);
8744
+ if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) s = schemelessReference && s[0] !== "/" ? removeDotSegmentsRelative(s) : removeDotSegments(s);
8745
+ if (schemelessReference) s = serializePathEncoding(s, true);
8704
8746
  if (authority === void 0 && s[0] === "/" && s[1] === "/") s = "/%2F" + s.slice(2);
8705
8747
  uriTokens.push(s);
8706
8748
  }
@@ -8764,15 +8806,50 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8764
8806
  * @returns {boolean} whether host conversion failed
8765
8807
  */
8766
8808
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
8767
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) try {
8768
- parsed.host = new URL("http://" + parsed.host).hostname;
8769
- } catch (e) {
8770
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
8809
+ if (hasForbiddenDomainHost(parsed, options, schemeHandler, isIP)) {
8810
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
8771
8811
  return true;
8772
8812
  }
8813
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
8814
+ let url;
8815
+ try {
8816
+ url = new URL("http://" + parsed.host);
8817
+ } catch (e) {
8818
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
8819
+ return true;
8820
+ }
8821
+ if (!isHostOnlyURL(url)) {
8822
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
8823
+ return true;
8824
+ }
8825
+ parsed.host = url.hostname;
8826
+ }
8773
8827
  return false;
8774
8828
  }
8775
8829
  /**
8830
+ * Whether a domain host contains an ASCII character that RFC 3986 does not
8831
+ * allow in a reg-name (unreserved, sub-delims, or pct-encoded). Such a host is
8832
+ * rejected instead of being handed to the WHATWG URL parser, which would treat
8833
+ * "@" as a userinfo delimiter (silently dropping the data before it) and strip
8834
+ * TAB, LF, and CR. Non-ASCII characters remain allowed for IDNs.
8835
+ *
8836
+ * @param {import('./types/index').URIComponent} parsed
8837
+ * @param {import('./types/index').Options} options
8838
+ * @param {{ domainHost?: boolean }|undefined} schemeHandler
8839
+ * @param {boolean} isIP
8840
+ * @returns {boolean}
8841
+ */
8842
+ function hasForbiddenDomainHost(parsed, options, schemeHandler, isIP) {
8843
+ return Boolean(parsed.host && isIP === false && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && FORBIDDEN_DOMAIN_HOST_CHARACTER.test(parsed.host));
8844
+ }
8845
+ /**
8846
+ * @param {URL} url
8847
+ * @returns {boolean}
8848
+ */
8849
+ function isHostOnlyURL(url) {
8850
+ return url.username === "" && url.password === "" && url.port === "" && url.pathname === "/" && url.search === "" && url.hash === "" && url.hostname !== "";
8851
+ }
8852
+ /**
8776
8853
  * @param {string} uri
8777
8854
  * @param {import('./types/index').Options} [opts]
8778
8855
  * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
@@ -8829,8 +8906,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8829
8906
  parsed.query = matches[7];
8830
8907
  parsed.fragment = matches[8];
8831
8908
  if (parsed.scheme !== void 0) {
8832
- const decodedScheme = unescape(parsed.scheme);
8833
- if (VALID_SCHEME.test(decodedScheme)) parsed.scheme = decodedScheme.toLowerCase();
8909
+ if (VALID_SCHEME.test(parsed.scheme)) parsed.scheme = parsed.scheme.toLowerCase();
8834
8910
  else {
8835
8911
  parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
8836
8912
  malformedScheme = true;
@@ -8853,7 +8929,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8853
8929
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
8854
8930
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
8855
8931
  if (malformedIPLiteral) {
8856
- parsed.error = parsed.error || "URI host is malformed.";
8932
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
8857
8933
  malformedAuthorityOrPort = true;
8858
8934
  }
8859
8935
  } else isIP = true;
@@ -8865,13 +8941,12 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8865
8941
  if (options.reference && options.reference !== "suffix" && options.reference !== parsed.reference) parsed.error = parsed.error || "URI is not a " + options.reference + " reference.";
8866
8942
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
8867
8943
  if (!malformedIPLiteral) malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
8944
+ if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
8945
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
8946
+ if (!isIP) host = normalizePercentEncoding(host.toLowerCase());
8947
+ parsed.host = isIP || options.scheme !== "null" ? reescapeHostDelimiters(host, isIP) : host;
8948
+ }
8868
8949
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
8869
- if (uri.indexOf("%") !== -1) {
8870
- if (parsed.host !== void 0 && !malformedIPLiteral) {
8871
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
8872
- parsed.host = reescapeHostDelimiters(host, isIP);
8873
- }
8874
- }
8875
8950
  if (parsed.path) parsed.path = normalizePathEncoding(parsed.path);
8876
8951
  if (parsed.query) parsed.query = normalizeQueryFragmentEncoding(parsed.query);
8877
8952
  if (parsed.fragment) parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
@@ -22767,6 +22842,7 @@ function nextlySource(options) {
22767
22842
  function isNextlyPackage(name) {
22768
22843
  return name === "nextly" || name.startsWith("@nextlyhq/");
22769
22844
  }
22845
+ /** The package.json fields that declare a dependency, in the order read. */
22770
22846
  const DEPENDENCY_FIELDS$1 = [
22771
22847
  "dependencies",
22772
22848
  "devDependencies",
@@ -22842,6 +22918,10 @@ const EXACT_FLAG = {
22842
22918
  yarn: "--exact",
22843
22919
  bun: "--exact"
22844
22920
  };
22921
+ /**
22922
+ * How each package manager adds a dependency. Exported so every message that
22923
+ * tells the person what to install spells the command the same way.
22924
+ */
22845
22925
  const ADD_COMMAND = {
22846
22926
  npm: "npm install",
22847
22927
  pnpm: "pnpm add",
@@ -42411,8 +42491,8 @@ var import_lib = (/* @__PURE__ */ __commonJSMin$1(((exports) => {
42411
42491
  exports.parse = parse;
42412
42492
  })))();
42413
42493
  /**
42414
- * Where Nextly explains why Cache Components is off and when it will be
42415
- * supported: the same page `nextly` links from the error it stops with.
42494
+ * Where Nextly explains the Cache Components settings every app it makes or
42495
+ * joins carries, and why each one is there.
42416
42496
  */
42417
42497
  const CACHE_COMPONENTS_DOCS_URL = "https://nextlyhq.com/docs/getting-started/installation#cache-components";
42418
42498
  /**
@@ -42450,13 +42530,42 @@ function packageListLiteral(database) {
42450
42530
  return JSON.stringify(getServerExternalPackages(database), null, 2).replace(/\n/g, "\n ");
42451
42531
  }
42452
42532
  /**
42453
- * The setting every config this writes carries, with its reason in the file:
42454
- * the developer who finds it is the one deciding whether to change it.
42533
+ * Nextly's cache handlers, as the settings that install them. Under Cache
42534
+ * Components, Next.js's default cache serves a page's previous shell once
42535
+ * after a write expires its tags; these expire it at once.
42536
+ */
42537
+ const HANDLER_SETTINGS = [
42538
+ "cacheHandler",
42539
+ "cacheMaxMemorySize",
42540
+ "cacheHandlers"
42541
+ ];
42542
+ const HANDLER_PROPERTIES = [
42543
+ "cacheHandler: require.resolve(\"nextly/cache-handler\")",
42544
+ "cacheMaxMemorySize: 0",
42545
+ "cacheHandlers: { default: require.resolve(\"nextly/cache-handlers\") }"
42546
+ ];
42547
+ const ON_OPTIONS = ["cacheComponents", "partialPrefetching"];
42548
+ /** Every setting Nextly needs, as lines of a config object. */
42549
+ const ON_LINES = [...ON_OPTIONS.map((option) => `${option}: true`), ...HANDLER_PROPERTIES].map((property) => ` ${property},`);
42550
+ /**
42551
+ * The settings every config this writes carries, with their reason in the
42552
+ * file: the developer who finds them is the one deciding whether to change
42553
+ * them.
42455
42554
  */
42456
- const CACHE_COMPONENTS_OFF = ` // Nextly does not support Cache Components yet: it caches with
42457
- // unstable_cache, and Cache Components refuses \`runtime\` route exports.
42555
+ const CACHE_COMPONENTS_ON = ` // Nextly runs with Cache Components and Partial Prefetching on. Its cache
42556
+ // handlers make the first read after a write show the new version, where
42557
+ // Next.js's default cache would serve the previous page once more:
42458
42558
  // ${CACHE_COMPONENTS_DOCS_URL}
42459
- cacheComponents: false,`;
42559
+ ${ON_LINES.join("\n")}`;
42560
+ /**
42561
+ * How an ES module config gets `require`, which Node defines only in
42562
+ * CommonJS, to resolve the handlers from the app's own node_modules.
42563
+ */
42564
+ const REQUIRE_IMPORT = "import { createRequire } from \"node:module\";";
42565
+ /** The declaration, calling `createRequire` by the name the file has it under. */
42566
+ const requireDeclaration = (createRequireName) => `// Resolves Nextly's cache handlers from this app's node_modules.
42567
+ const require = ${createRequireName}(import.meta.url);`;
42568
+ const REQUIRE_DECLARATION = requireDeclaration("createRequire");
42460
42569
  /**
42461
42570
  * Render a complete next.config.ts file for the selected database.
42462
42571
  */
@@ -42464,7 +42573,7 @@ function buildNextConfigTemplate(database) {
42464
42573
  return `import type { NextConfig } from "next";
42465
42574
 
42466
42575
  const nextConfig: NextConfig = {
42467
- ${CACHE_COMPONENTS_OFF}
42576
+ ${CACHE_COMPONENTS_ON}
42468
42577
  serverExternalPackages: ${packageListLiteral(database)},
42469
42578
  };
42470
42579
 
@@ -42473,9 +42582,13 @@ export default nextConfig;
42473
42582
  }
42474
42583
  /** The same config as a JavaScript module, for an app without TypeScript. */
42475
42584
  function buildNextConfigModule(database) {
42476
- return `/** @type {import("next").NextConfig} */
42585
+ return `${REQUIRE_IMPORT}
42586
+
42587
+ ${REQUIRE_DECLARATION}
42588
+
42589
+ /** @type {import("next").NextConfig} */
42477
42590
  const nextConfig = {
42478
- ${CACHE_COMPONENTS_OFF}
42591
+ ${CACHE_COMPONENTS_ON}
42479
42592
  serverExternalPackages: ${packageListLiteral(database)},
42480
42593
  };
42481
42594
 
@@ -42493,9 +42606,19 @@ const CONFIG_FILES = [
42493
42606
  "next.config.ts",
42494
42607
  "next.config.mts"
42495
42608
  ];
42496
- /** The first Next.js major whose option is the top-level `serverExternalPackages`. */
42497
- const FIRST_TOP_LEVEL_MAJOR = 15;
42498
42609
  const PROPERTY = "serverExternalPackages";
42610
+ /**
42611
+ * The deprecated spelling of the list, under `experimental`. Supported
42612
+ * releases still read it, by copying it over the top-level list, so an app
42613
+ * that has it would lose the packages this adds there.
42614
+ */
42615
+ const LEGACY_PROPERTY = "serverComponentsExternalPackages";
42616
+ /**
42617
+ * Why a config with the deprecated list is left alone. Named, so the advice
42618
+ * printed for it can say what to do with that list rather than the advice
42619
+ * for every other refusal, which says to keep the experimental keys there.
42620
+ */
42621
+ const LEGACY_LIST_REASON = `it lists packages under experimental.${LEGACY_PROPERTY}, which Next.js copies over ${PROPERTY}, replacing the packages added there`;
42499
42622
  /** Where the line holding `offset` starts. */
42500
42623
  const lineStart = (source, offset) => source.lastIndexOf("\n", offset - 1) + 1;
42501
42624
  /** The whitespace a line opens with, for the line holding `offset`. */
@@ -42676,7 +42799,7 @@ const TOP_LEVEL_SETTINGS = [
42676
42799
  "transpilePackages",
42677
42800
  "experimental"
42678
42801
  ];
42679
- const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", "serverComponentsExternalPackages"];
42802
+ const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", LEGACY_PROPERTY];
42680
42803
  /** Why an object might set one of `names` where this cannot read it, or null. */
42681
42804
  function hiddenSettings(object, names, where) {
42682
42805
  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;
@@ -42734,41 +42857,43 @@ function fillEmptyList(source, array, entries) {
42734
42857
  * so the app's own properties keep their order and their comments.
42735
42858
  */
42736
42859
  function addListProperty(source, object, entries) {
42860
+ return appendProperties(source, object, (indent, ownLines) => [ownLines ? `${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}]` : `${PROPERTY}: [${entries.join(", ")}]`]);
42861
+ }
42862
+ /**
42863
+ * Add properties after an object's last one, in the object's layout: on one
42864
+ * line, as `{ a: 1, b: 2 }`, when it is written that way, and otherwise one
42865
+ * per line at the indentation its properties use, before the closing brace,
42866
+ * with a comma put after the last property first when it has none.
42867
+ * `properties` gets that indentation, and whether each property has a line
42868
+ * of its own, so a property spanning lines can follow the same layout.
42869
+ */
42870
+ function appendProperties(source, object, properties) {
42737
42871
  const open = (object.start ?? 0) + 1;
42738
42872
  const close = (object.end ?? 0) - 1;
42739
- return spansLines(source, open, close) ? listOnItsOwnLines(source, object, entries) : [listOnTheSameLine(object, entries)];
42873
+ if (!spansLines(source, open, close)) return [appendOnTheSameLine(object, properties("", false).join(", "))];
42874
+ const last = object.properties.at(-1);
42875
+ const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
42876
+ const lines = properties(indent, true).map((property) => `${indent}${property},\n`).join("");
42877
+ return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
42878
+ at: last.end ?? 0,
42879
+ remove: 0,
42880
+ text: ","
42881
+ }] : [], beforeClosingBrace(source, close, lines)];
42740
42882
  }
42741
- /** The list added to an object written on one line, as `{ a: 1, serverExternalPackages: [...] }`. */
42742
- function listOnTheSameLine(object, entries) {
42743
- const property = `${PROPERTY}: [${entries.join(", ")}]`;
42883
+ /** Properties added to an object written on one line, as `{ a: 1, b: 2 }`. */
42884
+ function appendOnTheSameLine(object, text) {
42744
42885
  const last = object.properties.at(-1);
42745
42886
  const open = (object.start ?? 0) + 1;
42746
42887
  return last ? {
42747
42888
  at: last.end ?? 0,
42748
42889
  remove: 0,
42749
- text: `, ${property}`
42890
+ text: `, ${text}`
42750
42891
  } : {
42751
42892
  at: open,
42752
42893
  remove: (object.end ?? 0) - 1 - open,
42753
- text: ` ${property} `
42894
+ text: ` ${text} `
42754
42895
  };
42755
42896
  }
42756
- /**
42757
- * The list added as the last property of an object written over several
42758
- * lines, one entry per line at the indentation the object's properties use.
42759
- * A comma goes after the last property first when it has none.
42760
- */
42761
- function listOnItsOwnLines(source, object, entries) {
42762
- const close = (object.end ?? 0) - 1;
42763
- const last = object.properties.at(-1);
42764
- const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
42765
- const list = `${indent}${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}],\n`;
42766
- return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
42767
- at: last.end ?? 0,
42768
- remove: 0,
42769
- text: ","
42770
- }] : [], beforeClosingBrace(source, close, list)];
42771
- }
42772
42897
  /** Text put before an object's closing brace: on the line above it when the brace has a line of its own. */
42773
42898
  function beforeClosingBrace(source, close, text) {
42774
42899
  const closeLine = lineStart(source, close);
@@ -42856,7 +42981,7 @@ function conflictOf(object, required) {
42856
42981
  }
42857
42982
  /** Why the config's lists clash, given an `experimental` object it can read. */
42858
42983
  function experimentalConflict(object, experimental, required) {
42859
- if (propertyValue(experimental, "serverComponentsExternalPackages")) return `it lists packages under experimental.serverComponentsExternalPackages, which Next.js moves to ${PROPERTY}; keep a single list there`;
42984
+ if (propertyValue(experimental, LEGACY_PROPERTY)) return LEGACY_LIST_REASON;
42860
42985
  return clashReason("transpilePackages", object, "transpilePackages", required) ?? clashReason("experimental.optimizePackageImports", experimental, "optimizePackageImports", required);
42861
42986
  }
42862
42987
  /** The object literal the file exports as its config, or why there is none to edit. */
@@ -42926,58 +43051,6 @@ function editConfig(source, file, required) {
42926
43051
  added: plan.added
42927
43052
  };
42928
43053
  }
42929
- /** The string at `keys` inside a parsed JSON file, or null. */
42930
- function stringAt(json, keys) {
42931
- let value = json;
42932
- for (const key of keys) {
42933
- if (typeof value !== "object" || value === null || !(key in value)) return null;
42934
- value = value[key];
42935
- }
42936
- return typeof value === "string" ? value : null;
42937
- }
42938
- /** A JSON file of the app's, or null when it is missing or does not parse. */
42939
- async function readJsonOrNull(file) {
42940
- try {
42941
- return await import_lib$1.default.readJson(file);
42942
- } catch {
42943
- return null;
42944
- }
42945
- }
42946
- /**
42947
- * A version range that admits one major only, such as `^15.1.0`, `~16.0`,
42948
- * `15.x` or `16.0.0-canary.3`, with the major it admits. A range such as
42949
- * `>=14`, `14 || 15` or `latest` admits several, or names none.
42950
- */
42951
- const SINGLE_MAJOR = /^[\^~]?(\d+)(?:\.(?:\d+|[xX*])){0,2}(?:-[\w.]+)?$/;
42952
- /**
42953
- * The major version of the app's Next.js: the installed one, read from its
42954
- * own `node_modules`, or else the one its package.json range admits when
42955
- * that range admits a single major. Null when neither tells, as when
42956
- * dependencies are not installed yet and the range is `latest`.
42957
- */
42958
- async function nextMajor(cwd) {
42959
- const installed = stringAt(await readJsonOrNull(path.join(cwd, "node_modules", "next", "package.json")), ["version"]);
42960
- const manifest = await readJsonOrNull(path.join(cwd, "package.json"));
42961
- const range = stringAt(manifest, ["dependencies", "next"]) ?? stringAt(manifest, ["devDependencies", "next"]);
42962
- const match = SINGLE_MAJOR.exec((installed ?? range ?? "").trim());
42963
- return match ? Number(match[1]) : null;
42964
- }
42965
- /**
42966
- * Why the option cannot be written for the app's Next.js, and which spelling
42967
- * to print instead, or null when it can. Next.js 14 reads the list under
42968
- * another name, and an unknown version may be 14: writing the top-level list
42969
- * there would report success and externalise nothing.
42970
- */
42971
- function versionBlock(major) {
42972
- if (major === null) return {
42973
- reason: "the version of Next.js the app uses could not be read",
42974
- spelling: "either"
42975
- };
42976
- return major < FIRST_TOP_LEVEL_MAJOR ? {
42977
- reason: `the app runs Next.js ${major}, which reads the list as experimental.serverComponentsExternalPackages and runs instrumentation.ts only with experimental.instrumentationHook`,
42978
- spelling: "legacy"
42979
- } : null;
42980
- }
42981
43054
  /**
42982
43055
  * The Next config file any advice about the app's config names: the one it
42983
43056
  * has, or, when it has none, the one to create. `.mjs` is ESM whatever the
@@ -43029,14 +43102,6 @@ async function createNextConfig(cwd, database) {
43029
43102
  async function patchNextConfig(cwd, database) {
43030
43103
  const file = await findConfigFile(cwd);
43031
43104
  const required = getServerExternalPackages(database);
43032
- const blocked = versionBlock(await nextMajor(cwd));
43033
- if (blocked) return {
43034
- kind: "manual",
43035
- file: file ?? await nextConfigFileToChange(cwd),
43036
- packages: required,
43037
- ...blocked,
43038
- ...file ? {} : { create: true }
43039
- };
43040
43105
  if (!file) return createNextConfig(cwd, database);
43041
43106
  const edited = editConfig(await import_lib$1.default.readFile(path.join(cwd, file), "utf-8"), file, required);
43042
43107
  if ("reason" in edited) return {
@@ -43056,119 +43121,281 @@ async function patchNextConfig(cwd, database) {
43056
43121
  added: edited.added
43057
43122
  };
43058
43123
  }
43059
- /** The setting Nextly needs off, and the one Next.js accepts only with it on. */
43060
- const CACHE_COMPONENTS = "cacheComponents";
43061
- const PARTIAL_PREFETCHING = "partialPrefetching";
43062
43124
  /**
43063
- * Remove a property with the comma that separates it: the whole line when it
43064
- * has one of its own, else the property and the comma after it, or before it
43065
- * when it is the last one.
43125
+ * The exported config object, when every setting this edits can be read in
43126
+ * it; else why not. Not through a wrapper call: Next.js receives what the
43127
+ * wrapper returns, which may set an option again, so the literal passed in
43128
+ * says nothing final.
43066
43129
  */
43067
- function removeProperty(source, property) {
43068
- const start = property.start ?? 0;
43069
- const end = property.end ?? 0;
43070
- const ownLine = /^\s*$/.test(source.slice(lineStart(source, start), start));
43071
- const rest = /^[ \t]*,?[ \t]*\n/.exec(source.slice(end));
43072
- if (ownLine && rest) {
43073
- const from = lineStart(source, start);
43074
- return {
43075
- at: from,
43076
- remove: end + rest[0].length - from,
43077
- text: ""
43078
- };
43079
- }
43080
- const after = /^\s*,\s*/.exec(source.slice(end));
43081
- if (after) return {
43082
- at: start,
43083
- remove: end - start + after[0].length,
43084
- text: ""
43085
- };
43086
- const before = /,\s*$/.exec(source.slice(0, start));
43087
- const from = before ? start - before[0].length : start;
43130
+ function cacheComponentsObject(body) {
43131
+ const object = exportedConfig(body, false);
43132
+ if (typeof object === "string") return object;
43133
+ return hiddenSettings(object, [...ON_OPTIONS, ...HANDLER_SETTINGS], "it") ?? object;
43134
+ }
43135
+ /**
43136
+ * How to turn one option on: the `false` to flip, the property to add when the
43137
+ * config does not set it, nothing when it is on, or why the value cannot be
43138
+ * read.
43139
+ */
43140
+ function optionStep(object, name) {
43141
+ const value = propertyValue(object, name);
43142
+ if (!value) return { add: `${name}: true` };
43143
+ if (value.type !== "BooleanLiteral") return `its ${name} is not true or false written in place`;
43144
+ if (value.value) return {};
43145
+ return { flip: {
43146
+ at: value.start ?? 0,
43147
+ remove: (value.end ?? 0) - (value.start ?? 0),
43148
+ text: "true"
43149
+ } };
43150
+ }
43151
+ /**
43152
+ * Whether Node loads the config as an ES module, where `require` is not
43153
+ * defined: `.mjs` and `.mts` always, and `.js` when it imports or exports,
43154
+ * which a config in a package of `"type": "module"` must, and which Node
43155
+ * detects in any other. Next.js compiles `next.config.ts` to CommonJS.
43156
+ */
43157
+ function loadsAsModule(file, body) {
43158
+ if (/\.m[jt]s$/.test(file)) return true;
43159
+ return file.endsWith(".js") && body.some((statement) => statement.type === "ImportDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration");
43160
+ }
43161
+ /**
43162
+ * The name an ES module config imports `createRequire` under from
43163
+ * `node:module`, whatever it renamed it to, or null when it imports none.
43164
+ */
43165
+ function createRequireImport(body) {
43166
+ 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;
43167
+ }
43168
+ /** The name an import specifier takes from its module, written plain or quoted. */
43169
+ function importedName(specifier) {
43170
+ const { imported } = specifier;
43171
+ return imported.type === "Identifier" ? imported.name : imported.value;
43172
+ }
43173
+ /** Whether the file binds `require` at its top level, by a declaration or an import. */
43174
+ function declaresRequire(body) {
43175
+ return body.some((statement) => importsRequire(statement) || declaresRequireVariable(statement));
43176
+ }
43177
+ /** Whether `statement` imports a binding named `require`: hoisted, so bound anywhere. */
43178
+ function importsRequire(statement) {
43179
+ return statement.type === "ImportDeclaration" && statement.specifiers.some((specifier) => isNamed(specifier.local, "require"));
43180
+ }
43181
+ /** The declarators in `statement` that name a variable `require`, exported or not. */
43182
+ function requireDeclarators(statement) {
43183
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
43184
+ return declaration?.type === "VariableDeclaration" ? declaration.declarations.filter((declarator) => isNamed(declarator.id, "require")) : [];
43185
+ }
43186
+ /** Whether `statement` declares a variable named `require`, exported or not. */
43187
+ function declaresRequireVariable(statement) {
43188
+ return requireDeclarators(statement).length > 0;
43189
+ }
43190
+ /**
43191
+ * Whether the file binds `require` only by a variable declared after `at`,
43192
+ * where the config object starts. The object's `require.resolve` runs when
43193
+ * the object is evaluated, while that declaration is still in its temporal dead
43194
+ * zone, so it would throw: the lines are printed rather than written.
43195
+ */
43196
+ function declaresRequireAfter(body, at) {
43197
+ if (body.some(importsRequire)) return false;
43198
+ const declarators = body.flatMap(requireDeclarators);
43199
+ return declarators.length > 0 && declarators.every((declarator) => (declarator.start ?? 0) >= at);
43200
+ }
43201
+ /**
43202
+ * What an ES module config needs at its top for the handlers' `require`:
43203
+ * nothing when it binds `require` already, the declaration alone, calling its
43204
+ * own `createRequire` by its name, when it imports one, and otherwise the
43205
+ * import and the declaration.
43206
+ */
43207
+ function requireSetupFor(body) {
43208
+ if (declaresRequire(body)) return null;
43209
+ const imported = createRequireImport(body);
43210
+ return imported ? requireDeclaration(imported) : `${REQUIRE_IMPORT}\n\n${REQUIRE_DECLARATION}`;
43211
+ }
43212
+ /**
43213
+ * The edit that gives an ES module config `require`, before the first
43214
+ * statement that runs, and before the comments that lead it, such as a
43215
+ * `@type` annotation. Imports are hoisted, so one written further down still
43216
+ * loads first; the declaration is not, and a `require.resolve` evaluated
43217
+ * above it would throw.
43218
+ */
43219
+ function requireEdit(source, body, setup) {
43220
+ const first = body.find((statement) => statement.type !== "ImportDeclaration");
43088
43221
  return {
43089
- at: from,
43090
- remove: end - from,
43091
- text: ""
43222
+ at: lineStart(source, first?.leadingComments?.[0]?.start ?? first?.start ?? 0),
43223
+ remove: 0,
43224
+ text: `${setup}\n\n`
43092
43225
  };
43093
43226
  }
43094
- /** The exported config object, when both settings in it can be read; else why not. */
43095
- function cacheComponentsObject(source, file) {
43096
- const body = parseBody(source, file);
43097
- if (!body) return "it could not be parsed";
43098
- const object = exportedConfig(body, false);
43099
- if (typeof object === "string") return object;
43100
- return hiddenSettings(object, [CACHE_COMPONENTS, PARTIAL_PREFETCHING], "it") ?? object;
43227
+ /** Whether `node` is `require.resolve("<specifier>")`. */
43228
+ function resolvesTo(node, specifier) {
43229
+ if (node?.type !== "CallExpression") return false;
43230
+ const { callee, arguments: args } = node;
43231
+ const [only] = args;
43232
+ return callee.type === "MemberExpression" && isNamed(callee.object, "require") && isNamed(callee.property, "resolve") && args.length === 1 && only?.type === "StringLiteral" && only.value === specifier;
43101
43233
  }
43102
- /** The `true` to flip, null when Cache Components is off or unset, or why the value cannot be read. */
43103
- function enabledValue(object) {
43104
- const value = propertyValue(object, CACHE_COMPONENTS);
43105
- if (!value) return null;
43106
- if (value.type !== "BooleanLiteral") return `its ${CACHE_COMPONENTS} is not true or false written in place`;
43107
- return value.value ? value : null;
43234
+ /** Whether the config's cache settings are Nextly's handlers, as this writes them. */
43235
+ function hasNextlysHandlers(object) {
43236
+ const handlers = propertyValue(object, "cacheHandlers");
43237
+ const memory = propertyValue(object, "cacheMaxMemorySize");
43238
+ return resolvesTo(propertyValue(object, "cacheHandler"), "nextly/cache-handler") && handlers?.type === "ObjectExpression" && resolvesTo(propertyValue(handlers, "default"), "nextly/cache-handlers") && memory?.type === "NumericLiteral" && memory.value === 0;
43108
43239
  }
43109
- /** The edits that turn Cache Components off in a config, none when it is off, or why it cannot be changed. */
43110
- function cacheComponentsPlan(source, file) {
43111
- const object = cacheComponentsObject(source, file);
43112
- if (typeof object === "string") return { reason: object };
43113
- const value = enabledValue(object);
43114
- if (typeof value === "string") return { reason: value };
43115
- if (!value) return {
43240
+ /** The settings that make a cache of the app's own: a handler for either cache. */
43241
+ const OWN_CACHE_SETTINGS = ["cacheHandler", "cacheHandlers"];
43242
+ /** Whether the config gives `name` a value: written, and not `undefined`. */
43243
+ function setsValue(object, name) {
43244
+ const value = propertyValue(object, name);
43245
+ return value !== void 0 && !isNamed(value, "undefined");
43246
+ }
43247
+ /**
43248
+ * Why Nextly's handlers cannot go into this config as it stands, or null when
43249
+ * they can. A setting written as `undefined` is a placeholder to fill, not a
43250
+ * value; a handler of the app's own is not replaced; and `cacheMaxMemorySize`
43251
+ * must be 0, so Next.js keeps no second copy of the handlers' entries.
43252
+ */
43253
+ function handlerConflict(object) {
43254
+ const placeholders = HANDLER_SETTINGS.filter((name) => propertyValue(object, name) !== void 0 && !setsValue(object, name));
43255
+ if (placeholders.length > 0) return `it sets ${placeholders.join(", ")} to undefined`;
43256
+ const own = OWN_CACHE_SETTINGS.filter((name) => setsValue(object, name));
43257
+ if (own.length > 0) return `it has cache handlers of its own (${own.join(", ")}), which were kept`;
43258
+ const memory = propertyValue(object, "cacheMaxMemorySize");
43259
+ 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;
43260
+ }
43261
+ /**
43262
+ * How to install Nextly's cache handlers: the properties to add and any edit
43263
+ * that gives the file `require`, or why they are left out, with the lines to
43264
+ * put in place of the config's own settings.
43265
+ */
43266
+ function handlerStep(source, object, body, esm) {
43267
+ const none = {
43268
+ add: [],
43269
+ edits: []
43270
+ };
43271
+ if (hasNextlysHandlers(object)) return {
43272
+ ...none,
43273
+ outcome: "present"
43274
+ };
43275
+ const setup = esm ? requireSetupFor(body) : null;
43276
+ 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);
43277
+ if (conflict) return {
43278
+ ...none,
43279
+ outcome: {
43280
+ skipped: conflict,
43281
+ lines: HANDLER_PROPERTIES,
43282
+ requireSetup: setup
43283
+ }
43284
+ };
43285
+ const memorySet = propertyValue(object, "cacheMaxMemorySize") !== void 0;
43286
+ const add = HANDLER_PROPERTIES.filter((property) => !(memorySet && property.startsWith("cacheMaxMemorySize")));
43287
+ if (setup === null) return {
43288
+ add,
43116
43289
  edits: [],
43117
- removed: []
43290
+ outcome: "added"
43118
43291
  };
43119
- const flip = {
43120
- at: value.start ?? 0,
43121
- remove: (value.end ?? 0) - (value.start ?? 0),
43122
- text: "false"
43292
+ if (referenceCount("require", body) > 0 || createRequireImport(body) === null && referenceCount("createRequire", body) > 0) return {
43293
+ ...none,
43294
+ outcome: {
43295
+ 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",
43296
+ lines: add,
43297
+ requireSetup: setup
43298
+ }
43299
+ };
43300
+ return {
43301
+ add,
43302
+ edits: [requireEdit(source, body, setup)],
43303
+ outcome: "added"
43123
43304
  };
43124
- const prefetching = object.properties.filter((property) => isPropertyNamed(property, PARTIAL_PREFETCHING));
43305
+ }
43306
+ /** The edits that turn Cache Components on in a config, or why it cannot be changed. */
43307
+ function cacheComponentsPlan(source, file, esm) {
43308
+ const body = parseBody(source, file);
43309
+ if (!body) return { reason: "it could not be parsed" };
43310
+ const object = cacheComponentsObject(body);
43311
+ if (typeof object === "string") return { reason: object };
43312
+ const options = optionsPlan(object);
43313
+ if ("reason" in options) return options;
43314
+ const handlers = handlerStep(source, object, body, esm);
43315
+ const add = [...options.add, ...handlers.add];
43316
+ const edits = [...options.flips, ...handlers.edits];
43317
+ if (add.length > 0) edits.push(...appendProperties(source, object, () => add));
43125
43318
  return {
43126
- edits: [flip, ...prefetching.map((property) => removeProperty(source, property))],
43127
- removed: prefetching.length > 0 ? [PARTIAL_PREFETCHING] : []
43319
+ edits,
43320
+ turnedOn: options.turnedOn,
43321
+ handlers: handlers.outcome
43128
43322
  };
43129
43323
  }
43130
- /** Whether a config, read as code, still defines `partialPrefetching` anywhere in its exported object. */
43131
- function definesPartialPrefetching(source, file) {
43132
- const object = cacheComponentsObject(source, file);
43133
- return typeof object !== "string" && object.properties.some((property) => isPropertyNamed(property, PARTIAL_PREFETCHING));
43324
+ /** How to turn both options on: the values to flip, the properties to add, and which options change. */
43325
+ function optionsPlan(object) {
43326
+ const plan = {
43327
+ flips: [],
43328
+ add: [],
43329
+ turnedOn: []
43330
+ };
43331
+ for (const option of ON_OPTIONS) {
43332
+ const step = optionStep(object, option);
43333
+ if (typeof step === "string") return { reason: step };
43334
+ if (step.flip) plan.flips.push(step.flip);
43335
+ if (step.add) plan.add.push(step.add);
43336
+ if (step.flip || step.add) plan.turnedOn.push(option);
43337
+ }
43338
+ return plan;
43134
43339
  }
43135
43340
  /**
43136
- * Turn Next.js Cache Components off in an existing app's config, which
43137
- * Nextly does not support yet; `create-next-app` 16.4 turns it on by default.
43341
+ * Turn Next.js Cache Components and Partial Prefetching on in an existing
43342
+ * app's config, with Nextly's cache handlers: Nextly runs with both on.
43343
+ * Apps that an earlier Nextly release turned off are turned on again.
43138
43344
  *
43139
- * Only `cacheComponents: true` written in place is changed, and
43140
- * `partialPrefetching` goes with it, since Next.js refuses that option once
43141
- * Cache Components is off. The edit is trusted only once the result parses
43142
- * and reads as off; anything else is left as it was, with what to change.
43345
+ * Only settings written in place as `true` or `false`, or absent, are
43346
+ * changed; a value this cannot read leaves the file as it was, with what to
43347
+ * add. The edit is trusted only once the result parses and reads as on.
43143
43348
  */
43144
- async function turnOffCacheComponents(cwd) {
43145
- const file = await findConfigFile(cwd);
43146
- if (!file) return { kind: "unchanged" };
43147
- const source = await import_lib$1.default.readFile(path.join(cwd, file), "utf-8");
43148
- const plan = cacheComponentsPlan(source, file);
43349
+ async function turnOnCacheComponents(cwd) {
43350
+ const file = await nextConfigFileToChange(cwd);
43351
+ const source = await import_lib$1.default.readFile(path.join(cwd, file), "utf-8").catch(() => null);
43352
+ if (source === null) return {
43353
+ kind: "manual",
43354
+ file,
43355
+ reason: "the app has no Next config",
43356
+ requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
43357
+ };
43358
+ const body = parseBody(source, file);
43359
+ if (!body) return {
43360
+ kind: "manual",
43361
+ file,
43362
+ reason: "it could not be parsed",
43363
+ requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
43364
+ };
43365
+ const esm = loadsAsModule(file, body);
43366
+ const requireSetup = esm ? requireSetupFor(body) : null;
43367
+ const plan = cacheComponentsPlan(source, file, esm);
43149
43368
  if ("reason" in plan) return {
43150
43369
  kind: "manual",
43151
43370
  file,
43152
- reason: plan.reason
43371
+ reason: plan.reason,
43372
+ requireSetup
43373
+ };
43374
+ if (plan.edits.length === 0) return {
43375
+ kind: "unchanged",
43376
+ file,
43377
+ handlers: plan.handlers
43153
43378
  };
43154
- if (plan.edits.length === 0) return { kind: "unchanged" };
43155
43379
  const text = applyEdits(source, plan.edits);
43156
- const recheck = cacheComponentsPlan(text, file);
43157
- if ("reason" in recheck || recheck.edits.length > 0 || definesPartialPrefetching(text, file)) return {
43380
+ const recheck = cacheComponentsPlan(text, file, esm);
43381
+ if ("reason" in recheck || recheck.edits.length > 0) return {
43158
43382
  kind: "manual",
43159
43383
  file,
43160
- reason: "the edit could not be confirmed"
43384
+ reason: "the edit could not be confirmed",
43385
+ requireSetup
43161
43386
  };
43162
43387
  await import_lib$1.default.writeFile(path.join(cwd, file), text, "utf-8");
43163
43388
  return {
43164
- kind: "turned-off",
43389
+ kind: "turned-on",
43165
43390
  file,
43166
- removed: plan.removed
43391
+ turnedOn: plan.turnedOn,
43392
+ handlers: plan.handlers
43167
43393
  };
43168
43394
  }
43169
43395
  /**
43170
- * What turning Cache Components off did, for the run's report: the change and
43171
- * a notice saying why, or, for a config it could not change, what to change.
43396
+ * What turning Cache Components on did, for the run's report: each change and
43397
+ * a notice saying why, or, for a config it could not change, the exact lines
43398
+ * to add.
43172
43399
  */
43173
43400
  function cacheComponentsFiles(patch) {
43174
43401
  const files = {
@@ -43176,43 +43403,42 @@ function cacheComponentsFiles(patch) {
43176
43403
  changed: [],
43177
43404
  skipped: []
43178
43405
  };
43179
- if (patch.kind === "turned-off") {
43406
+ if (patch.kind === "manual") {
43407
+ files.skipped.push({
43408
+ path: patch.file,
43409
+ 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`,
43410
+ toAdd: linesToAdd$1(ON_LINES, patch.requireSetup)
43411
+ });
43412
+ return files;
43413
+ }
43414
+ const handlersAdded = patch.handlers === "added";
43415
+ if (patch.kind === "turned-on") {
43180
43416
  files.changed.push({
43181
43417
  path: patch.file,
43182
- added: [`${CACHE_COMPONENTS}: false`]
43418
+ added: [...patch.turnedOn.map((option) => `${option}: true`), ...handlersAdded ? [...HANDLER_SETTINGS] : []]
43183
43419
  });
43184
- const removed = patch.removed.length ? `, and removed ${patch.removed.join(", ")}, which Next.js accepts only with it on` : "";
43185
- 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}`];
43186
- } else if (patch.kind === "manual") files.skipped.push({
43420
+ const done = [...patch.turnedOn.length > 0 ? [`turned ${patch.turnedOn.join(" and ")} on`] : [], ...handlersAdded ? ["installed Nextly's cache handlers"] : []].join(" and ");
43421
+ files.notices = [`In ${patch.file}, ${done}: Nextly runs with Cache Components and Partial Prefetching on. What each setting does: ${CACHE_COMPONENTS_DOCS_URL}`];
43422
+ }
43423
+ if (typeof patch.handlers === "object") files.skipped.push({
43187
43424
  path: patch.file,
43188
- 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}`,
43189
- toAdd: ` ${CACHE_COMPONENTS}: false,`
43425
+ 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`,
43426
+ toAdd: linesToAdd$1(patch.handlers.lines.map((property) => ` ${property},`), patch.handlers.requireSetup)
43190
43427
  });
43191
43428
  return files;
43192
43429
  }
43193
43430
  /**
43194
- * What to add by hand, in the spelling the app's Next.js reads.
43195
- *
43196
- * On Next.js 14 the package list and the instrumentation opt-in are keys of
43197
- * one `experimental` object, so they are given as one: two `experimental`
43198
- * properties, pasted as given, leave only the second. The instrumentation
43199
- * hook Nextly starts from needs the opt-in there, and no other step says so.
43200
- * `wrap` turns each spelling into what to paste: the key itself, or a whole
43201
- * config file when the app has none.
43202
- */
43203
- function optionText(packages, spelling, wrap = (option) => option) {
43204
- const list = packages.map((name) => JSON.stringify(name)).join(", ");
43205
- const current = wrap(` ${PROPERTY}: [${list}],`);
43206
- const legacy = wrap(` experimental: { instrumentationHook: true, serverComponentsExternalPackages: [${list}] },`);
43207
- if (spelling === "legacy") return legacy;
43208
- return spelling === "either" ? `On Next.js 15 or later:\n\n${current}\n\nOn Next.js 14:\n\n${legacy}` : current;
43209
- }
43210
- /**
43211
- * A whole config holding `option`, for an app that has none. ESM in `.mjs`,
43212
- * which loads whatever the package's own "type" says.
43431
+ * The config lines to add by hand. An ES module config with no `require`
43432
+ * also gets the lines that define it for the handlers, at the top of the file.
43213
43433
  */
43214
- function wholeConfig(option) {
43215
- return `/** @type {import('next').NextConfig} */\nexport default {\n${option}\n};\n`;
43434
+ function linesToAdd$1(lines, requireSetup) {
43435
+ const config = lines.join("\n");
43436
+ return requireSetup ? `// At the top of the file:\n${requireSetup.replace("\n\n", "\n")}\n\n// In the config object:\n${config}` : config;
43437
+ }
43438
+ /** What to add by hand: the list, as the config's top-level option. */
43439
+ function optionText(packages) {
43440
+ const list = packages.map((name) => JSON.stringify(name)).join(", ");
43441
+ return ` ${PROPERTY}: [${list}],`;
43216
43442
  }
43217
43443
  /**
43218
43444
  * What a patch did, in the shape every existing-app step reports in: the
@@ -43236,109 +43462,14 @@ function nextConfigFiles(patch) {
43236
43462
  files.written.push(patch.file);
43237
43463
  break;
43238
43464
  case "unchanged": break;
43239
- case "manual": files.skipped.push(patch.create ? {
43465
+ case "manual": files.skipped.push({
43240
43466
  path: patch.file,
43241
- why: `Nextly's packages were not added: ${patch.reason}, and the app has no Next.js config. Create ${patch.file} with:`,
43242
- toAdd: optionText(patch.packages, patch.spelling, wholeConfig)
43243
- } : {
43244
- path: patch.file,
43245
- 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`,
43246
- toAdd: optionText(patch.packages, patch.spelling)
43467
+ 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`,
43468
+ toAdd: optionText(patch.packages)
43247
43469
  });
43248
43470
  }
43249
43471
  return files;
43250
43472
  }
43251
- // src/generators/page-extensions.ts
43252
- /**
43253
- * The page extension to name a new module file with, a hook or a route: a
43254
- * TypeScript one first, since Nextly's files are TypeScript, then a
43255
- * JavaScript one; never a content extension such as `mdx`, which Next.js
43256
- * would compile as content, and never `cjs`, since the files are ES modules
43257
- * and a `.cjs` file cannot hold one. Null when the list holds no extension a
43258
- * module can be written in.
43259
- */
43260
- function moduleExtension(extensions) {
43261
- const usable = writable(extensions);
43262
- return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
43263
- }
43264
- /**
43265
- * The extensions a file with code in it can be written under: not a
43266
- * declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
43267
- * types only and refuses code in.
43268
- */
43269
- function writable(extensions) {
43270
- return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
43271
- }
43272
- /**
43273
- * The `pageExtensions` the app's Next.js config sets, as every existing-app
43274
- * step that names a file Next.js routes reads it: the list; `"absent"` when
43275
- * the config sets none, or there is no config; or null when it is set in a
43276
- * way that cannot be read. Read from the config the app exports, as the Next
43277
- * config step reads it, so a commented-out or nested `pageExtensions` sets
43278
- * nothing. `configName` is the file it was read from, for the report.
43279
- */
43280
- async function appPageExtensions(cwd) {
43281
- const nextConfig = await readNextConfig(cwd);
43282
- return {
43283
- configName: nextConfig?.name ?? "The Next.js config",
43284
- extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
43285
- };
43286
- }
43287
- /**
43288
- * The text of the app's Next.js config, or null when it has none, from the
43289
- * file the Next config step reads and extends ({@link findConfigFile}).
43290
- */
43291
- async function readNextConfig(cwd) {
43292
- const name = await findConfigFile(cwd);
43293
- return name === null ? null : {
43294
- name,
43295
- text: await import_lib$1.default.readFile(path.join(cwd, name), "utf-8")
43296
- };
43297
- }
43298
- /**
43299
- * The page extension for a file of TypeScript with JSX, such as a page or a
43300
- * layout: one ending in `tsx`. Null when the list holds none.
43301
- */
43302
- function tsxExtension(extensions) {
43303
- return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
43304
- }
43305
- /** A JavaScript or TypeScript module without JSX, such as a route. */
43306
- const MODULE_FILE = {
43307
- pick: moduleExtension,
43308
- holds: "a JavaScript or TypeScript extension"
43309
- };
43310
- /** TypeScript with JSX, such as a page or a layout. */
43311
- const TSX_FILE = {
43312
- pick: tsxExtension,
43313
- holds: "an extension ending in tsx"
43314
- };
43315
- /**
43316
- * The report for a file an app that sets `pageExtensions` gets in place of
43317
- * the file: the name to create it under and its content. Next.js routes a
43318
- * file there only under a name with one of the app's extensions, and the app
43319
- * may already have the file under such a name, which the existing-app steps
43320
- * do not look for, so nothing is written. A list that cannot be read, or that
43321
- * holds no extension the content needs, gives a placeholder name and says
43322
- * which.
43323
- *
43324
- * @param base the file's path without its extension, as reported, such as
43325
- * `app/admin/[[...params]]/page`
43326
- */
43327
- function pageExtensionsSkip(base, text, pages, need) {
43328
- const { configName, extensions } = pages;
43329
- const extension = extensions === null ? null : need.pick(extensions);
43330
- const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
43331
- if (extensions !== null && extension === null) return {
43332
- path: `${base}.<${need.holds} added to pageExtensions>`,
43333
- 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`,
43334
- toAdd: text
43335
- };
43336
- return {
43337
- path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
43338
- why: `${sets}; unless the app already has this file, create it under that name with this content`,
43339
- toAdd: text
43340
- };
43341
- }
43342
43473
  // src/utils/detect-pm-from-user-agent.ts
43343
43474
  /**
43344
43475
  * Detect the package manager that invoked `create-nextly-app` by reading
@@ -45569,6 +45700,45 @@ function getDefaultApproach(name) {
45569
45700
  function shouldUseBundledTemplate(projectType, options) {
45570
45701
  return BUNDLED_TEMPLATES.some((name) => name === projectType) && !options.localTemplatePath && !options.useYalc && options.branch === void 0;
45571
45702
  }
45703
+ // src/supported-versions.ts
45704
+ /**
45705
+ * The Next.js and React this release of Nextly supports, in one place.
45706
+ *
45707
+ * Two facts, deliberately kept apart:
45708
+ *
45709
+ * - `REVIEWED_VERSIONS` is the newest release of each package this Nextly
45710
+ * release was reviewed against. A new app declares it as a `~` range
45711
+ * ({@link scaffoldRange}): patch releases of that minor, never a newer minor
45712
+ * or major that nobody has checked, and never whatever npm's `latest` names
45713
+ * on the day of the scaffold.
45714
+ * - `SUPPORTED_RANGES` is every release Nextly works with: the `nextly`
45715
+ * package's own peer ranges, inlined at build, so they are declared once.
45716
+ * Every other package declares the same as its peers. Adding Nextly to an
45717
+ * existing app refuses one whose Next.js, React or React DOM falls outside
45718
+ * them, and a scaffolded plugin declares them as its own peers.
45719
+ *
45720
+ * Only a release review moves either: a newer Next.js or React is supported
45721
+ * once a Nextly release has been checked against it, not before.
45722
+ */
45723
+ const REVIEWED_VERSIONS = {
45724
+ next: "16.4.2",
45725
+ "eslint-config-next": "16.4.2",
45726
+ react: "19.3.1",
45727
+ "react-dom": "19.3.1"
45728
+ };
45729
+ const SUPPORTED_RANGES = {
45730
+ "next": "^16.4.1",
45731
+ "react": "^19.3.0",
45732
+ "react-dom": "^19.3.0"
45733
+ };
45734
+ /**
45735
+ * The range a new project declares for `pkg`: patch releases of the reviewed
45736
+ * minor only. Derived here rather than written out, so the reviewed version is
45737
+ * the one thing a release review edits.
45738
+ */
45739
+ function scaffoldRange(pkg) {
45740
+ return `~${REVIEWED_VERSIONS[pkg]}`;
45741
+ }
45572
45742
  // src/utils/package-manager-commands.ts
45573
45743
  /**
45574
45744
  * The prefix that runs a package.json script.
@@ -45596,37 +45766,35 @@ function binaryRunner(packageManager) {
45596
45766
  case "bun": return "bunx";
45597
45767
  }
45598
45768
  }
45599
- // src/utils/plugin-node.ts
45600
- /**
45601
- * The Node versions a scaffolded plugin supports, and the check that refuses
45602
- * to scaffold one on a Node it cannot build on.
45603
- *
45604
- * A plugin builds with tsdown and tests with Vitest, and each declares the
45605
- * Node it runs on; the plugin's floor is where both agree. The repository's
45606
- * packages have a lower floor, because only a plugin author runs these tools.
45607
- * `scaffold-plugin.test.ts` derives this range from the installed tools' own
45608
- * `engines`, so a version bump that moves either one fails there.
45609
- */
45610
- const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
45769
+ // src/utils/version-range.ts
45611
45770
  function parseVersion$1(text) {
45612
- const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text.trim());
45613
- if (!match) throw new Error(`not a Node version: ${text}`);
45614
- return [
45771
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/.exec(text.trim());
45772
+ if (!match) throw new Error(`not a version: ${text}`);
45773
+ return Object.assign([
45615
45774
  Number(match[1]),
45616
45775
  Number(match[2]),
45617
45776
  Number(match[3])
45618
- ];
45777
+ ], { prerelease: match[4] !== void 0 });
45619
45778
  }
45779
+ /** Older first; of two with the same numbers, a prerelease comes before its release, as in semver. */
45620
45780
  function compare(a, b) {
45621
- return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
45781
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || Number(Boolean(b.prerelease)) - Number(Boolean(a.prerelease));
45622
45782
  }
45623
45783
  /**
45624
- * Whether `version` satisfies one comparator. Only the two forms these tools
45625
- * write are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
45626
- * than read approximately, so a tool that starts declaring another form fails
45784
+ * Negative when `a` is older than `b`, positive when newer, 0 when equal.
45785
+ * A prerelease is older than its release: `16.4.1-canary.1` is below `16.4.1`.
45786
+ */
45787
+ function compareVersions(a, b) {
45788
+ return compare(parseVersion$1(a), parseVersion$1(b));
45789
+ }
45790
+ /**
45791
+ * Whether `version` satisfies one comparator. Only the two forms this CLI
45792
+ * writes are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
45793
+ * than read approximately, so a range that starts using another form fails
45627
45794
  * the test that reads it.
45628
45795
  */
45629
45796
  function satisfiesComparator(version, comparator) {
45797
+ if (version.prerelease) return false;
45630
45798
  const caret = /^\^(\d+\.\d+\.\d+)$/.exec(comparator);
45631
45799
  if (caret) {
45632
45800
  const floor = parseVersion$1(caret[1]);
@@ -45634,20 +45802,82 @@ function satisfiesComparator(version, comparator) {
45634
45802
  }
45635
45803
  const atLeast = /^>=(\d+\.\d+\.\d+)$/.exec(comparator);
45636
45804
  if (atLeast) return compare(version, parseVersion$1(atLeast[1])) >= 0;
45637
- throw new Error(`unsupported Node range comparator: ${comparator}`);
45805
+ throw new Error(`unsupported range comparator: ${comparator}`);
45638
45806
  }
45639
45807
  /** Whether `version` satisfies a `||` list of `^X.Y.Z` / `>=X.Y.Z` comparators. */
45640
- function satisfiesNodeRange(version, range) {
45808
+ function satisfiesRange(version, range) {
45641
45809
  const parsed = parseVersion$1(version);
45642
45810
  return range.split("||").map((part) => part.trim()).some((comparator) => satisfiesComparator(parsed, comparator));
45643
45811
  }
45644
45812
  /**
45813
+ * The lowest version a package.json dependency spec admits, as `X.Y.Z`, or
45814
+ * `undefined` when the spec names no lower bound this can read: a dist-tag
45815
+ * (`latest`, `canary`), a protocol (`workspace:*`, `file:`, a URL), `*`, or an
45816
+ * upper bound alone (`<17`). An exact version is its own lowest; `^`, `~`, `=`
45817
+ * and `>=` start at the version they name; a missing or wildcard minor or
45818
+ * patch (`15`, `15.x`) counts as 0; across `||` the lowest alternative wins.
45819
+ */
45820
+ function lowestVersionIn(spec) {
45821
+ let lowest;
45822
+ for (const part of spec.split("||")) {
45823
+ const match = /^\s*(?:\^|~|>=|=)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?/.exec(part);
45824
+ if (!match) continue;
45825
+ const number = (text) => text === void 0 || !/^\d+$/.test(text) ? 0 : Number(text);
45826
+ const found = [
45827
+ Number(match[1]),
45828
+ number(match[2]),
45829
+ number(match[3])
45830
+ ];
45831
+ if (!lowest || compare(found, lowest) < 0) lowest = found;
45832
+ }
45833
+ return lowest ? lowest.join(".") : void 0;
45834
+ }
45835
+ /**
45836
+ * The highest major a package.json dependency spec admits, `Infinity` when it
45837
+ * has no upper bound (`>=16.4.1`, `*`, `x`), or `undefined` when no
45838
+ * alternative names one this can read (a dist-tag, a protocol). Across `||`
45839
+ * the highest alternative wins, since an install resolves the newest version
45840
+ * any of them admits. Within one alternative an upper bound (`<17`, `<=17.2`)
45841
+ * decides; otherwise the version a `^`, `~`, `=` or bare spec names.
45842
+ */
45843
+ function highestMajorIn(spec) {
45844
+ let highest;
45845
+ for (const part of spec.split("||")) {
45846
+ const found = majorReachedBy(part.trim());
45847
+ if (found !== void 0 && (highest === void 0 || found > highest)) highest = found;
45848
+ }
45849
+ return highest;
45850
+ }
45851
+ /** The highest major one `||` alternative reaches, as `highestMajorIn` reads it. */
45852
+ function majorReachedBy(part) {
45853
+ const upper = /<(=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(part);
45854
+ if (upper) {
45855
+ const [, inclusive, major, minor, patch] = upper;
45856
+ return inclusive !== void 0 || Number(minor ?? 0) > 0 || Number(patch ?? 0) > 0 ? Number(major) : Number(major) - 1;
45857
+ }
45858
+ if (part === "" || /^(\*|[xX])$/.test(part) || part.startsWith(">")) return Infinity;
45859
+ const named = /^(?:\^|~|=)?\s*v?(\d+)/.exec(part);
45860
+ return named ? Number(named[1]) : void 0;
45861
+ }
45862
+ // src/utils/plugin-node.ts
45863
+ /**
45864
+ * The Node versions a scaffolded plugin supports, and the check that refuses
45865
+ * to scaffold one on a Node it cannot build on.
45866
+ *
45867
+ * A plugin builds with tsdown and tests with Vitest, and each declares the
45868
+ * Node it runs on; the plugin's floor is where both agree. The repository's
45869
+ * packages have a lower floor, because only a plugin author runs these tools.
45870
+ * `scaffold-plugin.test.ts` derives this range from the installed tools' own
45871
+ * `engines`, so a version bump that moves either one fails there.
45872
+ */
45873
+ const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
45874
+ /**
45645
45875
  * Why the plugin template cannot be scaffolded on `nodeVersion`, or
45646
45876
  * `undefined` when it can. Checked before anything is written, since a plugin
45647
45877
  * scaffolded on an older Node would install and then fail its first build.
45648
45878
  */
45649
45879
  function pluginNodeRefusal(nodeVersion) {
45650
- if (satisfiesNodeRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
45880
+ if (satisfiesRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
45651
45881
  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.`;
45652
45882
  }
45653
45883
  // src/utils/template.ts
@@ -46292,14 +46522,12 @@ async function replacePlaceholders(dir, placeholders, skip = /* @__PURE__ */ new
46292
46522
  /**
46293
46523
  * Pinned dependency versions for generated projects.
46294
46524
  *
46295
- * `next` and `eslint-config-next` are resolved at runtime from the npm
46296
- * registry so that fresh projects always get the latest release without
46297
- * needing to republish create-nextly-app. The remaining versions use
46298
- * wide semver ranges that rarely need updating.
46525
+ * `next`, `eslint-config-next`, `react` and `react-dom` are not here: they
46526
+ * come from `supported-versions`, the reviewed release of each, so a project
46527
+ * never declares a Next.js or React no Nextly release was checked against.
46528
+ * The remaining versions use wide semver ranges that rarely need updating.
46299
46529
  */
46300
46530
  const PINNED_VERSIONS = {
46301
- react: "^19.3.1",
46302
- "react-dom": "^19.3.1",
46303
46531
  typescript: "^5",
46304
46532
  "@types/node": "^22",
46305
46533
  "@types/react": "^19",
@@ -46317,11 +46545,6 @@ const PINNED_VERSIONS = {
46317
46545
  */
46318
46546
  const CONFIG_TOOLING_DEV_DEPENDENCIES = [`esbuild@${PINNED_VERSIONS.esbuild}`];
46319
46547
  /**
46320
- * Packages whose latest version is fetched from the npm registry at
46321
- * runtime so the CLI always scaffolds with the newest release.
46322
- */
46323
- const RUNTIME_RESOLVED_PACKAGES = ["next", "eslint-config-next"];
46324
- /**
46325
46548
  * @nextlyhq packages whose latest version is fetched from npm at runtime.
46326
46549
  * This avoids having to republish create-nextly-app every time a
46327
46550
  * dependency package is updated.
@@ -46345,9 +46568,10 @@ const resolvedNextlyVersions = /* @__PURE__ */ new Map();
46345
46568
  * On any failure (non-OK response, timeout, missing tag, thrown error) it
46346
46569
  * returns the requested dist-tag NAME itself (e.g. "alpha") — a valid npm
46347
46570
  * install spec — so a project on the alpha line never silently drops to
46348
- * `latest`.
46571
+ * `latest`. The channel has no default: only the Nextly packages are read
46572
+ * from the registry, always on a tag their caller names.
46349
46573
  */
46350
- async function fetchLatestVersion(pkg, channel = "latest") {
46574
+ async function fetchLatestVersion(pkg, channel) {
46351
46575
  try {
46352
46576
  const res = await fetch(`https://registry.npmjs.org/-/package/${encodeURIComponent(pkg)}/dist-tags`, { signal: AbortSignal.timeout(5e3) });
46353
46577
  if (!res.ok) return channel;
@@ -46381,26 +46605,6 @@ async function nextlyVersions(source) {
46381
46605
  if (source.kind === "release") return Object.fromEntries(NEXTLY_PACKAGES.map((pkg) => [pkg, source.cliVersion]));
46382
46606
  return resolveNextlyVersions("alpha");
46383
46607
  }
46384
- /** Cache for runtime-resolved package versions (next, eslint-config-next). */
46385
- let resolvedRuntimeVersions = null;
46386
- /**
46387
- * Resolve latest versions for Next.js ecosystem packages in parallel.
46388
- * Falls back to a safe semver range if the registry is unreachable.
46389
- */
46390
- async function resolveRuntimeVersions() {
46391
- if (resolvedRuntimeVersions) return resolvedRuntimeVersions;
46392
- const NEXT_FLOOR = "^16.4.1";
46393
- const FALLBACKS = {
46394
- next: NEXT_FLOOR,
46395
- "eslint-config-next": NEXT_FLOOR
46396
- };
46397
- const entries = await Promise.all(RUNTIME_RESOLVED_PACKAGES.map(async (pkg) => {
46398
- const version = await fetchLatestVersion(pkg);
46399
- return [pkg, version === "latest" ? FALLBACKS[pkg] : version];
46400
- }));
46401
- resolvedRuntimeVersions = Object.fromEntries(entries);
46402
- return resolvedRuntimeVersions;
46403
- }
46404
46608
  /**
46405
46609
  * Generate a `package.json` string for a fresh Nextly project.
46406
46610
  *
@@ -46419,11 +46623,10 @@ async function resolveRuntimeVersions() {
46419
46623
  */
46420
46624
  async function generatePackageJson(projectName, database, source = nextlySource({ cliVersion: packageVersion() }), projectType = "blank", templateDirs = [], targetDir) {
46421
46625
  if (projectType === "plugin") return generatePluginPackageJson(projectName, source);
46422
- const runtimeVersions = await resolveRuntimeVersions();
46423
46626
  const dependencies = {
46424
- next: runtimeVersions.next,
46425
- react: PINNED_VERSIONS.react,
46426
- "react-dom": PINNED_VERSIONS["react-dom"]
46627
+ next: scaffoldRange("next"),
46628
+ react: scaffoldRange("react"),
46629
+ "react-dom": scaffoldRange("react-dom")
46427
46630
  };
46428
46631
  dependencies["@tanstack/react-query"] = "^5.62.0";
46429
46632
  dependencies["lucide-react"] = "^0.544.0";
@@ -46447,7 +46650,7 @@ async function generatePackageJson(projectName, database, source = nextlySource(
46447
46650
  "@tailwindcss/postcss": PINNED_VERSIONS["@tailwindcss/postcss"],
46448
46651
  tailwindcss: PINNED_VERSIONS.tailwindcss,
46449
46652
  eslint: PINNED_VERSIONS.eslint,
46450
- "eslint-config-next": runtimeVersions["eslint-config-next"],
46653
+ "eslint-config-next": scaffoldRange("eslint-config-next"),
46451
46654
  esbuild: PINNED_VERSIONS.esbuild
46452
46655
  };
46453
46656
  const shipsSearchIndex = targetDir ? await projectHasSearchIndexScript(targetDir) : false;
@@ -46523,7 +46726,6 @@ async function resolvePluginNextlyRange(source) {
46523
46726
  */
46524
46727
  async function generatePluginPackageJson(projectName, source) {
46525
46728
  const versions = await nextlyVersions(source);
46526
- const runtimeVersions = await resolveRuntimeVersions();
46527
46729
  const range = (pkg) => versions[pkg] ?? "alpha";
46528
46730
  const peerRange = (pkg) => pluginPeerRange(range(pkg), source.cliVersion);
46529
46731
  const peerDependencies = {
@@ -46531,8 +46733,8 @@ async function generatePluginPackageJson(projectName, source) {
46531
46733
  "@nextlyhq/admin": peerRange("@nextlyhq/admin"),
46532
46734
  "@nextlyhq/plugin-sdk": peerRange("@nextlyhq/plugin-sdk"),
46533
46735
  "@nextlyhq/ui": peerRange("@nextlyhq/ui"),
46534
- react: PINNED_VERSIONS.react,
46535
- "react-dom": PINNED_VERSIONS["react-dom"]
46736
+ react: SUPPORTED_RANGES.react,
46737
+ "react-dom": SUPPORTED_RANGES["react-dom"]
46536
46738
  };
46537
46739
  const devDependencies = {
46538
46740
  nextly: range("nextly"),
@@ -46541,9 +46743,9 @@ async function generatePluginPackageJson(projectName, source) {
46541
46743
  "@nextlyhq/plugin-sdk": range("@nextlyhq/plugin-sdk"),
46542
46744
  "@nextlyhq/adapter-drizzle": range("@nextlyhq/adapter-drizzle"),
46543
46745
  "@nextlyhq/adapter-sqlite": range("@nextlyhq/adapter-sqlite"),
46544
- next: runtimeVersions.next,
46545
- react: PINNED_VERSIONS.react,
46546
- "react-dom": PINNED_VERSIONS["react-dom"],
46746
+ next: scaffoldRange("next"),
46747
+ react: scaffoldRange("react"),
46748
+ "react-dom": scaffoldRange("react-dom"),
46547
46749
  "better-sqlite3": "^12.0.0",
46548
46750
  "@types/node": PINNED_VERSIONS["@types/node"],
46549
46751
  "@types/react": PINNED_VERSIONS["@types/react"],
@@ -46861,6 +47063,143 @@ async function copyPluginTemplate(opts) {
46861
47063
  await writeScaffoldNpmrc(targetDir, packageManager);
46862
47064
  await replacePlaceholders(targetDir, placeholders, alreadyRendered);
46863
47065
  }
47066
+ // src/generators/page-extensions.ts
47067
+ /**
47068
+ * The page extension to name a new module file with, a hook or a route: a
47069
+ * TypeScript one first, since Nextly's files are TypeScript, then a
47070
+ * JavaScript one; never a content extension such as `mdx`, which Next.js
47071
+ * would compile as content, and never `cjs`, since the files are ES modules
47072
+ * and a `.cjs` file cannot hold one. Null when the list holds no extension a
47073
+ * module can be written in.
47074
+ */
47075
+ function moduleExtension(extensions) {
47076
+ const usable = writable(extensions);
47077
+ return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
47078
+ }
47079
+ /**
47080
+ * The extensions a file with code in it can be written under: not a
47081
+ * declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
47082
+ * types only and refuses code in.
47083
+ */
47084
+ function writable(extensions) {
47085
+ return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
47086
+ }
47087
+ /**
47088
+ * The `pageExtensions` the app's Next.js config sets, as every existing-app
47089
+ * step that names a file Next.js routes reads it: the list; `"absent"` when
47090
+ * the config sets none, or there is no config; or null when it is set in a
47091
+ * way that cannot be read. Read from the config the app exports, as the Next
47092
+ * config step reads it, so a commented-out or nested `pageExtensions` sets
47093
+ * nothing. `configName` is the file it was read from, for the report.
47094
+ */
47095
+ async function appPageExtensions(cwd) {
47096
+ const nextConfig = await readNextConfig(cwd);
47097
+ return {
47098
+ configName: nextConfig?.name ?? "The Next.js config",
47099
+ extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
47100
+ };
47101
+ }
47102
+ /**
47103
+ * The text of the app's Next.js config, or null when it has none, from the
47104
+ * file the Next config step reads and extends ({@link findConfigFile}).
47105
+ */
47106
+ async function readNextConfig(cwd) {
47107
+ const name = await findConfigFile(cwd);
47108
+ return name === null ? null : {
47109
+ name,
47110
+ text: await import_lib$1.default.readFile(path.join(cwd, name), "utf-8")
47111
+ };
47112
+ }
47113
+ /**
47114
+ * The page extension for a file of TypeScript with JSX, such as a page or a
47115
+ * layout: one ending in `tsx`. Null when the list holds none.
47116
+ */
47117
+ function tsxExtension(extensions) {
47118
+ return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
47119
+ }
47120
+ /** A JavaScript or TypeScript module without JSX, such as a route. */
47121
+ const MODULE_FILE = {
47122
+ pick: moduleExtension,
47123
+ holds: "a JavaScript or TypeScript extension"
47124
+ };
47125
+ /** TypeScript with JSX, such as a page or a layout. */
47126
+ const TSX_FILE = {
47127
+ pick: tsxExtension,
47128
+ holds: "an extension ending in tsx"
47129
+ };
47130
+ /**
47131
+ * The report for a file an app that sets `pageExtensions` gets in place of
47132
+ * the file: the name to create it under and its content. Next.js routes a
47133
+ * file there only under a name with one of the app's extensions, and the app
47134
+ * may already have the file under such a name, which the existing-app steps
47135
+ * do not look for, so nothing is written. A list that cannot be read, or that
47136
+ * holds no extension the content needs, gives a placeholder name and says
47137
+ * which.
47138
+ *
47139
+ * @param base the file's path without its extension, as reported, such as
47140
+ * `app/admin/[[...params]]/page`
47141
+ */
47142
+ function pageExtensionsSkip(base, text, pages, need) {
47143
+ const { configName, extensions } = pages;
47144
+ const extension = extensions === null ? null : need.pick(extensions);
47145
+ const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
47146
+ if (extensions !== null && extension === null) return {
47147
+ path: `${base}.<${need.holds} added to pageExtensions>`,
47148
+ 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`,
47149
+ toAdd: text
47150
+ };
47151
+ return {
47152
+ path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
47153
+ why: `${sets}; unless the app already has this file, create it under that name with this content`,
47154
+ toAdd: text
47155
+ };
47156
+ }
47157
+ // src/generators/write-new.ts
47158
+ /**
47159
+ * Writes `text` to `relative` (a project-relative path, written with `/`)
47160
+ * only where nothing is, and records which it was.
47161
+ *
47162
+ * The file is created exclusively (`wx`), so a file the app already has, a
47163
+ * link included, is never replaced, whatever appeared since any earlier
47164
+ * check. One that is there is reported as skipped, with why and, when given,
47165
+ * what to add to it by hand. `wx` guards only the file's own name, so a
47166
+ * folder on its path that is a symbolic link, which would take the write
47167
+ * wherever it points, is found first, as the media routes find it, and
47168
+ * nothing is written through it.
47169
+ */
47170
+ async function writeNew(cwd, relative, text, files, ifThere) {
47171
+ const file = path.join(cwd, ...relative.split("/"));
47172
+ const link = await linkedFolder(cwd, file);
47173
+ if (link !== null) {
47174
+ const folder = path.relative(cwd, link.folder).split(path.sep).join("/");
47175
+ files.skipped.push({
47176
+ path: relative,
47177
+ 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}`,
47178
+ toAdd: ifThere.toAdd
47179
+ });
47180
+ return;
47181
+ }
47182
+ await import_lib$1.default.ensureDir(path.dirname(file));
47183
+ try {
47184
+ await import_lib$1.default.writeFile(file, text, {
47185
+ encoding: "utf-8",
47186
+ flag: "wx"
47187
+ });
47188
+ files.written.push(relative);
47189
+ } catch (error) {
47190
+ if (error.code !== "EEXIST") throw error;
47191
+ files.skipped.push({
47192
+ path: relative,
47193
+ ...ifThere
47194
+ });
47195
+ }
47196
+ }
47197
+ /** A step's report before it has done anything. */
47198
+ const nothingYet = () => ({
47199
+ written: [],
47200
+ changed: [],
47201
+ skipped: []
47202
+ });
46864
47203
  // src/generators/routes.ts
46865
47204
  const API_ROUTE_TEMPLATE = `// Imported from \`nextly/runtime\` (not the package root) so the
46866
47205
  // Next.js-coupled handler factory stays out of Node-only code paths
@@ -46917,17 +47256,22 @@ async function generateRoutes(cwd, projectInfo) {
46917
47256
  * lists and deletes media, every request checked against the signed-in
46918
47257
  * caller's media permissions (`requireAuth: true` in the template);
46919
47258
  * - the public media route, `api/media/...`, which serves reads only;
47259
+ * - the uploads route, `uploads/...`, which serves files stored on local disk
47260
+ * that `next start` has no static file for, those uploaded since it started;
46920
47261
  * - the health check.
46921
47262
  *
46922
47263
  * `segment` is the URL each route answers. A handler already answering it
46923
47264
  * means the app has its own route there, which is left alone. Next.js leaves
46924
47265
  * route groups `(name)` and slots `@name` out of the URL wherever they sit,
46925
47266
  * and never routes a private folder `_name`, so the folders that answer a URL
46926
- * are found by that rule rather than by name. `catchAll` marks the optional
47267
+ * are found by that rule rather than by name. `catchAll` marks the
46927
47268
  * catch-alls, which serve everything below their URL too: a child folder
46928
- * holding a handler is weighed by {@link childFinding}.
47269
+ * holding a handler is weighed by {@link childFinding}. The uploads route is
47270
+ * a required catch-all rather than an optional one, and is weighed the same
47271
+ * way: a handler at `/uploads` itself, or any route below it, is the app's
47272
+ * own and leaves it unwritten.
46929
47273
  *
46930
- * `role` says what Nextly's route does there, for a media route the app
47274
+ * `role` says what Nextly's route does there, for a media or uploads route the app
46931
47275
  * already has something in place of; the report then carries the route to
46932
47276
  * add by hand.
46933
47277
  */
@@ -46955,6 +47299,34 @@ const ROUTES = [
46955
47299
  }
46956
47300
  ];
46957
47301
  /**
47302
+ * The uploads route, written by {@link generateUploadsRoute} beside the media
47303
+ * routes. Apart from them because it is a different concern: it serves the
47304
+ * files local storage keeps, where they serve the media library's records.
47305
+ */
47306
+ const UPLOADS_ROUTE = {
47307
+ path: "uploads/[...path]/route.ts",
47308
+ segment: "uploads",
47309
+ catchAll: true,
47310
+ importsConfig: true,
47311
+ 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`
47312
+ };
47313
+ /**
47314
+ * The admin's layout, which `generateAdminPage` writes beside the admin page.
47315
+ * Read with the routes, from the template a new project gets, so an app Nextly
47316
+ * is added to renders its admin the same way: its shell prerendered, and its
47317
+ * request work beneath a Suspense boundary.
47318
+ */
47319
+ const ADMIN_LAYOUT = {
47320
+ path: "admin/[[...params]]/layout.tsx",
47321
+ importsConfig: true
47322
+ };
47323
+ /** Every file the existing-app path writes from the templates, read together. */
47324
+ const ALL_ROUTES = [
47325
+ ...ROUTES,
47326
+ UPLOADS_ROUTE,
47327
+ ADMIN_LAYOUT
47328
+ ];
47329
+ /**
46958
47330
  * The import that brings in the Nextly config: the template spells it through
46959
47331
  * the `@nextly-config` alias or a path relative to its own layout, neither of
46960
47332
  * which an existing app is guaranteed to resolve.
@@ -47041,7 +47413,7 @@ var UnreadableFolder = class extends Error {
47041
47413
  */
47042
47414
  async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
47043
47415
  const texts = /* @__PURE__ */ new Map();
47044
- for (const route of ROUTES) {
47416
+ for (const route of ALL_ROUTES) {
47045
47417
  const relative = `base/src/app/${route.path}`;
47046
47418
  let text;
47047
47419
  try {
@@ -47071,7 +47443,20 @@ async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
47071
47443
  * is for such an app.
47072
47444
  */
47073
47445
  async function generateMediaRoutes(cwd, projectInfo, templates) {
47074
- const prepared = ROUTES.map((route) => prepareRoute(cwd, projectInfo, templates, route));
47446
+ return writeRoutes(cwd, projectInfo, templates, ROUTES);
47447
+ }
47448
+ /**
47449
+ * Write the uploads route into an existing app, by the same rules as
47450
+ * {@link generateMediaRoutes}: never where the app already answers
47451
+ * `/uploads`, never through a link, and under `pageExtensions` reported with
47452
+ * its content rather than written.
47453
+ */
47454
+ async function generateUploadsRoute(cwd, projectInfo, templates) {
47455
+ return writeRoutes(cwd, projectInfo, templates, [UPLOADS_ROUTE]);
47456
+ }
47457
+ /** Write each of `routes` where nothing answers its URL, reporting the rest. */
47458
+ async function writeRoutes(cwd, projectInfo, templates, routes) {
47459
+ const prepared = routes.map((route) => prepareRoute(cwd, projectInfo, templates, route));
47075
47460
  const pages = await appPageExtensions(cwd);
47076
47461
  if (pages.extensions !== "absent") {
47077
47462
  const { configName, extensions } = pages;
@@ -47490,52 +47875,6 @@ function skipped(cwd, left, target, text, role, reason) {
47490
47875
  function toPosix(p) {
47491
47876
  return p.split(path.sep).join("/");
47492
47877
  }
47493
- // src/generators/write-new.ts
47494
- /**
47495
- * Writes `text` to `relative` (a project-relative path, written with `/`)
47496
- * only where nothing is, and records which it was.
47497
- *
47498
- * The file is created exclusively (`wx`), so a file the app already has, a
47499
- * link included, is never replaced, whatever appeared since any earlier
47500
- * check. One that is there is reported as skipped, with why and, when given,
47501
- * what to add to it by hand. `wx` guards only the file's own name, so a
47502
- * folder on its path that is a symbolic link, which would take the write
47503
- * wherever it points, is found first, as the media routes find it, and
47504
- * nothing is written through it.
47505
- */
47506
- async function writeNew(cwd, relative, text, files, ifThere) {
47507
- const file = path.join(cwd, ...relative.split("/"));
47508
- const link = await linkedFolder(cwd, file);
47509
- if (link !== null) {
47510
- const folder = path.relative(cwd, link.folder).split(path.sep).join("/");
47511
- files.skipped.push({
47512
- path: relative,
47513
- 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}`,
47514
- toAdd: ifThere.toAdd
47515
- });
47516
- return;
47517
- }
47518
- await import_lib$1.default.ensureDir(path.dirname(file));
47519
- try {
47520
- await import_lib$1.default.writeFile(file, text, {
47521
- encoding: "utf-8",
47522
- flag: "wx"
47523
- });
47524
- files.written.push(relative);
47525
- } catch (error) {
47526
- if (error.code !== "EEXIST") throw error;
47527
- files.skipped.push({
47528
- path: relative,
47529
- ...ifThere
47530
- });
47531
- }
47532
- }
47533
- /** A step's report before it has done anything. */
47534
- const nothingYet = () => ({
47535
- written: [],
47536
- changed: [],
47537
- skipped: []
47538
- });
47539
47878
  // src/generators/admin.ts
47540
47879
  const ADMIN_PAGE_TEMPLATE = `"use client";
47541
47880
 
@@ -47557,43 +47896,24 @@ export default function AdminPage() {
47557
47896
  }
47558
47897
  `;
47559
47898
  /**
47560
- * Generate the layout template with the correct relative import path
47561
- * to nextly.config based on whether the project uses a src/ directory.
47562
- *
47563
- * With src/: src/app/admin/[[...params]]/layout.tsx → ../../../../nextly.config
47564
- * Without src: app/admin/[[...params]]/layout.tsx → ../../../nextly.config
47899
+ * The admin layout, as the base template holds it, with its config import
47900
+ * pointing at the project's `nextly.config` from where the layout is written.
47901
+ * One source with a new project's layout, so an app Nextly is added to gets
47902
+ * the same per-request admin.
47565
47903
  */
47566
- function getAdminLayoutTemplate(srcDir) {
47567
- return `import { getBrandingCss } from "nextly/config";
47568
-
47569
- import config from "${srcDir ? "../../../../nextly.config" : "../../../nextly.config"}";
47570
-
47571
- const brandingCss = getBrandingCss(config.admin?.branding);
47572
-
47573
- export default function AdminLayout({
47574
- children,
47575
- }: {
47576
- children: React.ReactNode;
47577
- }) {
47578
- return (
47579
- <>
47580
- {brandingCss && (
47581
- <style dangerouslySetInnerHTML={{ __html: brandingCss }} />
47582
- )}
47583
- {children}
47584
- </>
47585
- );
47586
- }
47587
- `;
47904
+ function adminLayout(cwd, adminDir, templates) {
47905
+ const text = templates.get(ADMIN_LAYOUT.path);
47906
+ 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.`);
47907
+ return withRelativeConfigImport(text, cwd, path.join(cwd, adminDir, "layout.tsx"), ADMIN_LAYOUT.path);
47588
47908
  }
47589
47909
  /**
47590
47910
  * Generate the admin panel page and layout.
47591
47911
  * Uses [[...params]] to match the expected route pattern.
47592
47912
  */
47593
- async function generateAdminPage(cwd, projectInfo) {
47913
+ async function generateAdminPage(cwd, projectInfo, templates) {
47594
47914
  const files = nothingYet();
47595
47915
  const adminDir = `${projectInfo.appDir}/admin/[[...params]]`;
47596
- const layout = getAdminLayoutTemplate(projectInfo.srcDir);
47916
+ const layout = adminLayout(cwd, adminDir, templates);
47597
47917
  const pages = await appPageExtensions(cwd);
47598
47918
  if (pages.extensions !== "absent") {
47599
47919
  const { configName, extensions } = pages;
@@ -47723,8 +48043,9 @@ function startNextly(configImport) {
47723
48043
  const { createRegister } = await import("nextly");
47724
48044
  const { default: config } = await import("${configImport}");
47725
48045
  // Next.js writes its Cache Components setting into this file as it builds
47726
- // it. Nextly, loaded from node_modules, cannot read that for itself, and
47727
- // warns when Cache Components is on, which it does not support yet.
48046
+ // it. Nextly, loaded from node_modules, cannot read that for itself. With
48047
+ // it off, Nextly warns at start; with it on, a cached read that finds no
48048
+ // "use cache" read function says so.
47728
48049
  await createRegister(config, {
47729
48050
  cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
47730
48051
  })();`;
@@ -47822,10 +48143,7 @@ function pageExtensionsAdvice(dir, configImport, configName, extensions) {
47822
48143
  * never rewritten; it is reported as skipped with what to add. So is the case
47823
48144
  * where the app sets \`pageExtensions\`: Next.js then reads the hook only under a
47824
48145
  * name with one of those extensions, which this does not guess. The config file
47825
- * itself is never edited here. Next.js 14 runs the hook only with
47826
- * `experimental.instrumentationHook`, which the Next config step gives in the
47827
- * same `experimental` object as Nextly's packages (`patchNextConfig`), so two
47828
- * steps never hand the developer two objects for one key.
48146
+ * itself is never edited here.
47829
48147
  */
47830
48148
  async function generateInstrumentation(cwd, projectInfo) {
47831
48149
  const dir = projectInfo.srcDir ? "src" : "";
@@ -47873,12 +48191,13 @@ async function generateIntoExistingApp(cwd, projectType, projectInfo, database,
47873
48191
  return [
47874
48192
  await generateConfig(cwd, projectType, projectInfo),
47875
48193
  await generateRoutes(cwd, projectInfo),
47876
- await generateAdminPage(cwd, projectInfo),
48194
+ await generateAdminPage(cwd, projectInfo, templates),
47877
48195
  await generateMediaRoutes(cwd, projectInfo, templates),
48196
+ await generateUploadsRoute(cwd, projectInfo, templates),
47878
48197
  await generateInstrumentation(cwd, projectInfo),
47879
48198
  await generateTypesDirectory(cwd, projectInfo),
47880
48199
  nextConfigFiles(await patchNextConfig(cwd, database)),
47881
- cacheComponentsFiles(await turnOffCacheComponents(cwd))
48200
+ cacheComponentsFiles(await turnOnCacheComponents(cwd))
47882
48201
  ];
47883
48202
  }
47884
48203
  // src/generators/report.ts
@@ -50535,7 +50854,7 @@ var hr = Symbol("entry");
50535
50854
  var cs = Symbol("entryOpt");
50536
50855
  var ui = Symbol("writeEntryClass");
50537
50856
  var lr = Symbol("write");
50538
- var fs$6 = Symbol("ondrain");
50857
+ var fs$7 = Symbol("ondrain");
50539
50858
  var wt = class extends A {
50540
50859
  sync = !1;
50541
50860
  opt;
@@ -50569,8 +50888,8 @@ var wt = class extends A {
50569
50888
  if ((t.gzip ? 1 : 0) + (t.brotli ? 1 : 0) + (t.zstd ? 1 : 0) > 1) throw new TypeError("gzip, brotli, zstd are mutually exclusive");
50570
50889
  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");
50571
50890
  let e = this.zip;
50572
- e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$6]()), this.on("resume", () => e.resume());
50573
- } else this.on("drain", this[fs$6]);
50891
+ e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$7]()), this.on("resume", () => e.resume());
50892
+ } else this.on("drain", this[fs$7]);
50574
50893
  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;
50575
50894
  }
50576
50895
  [lr](t) {
@@ -50698,7 +51017,7 @@ var wt = class extends A {
50698
51017
  this.emit("error", e);
50699
51018
  }
50700
51019
  }
50701
- [fs$6]() {
51020
+ [fs$7]() {
50702
51021
  this[Et] && this[Et].entry && this[Et].entry.resume();
50703
51022
  }
50704
51023
  [di](t) {
@@ -51765,6 +52084,126 @@ async function cleanupDownload(source) {
51765
52084
  await import_lib$1.default.remove(extractionRoot).catch(() => {});
51766
52085
  }
51767
52086
  }
52087
+ // src/lib/framework-versions.ts
52088
+ /**
52089
+ * Whether an existing app's Next.js, React and React DOM are ones this
52090
+ * release of Nextly supports, and what to change when they are not.
52091
+ *
52092
+ * Nextly is reviewed against one Next.js major and one React major
52093
+ * (`SUPPORTED_RANGES`). An app below the floor or on a newer major would
52094
+ * install Nextly and then fail at build or at run time, so adding Nextly to
52095
+ * it stops before anything is written, saying what was found and the way on.
52096
+ */
52097
+ /** The first spec the app's package.json gives `name`, in field order. */
52098
+ function declaredSpec(manifest, name) {
52099
+ const fields = ["optionalDependencies", ...DEPENDENCY_FIELDS$1.filter((field) => field !== "optionalDependencies")];
52100
+ for (const field of fields) {
52101
+ const deps = manifest[field];
52102
+ if (!deps || typeof deps !== "object") continue;
52103
+ const spec = deps[name];
52104
+ if (typeof spec === "string") return spec;
52105
+ }
52106
+ }
52107
+ /**
52108
+ * The version an app is headed for, the one judged against the range: the
52109
+ * installed copy, unless the app's declaration points past it.
52110
+ */
52111
+ function judgedVersion(installed, declared, supported) {
52112
+ const lowestDeclared = declared !== void 0 ? lowestVersionIn(declared) : void 0;
52113
+ const floor = lowestVersionIn(supported);
52114
+ if (floor === void 0) return installed ?? lowestDeclared;
52115
+ if (lowestDeclared !== void 0 && compareVersions(lowestDeclared, floor) > 0 && !satisfiesRange(lowestDeclared, supported)) return lowestDeclared;
52116
+ const highest = declared !== void 0 ? highestMajorIn(declared) : void 0;
52117
+ if (installed === void 0 && highest !== void 0 && highest > major(floor)) return `${major(floor) + 1}.0.0`;
52118
+ return installed ?? lowestDeclared;
52119
+ }
52120
+ /**
52121
+ * Too new only on a later major than the range covers; anything else outside
52122
+ * it (below the floor, or a prerelease within the major) needs a supported
52123
+ * release of the same major. A range without a floor leaves nothing to
52124
+ * compare, and the version is then reported as too new.
52125
+ */
52126
+ function directionOf(judged, supported) {
52127
+ const floor = lowestVersionIn(supported);
52128
+ return floor === void 0 || major(judged) > major(floor) ? "too-new" : "too-old";
52129
+ }
52130
+ /**
52131
+ * Every supported framework package the app at `cwd` has outside its range.
52132
+ *
52133
+ * The version judged is the one the app is headed for (`judgedVersion`): the
52134
+ * installed copy, read from the `node_modules` Node would load it from,
52135
+ * unless the app's package.json declaration points past it; with none
52136
+ * installed, what an install of that declaration could resolve to. A package
52137
+ * the app neither has installed nor declares in a form that names a version
52138
+ * (a dist-tag, `workspace:*`) is not judged.
52139
+ */
52140
+ async function frameworkVersionProblems(cwd) {
52141
+ const manifest = await import_lib$1.default.readJson(path.join(cwd, "package.json")).catch(() => ({}));
52142
+ const problems = [];
52143
+ for (const name of Object.keys(SUPPORTED_RANGES)) {
52144
+ const supported = SUPPORTED_RANGES[name];
52145
+ const installed = await installedVersion(cwd, name);
52146
+ const declared = declaredSpec(manifest, name);
52147
+ const judged = judgedVersion(installed, declared, supported);
52148
+ if (judged === void 0 || satisfiesRange(judged, supported)) continue;
52149
+ const direction = directionOf(judged, supported);
52150
+ problems.push({
52151
+ name,
52152
+ installed,
52153
+ declared,
52154
+ supported,
52155
+ judged,
52156
+ direction
52157
+ });
52158
+ }
52159
+ return problems;
52160
+ }
52161
+ const LABEL = {
52162
+ next: "Next.js",
52163
+ react: "React",
52164
+ "react-dom": "React"
52165
+ };
52166
+ const major = (version) => Number(version.split(".")[0]);
52167
+ /** What `fix` reads from a problem: the supported major and the version found. */
52168
+ function majorsOf(problem) {
52169
+ const floor = lowestVersionIn(problem.supported) ?? problem.supported;
52170
+ return {
52171
+ supportedMajor: major(floor),
52172
+ foundMajor: major(problem.judged)
52173
+ };
52174
+ }
52175
+ /** A version past the supported major: install that major, or wait for Nextly. */
52176
+ function tooNewFix(problem, add) {
52177
+ const { supportedMajor, foundMajor } = majorsOf(problem);
52178
+ const label = LABEL[problem.name];
52179
+ 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}`}`;
52180
+ }
52181
+ /** A version below the supported floor: the codemod across a Next.js major, an install otherwise. */
52182
+ function tooOldFix(problem, add) {
52183
+ const { supportedMajor, foundMajor } = majorsOf(problem);
52184
+ 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}`;
52185
+ const install = problem.name === "next" ? `next@${problem.supported}` : `react@${SUPPORTED_RANGES.react} react-dom@${SUPPORTED_RANGES["react-dom"]}`;
52186
+ return `Upgrade ${LABEL[problem.name]} to ${problem.supported}:\n ${add} ${install}`;
52187
+ }
52188
+ /** The one step that brings `problem` into range, as a sentence and a command. */
52189
+ function fix(problem, pm) {
52190
+ const add = ADD_COMMAND[pm];
52191
+ return problem.direction === "too-new" ? tooNewFix(problem, add) : tooOldFix(problem, add);
52192
+ }
52193
+ /**
52194
+ * Why the run stopped: each package found, the range it falls outside, and
52195
+ * the change that brings it in. React and React DOM move together, so a
52196
+ * problem with both gives one command.
52197
+ */
52198
+ function frameworkVersionMessage(problems, pm) {
52199
+ const lines = problems.map(({ name, installed, declared, supported }) => {
52200
+ return ` ${name}: ${[installed !== void 0 ? `installed ${installed}` : void 0, declared !== void 0 ? `declared ${declared}` : void 0].filter(Boolean).join(", ")}; supported ${supported}`;
52201
+ });
52202
+ const fixes = [...new Set(problems.map((problem) => fix(problem, pm)))];
52203
+ return `This app's Next.js or React is outside what this release of Nextly supports:
52204
+ ${lines.join("\n")}\n\nNothing was changed. Change the app as follows, then run create-nextly-app again:
52205
+ ` + fixes.map((line) => ` ${line}`).join("\n");
52206
+ }
51768
52207
  // src/prompts/approach.ts
51769
52208
  /**
51770
52209
  * Labels shown in the CLI select prompt for each approach.
@@ -52205,6 +52644,16 @@ async function createNextly(options = {}) {
52205
52644
  const existingProject = options.projectType !== "plugin" && await isExistingNextProject(cwd);
52206
52645
  if (existingProject) {
52207
52646
  R.success(`${import_picocolors.default.green("Next.js")} project detected`);
52647
+ const frameworkProblems = await frameworkVersionProblems(cwd);
52648
+ if (frameworkProblems.length > 0) {
52649
+ capture("scaffold_failed", {
52650
+ stage: "detect",
52651
+ error_code: "unknown",
52652
+ duration_ms: Date.now() - overallStartedAt
52653
+ });
52654
+ Nt$1(frameworkVersionMessage(frameworkProblems, await detectAppPackageManager(cwd)));
52655
+ process.exit(1);
52656
+ }
52208
52657
  if (!defaults) {
52209
52658
  const shouldInstall = await Rt$1({
52210
52659
  message: "Install Nextly in this project?",
@@ -52547,4 +52996,4 @@ async function createNextly(options = {}) {
52547
52996
  }
52548
52997
  export { getAvailableTemplateNames as a, init as c, __require as d, __toESM$1 as f, validateTemplateName as i, shutdown as l, resolveProjectArg as n, packageVersion as o, validateProjectName as r, capture as s, createNextly as t, __commonJSMin$1 as u };
52549
52998
 
52550
- //# sourceMappingURL=create-nextly-5wiGMVI6.mjs.map
52999
+ //# sourceMappingURL=create-nextly-CuWn_ITx.mjs.map