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.
@@ -8066,6 +8066,38 @@ var require_utils$3 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8066
8066
  return output.join("");
8067
8067
  }
8068
8068
  /**
8069
+ * Removes dot segments from the path of a relative reference (a path that does
8070
+ * not start with "/") without changing what it resolves to against any base.
8071
+ * Unlike `removeDotSegments`, leading ".." segments that cannot be collapsed
8072
+ * are kept, the result never becomes root-relative, and a result that would be
8073
+ * empty or start with an empty segment is prefixed with "." (an empty path
8074
+ * means "same document" and a leading "//" would introduce an authority).
8075
+ *
8076
+ * @param {string} path
8077
+ * @returns {string}
8078
+ */
8079
+ function removeDotSegmentsRelative(path) {
8080
+ if (path === "" || path[0] === "/") return removeDotSegments(path);
8081
+ const segments = path.split("/");
8082
+ const last = segments.length - 1;
8083
+ /** @type {string[]} */
8084
+ const output = [];
8085
+ let trailingDirectory = false;
8086
+ for (let i = 0; i <= last; i++) {
8087
+ const segment = segments[i];
8088
+ if (segment === "." || segment === "..") {
8089
+ if (segment === "..") {
8090
+ if (output.length !== 0 && output[output.length - 1] !== "..") output.pop();
8091
+ else output.push("..");
8092
+ }
8093
+ trailingDirectory = i === last;
8094
+ } else output.push(segment);
8095
+ }
8096
+ if (trailingDirectory && output[output.length - 1] !== "..") output.push("");
8097
+ if (output.length === 0 || output[0] === "" || output[0].includes(":")) output.unshift(".");
8098
+ return output.join("/");
8099
+ }
8100
+ /**
8069
8101
  * Re-escape RFC 3986 gen-delims that must not appear literally in the host.
8070
8102
  * After the URI regex parses, these characters cannot be literal in the host
8071
8103
  * field, so any that appear after decoding came from percent-encoding and
@@ -8369,6 +8401,7 @@ var require_utils$3 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8369
8401
  encodeFragment,
8370
8402
  escapePreservingEscapes,
8371
8403
  removeDotSegments,
8404
+ removeDotSegmentsRelative,
8372
8405
  isIPv4,
8373
8406
  isUUID,
8374
8407
  normalizeIPv6,
@@ -8558,18 +8591,24 @@ var require_schemes = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8558
8591
  };
8559
8592
  }));
8560
8593
  var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8561
- const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils$3();
8594
+ const { normalizeIPv6, removeDotSegments, removeDotSegmentsRelative, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils$3();
8562
8595
  const { SCHEMES, getSchemeHandler } = require_schemes();
8563
8596
  const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
8564
8597
  const MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
8598
+ const MALFORMED_HOST_ERROR = "URI host is malformed.";
8599
+ const FORBIDDEN_DOMAIN_HOST_CHARACTER = /[\0-\x20"#/:<>?@[\\\]^`{|}\x7F]/u;
8565
8600
  /**
8601
+ * RFC 3986 section 3.1 does not allow percent-encoding in a scheme, so the
8602
+ * scheme is validated as-is and never decoded. Decoding would let inert input
8603
+ * such as `%6Aavascript:` become a live `javascript:` scheme.
8604
+ *
8566
8605
  * @param {string} scheme
8567
8606
  * @returns {string}
8568
8607
  */
8569
- function decodeValidScheme(scheme) {
8570
- const decodedScheme = unescape(String(scheme));
8571
- if (!VALID_SCHEME.test(decodedScheme)) throw new TypeError(MALFORMED_SCHEME_ERROR);
8572
- return decodedScheme;
8608
+ function validateScheme(scheme) {
8609
+ const stringScheme = String(scheme);
8610
+ if (!VALID_SCHEME.test(stringScheme)) throw new TypeError(MALFORMED_SCHEME_ERROR);
8611
+ return stringScheme;
8573
8612
  }
8574
8613
  /**
8575
8614
  * @template {import('./types/index').URIComponent|string} T
@@ -8597,6 +8636,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8597
8636
  const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
8598
8637
  const resolvedHost = resolved.host;
8599
8638
  const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
8639
+ if (hasForbiddenDomainHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)) throw new Error(MALFORMED_HOST_ERROR);
8600
8640
  canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
8601
8641
  const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !/\P{ASCII}/u.test(resolvedHost);
8602
8642
  if (resolved.error && !encodedASCIIHost) throw new Error(resolved.error);
@@ -8640,10 +8680,11 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8640
8680
  } else {
8641
8681
  if (relative.path[0] === "/") target.path = removeDotSegments(relative.path);
8642
8682
  else {
8643
- if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative.path;
8683
+ const baseHasAuthority = base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0;
8684
+ if (baseHasAuthority && !base.path) target.path = "/" + relative.path;
8644
8685
  else if (!base.path) target.path = relative.path;
8645
8686
  else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path;
8646
- target.path = removeDotSegments(target.path);
8687
+ target.path = base.scheme === void 0 && !baseHasAuthority && target.path[0] !== "/" ? removeDotSegmentsRelative(target.path) : removeDotSegments(target.path);
8647
8688
  }
8648
8689
  target.query = relative.query;
8649
8690
  }
@@ -8691,17 +8732,18 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8691
8732
  };
8692
8733
  const options = Object.assign({}, opts);
8693
8734
  const uriTokens = [];
8694
- if (component.scheme) component.scheme = decodeValidScheme(component.scheme);
8735
+ if (component.scheme) component.scheme = validateScheme(component.scheme);
8695
8736
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
8696
8737
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
8697
8738
  const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
8698
- const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
8739
+ const schemelessReference = (component.scheme === void 0 || options.reference === "suffix") && !hasAuthority;
8740
+ const pathNoScheme = !options.skipEscape && schemelessReference;
8699
8741
  if (component.path !== void 0) {
8700
8742
  if (!options.skipEscape) component.path = serializePathEncoding(component.path, pathNoScheme);
8701
8743
  else component.path = normalizePercentEncoding(component.path);
8702
8744
  }
8703
8745
  if (options.reference !== "suffix" && component.scheme) {
8704
- component.scheme = decodeValidScheme(component.scheme);
8746
+ component.scheme = validateScheme(component.scheme);
8705
8747
  uriTokens.push(component.scheme, ":");
8706
8748
  }
8707
8749
  const authority = recomposeAuthority(component);
@@ -8712,8 +8754,8 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8712
8754
  }
8713
8755
  if (component.path !== void 0) {
8714
8756
  let s = component.path;
8715
- if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) s = removeDotSegments(s);
8716
- if (pathNoScheme) s = serializePathEncoding(s, true);
8757
+ if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) s = schemelessReference && s[0] !== "/" ? removeDotSegmentsRelative(s) : removeDotSegments(s);
8758
+ if (schemelessReference) s = serializePathEncoding(s, true);
8717
8759
  if (authority === void 0 && s[0] === "/" && s[1] === "/") s = "/%2F" + s.slice(2);
8718
8760
  uriTokens.push(s);
8719
8761
  }
@@ -8777,15 +8819,50 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8777
8819
  * @returns {boolean} whether host conversion failed
8778
8820
  */
8779
8821
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
8780
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) try {
8781
- parsed.host = new URL("http://" + parsed.host).hostname;
8782
- } catch (e) {
8783
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
8822
+ if (hasForbiddenDomainHost(parsed, options, schemeHandler, isIP)) {
8823
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
8784
8824
  return true;
8785
8825
  }
8826
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
8827
+ let url;
8828
+ try {
8829
+ url = new URL("http://" + parsed.host);
8830
+ } catch (e) {
8831
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
8832
+ return true;
8833
+ }
8834
+ if (!isHostOnlyURL(url)) {
8835
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
8836
+ return true;
8837
+ }
8838
+ parsed.host = url.hostname;
8839
+ }
8786
8840
  return false;
8787
8841
  }
8788
8842
  /**
8843
+ * Whether a domain host contains an ASCII character that RFC 3986 does not
8844
+ * allow in a reg-name (unreserved, sub-delims, or pct-encoded). Such a host is
8845
+ * rejected instead of being handed to the WHATWG URL parser, which would treat
8846
+ * "@" as a userinfo delimiter (silently dropping the data before it) and strip
8847
+ * TAB, LF, and CR. Non-ASCII characters remain allowed for IDNs.
8848
+ *
8849
+ * @param {import('./types/index').URIComponent} parsed
8850
+ * @param {import('./types/index').Options} options
8851
+ * @param {{ domainHost?: boolean }|undefined} schemeHandler
8852
+ * @param {boolean} isIP
8853
+ * @returns {boolean}
8854
+ */
8855
+ function hasForbiddenDomainHost(parsed, options, schemeHandler, isIP) {
8856
+ return Boolean(parsed.host && isIP === false && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && FORBIDDEN_DOMAIN_HOST_CHARACTER.test(parsed.host));
8857
+ }
8858
+ /**
8859
+ * @param {URL} url
8860
+ * @returns {boolean}
8861
+ */
8862
+ function isHostOnlyURL(url) {
8863
+ return url.username === "" && url.password === "" && url.port === "" && url.pathname === "/" && url.search === "" && url.hash === "" && url.hostname !== "";
8864
+ }
8865
+ /**
8789
8866
  * @param {string} uri
8790
8867
  * @param {import('./types/index').Options} [opts]
8791
8868
  * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
@@ -8842,8 +8919,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8842
8919
  parsed.query = matches[7];
8843
8920
  parsed.fragment = matches[8];
8844
8921
  if (parsed.scheme !== void 0) {
8845
- const decodedScheme = unescape(parsed.scheme);
8846
- if (VALID_SCHEME.test(decodedScheme)) parsed.scheme = decodedScheme.toLowerCase();
8922
+ if (VALID_SCHEME.test(parsed.scheme)) parsed.scheme = parsed.scheme.toLowerCase();
8847
8923
  else {
8848
8924
  parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
8849
8925
  malformedScheme = true;
@@ -8866,7 +8942,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8866
8942
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
8867
8943
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
8868
8944
  if (malformedIPLiteral) {
8869
- parsed.error = parsed.error || "URI host is malformed.";
8945
+ parsed.error = parsed.error || MALFORMED_HOST_ERROR;
8870
8946
  malformedAuthorityOrPort = true;
8871
8947
  }
8872
8948
  } else isIP = true;
@@ -8878,13 +8954,12 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8878
8954
  if (options.reference && options.reference !== "suffix" && options.reference !== parsed.reference) parsed.error = parsed.error || "URI is not a " + options.reference + " reference.";
8879
8955
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
8880
8956
  if (!malformedIPLiteral) malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
8957
+ if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
8958
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
8959
+ if (!isIP) host = normalizePercentEncoding(host.toLowerCase());
8960
+ parsed.host = isIP || options.scheme !== "null" ? reescapeHostDelimiters(host, isIP) : host;
8961
+ }
8881
8962
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
8882
- if (uri.indexOf("%") !== -1) {
8883
- if (parsed.host !== void 0 && !malformedIPLiteral) {
8884
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
8885
- parsed.host = reescapeHostDelimiters(host, isIP);
8886
- }
8887
- }
8888
8963
  if (parsed.path) parsed.path = normalizePathEncoding(parsed.path);
8889
8964
  if (parsed.query) parsed.query = normalizeQueryFragmentEncoding(parsed.query);
8890
8965
  if (parsed.fragment) parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
@@ -22780,6 +22855,7 @@ function nextlySource(options) {
22780
22855
  function isNextlyPackage(name) {
22781
22856
  return name === "nextly" || name.startsWith("@nextlyhq/");
22782
22857
  }
22858
+ /** The package.json fields that declare a dependency, in the order read. */
22783
22859
  const DEPENDENCY_FIELDS$1 = [
22784
22860
  "dependencies",
22785
22861
  "devDependencies",
@@ -22855,6 +22931,10 @@ const EXACT_FLAG = {
22855
22931
  yarn: "--exact",
22856
22932
  bun: "--exact"
22857
22933
  };
22934
+ /**
22935
+ * How each package manager adds a dependency. Exported so every message that
22936
+ * tells the person what to install spells the command the same way.
22937
+ */
22858
22938
  const ADD_COMMAND = {
22859
22939
  npm: "npm install",
22860
22940
  pnpm: "pnpm add",
@@ -42424,8 +42504,8 @@ var import_lib = (/* @__PURE__ */ __commonJSMin$1(((exports) => {
42424
42504
  exports.parse = parse;
42425
42505
  })))();
42426
42506
  /**
42427
- * Where Nextly explains why Cache Components is off and when it will be
42428
- * supported: the same page `nextly` links from the error it stops with.
42507
+ * Where Nextly explains the Cache Components settings every app it makes or
42508
+ * joins carries, and why each one is there.
42429
42509
  */
42430
42510
  const CACHE_COMPONENTS_DOCS_URL = "https://nextlyhq.com/docs/getting-started/installation#cache-components";
42431
42511
  /**
@@ -42463,13 +42543,42 @@ function packageListLiteral(database) {
42463
42543
  return JSON.stringify(getServerExternalPackages(database), null, 2).replace(/\n/g, "\n ");
42464
42544
  }
42465
42545
  /**
42466
- * The setting every config this writes carries, with its reason in the file:
42467
- * the developer who finds it is the one deciding whether to change it.
42546
+ * Nextly's cache handlers, as the settings that install them. Under Cache
42547
+ * Components, Next.js's default cache serves a page's previous shell once
42548
+ * after a write expires its tags; these expire it at once.
42549
+ */
42550
+ const HANDLER_SETTINGS = [
42551
+ "cacheHandler",
42552
+ "cacheMaxMemorySize",
42553
+ "cacheHandlers"
42554
+ ];
42555
+ const HANDLER_PROPERTIES = [
42556
+ "cacheHandler: require.resolve(\"nextly/cache-handler\")",
42557
+ "cacheMaxMemorySize: 0",
42558
+ "cacheHandlers: { default: require.resolve(\"nextly/cache-handlers\") }"
42559
+ ];
42560
+ const ON_OPTIONS = ["cacheComponents", "partialPrefetching"];
42561
+ /** Every setting Nextly needs, as lines of a config object. */
42562
+ const ON_LINES = [...ON_OPTIONS.map((option) => `${option}: true`), ...HANDLER_PROPERTIES].map((property) => ` ${property},`);
42563
+ /**
42564
+ * The settings every config this writes carries, with their reason in the
42565
+ * file: the developer who finds them is the one deciding whether to change
42566
+ * them.
42468
42567
  */
42469
- const CACHE_COMPONENTS_OFF = ` // Nextly does not support Cache Components yet: it caches with
42470
- // unstable_cache, and Cache Components refuses \`runtime\` route exports.
42568
+ const CACHE_COMPONENTS_ON = ` // Nextly runs with Cache Components and Partial Prefetching on. Its cache
42569
+ // handlers make the first read after a write show the new version, where
42570
+ // Next.js's default cache would serve the previous page once more:
42471
42571
  // ${CACHE_COMPONENTS_DOCS_URL}
42472
- cacheComponents: false,`;
42572
+ ${ON_LINES.join("\n")}`;
42573
+ /**
42574
+ * How an ES module config gets `require`, which Node defines only in
42575
+ * CommonJS, to resolve the handlers from the app's own node_modules.
42576
+ */
42577
+ const REQUIRE_IMPORT = "import { createRequire } from \"node:module\";";
42578
+ /** The declaration, calling `createRequire` by the name the file has it under. */
42579
+ const requireDeclaration = (createRequireName) => `// Resolves Nextly's cache handlers from this app's node_modules.
42580
+ const require = ${createRequireName}(import.meta.url);`;
42581
+ const REQUIRE_DECLARATION = requireDeclaration("createRequire");
42473
42582
  /**
42474
42583
  * Render a complete next.config.ts file for the selected database.
42475
42584
  */
@@ -42477,7 +42586,7 @@ function buildNextConfigTemplate(database) {
42477
42586
  return `import type { NextConfig } from "next";
42478
42587
 
42479
42588
  const nextConfig: NextConfig = {
42480
- ${CACHE_COMPONENTS_OFF}
42589
+ ${CACHE_COMPONENTS_ON}
42481
42590
  serverExternalPackages: ${packageListLiteral(database)},
42482
42591
  };
42483
42592
 
@@ -42486,9 +42595,13 @@ export default nextConfig;
42486
42595
  }
42487
42596
  /** The same config as a JavaScript module, for an app without TypeScript. */
42488
42597
  function buildNextConfigModule(database) {
42489
- return `/** @type {import("next").NextConfig} */
42598
+ return `${REQUIRE_IMPORT}
42599
+
42600
+ ${REQUIRE_DECLARATION}
42601
+
42602
+ /** @type {import("next").NextConfig} */
42490
42603
  const nextConfig = {
42491
- ${CACHE_COMPONENTS_OFF}
42604
+ ${CACHE_COMPONENTS_ON}
42492
42605
  serverExternalPackages: ${packageListLiteral(database)},
42493
42606
  };
42494
42607
 
@@ -42506,9 +42619,19 @@ const CONFIG_FILES = [
42506
42619
  "next.config.ts",
42507
42620
  "next.config.mts"
42508
42621
  ];
42509
- /** The first Next.js major whose option is the top-level `serverExternalPackages`. */
42510
- const FIRST_TOP_LEVEL_MAJOR = 15;
42511
42622
  const PROPERTY = "serverExternalPackages";
42623
+ /**
42624
+ * The deprecated spelling of the list, under `experimental`. Supported
42625
+ * releases still read it, by copying it over the top-level list, so an app
42626
+ * that has it would lose the packages this adds there.
42627
+ */
42628
+ const LEGACY_PROPERTY = "serverComponentsExternalPackages";
42629
+ /**
42630
+ * Why a config with the deprecated list is left alone. Named, so the advice
42631
+ * printed for it can say what to do with that list rather than the advice
42632
+ * for every other refusal, which says to keep the experimental keys there.
42633
+ */
42634
+ const LEGACY_LIST_REASON = `it lists packages under experimental.${LEGACY_PROPERTY}, which Next.js copies over ${PROPERTY}, replacing the packages added there`;
42512
42635
  /** Where the line holding `offset` starts. */
42513
42636
  const lineStart = (source, offset) => source.lastIndexOf("\n", offset - 1) + 1;
42514
42637
  /** The whitespace a line opens with, for the line holding `offset`. */
@@ -42689,7 +42812,7 @@ const TOP_LEVEL_SETTINGS = [
42689
42812
  "transpilePackages",
42690
42813
  "experimental"
42691
42814
  ];
42692
- const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", "serverComponentsExternalPackages"];
42815
+ const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", LEGACY_PROPERTY];
42693
42816
  /** Why an object might set one of `names` where this cannot read it, or null. */
42694
42817
  function hiddenSettings(object, names, where) {
42695
42818
  return object.properties.some((property) => mayHide(property, names)) ? `${where} spreads another object or has a computed or accessor property, which may set ${names.join(", ")}` : null;
@@ -42747,41 +42870,43 @@ function fillEmptyList(source, array, entries) {
42747
42870
  * so the app's own properties keep their order and their comments.
42748
42871
  */
42749
42872
  function addListProperty(source, object, entries) {
42873
+ return appendProperties(source, object, (indent, ownLines) => [ownLines ? `${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}]` : `${PROPERTY}: [${entries.join(", ")}]`]);
42874
+ }
42875
+ /**
42876
+ * Add properties after an object's last one, in the object's layout: on one
42877
+ * line, as `{ a: 1, b: 2 }`, when it is written that way, and otherwise one
42878
+ * per line at the indentation its properties use, before the closing brace,
42879
+ * with a comma put after the last property first when it has none.
42880
+ * `properties` gets that indentation, and whether each property has a line
42881
+ * of its own, so a property spanning lines can follow the same layout.
42882
+ */
42883
+ function appendProperties(source, object, properties) {
42750
42884
  const open = (object.start ?? 0) + 1;
42751
42885
  const close = (object.end ?? 0) - 1;
42752
- return spansLines(source, open, close) ? listOnItsOwnLines(source, object, entries) : [listOnTheSameLine(object, entries)];
42886
+ if (!spansLines(source, open, close)) return [appendOnTheSameLine(object, properties("", false).join(", "))];
42887
+ const last = object.properties.at(-1);
42888
+ const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
42889
+ const lines = properties(indent, true).map((property) => `${indent}${property},\n`).join("");
42890
+ return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
42891
+ at: last.end ?? 0,
42892
+ remove: 0,
42893
+ text: ","
42894
+ }] : [], beforeClosingBrace(source, close, lines)];
42753
42895
  }
42754
- /** The list added to an object written on one line, as `{ a: 1, serverExternalPackages: [...] }`. */
42755
- function listOnTheSameLine(object, entries) {
42756
- const property = `${PROPERTY}: [${entries.join(", ")}]`;
42896
+ /** Properties added to an object written on one line, as `{ a: 1, b: 2 }`. */
42897
+ function appendOnTheSameLine(object, text) {
42757
42898
  const last = object.properties.at(-1);
42758
42899
  const open = (object.start ?? 0) + 1;
42759
42900
  return last ? {
42760
42901
  at: last.end ?? 0,
42761
42902
  remove: 0,
42762
- text: `, ${property}`
42903
+ text: `, ${text}`
42763
42904
  } : {
42764
42905
  at: open,
42765
42906
  remove: (object.end ?? 0) - 1 - open,
42766
- text: ` ${property} `
42907
+ text: ` ${text} `
42767
42908
  };
42768
42909
  }
42769
- /**
42770
- * The list added as the last property of an object written over several
42771
- * lines, one entry per line at the indentation the object's properties use.
42772
- * A comma goes after the last property first when it has none.
42773
- */
42774
- function listOnItsOwnLines(source, object, entries) {
42775
- const close = (object.end ?? 0) - 1;
42776
- const last = object.properties.at(-1);
42777
- const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
42778
- const list = `${indent}${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}],\n`;
42779
- return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
42780
- at: last.end ?? 0,
42781
- remove: 0,
42782
- text: ","
42783
- }] : [], beforeClosingBrace(source, close, list)];
42784
- }
42785
42910
  /** Text put before an object's closing brace: on the line above it when the brace has a line of its own. */
42786
42911
  function beforeClosingBrace(source, close, text) {
42787
42912
  const closeLine = lineStart(source, close);
@@ -42869,7 +42994,7 @@ function conflictOf(object, required) {
42869
42994
  }
42870
42995
  /** Why the config's lists clash, given an `experimental` object it can read. */
42871
42996
  function experimentalConflict(object, experimental, required) {
42872
- if (propertyValue(experimental, "serverComponentsExternalPackages")) return `it lists packages under experimental.serverComponentsExternalPackages, which Next.js moves to ${PROPERTY}; keep a single list there`;
42997
+ if (propertyValue(experimental, LEGACY_PROPERTY)) return LEGACY_LIST_REASON;
42873
42998
  return clashReason("transpilePackages", object, "transpilePackages", required) ?? clashReason("experimental.optimizePackageImports", experimental, "optimizePackageImports", required);
42874
42999
  }
42875
43000
  /** The object literal the file exports as its config, or why there is none to edit. */
@@ -42939,58 +43064,6 @@ function editConfig(source, file, required) {
42939
43064
  added: plan.added
42940
43065
  };
42941
43066
  }
42942
- /** The string at `keys` inside a parsed JSON file, or null. */
42943
- function stringAt(json, keys) {
42944
- let value = json;
42945
- for (const key of keys) {
42946
- if (typeof value !== "object" || value === null || !(key in value)) return null;
42947
- value = value[key];
42948
- }
42949
- return typeof value === "string" ? value : null;
42950
- }
42951
- /** A JSON file of the app's, or null when it is missing or does not parse. */
42952
- async function readJsonOrNull(file) {
42953
- try {
42954
- return await import_lib$1.default.readJson(file);
42955
- } catch {
42956
- return null;
42957
- }
42958
- }
42959
- /**
42960
- * A version range that admits one major only, such as `^15.1.0`, `~16.0`,
42961
- * `15.x` or `16.0.0-canary.3`, with the major it admits. A range such as
42962
- * `>=14`, `14 || 15` or `latest` admits several, or names none.
42963
- */
42964
- const SINGLE_MAJOR = /^[\^~]?(\d+)(?:\.(?:\d+|[xX*])){0,2}(?:-[\w.]+)?$/;
42965
- /**
42966
- * The major version of the app's Next.js: the installed one, read from its
42967
- * own `node_modules`, or else the one its package.json range admits when
42968
- * that range admits a single major. Null when neither tells, as when
42969
- * dependencies are not installed yet and the range is `latest`.
42970
- */
42971
- async function nextMajor(cwd) {
42972
- const installed = stringAt(await readJsonOrNull(path.default.join(cwd, "node_modules", "next", "package.json")), ["version"]);
42973
- const manifest = await readJsonOrNull(path.default.join(cwd, "package.json"));
42974
- const range = stringAt(manifest, ["dependencies", "next"]) ?? stringAt(manifest, ["devDependencies", "next"]);
42975
- const match = SINGLE_MAJOR.exec((installed ?? range ?? "").trim());
42976
- return match ? Number(match[1]) : null;
42977
- }
42978
- /**
42979
- * Why the option cannot be written for the app's Next.js, and which spelling
42980
- * to print instead, or null when it can. Next.js 14 reads the list under
42981
- * another name, and an unknown version may be 14: writing the top-level list
42982
- * there would report success and externalise nothing.
42983
- */
42984
- function versionBlock(major) {
42985
- if (major === null) return {
42986
- reason: "the version of Next.js the app uses could not be read",
42987
- spelling: "either"
42988
- };
42989
- return major < FIRST_TOP_LEVEL_MAJOR ? {
42990
- reason: `the app runs Next.js ${major}, which reads the list as experimental.serverComponentsExternalPackages and runs instrumentation.ts only with experimental.instrumentationHook`,
42991
- spelling: "legacy"
42992
- } : null;
42993
- }
42994
43067
  /**
42995
43068
  * The Next config file any advice about the app's config names: the one it
42996
43069
  * has, or, when it has none, the one to create. `.mjs` is ESM whatever the
@@ -43042,14 +43115,6 @@ async function createNextConfig(cwd, database) {
43042
43115
  async function patchNextConfig(cwd, database) {
43043
43116
  const file = await findConfigFile(cwd);
43044
43117
  const required = getServerExternalPackages(database);
43045
- const blocked = versionBlock(await nextMajor(cwd));
43046
- if (blocked) return {
43047
- kind: "manual",
43048
- file: file ?? await nextConfigFileToChange(cwd),
43049
- packages: required,
43050
- ...blocked,
43051
- ...file ? {} : { create: true }
43052
- };
43053
43118
  if (!file) return createNextConfig(cwd, database);
43054
43119
  const edited = editConfig(await import_lib$1.default.readFile(path.default.join(cwd, file), "utf-8"), file, required);
43055
43120
  if ("reason" in edited) return {
@@ -43069,119 +43134,281 @@ async function patchNextConfig(cwd, database) {
43069
43134
  added: edited.added
43070
43135
  };
43071
43136
  }
43072
- /** The setting Nextly needs off, and the one Next.js accepts only with it on. */
43073
- const CACHE_COMPONENTS = "cacheComponents";
43074
- const PARTIAL_PREFETCHING = "partialPrefetching";
43075
43137
  /**
43076
- * Remove a property with the comma that separates it: the whole line when it
43077
- * has one of its own, else the property and the comma after it, or before it
43078
- * when it is the last one.
43138
+ * The exported config object, when every setting this edits can be read in
43139
+ * it; else why not. Not through a wrapper call: Next.js receives what the
43140
+ * wrapper returns, which may set an option again, so the literal passed in
43141
+ * says nothing final.
43079
43142
  */
43080
- function removeProperty(source, property) {
43081
- const start = property.start ?? 0;
43082
- const end = property.end ?? 0;
43083
- const ownLine = /^\s*$/.test(source.slice(lineStart(source, start), start));
43084
- const rest = /^[ \t]*,?[ \t]*\n/.exec(source.slice(end));
43085
- if (ownLine && rest) {
43086
- const from = lineStart(source, start);
43087
- return {
43088
- at: from,
43089
- remove: end + rest[0].length - from,
43090
- text: ""
43091
- };
43092
- }
43093
- const after = /^\s*,\s*/.exec(source.slice(end));
43094
- if (after) return {
43095
- at: start,
43096
- remove: end - start + after[0].length,
43097
- text: ""
43098
- };
43099
- const before = /,\s*$/.exec(source.slice(0, start));
43100
- const from = before ? start - before[0].length : start;
43143
+ function cacheComponentsObject(body) {
43144
+ const object = exportedConfig(body, false);
43145
+ if (typeof object === "string") return object;
43146
+ return hiddenSettings(object, [...ON_OPTIONS, ...HANDLER_SETTINGS], "it") ?? object;
43147
+ }
43148
+ /**
43149
+ * How to turn one option on: the `false` to flip, the property to add when the
43150
+ * config does not set it, nothing when it is on, or why the value cannot be
43151
+ * read.
43152
+ */
43153
+ function optionStep(object, name) {
43154
+ const value = propertyValue(object, name);
43155
+ if (!value) return { add: `${name}: true` };
43156
+ if (value.type !== "BooleanLiteral") return `its ${name} is not true or false written in place`;
43157
+ if (value.value) return {};
43158
+ return { flip: {
43159
+ at: value.start ?? 0,
43160
+ remove: (value.end ?? 0) - (value.start ?? 0),
43161
+ text: "true"
43162
+ } };
43163
+ }
43164
+ /**
43165
+ * Whether Node loads the config as an ES module, where `require` is not
43166
+ * defined: `.mjs` and `.mts` always, and `.js` when it imports or exports,
43167
+ * which a config in a package of `"type": "module"` must, and which Node
43168
+ * detects in any other. Next.js compiles `next.config.ts` to CommonJS.
43169
+ */
43170
+ function loadsAsModule(file, body) {
43171
+ if (/\.m[jt]s$/.test(file)) return true;
43172
+ return file.endsWith(".js") && body.some((statement) => statement.type === "ImportDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration");
43173
+ }
43174
+ /**
43175
+ * The name an ES module config imports `createRequire` under from
43176
+ * `node:module`, whatever it renamed it to, or null when it imports none.
43177
+ */
43178
+ function createRequireImport(body) {
43179
+ return body.flatMap((statement) => statement.type === "ImportDeclaration" && (statement.source.value === "node:module" || statement.source.value === "module") ? statement.specifiers : []).find((item) => item.type === "ImportSpecifier" && importedName(item) === "createRequire")?.local.name ?? null;
43180
+ }
43181
+ /** The name an import specifier takes from its module, written plain or quoted. */
43182
+ function importedName(specifier) {
43183
+ const { imported } = specifier;
43184
+ return imported.type === "Identifier" ? imported.name : imported.value;
43185
+ }
43186
+ /** Whether the file binds `require` at its top level, by a declaration or an import. */
43187
+ function declaresRequire(body) {
43188
+ return body.some((statement) => importsRequire(statement) || declaresRequireVariable(statement));
43189
+ }
43190
+ /** Whether `statement` imports a binding named `require`: hoisted, so bound anywhere. */
43191
+ function importsRequire(statement) {
43192
+ return statement.type === "ImportDeclaration" && statement.specifiers.some((specifier) => isNamed(specifier.local, "require"));
43193
+ }
43194
+ /** The declarators in `statement` that name a variable `require`, exported or not. */
43195
+ function requireDeclarators(statement) {
43196
+ const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
43197
+ return declaration?.type === "VariableDeclaration" ? declaration.declarations.filter((declarator) => isNamed(declarator.id, "require")) : [];
43198
+ }
43199
+ /** Whether `statement` declares a variable named `require`, exported or not. */
43200
+ function declaresRequireVariable(statement) {
43201
+ return requireDeclarators(statement).length > 0;
43202
+ }
43203
+ /**
43204
+ * Whether the file binds `require` only by a variable declared after `at`,
43205
+ * where the config object starts. The object's `require.resolve` runs when
43206
+ * the object is evaluated, while that declaration is still in its temporal dead
43207
+ * zone, so it would throw: the lines are printed rather than written.
43208
+ */
43209
+ function declaresRequireAfter(body, at) {
43210
+ if (body.some(importsRequire)) return false;
43211
+ const declarators = body.flatMap(requireDeclarators);
43212
+ return declarators.length > 0 && declarators.every((declarator) => (declarator.start ?? 0) >= at);
43213
+ }
43214
+ /**
43215
+ * What an ES module config needs at its top for the handlers' `require`:
43216
+ * nothing when it binds `require` already, the declaration alone, calling its
43217
+ * own `createRequire` by its name, when it imports one, and otherwise the
43218
+ * import and the declaration.
43219
+ */
43220
+ function requireSetupFor(body) {
43221
+ if (declaresRequire(body)) return null;
43222
+ const imported = createRequireImport(body);
43223
+ return imported ? requireDeclaration(imported) : `${REQUIRE_IMPORT}\n\n${REQUIRE_DECLARATION}`;
43224
+ }
43225
+ /**
43226
+ * The edit that gives an ES module config `require`, before the first
43227
+ * statement that runs, and before the comments that lead it, such as a
43228
+ * `@type` annotation. Imports are hoisted, so one written further down still
43229
+ * loads first; the declaration is not, and a `require.resolve` evaluated
43230
+ * above it would throw.
43231
+ */
43232
+ function requireEdit(source, body, setup) {
43233
+ const first = body.find((statement) => statement.type !== "ImportDeclaration");
43101
43234
  return {
43102
- at: from,
43103
- remove: end - from,
43104
- text: ""
43235
+ at: lineStart(source, first?.leadingComments?.[0]?.start ?? first?.start ?? 0),
43236
+ remove: 0,
43237
+ text: `${setup}\n\n`
43105
43238
  };
43106
43239
  }
43107
- /** The exported config object, when both settings in it can be read; else why not. */
43108
- function cacheComponentsObject(source, file) {
43109
- const body = parseBody(source, file);
43110
- if (!body) return "it could not be parsed";
43111
- const object = exportedConfig(body, false);
43112
- if (typeof object === "string") return object;
43113
- return hiddenSettings(object, [CACHE_COMPONENTS, PARTIAL_PREFETCHING], "it") ?? object;
43240
+ /** Whether `node` is `require.resolve("<specifier>")`. */
43241
+ function resolvesTo(node, specifier) {
43242
+ if (node?.type !== "CallExpression") return false;
43243
+ const { callee, arguments: args } = node;
43244
+ const [only] = args;
43245
+ return callee.type === "MemberExpression" && isNamed(callee.object, "require") && isNamed(callee.property, "resolve") && args.length === 1 && only?.type === "StringLiteral" && only.value === specifier;
43114
43246
  }
43115
- /** The `true` to flip, null when Cache Components is off or unset, or why the value cannot be read. */
43116
- function enabledValue(object) {
43117
- const value = propertyValue(object, CACHE_COMPONENTS);
43118
- if (!value) return null;
43119
- if (value.type !== "BooleanLiteral") return `its ${CACHE_COMPONENTS} is not true or false written in place`;
43120
- return value.value ? value : null;
43247
+ /** Whether the config's cache settings are Nextly's handlers, as this writes them. */
43248
+ function hasNextlysHandlers(object) {
43249
+ const handlers = propertyValue(object, "cacheHandlers");
43250
+ const memory = propertyValue(object, "cacheMaxMemorySize");
43251
+ return resolvesTo(propertyValue(object, "cacheHandler"), "nextly/cache-handler") && handlers?.type === "ObjectExpression" && resolvesTo(propertyValue(handlers, "default"), "nextly/cache-handlers") && memory?.type === "NumericLiteral" && memory.value === 0;
43121
43252
  }
43122
- /** The edits that turn Cache Components off in a config, none when it is off, or why it cannot be changed. */
43123
- function cacheComponentsPlan(source, file) {
43124
- const object = cacheComponentsObject(source, file);
43125
- if (typeof object === "string") return { reason: object };
43126
- const value = enabledValue(object);
43127
- if (typeof value === "string") return { reason: value };
43128
- if (!value) return {
43253
+ /** The settings that make a cache of the app's own: a handler for either cache. */
43254
+ const OWN_CACHE_SETTINGS = ["cacheHandler", "cacheHandlers"];
43255
+ /** Whether the config gives `name` a value: written, and not `undefined`. */
43256
+ function setsValue(object, name) {
43257
+ const value = propertyValue(object, name);
43258
+ return value !== void 0 && !isNamed(value, "undefined");
43259
+ }
43260
+ /**
43261
+ * Why Nextly's handlers cannot go into this config as it stands, or null when
43262
+ * they can. A setting written as `undefined` is a placeholder to fill, not a
43263
+ * value; a handler of the app's own is not replaced; and `cacheMaxMemorySize`
43264
+ * must be 0, so Next.js keeps no second copy of the handlers' entries.
43265
+ */
43266
+ function handlerConflict(object) {
43267
+ const placeholders = HANDLER_SETTINGS.filter((name) => propertyValue(object, name) !== void 0 && !setsValue(object, name));
43268
+ if (placeholders.length > 0) return `it sets ${placeholders.join(", ")} to undefined`;
43269
+ const own = OWN_CACHE_SETTINGS.filter((name) => setsValue(object, name));
43270
+ if (own.length > 0) return `it has cache handlers of its own (${own.join(", ")}), which were kept`;
43271
+ const memory = propertyValue(object, "cacheMaxMemorySize");
43272
+ return memory && !(memory.type === "NumericLiteral" && memory.value === 0) ? "it sets cacheMaxMemorySize to a value other than 0, which Nextly's handlers need so that Next.js keeps no second copy of their entries" : null;
43273
+ }
43274
+ /**
43275
+ * How to install Nextly's cache handlers: the properties to add and any edit
43276
+ * that gives the file `require`, or why they are left out, with the lines to
43277
+ * put in place of the config's own settings.
43278
+ */
43279
+ function handlerStep(source, object, body, esm) {
43280
+ const none = {
43281
+ add: [],
43282
+ edits: []
43283
+ };
43284
+ if (hasNextlysHandlers(object)) return {
43285
+ ...none,
43286
+ outcome: "present"
43287
+ };
43288
+ const setup = esm ? requireSetupFor(body) : null;
43289
+ const conflict = esm && declaresRequireAfter(body, object.start ?? 0) ? "it declares require after its config object, so the handlers' require.resolve would run before require exists; move that declaration above the object" : handlerConflict(object);
43290
+ if (conflict) return {
43291
+ ...none,
43292
+ outcome: {
43293
+ skipped: conflict,
43294
+ lines: HANDLER_PROPERTIES,
43295
+ requireSetup: setup
43296
+ }
43297
+ };
43298
+ const memorySet = propertyValue(object, "cacheMaxMemorySize") !== void 0;
43299
+ const add = HANDLER_PROPERTIES.filter((property) => !(memorySet && property.startsWith("cacheMaxMemorySize")));
43300
+ if (setup === null) return {
43301
+ add,
43129
43302
  edits: [],
43130
- removed: []
43303
+ outcome: "added"
43131
43304
  };
43132
- const flip = {
43133
- at: value.start ?? 0,
43134
- remove: (value.end ?? 0) - (value.start ?? 0),
43135
- text: "false"
43305
+ if (referenceCount("require", body) > 0 || createRequireImport(body) === null && referenceCount("createRequire", body) > 0) return {
43306
+ ...none,
43307
+ outcome: {
43308
+ skipped: "it is an ES module that uses require, or a createRequire not imported from node:module, without binding require, so the lines that define it could clash",
43309
+ lines: add,
43310
+ requireSetup: setup
43311
+ }
43312
+ };
43313
+ return {
43314
+ add,
43315
+ edits: [requireEdit(source, body, setup)],
43316
+ outcome: "added"
43136
43317
  };
43137
- const prefetching = object.properties.filter((property) => isPropertyNamed(property, PARTIAL_PREFETCHING));
43318
+ }
43319
+ /** The edits that turn Cache Components on in a config, or why it cannot be changed. */
43320
+ function cacheComponentsPlan(source, file, esm) {
43321
+ const body = parseBody(source, file);
43322
+ if (!body) return { reason: "it could not be parsed" };
43323
+ const object = cacheComponentsObject(body);
43324
+ if (typeof object === "string") return { reason: object };
43325
+ const options = optionsPlan(object);
43326
+ if ("reason" in options) return options;
43327
+ const handlers = handlerStep(source, object, body, esm);
43328
+ const add = [...options.add, ...handlers.add];
43329
+ const edits = [...options.flips, ...handlers.edits];
43330
+ if (add.length > 0) edits.push(...appendProperties(source, object, () => add));
43138
43331
  return {
43139
- edits: [flip, ...prefetching.map((property) => removeProperty(source, property))],
43140
- removed: prefetching.length > 0 ? [PARTIAL_PREFETCHING] : []
43332
+ edits,
43333
+ turnedOn: options.turnedOn,
43334
+ handlers: handlers.outcome
43141
43335
  };
43142
43336
  }
43143
- /** Whether a config, read as code, still defines `partialPrefetching` anywhere in its exported object. */
43144
- function definesPartialPrefetching(source, file) {
43145
- const object = cacheComponentsObject(source, file);
43146
- return typeof object !== "string" && object.properties.some((property) => isPropertyNamed(property, PARTIAL_PREFETCHING));
43337
+ /** How to turn both options on: the values to flip, the properties to add, and which options change. */
43338
+ function optionsPlan(object) {
43339
+ const plan = {
43340
+ flips: [],
43341
+ add: [],
43342
+ turnedOn: []
43343
+ };
43344
+ for (const option of ON_OPTIONS) {
43345
+ const step = optionStep(object, option);
43346
+ if (typeof step === "string") return { reason: step };
43347
+ if (step.flip) plan.flips.push(step.flip);
43348
+ if (step.add) plan.add.push(step.add);
43349
+ if (step.flip || step.add) plan.turnedOn.push(option);
43350
+ }
43351
+ return plan;
43147
43352
  }
43148
43353
  /**
43149
- * Turn Next.js Cache Components off in an existing app's config, which
43150
- * Nextly does not support yet; `create-next-app` 16.4 turns it on by default.
43354
+ * Turn Next.js Cache Components and Partial Prefetching on in an existing
43355
+ * app's config, with Nextly's cache handlers: Nextly runs with both on.
43356
+ * Apps that an earlier Nextly release turned off are turned on again.
43151
43357
  *
43152
- * Only `cacheComponents: true` written in place is changed, and
43153
- * `partialPrefetching` goes with it, since Next.js refuses that option once
43154
- * Cache Components is off. The edit is trusted only once the result parses
43155
- * and reads as off; anything else is left as it was, with what to change.
43358
+ * Only settings written in place as `true` or `false`, or absent, are
43359
+ * changed; a value this cannot read leaves the file as it was, with what to
43360
+ * add. The edit is trusted only once the result parses and reads as on.
43156
43361
  */
43157
- async function turnOffCacheComponents(cwd) {
43158
- const file = await findConfigFile(cwd);
43159
- if (!file) return { kind: "unchanged" };
43160
- const source = await import_lib$1.default.readFile(path.default.join(cwd, file), "utf-8");
43161
- const plan = cacheComponentsPlan(source, file);
43362
+ async function turnOnCacheComponents(cwd) {
43363
+ const file = await nextConfigFileToChange(cwd);
43364
+ const source = await import_lib$1.default.readFile(path.default.join(cwd, file), "utf-8").catch(() => null);
43365
+ if (source === null) return {
43366
+ kind: "manual",
43367
+ file,
43368
+ reason: "the app has no Next config",
43369
+ requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
43370
+ };
43371
+ const body = parseBody(source, file);
43372
+ if (!body) return {
43373
+ kind: "manual",
43374
+ file,
43375
+ reason: "it could not be parsed",
43376
+ requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
43377
+ };
43378
+ const esm = loadsAsModule(file, body);
43379
+ const requireSetup = esm ? requireSetupFor(body) : null;
43380
+ const plan = cacheComponentsPlan(source, file, esm);
43162
43381
  if ("reason" in plan) return {
43163
43382
  kind: "manual",
43164
43383
  file,
43165
- reason: plan.reason
43384
+ reason: plan.reason,
43385
+ requireSetup
43386
+ };
43387
+ if (plan.edits.length === 0) return {
43388
+ kind: "unchanged",
43389
+ file,
43390
+ handlers: plan.handlers
43166
43391
  };
43167
- if (plan.edits.length === 0) return { kind: "unchanged" };
43168
43392
  const text = applyEdits(source, plan.edits);
43169
- const recheck = cacheComponentsPlan(text, file);
43170
- if ("reason" in recheck || recheck.edits.length > 0 || definesPartialPrefetching(text, file)) return {
43393
+ const recheck = cacheComponentsPlan(text, file, esm);
43394
+ if ("reason" in recheck || recheck.edits.length > 0) return {
43171
43395
  kind: "manual",
43172
43396
  file,
43173
- reason: "the edit could not be confirmed"
43397
+ reason: "the edit could not be confirmed",
43398
+ requireSetup
43174
43399
  };
43175
43400
  await import_lib$1.default.writeFile(path.default.join(cwd, file), text, "utf-8");
43176
43401
  return {
43177
- kind: "turned-off",
43402
+ kind: "turned-on",
43178
43403
  file,
43179
- removed: plan.removed
43404
+ turnedOn: plan.turnedOn,
43405
+ handlers: plan.handlers
43180
43406
  };
43181
43407
  }
43182
43408
  /**
43183
- * What turning Cache Components off did, for the run's report: the change and
43184
- * a notice saying why, or, for a config it could not change, what to change.
43409
+ * What turning Cache Components on did, for the run's report: each change and
43410
+ * a notice saying why, or, for a config it could not change, the exact lines
43411
+ * to add.
43185
43412
  */
43186
43413
  function cacheComponentsFiles(patch) {
43187
43414
  const files = {
@@ -43189,43 +43416,42 @@ function cacheComponentsFiles(patch) {
43189
43416
  changed: [],
43190
43417
  skipped: []
43191
43418
  };
43192
- if (patch.kind === "turned-off") {
43419
+ if (patch.kind === "manual") {
43420
+ files.skipped.push({
43421
+ path: patch.file,
43422
+ why: `Cache Components was not turned on: ${patch.reason}. Nextly runs with Cache Components and Partial Prefetching on, with its cache handlers (${CACHE_COMPONENTS_DOCS_URL}); add these to the config object Next.js receives, in place of any value of theirs`,
43423
+ toAdd: linesToAdd$1(ON_LINES, patch.requireSetup)
43424
+ });
43425
+ return files;
43426
+ }
43427
+ const handlersAdded = patch.handlers === "added";
43428
+ if (patch.kind === "turned-on") {
43193
43429
  files.changed.push({
43194
43430
  path: patch.file,
43195
- added: [`${CACHE_COMPONENTS}: false`]
43431
+ added: [...patch.turnedOn.map((option) => `${option}: true`), ...handlersAdded ? [...HANDLER_SETTINGS] : []]
43196
43432
  });
43197
- const removed = patch.removed.length ? `, and removed ${patch.removed.join(", ")}, which Next.js accepts only with it on` : "";
43198
- 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}`];
43199
- } else if (patch.kind === "manual") files.skipped.push({
43433
+ const done = [...patch.turnedOn.length > 0 ? [`turned ${patch.turnedOn.join(" and ")} on`] : [], ...handlersAdded ? ["installed Nextly's cache handlers"] : []].join(" and ");
43434
+ files.notices = [`In ${patch.file}, ${done}: Nextly runs with Cache Components and Partial Prefetching on. What each setting does: ${CACHE_COMPONENTS_DOCS_URL}`];
43435
+ }
43436
+ if (typeof patch.handlers === "object") files.skipped.push({
43200
43437
  path: patch.file,
43201
- 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}`,
43202
- toAdd: ` ${CACHE_COMPONENTS}: false,`
43438
+ why: `Nextly's cache handlers were not added: ${patch.handlers.skipped}. Without them a page read right after a write can show the previous version once, and in production every cached public read refreshes within a minute of a visit, since a write from another instance, the CLI or a job reaches no other cache (${CACHE_COMPONENTS_DOCS_URL}); to use them, put these in place of the config's own cache settings`,
43439
+ toAdd: linesToAdd$1(patch.handlers.lines.map((property) => ` ${property},`), patch.handlers.requireSetup)
43203
43440
  });
43204
43441
  return files;
43205
43442
  }
43206
43443
  /**
43207
- * What to add by hand, in the spelling the app's Next.js reads.
43208
- *
43209
- * On Next.js 14 the package list and the instrumentation opt-in are keys of
43210
- * one `experimental` object, so they are given as one: two `experimental`
43211
- * properties, pasted as given, leave only the second. The instrumentation
43212
- * hook Nextly starts from needs the opt-in there, and no other step says so.
43213
- * `wrap` turns each spelling into what to paste: the key itself, or a whole
43214
- * config file when the app has none.
43215
- */
43216
- function optionText(packages, spelling, wrap = (option) => option) {
43217
- const list = packages.map((name) => JSON.stringify(name)).join(", ");
43218
- const current = wrap(` ${PROPERTY}: [${list}],`);
43219
- const legacy = wrap(` experimental: { instrumentationHook: true, serverComponentsExternalPackages: [${list}] },`);
43220
- if (spelling === "legacy") return legacy;
43221
- return spelling === "either" ? `On Next.js 15 or later:\n\n${current}\n\nOn Next.js 14:\n\n${legacy}` : current;
43222
- }
43223
- /**
43224
- * A whole config holding `option`, for an app that has none. ESM in `.mjs`,
43225
- * which loads whatever the package's own "type" says.
43444
+ * The config lines to add by hand. An ES module config with no `require`
43445
+ * also gets the lines that define it for the handlers, at the top of the file.
43226
43446
  */
43227
- function wholeConfig(option) {
43228
- return `/** @type {import('next').NextConfig} */\nexport default {\n${option}\n};\n`;
43447
+ function linesToAdd$1(lines, requireSetup) {
43448
+ const config = lines.join("\n");
43449
+ return requireSetup ? `// At the top of the file:\n${requireSetup.replace("\n\n", "\n")}\n\n// In the config object:\n${config}` : config;
43450
+ }
43451
+ /** What to add by hand: the list, as the config's top-level option. */
43452
+ function optionText(packages) {
43453
+ const list = packages.map((name) => JSON.stringify(name)).join(", ");
43454
+ return ` ${PROPERTY}: [${list}],`;
43229
43455
  }
43230
43456
  /**
43231
43457
  * What a patch did, in the shape every existing-app step reports in: the
@@ -43249,109 +43475,14 @@ function nextConfigFiles(patch) {
43249
43475
  files.written.push(patch.file);
43250
43476
  break;
43251
43477
  case "unchanged": break;
43252
- case "manual": files.skipped.push(patch.create ? {
43478
+ case "manual": files.skipped.push({
43253
43479
  path: patch.file,
43254
- why: `Nextly's packages were not added: ${patch.reason}, and the app has no Next.js config. Create ${patch.file} with:`,
43255
- toAdd: optionText(patch.packages, patch.spelling, wholeConfig)
43256
- } : {
43257
- path: patch.file,
43258
- 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`,
43259
- toAdd: optionText(patch.packages, patch.spelling)
43480
+ why: patch.reason === LEGACY_LIST_REASON ? `Nextly's packages were not added: ${patch.reason}. Move the packages under experimental.${LEGACY_PROPERTY} into a top-level ${PROPERTY} list, add Nextly's to it, and delete experimental.${LEGACY_PROPERTY}, keeping the config's other experimental keys` : `Nextly's packages were not added: ${patch.reason}. Add them to the config object it exports, keeping any packages and experimental keys already there`,
43481
+ toAdd: optionText(patch.packages)
43260
43482
  });
43261
43483
  }
43262
43484
  return files;
43263
43485
  }
43264
- // src/generators/page-extensions.ts
43265
- /**
43266
- * The page extension to name a new module file with, a hook or a route: a
43267
- * TypeScript one first, since Nextly's files are TypeScript, then a
43268
- * JavaScript one; never a content extension such as `mdx`, which Next.js
43269
- * would compile as content, and never `cjs`, since the files are ES modules
43270
- * and a `.cjs` file cannot hold one. Null when the list holds no extension a
43271
- * module can be written in.
43272
- */
43273
- function moduleExtension(extensions) {
43274
- const usable = writable(extensions);
43275
- return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
43276
- }
43277
- /**
43278
- * The extensions a file with code in it can be written under: not a
43279
- * declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
43280
- * types only and refuses code in.
43281
- */
43282
- function writable(extensions) {
43283
- return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
43284
- }
43285
- /**
43286
- * The `pageExtensions` the app's Next.js config sets, as every existing-app
43287
- * step that names a file Next.js routes reads it: the list; `"absent"` when
43288
- * the config sets none, or there is no config; or null when it is set in a
43289
- * way that cannot be read. Read from the config the app exports, as the Next
43290
- * config step reads it, so a commented-out or nested `pageExtensions` sets
43291
- * nothing. `configName` is the file it was read from, for the report.
43292
- */
43293
- async function appPageExtensions(cwd) {
43294
- const nextConfig = await readNextConfig(cwd);
43295
- return {
43296
- configName: nextConfig?.name ?? "The Next.js config",
43297
- extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
43298
- };
43299
- }
43300
- /**
43301
- * The text of the app's Next.js config, or null when it has none, from the
43302
- * file the Next config step reads and extends ({@link findConfigFile}).
43303
- */
43304
- async function readNextConfig(cwd) {
43305
- const name = await findConfigFile(cwd);
43306
- return name === null ? null : {
43307
- name,
43308
- text: await import_lib$1.default.readFile(path.default.join(cwd, name), "utf-8")
43309
- };
43310
- }
43311
- /**
43312
- * The page extension for a file of TypeScript with JSX, such as a page or a
43313
- * layout: one ending in `tsx`. Null when the list holds none.
43314
- */
43315
- function tsxExtension(extensions) {
43316
- return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
43317
- }
43318
- /** A JavaScript or TypeScript module without JSX, such as a route. */
43319
- const MODULE_FILE = {
43320
- pick: moduleExtension,
43321
- holds: "a JavaScript or TypeScript extension"
43322
- };
43323
- /** TypeScript with JSX, such as a page or a layout. */
43324
- const TSX_FILE = {
43325
- pick: tsxExtension,
43326
- holds: "an extension ending in tsx"
43327
- };
43328
- /**
43329
- * The report for a file an app that sets `pageExtensions` gets in place of
43330
- * the file: the name to create it under and its content. Next.js routes a
43331
- * file there only under a name with one of the app's extensions, and the app
43332
- * may already have the file under such a name, which the existing-app steps
43333
- * do not look for, so nothing is written. A list that cannot be read, or that
43334
- * holds no extension the content needs, gives a placeholder name and says
43335
- * which.
43336
- *
43337
- * @param base the file's path without its extension, as reported, such as
43338
- * `app/admin/[[...params]]/page`
43339
- */
43340
- function pageExtensionsSkip(base, text, pages, need) {
43341
- const { configName, extensions } = pages;
43342
- const extension = extensions === null ? null : need.pick(extensions);
43343
- const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
43344
- if (extensions !== null && extension === null) return {
43345
- path: `${base}.<${need.holds} added to pageExtensions>`,
43346
- 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`,
43347
- toAdd: text
43348
- };
43349
- return {
43350
- path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
43351
- why: `${sets}; unless the app already has this file, create it under that name with this content`,
43352
- toAdd: text
43353
- };
43354
- }
43355
43486
  // src/utils/detect-pm-from-user-agent.ts
43356
43487
  /**
43357
43488
  * Detect the package manager that invoked `create-nextly-app` by reading
@@ -45582,6 +45713,45 @@ function getDefaultApproach(name) {
45582
45713
  function shouldUseBundledTemplate(projectType, options) {
45583
45714
  return BUNDLED_TEMPLATES.some((name) => name === projectType) && !options.localTemplatePath && !options.useYalc && options.branch === void 0;
45584
45715
  }
45716
+ // src/supported-versions.ts
45717
+ /**
45718
+ * The Next.js and React this release of Nextly supports, in one place.
45719
+ *
45720
+ * Two facts, deliberately kept apart:
45721
+ *
45722
+ * - `REVIEWED_VERSIONS` is the newest release of each package this Nextly
45723
+ * release was reviewed against. A new app declares it as a `~` range
45724
+ * ({@link scaffoldRange}): patch releases of that minor, never a newer minor
45725
+ * or major that nobody has checked, and never whatever npm's `latest` names
45726
+ * on the day of the scaffold.
45727
+ * - `SUPPORTED_RANGES` is every release Nextly works with: the `nextly`
45728
+ * package's own peer ranges, inlined at build, so they are declared once.
45729
+ * Every other package declares the same as its peers. Adding Nextly to an
45730
+ * existing app refuses one whose Next.js, React or React DOM falls outside
45731
+ * them, and a scaffolded plugin declares them as its own peers.
45732
+ *
45733
+ * Only a release review moves either: a newer Next.js or React is supported
45734
+ * once a Nextly release has been checked against it, not before.
45735
+ */
45736
+ const REVIEWED_VERSIONS = {
45737
+ next: "16.4.2",
45738
+ "eslint-config-next": "16.4.2",
45739
+ react: "19.3.1",
45740
+ "react-dom": "19.3.1"
45741
+ };
45742
+ const SUPPORTED_RANGES = {
45743
+ "next": "^16.4.1",
45744
+ "react": "^19.3.0",
45745
+ "react-dom": "^19.3.0"
45746
+ };
45747
+ /**
45748
+ * The range a new project declares for `pkg`: patch releases of the reviewed
45749
+ * minor only. Derived here rather than written out, so the reviewed version is
45750
+ * the one thing a release review edits.
45751
+ */
45752
+ function scaffoldRange(pkg) {
45753
+ return `~${REVIEWED_VERSIONS[pkg]}`;
45754
+ }
45585
45755
  // src/utils/package-manager-commands.ts
45586
45756
  /**
45587
45757
  * The prefix that runs a package.json script.
@@ -45609,37 +45779,35 @@ function binaryRunner(packageManager) {
45609
45779
  case "bun": return "bunx";
45610
45780
  }
45611
45781
  }
45612
- // src/utils/plugin-node.ts
45613
- /**
45614
- * The Node versions a scaffolded plugin supports, and the check that refuses
45615
- * to scaffold one on a Node it cannot build on.
45616
- *
45617
- * A plugin builds with tsdown and tests with Vitest, and each declares the
45618
- * Node it runs on; the plugin's floor is where both agree. The repository's
45619
- * packages have a lower floor, because only a plugin author runs these tools.
45620
- * `scaffold-plugin.test.ts` derives this range from the installed tools' own
45621
- * `engines`, so a version bump that moves either one fails there.
45622
- */
45623
- const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
45782
+ // src/utils/version-range.ts
45624
45783
  function parseVersion$1(text) {
45625
- const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text.trim());
45626
- if (!match) throw new Error(`not a Node version: ${text}`);
45627
- return [
45784
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/.exec(text.trim());
45785
+ if (!match) throw new Error(`not a version: ${text}`);
45786
+ return Object.assign([
45628
45787
  Number(match[1]),
45629
45788
  Number(match[2]),
45630
45789
  Number(match[3])
45631
- ];
45790
+ ], { prerelease: match[4] !== void 0 });
45632
45791
  }
45792
+ /** Older first; of two with the same numbers, a prerelease comes before its release, as in semver. */
45633
45793
  function compare(a, b) {
45634
- return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
45794
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || Number(Boolean(b.prerelease)) - Number(Boolean(a.prerelease));
45635
45795
  }
45636
45796
  /**
45637
- * Whether `version` satisfies one comparator. Only the two forms these tools
45638
- * write are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
45639
- * than read approximately, so a tool that starts declaring another form fails
45797
+ * Negative when `a` is older than `b`, positive when newer, 0 when equal.
45798
+ * A prerelease is older than its release: `16.4.1-canary.1` is below `16.4.1`.
45799
+ */
45800
+ function compareVersions(a, b) {
45801
+ return compare(parseVersion$1(a), parseVersion$1(b));
45802
+ }
45803
+ /**
45804
+ * Whether `version` satisfies one comparator. Only the two forms this CLI
45805
+ * writes are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
45806
+ * than read approximately, so a range that starts using another form fails
45640
45807
  * the test that reads it.
45641
45808
  */
45642
45809
  function satisfiesComparator(version, comparator) {
45810
+ if (version.prerelease) return false;
45643
45811
  const caret = /^\^(\d+\.\d+\.\d+)$/.exec(comparator);
45644
45812
  if (caret) {
45645
45813
  const floor = parseVersion$1(caret[1]);
@@ -45647,20 +45815,82 @@ function satisfiesComparator(version, comparator) {
45647
45815
  }
45648
45816
  const atLeast = /^>=(\d+\.\d+\.\d+)$/.exec(comparator);
45649
45817
  if (atLeast) return compare(version, parseVersion$1(atLeast[1])) >= 0;
45650
- throw new Error(`unsupported Node range comparator: ${comparator}`);
45818
+ throw new Error(`unsupported range comparator: ${comparator}`);
45651
45819
  }
45652
45820
  /** Whether `version` satisfies a `||` list of `^X.Y.Z` / `>=X.Y.Z` comparators. */
45653
- function satisfiesNodeRange(version, range) {
45821
+ function satisfiesRange(version, range) {
45654
45822
  const parsed = parseVersion$1(version);
45655
45823
  return range.split("||").map((part) => part.trim()).some((comparator) => satisfiesComparator(parsed, comparator));
45656
45824
  }
45657
45825
  /**
45826
+ * The lowest version a package.json dependency spec admits, as `X.Y.Z`, or
45827
+ * `undefined` when the spec names no lower bound this can read: a dist-tag
45828
+ * (`latest`, `canary`), a protocol (`workspace:*`, `file:`, a URL), `*`, or an
45829
+ * upper bound alone (`<17`). An exact version is its own lowest; `^`, `~`, `=`
45830
+ * and `>=` start at the version they name; a missing or wildcard minor or
45831
+ * patch (`15`, `15.x`) counts as 0; across `||` the lowest alternative wins.
45832
+ */
45833
+ function lowestVersionIn(spec) {
45834
+ let lowest;
45835
+ for (const part of spec.split("||")) {
45836
+ const match = /^\s*(?:\^|~|>=|=)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?/.exec(part);
45837
+ if (!match) continue;
45838
+ const number = (text) => text === void 0 || !/^\d+$/.test(text) ? 0 : Number(text);
45839
+ const found = [
45840
+ Number(match[1]),
45841
+ number(match[2]),
45842
+ number(match[3])
45843
+ ];
45844
+ if (!lowest || compare(found, lowest) < 0) lowest = found;
45845
+ }
45846
+ return lowest ? lowest.join(".") : void 0;
45847
+ }
45848
+ /**
45849
+ * The highest major a package.json dependency spec admits, `Infinity` when it
45850
+ * has no upper bound (`>=16.4.1`, `*`, `x`), or `undefined` when no
45851
+ * alternative names one this can read (a dist-tag, a protocol). Across `||`
45852
+ * the highest alternative wins, since an install resolves the newest version
45853
+ * any of them admits. Within one alternative an upper bound (`<17`, `<=17.2`)
45854
+ * decides; otherwise the version a `^`, `~`, `=` or bare spec names.
45855
+ */
45856
+ function highestMajorIn(spec) {
45857
+ let highest;
45858
+ for (const part of spec.split("||")) {
45859
+ const found = majorReachedBy(part.trim());
45860
+ if (found !== void 0 && (highest === void 0 || found > highest)) highest = found;
45861
+ }
45862
+ return highest;
45863
+ }
45864
+ /** The highest major one `||` alternative reaches, as `highestMajorIn` reads it. */
45865
+ function majorReachedBy(part) {
45866
+ const upper = /<(=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(part);
45867
+ if (upper) {
45868
+ const [, inclusive, major, minor, patch] = upper;
45869
+ return inclusive !== void 0 || Number(minor ?? 0) > 0 || Number(patch ?? 0) > 0 ? Number(major) : Number(major) - 1;
45870
+ }
45871
+ if (part === "" || /^(\*|[xX])$/.test(part) || part.startsWith(">")) return Infinity;
45872
+ const named = /^(?:\^|~|=)?\s*v?(\d+)/.exec(part);
45873
+ return named ? Number(named[1]) : void 0;
45874
+ }
45875
+ // src/utils/plugin-node.ts
45876
+ /**
45877
+ * The Node versions a scaffolded plugin supports, and the check that refuses
45878
+ * to scaffold one on a Node it cannot build on.
45879
+ *
45880
+ * A plugin builds with tsdown and tests with Vitest, and each declares the
45881
+ * Node it runs on; the plugin's floor is where both agree. The repository's
45882
+ * packages have a lower floor, because only a plugin author runs these tools.
45883
+ * `scaffold-plugin.test.ts` derives this range from the installed tools' own
45884
+ * `engines`, so a version bump that moves either one fails there.
45885
+ */
45886
+ const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
45887
+ /**
45658
45888
  * Why the plugin template cannot be scaffolded on `nodeVersion`, or
45659
45889
  * `undefined` when it can. Checked before anything is written, since a plugin
45660
45890
  * scaffolded on an older Node would install and then fail its first build.
45661
45891
  */
45662
45892
  function pluginNodeRefusal(nodeVersion) {
45663
- if (satisfiesNodeRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
45893
+ if (satisfiesRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
45664
45894
  return `The plugin template builds with tools that need Node ${PLUGIN_NODE_ENGINES}, and this is Node ${nodeVersion}. Install a supported Node (for example \`nvm install 24\`) and run create-nextly-app again.`;
45665
45895
  }
45666
45896
  // src/utils/template.ts
@@ -46305,14 +46535,12 @@ async function replacePlaceholders(dir, placeholders, skip = /* @__PURE__ */ new
46305
46535
  /**
46306
46536
  * Pinned dependency versions for generated projects.
46307
46537
  *
46308
- * `next` and `eslint-config-next` are resolved at runtime from the npm
46309
- * registry so that fresh projects always get the latest release without
46310
- * needing to republish create-nextly-app. The remaining versions use
46311
- * wide semver ranges that rarely need updating.
46538
+ * `next`, `eslint-config-next`, `react` and `react-dom` are not here: they
46539
+ * come from `supported-versions`, the reviewed release of each, so a project
46540
+ * never declares a Next.js or React no Nextly release was checked against.
46541
+ * The remaining versions use wide semver ranges that rarely need updating.
46312
46542
  */
46313
46543
  const PINNED_VERSIONS = {
46314
- react: "^19.3.1",
46315
- "react-dom": "^19.3.1",
46316
46544
  typescript: "^5",
46317
46545
  "@types/node": "^22",
46318
46546
  "@types/react": "^19",
@@ -46330,11 +46558,6 @@ const PINNED_VERSIONS = {
46330
46558
  */
46331
46559
  const CONFIG_TOOLING_DEV_DEPENDENCIES = [`esbuild@${PINNED_VERSIONS.esbuild}`];
46332
46560
  /**
46333
- * Packages whose latest version is fetched from the npm registry at
46334
- * runtime so the CLI always scaffolds with the newest release.
46335
- */
46336
- const RUNTIME_RESOLVED_PACKAGES = ["next", "eslint-config-next"];
46337
- /**
46338
46561
  * @nextlyhq packages whose latest version is fetched from npm at runtime.
46339
46562
  * This avoids having to republish create-nextly-app every time a
46340
46563
  * dependency package is updated.
@@ -46358,9 +46581,10 @@ const resolvedNextlyVersions = /* @__PURE__ */ new Map();
46358
46581
  * On any failure (non-OK response, timeout, missing tag, thrown error) it
46359
46582
  * returns the requested dist-tag NAME itself (e.g. "alpha") — a valid npm
46360
46583
  * install spec — so a project on the alpha line never silently drops to
46361
- * `latest`.
46584
+ * `latest`. The channel has no default: only the Nextly packages are read
46585
+ * from the registry, always on a tag their caller names.
46362
46586
  */
46363
- async function fetchLatestVersion(pkg, channel = "latest") {
46587
+ async function fetchLatestVersion(pkg, channel) {
46364
46588
  try {
46365
46589
  const res = await fetch(`https://registry.npmjs.org/-/package/${encodeURIComponent(pkg)}/dist-tags`, { signal: AbortSignal.timeout(5e3) });
46366
46590
  if (!res.ok) return channel;
@@ -46394,26 +46618,6 @@ async function nextlyVersions(source) {
46394
46618
  if (source.kind === "release") return Object.fromEntries(NEXTLY_PACKAGES.map((pkg) => [pkg, source.cliVersion]));
46395
46619
  return resolveNextlyVersions("alpha");
46396
46620
  }
46397
- /** Cache for runtime-resolved package versions (next, eslint-config-next). */
46398
- let resolvedRuntimeVersions = null;
46399
- /**
46400
- * Resolve latest versions for Next.js ecosystem packages in parallel.
46401
- * Falls back to a safe semver range if the registry is unreachable.
46402
- */
46403
- async function resolveRuntimeVersions() {
46404
- if (resolvedRuntimeVersions) return resolvedRuntimeVersions;
46405
- const NEXT_FLOOR = "^16.4.1";
46406
- const FALLBACKS = {
46407
- next: NEXT_FLOOR,
46408
- "eslint-config-next": NEXT_FLOOR
46409
- };
46410
- const entries = await Promise.all(RUNTIME_RESOLVED_PACKAGES.map(async (pkg) => {
46411
- const version = await fetchLatestVersion(pkg);
46412
- return [pkg, version === "latest" ? FALLBACKS[pkg] : version];
46413
- }));
46414
- resolvedRuntimeVersions = Object.fromEntries(entries);
46415
- return resolvedRuntimeVersions;
46416
- }
46417
46621
  /**
46418
46622
  * Generate a `package.json` string for a fresh Nextly project.
46419
46623
  *
@@ -46432,11 +46636,10 @@ async function resolveRuntimeVersions() {
46432
46636
  */
46433
46637
  async function generatePackageJson(projectName, database, source = nextlySource({ cliVersion: packageVersion() }), projectType = "blank", templateDirs = [], targetDir) {
46434
46638
  if (projectType === "plugin") return generatePluginPackageJson(projectName, source);
46435
- const runtimeVersions = await resolveRuntimeVersions();
46436
46639
  const dependencies = {
46437
- next: runtimeVersions.next,
46438
- react: PINNED_VERSIONS.react,
46439
- "react-dom": PINNED_VERSIONS["react-dom"]
46640
+ next: scaffoldRange("next"),
46641
+ react: scaffoldRange("react"),
46642
+ "react-dom": scaffoldRange("react-dom")
46440
46643
  };
46441
46644
  dependencies["@tanstack/react-query"] = "^5.62.0";
46442
46645
  dependencies["lucide-react"] = "^0.544.0";
@@ -46460,7 +46663,7 @@ async function generatePackageJson(projectName, database, source = nextlySource(
46460
46663
  "@tailwindcss/postcss": PINNED_VERSIONS["@tailwindcss/postcss"],
46461
46664
  tailwindcss: PINNED_VERSIONS.tailwindcss,
46462
46665
  eslint: PINNED_VERSIONS.eslint,
46463
- "eslint-config-next": runtimeVersions["eslint-config-next"],
46666
+ "eslint-config-next": scaffoldRange("eslint-config-next"),
46464
46667
  esbuild: PINNED_VERSIONS.esbuild
46465
46668
  };
46466
46669
  const shipsSearchIndex = targetDir ? await projectHasSearchIndexScript(targetDir) : false;
@@ -46536,7 +46739,6 @@ async function resolvePluginNextlyRange(source) {
46536
46739
  */
46537
46740
  async function generatePluginPackageJson(projectName, source) {
46538
46741
  const versions = await nextlyVersions(source);
46539
- const runtimeVersions = await resolveRuntimeVersions();
46540
46742
  const range = (pkg) => versions[pkg] ?? "alpha";
46541
46743
  const peerRange = (pkg) => pluginPeerRange(range(pkg), source.cliVersion);
46542
46744
  const peerDependencies = {
@@ -46544,8 +46746,8 @@ async function generatePluginPackageJson(projectName, source) {
46544
46746
  "@nextlyhq/admin": peerRange("@nextlyhq/admin"),
46545
46747
  "@nextlyhq/plugin-sdk": peerRange("@nextlyhq/plugin-sdk"),
46546
46748
  "@nextlyhq/ui": peerRange("@nextlyhq/ui"),
46547
- react: PINNED_VERSIONS.react,
46548
- "react-dom": PINNED_VERSIONS["react-dom"]
46749
+ react: SUPPORTED_RANGES.react,
46750
+ "react-dom": SUPPORTED_RANGES["react-dom"]
46549
46751
  };
46550
46752
  const devDependencies = {
46551
46753
  nextly: range("nextly"),
@@ -46554,9 +46756,9 @@ async function generatePluginPackageJson(projectName, source) {
46554
46756
  "@nextlyhq/plugin-sdk": range("@nextlyhq/plugin-sdk"),
46555
46757
  "@nextlyhq/adapter-drizzle": range("@nextlyhq/adapter-drizzle"),
46556
46758
  "@nextlyhq/adapter-sqlite": range("@nextlyhq/adapter-sqlite"),
46557
- next: runtimeVersions.next,
46558
- react: PINNED_VERSIONS.react,
46559
- "react-dom": PINNED_VERSIONS["react-dom"],
46759
+ next: scaffoldRange("next"),
46760
+ react: scaffoldRange("react"),
46761
+ "react-dom": scaffoldRange("react-dom"),
46560
46762
  "better-sqlite3": "^12.0.0",
46561
46763
  "@types/node": PINNED_VERSIONS["@types/node"],
46562
46764
  "@types/react": PINNED_VERSIONS["@types/react"],
@@ -46874,6 +47076,143 @@ async function copyPluginTemplate(opts) {
46874
47076
  await writeScaffoldNpmrc(targetDir, packageManager);
46875
47077
  await replacePlaceholders(targetDir, placeholders, alreadyRendered);
46876
47078
  }
47079
+ // src/generators/page-extensions.ts
47080
+ /**
47081
+ * The page extension to name a new module file with, a hook or a route: a
47082
+ * TypeScript one first, since Nextly's files are TypeScript, then a
47083
+ * JavaScript one; never a content extension such as `mdx`, which Next.js
47084
+ * would compile as content, and never `cjs`, since the files are ES modules
47085
+ * and a `.cjs` file cannot hold one. Null when the list holds no extension a
47086
+ * module can be written in.
47087
+ */
47088
+ function moduleExtension(extensions) {
47089
+ const usable = writable(extensions);
47090
+ return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
47091
+ }
47092
+ /**
47093
+ * The extensions a file with code in it can be written under: not a
47094
+ * declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
47095
+ * types only and refuses code in.
47096
+ */
47097
+ function writable(extensions) {
47098
+ return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
47099
+ }
47100
+ /**
47101
+ * The `pageExtensions` the app's Next.js config sets, as every existing-app
47102
+ * step that names a file Next.js routes reads it: the list; `"absent"` when
47103
+ * the config sets none, or there is no config; or null when it is set in a
47104
+ * way that cannot be read. Read from the config the app exports, as the Next
47105
+ * config step reads it, so a commented-out or nested `pageExtensions` sets
47106
+ * nothing. `configName` is the file it was read from, for the report.
47107
+ */
47108
+ async function appPageExtensions(cwd) {
47109
+ const nextConfig = await readNextConfig(cwd);
47110
+ return {
47111
+ configName: nextConfig?.name ?? "The Next.js config",
47112
+ extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
47113
+ };
47114
+ }
47115
+ /**
47116
+ * The text of the app's Next.js config, or null when it has none, from the
47117
+ * file the Next config step reads and extends ({@link findConfigFile}).
47118
+ */
47119
+ async function readNextConfig(cwd) {
47120
+ const name = await findConfigFile(cwd);
47121
+ return name === null ? null : {
47122
+ name,
47123
+ text: await import_lib$1.default.readFile(path.default.join(cwd, name), "utf-8")
47124
+ };
47125
+ }
47126
+ /**
47127
+ * The page extension for a file of TypeScript with JSX, such as a page or a
47128
+ * layout: one ending in `tsx`. Null when the list holds none.
47129
+ */
47130
+ function tsxExtension(extensions) {
47131
+ return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
47132
+ }
47133
+ /** A JavaScript or TypeScript module without JSX, such as a route. */
47134
+ const MODULE_FILE = {
47135
+ pick: moduleExtension,
47136
+ holds: "a JavaScript or TypeScript extension"
47137
+ };
47138
+ /** TypeScript with JSX, such as a page or a layout. */
47139
+ const TSX_FILE = {
47140
+ pick: tsxExtension,
47141
+ holds: "an extension ending in tsx"
47142
+ };
47143
+ /**
47144
+ * The report for a file an app that sets `pageExtensions` gets in place of
47145
+ * the file: the name to create it under and its content. Next.js routes a
47146
+ * file there only under a name with one of the app's extensions, and the app
47147
+ * may already have the file under such a name, which the existing-app steps
47148
+ * do not look for, so nothing is written. A list that cannot be read, or that
47149
+ * holds no extension the content needs, gives a placeholder name and says
47150
+ * which.
47151
+ *
47152
+ * @param base the file's path without its extension, as reported, such as
47153
+ * `app/admin/[[...params]]/page`
47154
+ */
47155
+ function pageExtensionsSkip(base, text, pages, need) {
47156
+ const { configName, extensions } = pages;
47157
+ const extension = extensions === null ? null : need.pick(extensions);
47158
+ const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
47159
+ if (extensions !== null && extension === null) return {
47160
+ path: `${base}.<${need.holds} added to pageExtensions>`,
47161
+ 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`,
47162
+ toAdd: text
47163
+ };
47164
+ return {
47165
+ path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
47166
+ why: `${sets}; unless the app already has this file, create it under that name with this content`,
47167
+ toAdd: text
47168
+ };
47169
+ }
47170
+ // src/generators/write-new.ts
47171
+ /**
47172
+ * Writes `text` to `relative` (a project-relative path, written with `/`)
47173
+ * only where nothing is, and records which it was.
47174
+ *
47175
+ * The file is created exclusively (`wx`), so a file the app already has, a
47176
+ * link included, is never replaced, whatever appeared since any earlier
47177
+ * check. One that is there is reported as skipped, with why and, when given,
47178
+ * what to add to it by hand. `wx` guards only the file's own name, so a
47179
+ * folder on its path that is a symbolic link, which would take the write
47180
+ * wherever it points, is found first, as the media routes find it, and
47181
+ * nothing is written through it.
47182
+ */
47183
+ async function writeNew(cwd, relative, text, files, ifThere) {
47184
+ const file = path.default.join(cwd, ...relative.split("/"));
47185
+ const link = await linkedFolder(cwd, file);
47186
+ if (link !== null) {
47187
+ const folder = path.default.relative(cwd, link.folder).split(path.default.sep).join("/");
47188
+ files.skipped.push({
47189
+ path: relative,
47190
+ 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}`,
47191
+ toAdd: ifThere.toAdd
47192
+ });
47193
+ return;
47194
+ }
47195
+ await import_lib$1.default.ensureDir(path.default.dirname(file));
47196
+ try {
47197
+ await import_lib$1.default.writeFile(file, text, {
47198
+ encoding: "utf-8",
47199
+ flag: "wx"
47200
+ });
47201
+ files.written.push(relative);
47202
+ } catch (error) {
47203
+ if (error.code !== "EEXIST") throw error;
47204
+ files.skipped.push({
47205
+ path: relative,
47206
+ ...ifThere
47207
+ });
47208
+ }
47209
+ }
47210
+ /** A step's report before it has done anything. */
47211
+ const nothingYet = () => ({
47212
+ written: [],
47213
+ changed: [],
47214
+ skipped: []
47215
+ });
46877
47216
  // src/generators/routes.ts
46878
47217
  const API_ROUTE_TEMPLATE = `// Imported from \`nextly/runtime\` (not the package root) so the
46879
47218
  // Next.js-coupled handler factory stays out of Node-only code paths
@@ -46930,17 +47269,22 @@ async function generateRoutes(cwd, projectInfo) {
46930
47269
  * lists and deletes media, every request checked against the signed-in
46931
47270
  * caller's media permissions (`requireAuth: true` in the template);
46932
47271
  * - the public media route, `api/media/...`, which serves reads only;
47272
+ * - the uploads route, `uploads/...`, which serves files stored on local disk
47273
+ * that `next start` has no static file for, those uploaded since it started;
46933
47274
  * - the health check.
46934
47275
  *
46935
47276
  * `segment` is the URL each route answers. A handler already answering it
46936
47277
  * means the app has its own route there, which is left alone. Next.js leaves
46937
47278
  * route groups `(name)` and slots `@name` out of the URL wherever they sit,
46938
47279
  * and never routes a private folder `_name`, so the folders that answer a URL
46939
- * are found by that rule rather than by name. `catchAll` marks the optional
47280
+ * are found by that rule rather than by name. `catchAll` marks the
46940
47281
  * catch-alls, which serve everything below their URL too: a child folder
46941
- * holding a handler is weighed by {@link childFinding}.
47282
+ * holding a handler is weighed by {@link childFinding}. The uploads route is
47283
+ * a required catch-all rather than an optional one, and is weighed the same
47284
+ * way: a handler at `/uploads` itself, or any route below it, is the app's
47285
+ * own and leaves it unwritten.
46942
47286
  *
46943
- * `role` says what Nextly's route does there, for a media route the app
47287
+ * `role` says what Nextly's route does there, for a media or uploads route the app
46944
47288
  * already has something in place of; the report then carries the route to
46945
47289
  * add by hand.
46946
47290
  */
@@ -46968,6 +47312,34 @@ const ROUTES = [
46968
47312
  }
46969
47313
  ];
46970
47314
  /**
47315
+ * The uploads route, written by {@link generateUploadsRoute} beside the media
47316
+ * routes. Apart from them because it is a different concern: it serves the
47317
+ * files local storage keeps, where they serve the media library's records.
47318
+ */
47319
+ const UPLOADS_ROUTE = {
47320
+ path: "uploads/[...path]/route.ts",
47321
+ segment: "uploads",
47322
+ catchAll: true,
47323
+ importsConfig: true,
47324
+ 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`
47325
+ };
47326
+ /**
47327
+ * The admin's layout, which `generateAdminPage` writes beside the admin page.
47328
+ * Read with the routes, from the template a new project gets, so an app Nextly
47329
+ * is added to renders its admin the same way: its shell prerendered, and its
47330
+ * request work beneath a Suspense boundary.
47331
+ */
47332
+ const ADMIN_LAYOUT = {
47333
+ path: "admin/[[...params]]/layout.tsx",
47334
+ importsConfig: true
47335
+ };
47336
+ /** Every file the existing-app path writes from the templates, read together. */
47337
+ const ALL_ROUTES = [
47338
+ ...ROUTES,
47339
+ UPLOADS_ROUTE,
47340
+ ADMIN_LAYOUT
47341
+ ];
47342
+ /**
46971
47343
  * The import that brings in the Nextly config: the template spells it through
46972
47344
  * the `@nextly-config` alias or a path relative to its own layout, neither of
46973
47345
  * which an existing app is guaranteed to resolve.
@@ -47054,7 +47426,7 @@ var UnreadableFolder = class extends Error {
47054
47426
  */
47055
47427
  async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
47056
47428
  const texts = /* @__PURE__ */ new Map();
47057
- for (const route of ROUTES) {
47429
+ for (const route of ALL_ROUTES) {
47058
47430
  const relative = `base/src/app/${route.path}`;
47059
47431
  let text;
47060
47432
  try {
@@ -47084,7 +47456,20 @@ async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
47084
47456
  * is for such an app.
47085
47457
  */
47086
47458
  async function generateMediaRoutes(cwd, projectInfo, templates) {
47087
- const prepared = ROUTES.map((route) => prepareRoute(cwd, projectInfo, templates, route));
47459
+ return writeRoutes(cwd, projectInfo, templates, ROUTES);
47460
+ }
47461
+ /**
47462
+ * Write the uploads route into an existing app, by the same rules as
47463
+ * {@link generateMediaRoutes}: never where the app already answers
47464
+ * `/uploads`, never through a link, and under `pageExtensions` reported with
47465
+ * its content rather than written.
47466
+ */
47467
+ async function generateUploadsRoute(cwd, projectInfo, templates) {
47468
+ return writeRoutes(cwd, projectInfo, templates, [UPLOADS_ROUTE]);
47469
+ }
47470
+ /** Write each of `routes` where nothing answers its URL, reporting the rest. */
47471
+ async function writeRoutes(cwd, projectInfo, templates, routes) {
47472
+ const prepared = routes.map((route) => prepareRoute(cwd, projectInfo, templates, route));
47088
47473
  const pages = await appPageExtensions(cwd);
47089
47474
  if (pages.extensions !== "absent") {
47090
47475
  const { configName, extensions } = pages;
@@ -47503,52 +47888,6 @@ function skipped(cwd, left, target, text, role, reason) {
47503
47888
  function toPosix(p) {
47504
47889
  return p.split(path.default.sep).join("/");
47505
47890
  }
47506
- // src/generators/write-new.ts
47507
- /**
47508
- * Writes `text` to `relative` (a project-relative path, written with `/`)
47509
- * only where nothing is, and records which it was.
47510
- *
47511
- * The file is created exclusively (`wx`), so a file the app already has, a
47512
- * link included, is never replaced, whatever appeared since any earlier
47513
- * check. One that is there is reported as skipped, with why and, when given,
47514
- * what to add to it by hand. `wx` guards only the file's own name, so a
47515
- * folder on its path that is a symbolic link, which would take the write
47516
- * wherever it points, is found first, as the media routes find it, and
47517
- * nothing is written through it.
47518
- */
47519
- async function writeNew(cwd, relative, text, files, ifThere) {
47520
- const file = path.default.join(cwd, ...relative.split("/"));
47521
- const link = await linkedFolder(cwd, file);
47522
- if (link !== null) {
47523
- const folder = path.default.relative(cwd, link.folder).split(path.default.sep).join("/");
47524
- files.skipped.push({
47525
- path: relative,
47526
- 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}`,
47527
- toAdd: ifThere.toAdd
47528
- });
47529
- return;
47530
- }
47531
- await import_lib$1.default.ensureDir(path.default.dirname(file));
47532
- try {
47533
- await import_lib$1.default.writeFile(file, text, {
47534
- encoding: "utf-8",
47535
- flag: "wx"
47536
- });
47537
- files.written.push(relative);
47538
- } catch (error) {
47539
- if (error.code !== "EEXIST") throw error;
47540
- files.skipped.push({
47541
- path: relative,
47542
- ...ifThere
47543
- });
47544
- }
47545
- }
47546
- /** A step's report before it has done anything. */
47547
- const nothingYet = () => ({
47548
- written: [],
47549
- changed: [],
47550
- skipped: []
47551
- });
47552
47891
  // src/generators/admin.ts
47553
47892
  const ADMIN_PAGE_TEMPLATE = `"use client";
47554
47893
 
@@ -47570,43 +47909,24 @@ export default function AdminPage() {
47570
47909
  }
47571
47910
  `;
47572
47911
  /**
47573
- * Generate the layout template with the correct relative import path
47574
- * to nextly.config based on whether the project uses a src/ directory.
47575
- *
47576
- * With src/: src/app/admin/[[...params]]/layout.tsx → ../../../../nextly.config
47577
- * Without src: app/admin/[[...params]]/layout.tsx → ../../../nextly.config
47912
+ * The admin layout, as the base template holds it, with its config import
47913
+ * pointing at the project's `nextly.config` from where the layout is written.
47914
+ * One source with a new project's layout, so an app Nextly is added to gets
47915
+ * the same per-request admin.
47578
47916
  */
47579
- function getAdminLayoutTemplate(srcDir) {
47580
- return `import { getBrandingCss } from "nextly/config";
47581
-
47582
- import config from "${srcDir ? "../../../../nextly.config" : "../../../nextly.config"}";
47583
-
47584
- const brandingCss = getBrandingCss(config.admin?.branding);
47585
-
47586
- export default function AdminLayout({
47587
- children,
47588
- }: {
47589
- children: React.ReactNode;
47590
- }) {
47591
- return (
47592
- <>
47593
- {brandingCss && (
47594
- <style dangerouslySetInnerHTML={{ __html: brandingCss }} />
47595
- )}
47596
- {children}
47597
- </>
47598
- );
47599
- }
47600
- `;
47917
+ function adminLayout(cwd, adminDir, templates) {
47918
+ const text = templates.get(ADMIN_LAYOUT.path);
47919
+ 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.`);
47920
+ return withRelativeConfigImport(text, cwd, path.default.join(cwd, adminDir, "layout.tsx"), ADMIN_LAYOUT.path);
47601
47921
  }
47602
47922
  /**
47603
47923
  * Generate the admin panel page and layout.
47604
47924
  * Uses [[...params]] to match the expected route pattern.
47605
47925
  */
47606
- async function generateAdminPage(cwd, projectInfo) {
47926
+ async function generateAdminPage(cwd, projectInfo, templates) {
47607
47927
  const files = nothingYet();
47608
47928
  const adminDir = `${projectInfo.appDir}/admin/[[...params]]`;
47609
- const layout = getAdminLayoutTemplate(projectInfo.srcDir);
47929
+ const layout = adminLayout(cwd, adminDir, templates);
47610
47930
  const pages = await appPageExtensions(cwd);
47611
47931
  if (pages.extensions !== "absent") {
47612
47932
  const { configName, extensions } = pages;
@@ -47736,8 +48056,9 @@ function startNextly(configImport) {
47736
48056
  const { createRegister } = await import("nextly");
47737
48057
  const { default: config } = await import("${configImport}");
47738
48058
  // Next.js writes its Cache Components setting into this file as it builds
47739
- // it. Nextly, loaded from node_modules, cannot read that for itself, and
47740
- // warns when Cache Components is on, which it does not support yet.
48059
+ // it. Nextly, loaded from node_modules, cannot read that for itself. With
48060
+ // it off, Nextly warns at start; with it on, a cached read that finds no
48061
+ // "use cache" read function says so.
47741
48062
  await createRegister(config, {
47742
48063
  cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
47743
48064
  })();`;
@@ -47835,10 +48156,7 @@ function pageExtensionsAdvice(dir, configImport, configName, extensions) {
47835
48156
  * never rewritten; it is reported as skipped with what to add. So is the case
47836
48157
  * where the app sets \`pageExtensions\`: Next.js then reads the hook only under a
47837
48158
  * name with one of those extensions, which this does not guess. The config file
47838
- * itself is never edited here. Next.js 14 runs the hook only with
47839
- * `experimental.instrumentationHook`, which the Next config step gives in the
47840
- * same `experimental` object as Nextly's packages (`patchNextConfig`), so two
47841
- * steps never hand the developer two objects for one key.
48159
+ * itself is never edited here.
47842
48160
  */
47843
48161
  async function generateInstrumentation(cwd, projectInfo) {
47844
48162
  const dir = projectInfo.srcDir ? "src" : "";
@@ -47886,12 +48204,13 @@ async function generateIntoExistingApp(cwd, projectType, projectInfo, database,
47886
48204
  return [
47887
48205
  await generateConfig(cwd, projectType, projectInfo),
47888
48206
  await generateRoutes(cwd, projectInfo),
47889
- await generateAdminPage(cwd, projectInfo),
48207
+ await generateAdminPage(cwd, projectInfo, templates),
47890
48208
  await generateMediaRoutes(cwd, projectInfo, templates),
48209
+ await generateUploadsRoute(cwd, projectInfo, templates),
47891
48210
  await generateInstrumentation(cwd, projectInfo),
47892
48211
  await generateTypesDirectory(cwd, projectInfo),
47893
48212
  nextConfigFiles(await patchNextConfig(cwd, database)),
47894
- cacheComponentsFiles(await turnOffCacheComponents(cwd))
48213
+ cacheComponentsFiles(await turnOnCacheComponents(cwd))
47895
48214
  ];
47896
48215
  }
47897
48216
  // src/generators/report.ts
@@ -50548,7 +50867,7 @@ var hr = Symbol("entry");
50548
50867
  var cs = Symbol("entryOpt");
50549
50868
  var ui = Symbol("writeEntryClass");
50550
50869
  var lr = Symbol("write");
50551
- var fs$6 = Symbol("ondrain");
50870
+ var fs$7 = Symbol("ondrain");
50552
50871
  var wt = class extends A {
50553
50872
  sync = !1;
50554
50873
  opt;
@@ -50582,8 +50901,8 @@ var wt = class extends A {
50582
50901
  if ((t.gzip ? 1 : 0) + (t.brotli ? 1 : 0) + (t.zstd ? 1 : 0) > 1) throw new TypeError("gzip, brotli, zstd are mutually exclusive");
50583
50902
  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");
50584
50903
  let e = this.zip;
50585
- e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$6]()), this.on("resume", () => e.resume());
50586
- } else this.on("drain", this[fs$6]);
50904
+ e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$7]()), this.on("resume", () => e.resume());
50905
+ } else this.on("drain", this[fs$7]);
50587
50906
  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;
50588
50907
  }
50589
50908
  [lr](t) {
@@ -50711,7 +51030,7 @@ var wt = class extends A {
50711
51030
  this.emit("error", e);
50712
51031
  }
50713
51032
  }
50714
- [fs$6]() {
51033
+ [fs$7]() {
50715
51034
  this[Et] && this[Et].entry && this[Et].entry.resume();
50716
51035
  }
50717
51036
  [di](t) {
@@ -51778,6 +52097,126 @@ async function cleanupDownload(source) {
51778
52097
  await import_lib$1.default.remove(extractionRoot).catch(() => {});
51779
52098
  }
51780
52099
  }
52100
+ // src/lib/framework-versions.ts
52101
+ /**
52102
+ * Whether an existing app's Next.js, React and React DOM are ones this
52103
+ * release of Nextly supports, and what to change when they are not.
52104
+ *
52105
+ * Nextly is reviewed against one Next.js major and one React major
52106
+ * (`SUPPORTED_RANGES`). An app below the floor or on a newer major would
52107
+ * install Nextly and then fail at build or at run time, so adding Nextly to
52108
+ * it stops before anything is written, saying what was found and the way on.
52109
+ */
52110
+ /** The first spec the app's package.json gives `name`, in field order. */
52111
+ function declaredSpec(manifest, name) {
52112
+ const fields = ["optionalDependencies", ...DEPENDENCY_FIELDS$1.filter((field) => field !== "optionalDependencies")];
52113
+ for (const field of fields) {
52114
+ const deps = manifest[field];
52115
+ if (!deps || typeof deps !== "object") continue;
52116
+ const spec = deps[name];
52117
+ if (typeof spec === "string") return spec;
52118
+ }
52119
+ }
52120
+ /**
52121
+ * The version an app is headed for, the one judged against the range: the
52122
+ * installed copy, unless the app's declaration points past it.
52123
+ */
52124
+ function judgedVersion(installed, declared, supported) {
52125
+ const lowestDeclared = declared !== void 0 ? lowestVersionIn(declared) : void 0;
52126
+ const floor = lowestVersionIn(supported);
52127
+ if (floor === void 0) return installed ?? lowestDeclared;
52128
+ if (lowestDeclared !== void 0 && compareVersions(lowestDeclared, floor) > 0 && !satisfiesRange(lowestDeclared, supported)) return lowestDeclared;
52129
+ const highest = declared !== void 0 ? highestMajorIn(declared) : void 0;
52130
+ if (installed === void 0 && highest !== void 0 && highest > major(floor)) return `${major(floor) + 1}.0.0`;
52131
+ return installed ?? lowestDeclared;
52132
+ }
52133
+ /**
52134
+ * Too new only on a later major than the range covers; anything else outside
52135
+ * it (below the floor, or a prerelease within the major) needs a supported
52136
+ * release of the same major. A range without a floor leaves nothing to
52137
+ * compare, and the version is then reported as too new.
52138
+ */
52139
+ function directionOf(judged, supported) {
52140
+ const floor = lowestVersionIn(supported);
52141
+ return floor === void 0 || major(judged) > major(floor) ? "too-new" : "too-old";
52142
+ }
52143
+ /**
52144
+ * Every supported framework package the app at `cwd` has outside its range.
52145
+ *
52146
+ * The version judged is the one the app is headed for (`judgedVersion`): the
52147
+ * installed copy, read from the `node_modules` Node would load it from,
52148
+ * unless the app's package.json declaration points past it; with none
52149
+ * installed, what an install of that declaration could resolve to. A package
52150
+ * the app neither has installed nor declares in a form that names a version
52151
+ * (a dist-tag, `workspace:*`) is not judged.
52152
+ */
52153
+ async function frameworkVersionProblems(cwd) {
52154
+ const manifest = await import_lib$1.default.readJson(path.default.join(cwd, "package.json")).catch(() => ({}));
52155
+ const problems = [];
52156
+ for (const name of Object.keys(SUPPORTED_RANGES)) {
52157
+ const supported = SUPPORTED_RANGES[name];
52158
+ const installed = await installedVersion(cwd, name);
52159
+ const declared = declaredSpec(manifest, name);
52160
+ const judged = judgedVersion(installed, declared, supported);
52161
+ if (judged === void 0 || satisfiesRange(judged, supported)) continue;
52162
+ const direction = directionOf(judged, supported);
52163
+ problems.push({
52164
+ name,
52165
+ installed,
52166
+ declared,
52167
+ supported,
52168
+ judged,
52169
+ direction
52170
+ });
52171
+ }
52172
+ return problems;
52173
+ }
52174
+ const LABEL = {
52175
+ next: "Next.js",
52176
+ react: "React",
52177
+ "react-dom": "React"
52178
+ };
52179
+ const major = (version) => Number(version.split(".")[0]);
52180
+ /** What `fix` reads from a problem: the supported major and the version found. */
52181
+ function majorsOf(problem) {
52182
+ const floor = lowestVersionIn(problem.supported) ?? problem.supported;
52183
+ return {
52184
+ supportedMajor: major(floor),
52185
+ foundMajor: major(problem.judged)
52186
+ };
52187
+ }
52188
+ /** A version past the supported major: install that major, or wait for Nextly. */
52189
+ function tooNewFix(problem, add) {
52190
+ const { supportedMajor, foundMajor } = majorsOf(problem);
52191
+ const label = LABEL[problem.name];
52192
+ 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}`}`;
52193
+ }
52194
+ /** A version below the supported floor: the codemod across a Next.js major, an install otherwise. */
52195
+ function tooOldFix(problem, add) {
52196
+ const { supportedMajor, foundMajor } = majorsOf(problem);
52197
+ 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}`;
52198
+ const install = problem.name === "next" ? `next@${problem.supported}` : `react@${SUPPORTED_RANGES.react} react-dom@${SUPPORTED_RANGES["react-dom"]}`;
52199
+ return `Upgrade ${LABEL[problem.name]} to ${problem.supported}:\n ${add} ${install}`;
52200
+ }
52201
+ /** The one step that brings `problem` into range, as a sentence and a command. */
52202
+ function fix(problem, pm) {
52203
+ const add = ADD_COMMAND[pm];
52204
+ return problem.direction === "too-new" ? tooNewFix(problem, add) : tooOldFix(problem, add);
52205
+ }
52206
+ /**
52207
+ * Why the run stopped: each package found, the range it falls outside, and
52208
+ * the change that brings it in. React and React DOM move together, so a
52209
+ * problem with both gives one command.
52210
+ */
52211
+ function frameworkVersionMessage(problems, pm) {
52212
+ const lines = problems.map(({ name, installed, declared, supported }) => {
52213
+ return ` ${name}: ${[installed !== void 0 ? `installed ${installed}` : void 0, declared !== void 0 ? `declared ${declared}` : void 0].filter(Boolean).join(", ")}; supported ${supported}`;
52214
+ });
52215
+ const fixes = [...new Set(problems.map((problem) => fix(problem, pm)))];
52216
+ return `This app's Next.js or React is outside what this release of Nextly supports:
52217
+ ${lines.join("\n")}\n\nNothing was changed. Change the app as follows, then run create-nextly-app again:
52218
+ ` + fixes.map((line) => ` ${line}`).join("\n");
52219
+ }
51781
52220
  // src/prompts/approach.ts
51782
52221
  /**
51783
52222
  * Labels shown in the CLI select prompt for each approach.
@@ -52218,6 +52657,16 @@ async function createNextly(options = {}) {
52218
52657
  const existingProject = options.projectType !== "plugin" && await isExistingNextProject(cwd);
52219
52658
  if (existingProject) {
52220
52659
  R$1.success(`${import_picocolors.default.green("Next.js")} project detected`);
52660
+ const frameworkProblems = await frameworkVersionProblems(cwd);
52661
+ if (frameworkProblems.length > 0) {
52662
+ capture("scaffold_failed", {
52663
+ stage: "detect",
52664
+ error_code: "unknown",
52665
+ duration_ms: Date.now() - overallStartedAt
52666
+ });
52667
+ Nt$1(frameworkVersionMessage(frameworkProblems, await detectAppPackageManager(cwd)));
52668
+ process.exit(1);
52669
+ }
52221
52670
  if (!defaults) {
52222
52671
  const shouldInstall = await Rt$1({
52223
52672
  message: "Install Nextly in this project?",
@@ -52625,4 +53074,4 @@ Object.defineProperty(exports, "validateTemplateName", {
52625
53074
  }
52626
53075
  });
52627
53076
 
52628
- //# sourceMappingURL=create-nextly-BbKGDrEC.cjs.map
53077
+ //# sourceMappingURL=create-nextly-Q62KZJib.cjs.map