create-nextly-app 0.0.2-alpha.72 → 0.0.2-alpha.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{create-nextly-CyI8XgVc.mjs → create-nextly-ChHdzG83.mjs} +948 -531
- package/dist/create-nextly-ChHdzG83.mjs.map +1 -0
- package/dist/{create-nextly-BGu1tXYs.cjs → create-nextly-Decg8dye.cjs} +948 -531
- package/dist/create-nextly-Decg8dye.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +5 -5
- package/skills/nextly-authentication/SKILL.md +1 -1
- package/skills/nextly-collections/SKILL.md +10 -4
- package/skills/nextly-deploy/SKILL.md +5 -3
- package/skills/nextly-install/SKILL.md +5 -4
- package/templates/base/src/app/admin/[[...params]]/layout.tsx +23 -1
- package/templates/base/src/app/globals.css +13 -2
- package/templates/base/src/app/layout.tsx +5 -0
- package/templates/base/src/nextly/cached-read.ts +52 -0
- package/templates/blank/src/app/(frontend)/page.tsx +43 -21
- package/templates/blank/src/app/layout.tsx +5 -0
- package/templates/blank/src/instrumentation.ts +3 -2
- package/templates/plugin/dev/next.config.ts +8 -2
- package/templates/plugin/dev/src/app/admin/[[...params]]/layout.tsx +24 -0
- package/templates/plugin/dev/src/app/layout.tsx +6 -0
- package/templates/plugin/dev/src/instrumentation.ts +3 -2
- package/templates/plugin/dev/src/nextly/cached-read.ts +52 -0
- package/dist/create-nextly-BGu1tXYs.cjs.map +0 -1
- package/dist/create-nextly-CyI8XgVc.mjs.map +0 -1
|
@@ -22842,6 +22842,7 @@ function nextlySource(options) {
|
|
|
22842
22842
|
function isNextlyPackage(name) {
|
|
22843
22843
|
return name === "nextly" || name.startsWith("@nextlyhq/");
|
|
22844
22844
|
}
|
|
22845
|
+
/** The package.json fields that declare a dependency, in the order read. */
|
|
22845
22846
|
const DEPENDENCY_FIELDS$1 = [
|
|
22846
22847
|
"dependencies",
|
|
22847
22848
|
"devDependencies",
|
|
@@ -22917,6 +22918,10 @@ const EXACT_FLAG = {
|
|
|
22917
22918
|
yarn: "--exact",
|
|
22918
22919
|
bun: "--exact"
|
|
22919
22920
|
};
|
|
22921
|
+
/**
|
|
22922
|
+
* How each package manager adds a dependency. Exported so every message that
|
|
22923
|
+
* tells the person what to install spells the command the same way.
|
|
22924
|
+
*/
|
|
22920
22925
|
const ADD_COMMAND = {
|
|
22921
22926
|
npm: "npm install",
|
|
22922
22927
|
pnpm: "pnpm add",
|
|
@@ -42486,8 +42491,8 @@ var import_lib = (/* @__PURE__ */ __commonJSMin$1(((exports) => {
|
|
|
42486
42491
|
exports.parse = parse;
|
|
42487
42492
|
})))();
|
|
42488
42493
|
/**
|
|
42489
|
-
* Where Nextly explains
|
|
42490
|
-
*
|
|
42494
|
+
* Where Nextly explains the Cache Components settings every app it makes or
|
|
42495
|
+
* joins carries, and why each one is there.
|
|
42491
42496
|
*/
|
|
42492
42497
|
const CACHE_COMPONENTS_DOCS_URL = "https://nextlyhq.com/docs/getting-started/installation#cache-components";
|
|
42493
42498
|
/**
|
|
@@ -42525,13 +42530,42 @@ function packageListLiteral(database) {
|
|
|
42525
42530
|
return JSON.stringify(getServerExternalPackages(database), null, 2).replace(/\n/g, "\n ");
|
|
42526
42531
|
}
|
|
42527
42532
|
/**
|
|
42528
|
-
*
|
|
42529
|
-
*
|
|
42533
|
+
* Nextly's cache handlers, as the settings that install them. Under Cache
|
|
42534
|
+
* Components, Next.js's default cache serves a page's previous shell once
|
|
42535
|
+
* after a write expires its tags; these expire it at once.
|
|
42536
|
+
*/
|
|
42537
|
+
const HANDLER_SETTINGS = [
|
|
42538
|
+
"cacheHandler",
|
|
42539
|
+
"cacheMaxMemorySize",
|
|
42540
|
+
"cacheHandlers"
|
|
42541
|
+
];
|
|
42542
|
+
const HANDLER_PROPERTIES = [
|
|
42543
|
+
"cacheHandler: require.resolve(\"nextly/cache-handler\")",
|
|
42544
|
+
"cacheMaxMemorySize: 0",
|
|
42545
|
+
"cacheHandlers: { default: require.resolve(\"nextly/cache-handlers\") }"
|
|
42546
|
+
];
|
|
42547
|
+
const ON_OPTIONS = ["cacheComponents", "partialPrefetching"];
|
|
42548
|
+
/** Every setting Nextly needs, as lines of a config object. */
|
|
42549
|
+
const ON_LINES = [...ON_OPTIONS.map((option) => `${option}: true`), ...HANDLER_PROPERTIES].map((property) => ` ${property},`);
|
|
42550
|
+
/**
|
|
42551
|
+
* The settings every config this writes carries, with their reason in the
|
|
42552
|
+
* file: the developer who finds them is the one deciding whether to change
|
|
42553
|
+
* them.
|
|
42530
42554
|
*/
|
|
42531
|
-
const
|
|
42532
|
-
//
|
|
42555
|
+
const CACHE_COMPONENTS_ON = ` // Nextly runs with Cache Components and Partial Prefetching on. Its cache
|
|
42556
|
+
// handlers make the first read after a write show the new version, where
|
|
42557
|
+
// Next.js's default cache would serve the previous page once more:
|
|
42533
42558
|
// ${CACHE_COMPONENTS_DOCS_URL}
|
|
42534
|
-
|
|
42559
|
+
${ON_LINES.join("\n")}`;
|
|
42560
|
+
/**
|
|
42561
|
+
* How an ES module config gets `require`, which Node defines only in
|
|
42562
|
+
* CommonJS, to resolve the handlers from the app's own node_modules.
|
|
42563
|
+
*/
|
|
42564
|
+
const REQUIRE_IMPORT = "import { createRequire } from \"node:module\";";
|
|
42565
|
+
/** The declaration, calling `createRequire` by the name the file has it under. */
|
|
42566
|
+
const requireDeclaration = (createRequireName) => `// Resolves Nextly's cache handlers from this app's node_modules.
|
|
42567
|
+
const require = ${createRequireName}(import.meta.url);`;
|
|
42568
|
+
const REQUIRE_DECLARATION = requireDeclaration("createRequire");
|
|
42535
42569
|
/**
|
|
42536
42570
|
* Render a complete next.config.ts file for the selected database.
|
|
42537
42571
|
*/
|
|
@@ -42539,7 +42573,7 @@ function buildNextConfigTemplate(database) {
|
|
|
42539
42573
|
return `import type { NextConfig } from "next";
|
|
42540
42574
|
|
|
42541
42575
|
const nextConfig: NextConfig = {
|
|
42542
|
-
${
|
|
42576
|
+
${CACHE_COMPONENTS_ON}
|
|
42543
42577
|
serverExternalPackages: ${packageListLiteral(database)},
|
|
42544
42578
|
};
|
|
42545
42579
|
|
|
@@ -42548,9 +42582,13 @@ export default nextConfig;
|
|
|
42548
42582
|
}
|
|
42549
42583
|
/** The same config as a JavaScript module, for an app without TypeScript. */
|
|
42550
42584
|
function buildNextConfigModule(database) {
|
|
42551
|
-
return
|
|
42585
|
+
return `${REQUIRE_IMPORT}
|
|
42586
|
+
|
|
42587
|
+
${REQUIRE_DECLARATION}
|
|
42588
|
+
|
|
42589
|
+
/** @type {import("next").NextConfig} */
|
|
42552
42590
|
const nextConfig = {
|
|
42553
|
-
${
|
|
42591
|
+
${CACHE_COMPONENTS_ON}
|
|
42554
42592
|
serverExternalPackages: ${packageListLiteral(database)},
|
|
42555
42593
|
};
|
|
42556
42594
|
|
|
@@ -42568,9 +42606,19 @@ const CONFIG_FILES = [
|
|
|
42568
42606
|
"next.config.ts",
|
|
42569
42607
|
"next.config.mts"
|
|
42570
42608
|
];
|
|
42571
|
-
/** The first Next.js major whose option is the top-level `serverExternalPackages`. */
|
|
42572
|
-
const FIRST_TOP_LEVEL_MAJOR = 15;
|
|
42573
42609
|
const PROPERTY = "serverExternalPackages";
|
|
42610
|
+
/**
|
|
42611
|
+
* The deprecated spelling of the list, under `experimental`. Supported
|
|
42612
|
+
* releases still read it, by copying it over the top-level list, so an app
|
|
42613
|
+
* that has it would lose the packages this adds there.
|
|
42614
|
+
*/
|
|
42615
|
+
const LEGACY_PROPERTY = "serverComponentsExternalPackages";
|
|
42616
|
+
/**
|
|
42617
|
+
* Why a config with the deprecated list is left alone. Named, so the advice
|
|
42618
|
+
* printed for it can say what to do with that list rather than the advice
|
|
42619
|
+
* for every other refusal, which says to keep the experimental keys there.
|
|
42620
|
+
*/
|
|
42621
|
+
const LEGACY_LIST_REASON = `it lists packages under experimental.${LEGACY_PROPERTY}, which Next.js copies over ${PROPERTY}, replacing the packages added there`;
|
|
42574
42622
|
/** Where the line holding `offset` starts. */
|
|
42575
42623
|
const lineStart = (source, offset) => source.lastIndexOf("\n", offset - 1) + 1;
|
|
42576
42624
|
/** The whitespace a line opens with, for the line holding `offset`. */
|
|
@@ -42751,7 +42799,7 @@ const TOP_LEVEL_SETTINGS = [
|
|
|
42751
42799
|
"transpilePackages",
|
|
42752
42800
|
"experimental"
|
|
42753
42801
|
];
|
|
42754
|
-
const EXPERIMENTAL_SETTINGS = ["optimizePackageImports",
|
|
42802
|
+
const EXPERIMENTAL_SETTINGS = ["optimizePackageImports", LEGACY_PROPERTY];
|
|
42755
42803
|
/** Why an object might set one of `names` where this cannot read it, or null. */
|
|
42756
42804
|
function hiddenSettings(object, names, where) {
|
|
42757
42805
|
return object.properties.some((property) => mayHide(property, names)) ? `${where} spreads another object or has a computed or accessor property, which may set ${names.join(", ")}` : null;
|
|
@@ -42809,41 +42857,43 @@ function fillEmptyList(source, array, entries) {
|
|
|
42809
42857
|
* so the app's own properties keep their order and their comments.
|
|
42810
42858
|
*/
|
|
42811
42859
|
function addListProperty(source, object, entries) {
|
|
42860
|
+
return appendProperties(source, object, (indent, ownLines) => [ownLines ? `${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}]` : `${PROPERTY}: [${entries.join(", ")}]`]);
|
|
42861
|
+
}
|
|
42862
|
+
/**
|
|
42863
|
+
* Add properties after an object's last one, in the object's layout: on one
|
|
42864
|
+
* line, as `{ a: 1, b: 2 }`, when it is written that way, and otherwise one
|
|
42865
|
+
* per line at the indentation its properties use, before the closing brace,
|
|
42866
|
+
* with a comma put after the last property first when it has none.
|
|
42867
|
+
* `properties` gets that indentation, and whether each property has a line
|
|
42868
|
+
* of its own, so a property spanning lines can follow the same layout.
|
|
42869
|
+
*/
|
|
42870
|
+
function appendProperties(source, object, properties) {
|
|
42812
42871
|
const open = (object.start ?? 0) + 1;
|
|
42813
42872
|
const close = (object.end ?? 0) - 1;
|
|
42814
|
-
|
|
42873
|
+
if (!spansLines(source, open, close)) return [appendOnTheSameLine(object, properties("", false).join(", "))];
|
|
42874
|
+
const last = object.properties.at(-1);
|
|
42875
|
+
const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
|
|
42876
|
+
const lines = properties(indent, true).map((property) => `${indent}${property},\n`).join("");
|
|
42877
|
+
return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
|
|
42878
|
+
at: last.end ?? 0,
|
|
42879
|
+
remove: 0,
|
|
42880
|
+
text: ","
|
|
42881
|
+
}] : [], beforeClosingBrace(source, close, lines)];
|
|
42815
42882
|
}
|
|
42816
|
-
/**
|
|
42817
|
-
function
|
|
42818
|
-
const property = `${PROPERTY}: [${entries.join(", ")}]`;
|
|
42883
|
+
/** Properties added to an object written on one line, as `{ a: 1, b: 2 }`. */
|
|
42884
|
+
function appendOnTheSameLine(object, text) {
|
|
42819
42885
|
const last = object.properties.at(-1);
|
|
42820
42886
|
const open = (object.start ?? 0) + 1;
|
|
42821
42887
|
return last ? {
|
|
42822
42888
|
at: last.end ?? 0,
|
|
42823
42889
|
remove: 0,
|
|
42824
|
-
text: `, ${
|
|
42890
|
+
text: `, ${text}`
|
|
42825
42891
|
} : {
|
|
42826
42892
|
at: open,
|
|
42827
42893
|
remove: (object.end ?? 0) - 1 - open,
|
|
42828
|
-
text: ` ${
|
|
42894
|
+
text: ` ${text} `
|
|
42829
42895
|
};
|
|
42830
42896
|
}
|
|
42831
|
-
/**
|
|
42832
|
-
* The list added as the last property of an object written over several
|
|
42833
|
-
* lines, one entry per line at the indentation the object's properties use.
|
|
42834
|
-
* A comma goes after the last property first when it has none.
|
|
42835
|
-
*/
|
|
42836
|
-
function listOnItsOwnLines(source, object, entries) {
|
|
42837
|
-
const close = (object.end ?? 0) - 1;
|
|
42838
|
-
const last = object.properties.at(-1);
|
|
42839
|
-
const indent = last ? indentAt(source, last.start ?? 0) : `${indentAt(source, close)} `;
|
|
42840
|
-
const list = `${indent}${PROPERTY}: [\n${entries.map((entry) => `${indent} ${entry},\n`).join("")}${indent}],\n`;
|
|
42841
|
-
return [...last !== void 0 && !/^\s*,/.test(source.slice(last.end ?? 0, close)) ? [{
|
|
42842
|
-
at: last.end ?? 0,
|
|
42843
|
-
remove: 0,
|
|
42844
|
-
text: ","
|
|
42845
|
-
}] : [], beforeClosingBrace(source, close, list)];
|
|
42846
|
-
}
|
|
42847
42897
|
/** Text put before an object's closing brace: on the line above it when the brace has a line of its own. */
|
|
42848
42898
|
function beforeClosingBrace(source, close, text) {
|
|
42849
42899
|
const closeLine = lineStart(source, close);
|
|
@@ -42931,7 +42981,7 @@ function conflictOf(object, required) {
|
|
|
42931
42981
|
}
|
|
42932
42982
|
/** Why the config's lists clash, given an `experimental` object it can read. */
|
|
42933
42983
|
function experimentalConflict(object, experimental, required) {
|
|
42934
|
-
if (propertyValue(experimental,
|
|
42984
|
+
if (propertyValue(experimental, LEGACY_PROPERTY)) return LEGACY_LIST_REASON;
|
|
42935
42985
|
return clashReason("transpilePackages", object, "transpilePackages", required) ?? clashReason("experimental.optimizePackageImports", experimental, "optimizePackageImports", required);
|
|
42936
42986
|
}
|
|
42937
42987
|
/** The object literal the file exports as its config, or why there is none to edit. */
|
|
@@ -43001,58 +43051,6 @@ function editConfig(source, file, required) {
|
|
|
43001
43051
|
added: plan.added
|
|
43002
43052
|
};
|
|
43003
43053
|
}
|
|
43004
|
-
/** The string at `keys` inside a parsed JSON file, or null. */
|
|
43005
|
-
function stringAt(json, keys) {
|
|
43006
|
-
let value = json;
|
|
43007
|
-
for (const key of keys) {
|
|
43008
|
-
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
43009
|
-
value = value[key];
|
|
43010
|
-
}
|
|
43011
|
-
return typeof value === "string" ? value : null;
|
|
43012
|
-
}
|
|
43013
|
-
/** A JSON file of the app's, or null when it is missing or does not parse. */
|
|
43014
|
-
async function readJsonOrNull(file) {
|
|
43015
|
-
try {
|
|
43016
|
-
return await import_lib$1.default.readJson(file);
|
|
43017
|
-
} catch {
|
|
43018
|
-
return null;
|
|
43019
|
-
}
|
|
43020
|
-
}
|
|
43021
|
-
/**
|
|
43022
|
-
* A version range that admits one major only, such as `^15.1.0`, `~16.0`,
|
|
43023
|
-
* `15.x` or `16.0.0-canary.3`, with the major it admits. A range such as
|
|
43024
|
-
* `>=14`, `14 || 15` or `latest` admits several, or names none.
|
|
43025
|
-
*/
|
|
43026
|
-
const SINGLE_MAJOR = /^[\^~]?(\d+)(?:\.(?:\d+|[xX*])){0,2}(?:-[\w.]+)?$/;
|
|
43027
|
-
/**
|
|
43028
|
-
* The major version of the app's Next.js: the installed one, read from its
|
|
43029
|
-
* own `node_modules`, or else the one its package.json range admits when
|
|
43030
|
-
* that range admits a single major. Null when neither tells, as when
|
|
43031
|
-
* dependencies are not installed yet and the range is `latest`.
|
|
43032
|
-
*/
|
|
43033
|
-
async function nextMajor(cwd) {
|
|
43034
|
-
const installed = stringAt(await readJsonOrNull(path.join(cwd, "node_modules", "next", "package.json")), ["version"]);
|
|
43035
|
-
const manifest = await readJsonOrNull(path.join(cwd, "package.json"));
|
|
43036
|
-
const range = stringAt(manifest, ["dependencies", "next"]) ?? stringAt(manifest, ["devDependencies", "next"]);
|
|
43037
|
-
const match = SINGLE_MAJOR.exec((installed ?? range ?? "").trim());
|
|
43038
|
-
return match ? Number(match[1]) : null;
|
|
43039
|
-
}
|
|
43040
|
-
/**
|
|
43041
|
-
* Why the option cannot be written for the app's Next.js, and which spelling
|
|
43042
|
-
* to print instead, or null when it can. Next.js 14 reads the list under
|
|
43043
|
-
* another name, and an unknown version may be 14: writing the top-level list
|
|
43044
|
-
* there would report success and externalise nothing.
|
|
43045
|
-
*/
|
|
43046
|
-
function versionBlock(major) {
|
|
43047
|
-
if (major === null) return {
|
|
43048
|
-
reason: "the version of Next.js the app uses could not be read",
|
|
43049
|
-
spelling: "either"
|
|
43050
|
-
};
|
|
43051
|
-
return major < FIRST_TOP_LEVEL_MAJOR ? {
|
|
43052
|
-
reason: `the app runs Next.js ${major}, which reads the list as experimental.serverComponentsExternalPackages and runs instrumentation.ts only with experimental.instrumentationHook`,
|
|
43053
|
-
spelling: "legacy"
|
|
43054
|
-
} : null;
|
|
43055
|
-
}
|
|
43056
43054
|
/**
|
|
43057
43055
|
* The Next config file any advice about the app's config names: the one it
|
|
43058
43056
|
* has, or, when it has none, the one to create. `.mjs` is ESM whatever the
|
|
@@ -43104,14 +43102,6 @@ async function createNextConfig(cwd, database) {
|
|
|
43104
43102
|
async function patchNextConfig(cwd, database) {
|
|
43105
43103
|
const file = await findConfigFile(cwd);
|
|
43106
43104
|
const required = getServerExternalPackages(database);
|
|
43107
|
-
const blocked = versionBlock(await nextMajor(cwd));
|
|
43108
|
-
if (blocked) return {
|
|
43109
|
-
kind: "manual",
|
|
43110
|
-
file: file ?? await nextConfigFileToChange(cwd),
|
|
43111
|
-
packages: required,
|
|
43112
|
-
...blocked,
|
|
43113
|
-
...file ? {} : { create: true }
|
|
43114
|
-
};
|
|
43115
43105
|
if (!file) return createNextConfig(cwd, database);
|
|
43116
43106
|
const edited = editConfig(await import_lib$1.default.readFile(path.join(cwd, file), "utf-8"), file, required);
|
|
43117
43107
|
if ("reason" in edited) return {
|
|
@@ -43131,119 +43121,281 @@ async function patchNextConfig(cwd, database) {
|
|
|
43131
43121
|
added: edited.added
|
|
43132
43122
|
};
|
|
43133
43123
|
}
|
|
43134
|
-
/** The setting Nextly needs off, and the one Next.js accepts only with it on. */
|
|
43135
|
-
const CACHE_COMPONENTS = "cacheComponents";
|
|
43136
|
-
const PARTIAL_PREFETCHING = "partialPrefetching";
|
|
43137
43124
|
/**
|
|
43138
|
-
*
|
|
43139
|
-
*
|
|
43140
|
-
*
|
|
43125
|
+
* The exported config object, when every setting this edits can be read in
|
|
43126
|
+
* it; else why not. Not through a wrapper call: Next.js receives what the
|
|
43127
|
+
* wrapper returns, which may set an option again, so the literal passed in
|
|
43128
|
+
* says nothing final.
|
|
43141
43129
|
*/
|
|
43142
|
-
function
|
|
43143
|
-
const
|
|
43144
|
-
|
|
43145
|
-
|
|
43146
|
-
|
|
43147
|
-
|
|
43148
|
-
|
|
43149
|
-
|
|
43150
|
-
|
|
43151
|
-
|
|
43152
|
-
|
|
43153
|
-
|
|
43154
|
-
}
|
|
43155
|
-
|
|
43156
|
-
if (
|
|
43157
|
-
|
|
43158
|
-
|
|
43159
|
-
|
|
43160
|
-
|
|
43161
|
-
|
|
43162
|
-
|
|
43130
|
+
function cacheComponentsObject(body) {
|
|
43131
|
+
const object = exportedConfig(body, false);
|
|
43132
|
+
if (typeof object === "string") return object;
|
|
43133
|
+
return hiddenSettings(object, [...ON_OPTIONS, ...HANDLER_SETTINGS], "it") ?? object;
|
|
43134
|
+
}
|
|
43135
|
+
/**
|
|
43136
|
+
* How to turn one option on: the `false` to flip, the property to add when the
|
|
43137
|
+
* config does not set it, nothing when it is on, or why the value cannot be
|
|
43138
|
+
* read.
|
|
43139
|
+
*/
|
|
43140
|
+
function optionStep(object, name) {
|
|
43141
|
+
const value = propertyValue(object, name);
|
|
43142
|
+
if (!value) return { add: `${name}: true` };
|
|
43143
|
+
if (value.type !== "BooleanLiteral") return `its ${name} is not true or false written in place`;
|
|
43144
|
+
if (value.value) return {};
|
|
43145
|
+
return { flip: {
|
|
43146
|
+
at: value.start ?? 0,
|
|
43147
|
+
remove: (value.end ?? 0) - (value.start ?? 0),
|
|
43148
|
+
text: "true"
|
|
43149
|
+
} };
|
|
43150
|
+
}
|
|
43151
|
+
/**
|
|
43152
|
+
* Whether Node loads the config as an ES module, where `require` is not
|
|
43153
|
+
* defined: `.mjs` and `.mts` always, and `.js` when it imports or exports,
|
|
43154
|
+
* which a config in a package of `"type": "module"` must, and which Node
|
|
43155
|
+
* detects in any other. Next.js compiles `next.config.ts` to CommonJS.
|
|
43156
|
+
*/
|
|
43157
|
+
function loadsAsModule(file, body) {
|
|
43158
|
+
if (/\.m[jt]s$/.test(file)) return true;
|
|
43159
|
+
return file.endsWith(".js") && body.some((statement) => statement.type === "ImportDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration");
|
|
43160
|
+
}
|
|
43161
|
+
/**
|
|
43162
|
+
* The name an ES module config imports `createRequire` under from
|
|
43163
|
+
* `node:module`, whatever it renamed it to, or null when it imports none.
|
|
43164
|
+
*/
|
|
43165
|
+
function createRequireImport(body) {
|
|
43166
|
+
return body.flatMap((statement) => statement.type === "ImportDeclaration" && (statement.source.value === "node:module" || statement.source.value === "module") ? statement.specifiers : []).find((item) => item.type === "ImportSpecifier" && importedName(item) === "createRequire")?.local.name ?? null;
|
|
43167
|
+
}
|
|
43168
|
+
/** The name an import specifier takes from its module, written plain or quoted. */
|
|
43169
|
+
function importedName(specifier) {
|
|
43170
|
+
const { imported } = specifier;
|
|
43171
|
+
return imported.type === "Identifier" ? imported.name : imported.value;
|
|
43172
|
+
}
|
|
43173
|
+
/** Whether the file binds `require` at its top level, by a declaration or an import. */
|
|
43174
|
+
function declaresRequire(body) {
|
|
43175
|
+
return body.some((statement) => importsRequire(statement) || declaresRequireVariable(statement));
|
|
43176
|
+
}
|
|
43177
|
+
/** Whether `statement` imports a binding named `require`: hoisted, so bound anywhere. */
|
|
43178
|
+
function importsRequire(statement) {
|
|
43179
|
+
return statement.type === "ImportDeclaration" && statement.specifiers.some((specifier) => isNamed(specifier.local, "require"));
|
|
43180
|
+
}
|
|
43181
|
+
/** The declarators in `statement` that name a variable `require`, exported or not. */
|
|
43182
|
+
function requireDeclarators(statement) {
|
|
43183
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
43184
|
+
return declaration?.type === "VariableDeclaration" ? declaration.declarations.filter((declarator) => isNamed(declarator.id, "require")) : [];
|
|
43185
|
+
}
|
|
43186
|
+
/** Whether `statement` declares a variable named `require`, exported or not. */
|
|
43187
|
+
function declaresRequireVariable(statement) {
|
|
43188
|
+
return requireDeclarators(statement).length > 0;
|
|
43189
|
+
}
|
|
43190
|
+
/**
|
|
43191
|
+
* Whether the file binds `require` only by a variable declared after `at`,
|
|
43192
|
+
* where the config object starts. The object's `require.resolve` runs when
|
|
43193
|
+
* the object is evaluated, while that declaration is still in its temporal dead
|
|
43194
|
+
* zone, so it would throw: the lines are printed rather than written.
|
|
43195
|
+
*/
|
|
43196
|
+
function declaresRequireAfter(body, at) {
|
|
43197
|
+
if (body.some(importsRequire)) return false;
|
|
43198
|
+
const declarators = body.flatMap(requireDeclarators);
|
|
43199
|
+
return declarators.length > 0 && declarators.every((declarator) => (declarator.start ?? 0) >= at);
|
|
43200
|
+
}
|
|
43201
|
+
/**
|
|
43202
|
+
* What an ES module config needs at its top for the handlers' `require`:
|
|
43203
|
+
* nothing when it binds `require` already, the declaration alone, calling its
|
|
43204
|
+
* own `createRequire` by its name, when it imports one, and otherwise the
|
|
43205
|
+
* import and the declaration.
|
|
43206
|
+
*/
|
|
43207
|
+
function requireSetupFor(body) {
|
|
43208
|
+
if (declaresRequire(body)) return null;
|
|
43209
|
+
const imported = createRequireImport(body);
|
|
43210
|
+
return imported ? requireDeclaration(imported) : `${REQUIRE_IMPORT}\n\n${REQUIRE_DECLARATION}`;
|
|
43211
|
+
}
|
|
43212
|
+
/**
|
|
43213
|
+
* The edit that gives an ES module config `require`, before the first
|
|
43214
|
+
* statement that runs, and before the comments that lead it, such as a
|
|
43215
|
+
* `@type` annotation. Imports are hoisted, so one written further down still
|
|
43216
|
+
* loads first; the declaration is not, and a `require.resolve` evaluated
|
|
43217
|
+
* above it would throw.
|
|
43218
|
+
*/
|
|
43219
|
+
function requireEdit(source, body, setup) {
|
|
43220
|
+
const first = body.find((statement) => statement.type !== "ImportDeclaration");
|
|
43163
43221
|
return {
|
|
43164
|
-
at:
|
|
43165
|
-
remove:
|
|
43166
|
-
text:
|
|
43222
|
+
at: lineStart(source, first?.leadingComments?.[0]?.start ?? first?.start ?? 0),
|
|
43223
|
+
remove: 0,
|
|
43224
|
+
text: `${setup}\n\n`
|
|
43167
43225
|
};
|
|
43168
43226
|
}
|
|
43169
|
-
/**
|
|
43170
|
-
function
|
|
43171
|
-
|
|
43172
|
-
|
|
43173
|
-
const
|
|
43174
|
-
|
|
43175
|
-
return hiddenSettings(object, [CACHE_COMPONENTS, PARTIAL_PREFETCHING], "it") ?? object;
|
|
43227
|
+
/** Whether `node` is `require.resolve("<specifier>")`. */
|
|
43228
|
+
function resolvesTo(node, specifier) {
|
|
43229
|
+
if (node?.type !== "CallExpression") return false;
|
|
43230
|
+
const { callee, arguments: args } = node;
|
|
43231
|
+
const [only] = args;
|
|
43232
|
+
return callee.type === "MemberExpression" && isNamed(callee.object, "require") && isNamed(callee.property, "resolve") && args.length === 1 && only?.type === "StringLiteral" && only.value === specifier;
|
|
43176
43233
|
}
|
|
43177
|
-
/**
|
|
43178
|
-
function
|
|
43179
|
-
const
|
|
43180
|
-
|
|
43181
|
-
|
|
43182
|
-
return value.value ? value : null;
|
|
43234
|
+
/** Whether the config's cache settings are Nextly's handlers, as this writes them. */
|
|
43235
|
+
function hasNextlysHandlers(object) {
|
|
43236
|
+
const handlers = propertyValue(object, "cacheHandlers");
|
|
43237
|
+
const memory = propertyValue(object, "cacheMaxMemorySize");
|
|
43238
|
+
return resolvesTo(propertyValue(object, "cacheHandler"), "nextly/cache-handler") && handlers?.type === "ObjectExpression" && resolvesTo(propertyValue(handlers, "default"), "nextly/cache-handlers") && memory?.type === "NumericLiteral" && memory.value === 0;
|
|
43183
43239
|
}
|
|
43184
|
-
/** The
|
|
43185
|
-
|
|
43186
|
-
|
|
43187
|
-
|
|
43188
|
-
const value =
|
|
43189
|
-
|
|
43190
|
-
|
|
43240
|
+
/** The settings that make a cache of the app's own: a handler for either cache. */
|
|
43241
|
+
const OWN_CACHE_SETTINGS = ["cacheHandler", "cacheHandlers"];
|
|
43242
|
+
/** Whether the config gives `name` a value: written, and not `undefined`. */
|
|
43243
|
+
function setsValue(object, name) {
|
|
43244
|
+
const value = propertyValue(object, name);
|
|
43245
|
+
return value !== void 0 && !isNamed(value, "undefined");
|
|
43246
|
+
}
|
|
43247
|
+
/**
|
|
43248
|
+
* Why Nextly's handlers cannot go into this config as it stands, or null when
|
|
43249
|
+
* they can. A setting written as `undefined` is a placeholder to fill, not a
|
|
43250
|
+
* value; a handler of the app's own is not replaced; and `cacheMaxMemorySize`
|
|
43251
|
+
* must be 0, so Next.js keeps no second copy of the handlers' entries.
|
|
43252
|
+
*/
|
|
43253
|
+
function handlerConflict(object) {
|
|
43254
|
+
const placeholders = HANDLER_SETTINGS.filter((name) => propertyValue(object, name) !== void 0 && !setsValue(object, name));
|
|
43255
|
+
if (placeholders.length > 0) return `it sets ${placeholders.join(", ")} to undefined`;
|
|
43256
|
+
const own = OWN_CACHE_SETTINGS.filter((name) => setsValue(object, name));
|
|
43257
|
+
if (own.length > 0) return `it has cache handlers of its own (${own.join(", ")}), which were kept`;
|
|
43258
|
+
const memory = propertyValue(object, "cacheMaxMemorySize");
|
|
43259
|
+
return memory && !(memory.type === "NumericLiteral" && memory.value === 0) ? "it sets cacheMaxMemorySize to a value other than 0, which Nextly's handlers need so that Next.js keeps no second copy of their entries" : null;
|
|
43260
|
+
}
|
|
43261
|
+
/**
|
|
43262
|
+
* How to install Nextly's cache handlers: the properties to add and any edit
|
|
43263
|
+
* that gives the file `require`, or why they are left out, with the lines to
|
|
43264
|
+
* put in place of the config's own settings.
|
|
43265
|
+
*/
|
|
43266
|
+
function handlerStep(source, object, body, esm) {
|
|
43267
|
+
const none = {
|
|
43268
|
+
add: [],
|
|
43269
|
+
edits: []
|
|
43270
|
+
};
|
|
43271
|
+
if (hasNextlysHandlers(object)) return {
|
|
43272
|
+
...none,
|
|
43273
|
+
outcome: "present"
|
|
43274
|
+
};
|
|
43275
|
+
const setup = esm ? requireSetupFor(body) : null;
|
|
43276
|
+
const conflict = esm && declaresRequireAfter(body, object.start ?? 0) ? "it declares require after its config object, so the handlers' require.resolve would run before require exists; move that declaration above the object" : handlerConflict(object);
|
|
43277
|
+
if (conflict) return {
|
|
43278
|
+
...none,
|
|
43279
|
+
outcome: {
|
|
43280
|
+
skipped: conflict,
|
|
43281
|
+
lines: HANDLER_PROPERTIES,
|
|
43282
|
+
requireSetup: setup
|
|
43283
|
+
}
|
|
43284
|
+
};
|
|
43285
|
+
const memorySet = propertyValue(object, "cacheMaxMemorySize") !== void 0;
|
|
43286
|
+
const add = HANDLER_PROPERTIES.filter((property) => !(memorySet && property.startsWith("cacheMaxMemorySize")));
|
|
43287
|
+
if (setup === null) return {
|
|
43288
|
+
add,
|
|
43191
43289
|
edits: [],
|
|
43192
|
-
|
|
43290
|
+
outcome: "added"
|
|
43193
43291
|
};
|
|
43194
|
-
|
|
43195
|
-
|
|
43196
|
-
|
|
43197
|
-
|
|
43292
|
+
if (referenceCount("require", body) > 0 || createRequireImport(body) === null && referenceCount("createRequire", body) > 0) return {
|
|
43293
|
+
...none,
|
|
43294
|
+
outcome: {
|
|
43295
|
+
skipped: "it is an ES module that uses require, or a createRequire not imported from node:module, without binding require, so the lines that define it could clash",
|
|
43296
|
+
lines: add,
|
|
43297
|
+
requireSetup: setup
|
|
43298
|
+
}
|
|
43299
|
+
};
|
|
43300
|
+
return {
|
|
43301
|
+
add,
|
|
43302
|
+
edits: [requireEdit(source, body, setup)],
|
|
43303
|
+
outcome: "added"
|
|
43198
43304
|
};
|
|
43199
|
-
|
|
43305
|
+
}
|
|
43306
|
+
/** The edits that turn Cache Components on in a config, or why it cannot be changed. */
|
|
43307
|
+
function cacheComponentsPlan(source, file, esm) {
|
|
43308
|
+
const body = parseBody(source, file);
|
|
43309
|
+
if (!body) return { reason: "it could not be parsed" };
|
|
43310
|
+
const object = cacheComponentsObject(body);
|
|
43311
|
+
if (typeof object === "string") return { reason: object };
|
|
43312
|
+
const options = optionsPlan(object);
|
|
43313
|
+
if ("reason" in options) return options;
|
|
43314
|
+
const handlers = handlerStep(source, object, body, esm);
|
|
43315
|
+
const add = [...options.add, ...handlers.add];
|
|
43316
|
+
const edits = [...options.flips, ...handlers.edits];
|
|
43317
|
+
if (add.length > 0) edits.push(...appendProperties(source, object, () => add));
|
|
43200
43318
|
return {
|
|
43201
|
-
edits
|
|
43202
|
-
|
|
43319
|
+
edits,
|
|
43320
|
+
turnedOn: options.turnedOn,
|
|
43321
|
+
handlers: handlers.outcome
|
|
43203
43322
|
};
|
|
43204
43323
|
}
|
|
43205
|
-
/**
|
|
43206
|
-
function
|
|
43207
|
-
const
|
|
43208
|
-
|
|
43324
|
+
/** How to turn both options on: the values to flip, the properties to add, and which options change. */
|
|
43325
|
+
function optionsPlan(object) {
|
|
43326
|
+
const plan = {
|
|
43327
|
+
flips: [],
|
|
43328
|
+
add: [],
|
|
43329
|
+
turnedOn: []
|
|
43330
|
+
};
|
|
43331
|
+
for (const option of ON_OPTIONS) {
|
|
43332
|
+
const step = optionStep(object, option);
|
|
43333
|
+
if (typeof step === "string") return { reason: step };
|
|
43334
|
+
if (step.flip) plan.flips.push(step.flip);
|
|
43335
|
+
if (step.add) plan.add.push(step.add);
|
|
43336
|
+
if (step.flip || step.add) plan.turnedOn.push(option);
|
|
43337
|
+
}
|
|
43338
|
+
return plan;
|
|
43209
43339
|
}
|
|
43210
43340
|
/**
|
|
43211
|
-
* Turn Next.js Cache Components
|
|
43212
|
-
*
|
|
43341
|
+
* Turn Next.js Cache Components and Partial Prefetching on in an existing
|
|
43342
|
+
* app's config, with Nextly's cache handlers: Nextly runs with both on.
|
|
43343
|
+
* Apps that an earlier Nextly release turned off are turned on again.
|
|
43213
43344
|
*
|
|
43214
|
-
* Only
|
|
43215
|
-
*
|
|
43216
|
-
*
|
|
43217
|
-
* and reads as off; anything else is left as it was, with what to change.
|
|
43345
|
+
* Only settings written in place as `true` or `false`, or absent, are
|
|
43346
|
+
* changed; a value this cannot read leaves the file as it was, with what to
|
|
43347
|
+
* add. The edit is trusted only once the result parses and reads as on.
|
|
43218
43348
|
*/
|
|
43219
|
-
async function
|
|
43220
|
-
const file = await
|
|
43221
|
-
|
|
43222
|
-
|
|
43223
|
-
|
|
43349
|
+
async function turnOnCacheComponents(cwd) {
|
|
43350
|
+
const file = await nextConfigFileToChange(cwd);
|
|
43351
|
+
const source = await import_lib$1.default.readFile(path.join(cwd, file), "utf-8").catch(() => null);
|
|
43352
|
+
if (source === null) return {
|
|
43353
|
+
kind: "manual",
|
|
43354
|
+
file,
|
|
43355
|
+
reason: "the app has no Next config",
|
|
43356
|
+
requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
|
|
43357
|
+
};
|
|
43358
|
+
const body = parseBody(source, file);
|
|
43359
|
+
if (!body) return {
|
|
43360
|
+
kind: "manual",
|
|
43361
|
+
file,
|
|
43362
|
+
reason: "it could not be parsed",
|
|
43363
|
+
requireSetup: loadsAsModule(file, []) ? requireSetupFor([]) : null
|
|
43364
|
+
};
|
|
43365
|
+
const esm = loadsAsModule(file, body);
|
|
43366
|
+
const requireSetup = esm ? requireSetupFor(body) : null;
|
|
43367
|
+
const plan = cacheComponentsPlan(source, file, esm);
|
|
43224
43368
|
if ("reason" in plan) return {
|
|
43225
43369
|
kind: "manual",
|
|
43226
43370
|
file,
|
|
43227
|
-
reason: plan.reason
|
|
43371
|
+
reason: plan.reason,
|
|
43372
|
+
requireSetup
|
|
43373
|
+
};
|
|
43374
|
+
if (plan.edits.length === 0) return {
|
|
43375
|
+
kind: "unchanged",
|
|
43376
|
+
file,
|
|
43377
|
+
handlers: plan.handlers
|
|
43228
43378
|
};
|
|
43229
|
-
if (plan.edits.length === 0) return { kind: "unchanged" };
|
|
43230
43379
|
const text = applyEdits(source, plan.edits);
|
|
43231
|
-
const recheck = cacheComponentsPlan(text, file);
|
|
43232
|
-
if ("reason" in recheck || recheck.edits.length > 0
|
|
43380
|
+
const recheck = cacheComponentsPlan(text, file, esm);
|
|
43381
|
+
if ("reason" in recheck || recheck.edits.length > 0) return {
|
|
43233
43382
|
kind: "manual",
|
|
43234
43383
|
file,
|
|
43235
|
-
reason: "the edit could not be confirmed"
|
|
43384
|
+
reason: "the edit could not be confirmed",
|
|
43385
|
+
requireSetup
|
|
43236
43386
|
};
|
|
43237
43387
|
await import_lib$1.default.writeFile(path.join(cwd, file), text, "utf-8");
|
|
43238
43388
|
return {
|
|
43239
|
-
kind: "turned-
|
|
43389
|
+
kind: "turned-on",
|
|
43240
43390
|
file,
|
|
43241
|
-
|
|
43391
|
+
turnedOn: plan.turnedOn,
|
|
43392
|
+
handlers: plan.handlers
|
|
43242
43393
|
};
|
|
43243
43394
|
}
|
|
43244
43395
|
/**
|
|
43245
|
-
* What turning Cache Components
|
|
43246
|
-
* a notice saying why, or, for a config it could not change,
|
|
43396
|
+
* What turning Cache Components on did, for the run's report: each change and
|
|
43397
|
+
* a notice saying why, or, for a config it could not change, the exact lines
|
|
43398
|
+
* to add.
|
|
43247
43399
|
*/
|
|
43248
43400
|
function cacheComponentsFiles(patch) {
|
|
43249
43401
|
const files = {
|
|
@@ -43251,43 +43403,42 @@ function cacheComponentsFiles(patch) {
|
|
|
43251
43403
|
changed: [],
|
|
43252
43404
|
skipped: []
|
|
43253
43405
|
};
|
|
43254
|
-
if (patch.kind === "
|
|
43406
|
+
if (patch.kind === "manual") {
|
|
43407
|
+
files.skipped.push({
|
|
43408
|
+
path: patch.file,
|
|
43409
|
+
why: `Cache Components was not turned on: ${patch.reason}. Nextly runs with Cache Components and Partial Prefetching on, with its cache handlers (${CACHE_COMPONENTS_DOCS_URL}); add these to the config object Next.js receives, in place of any value of theirs`,
|
|
43410
|
+
toAdd: linesToAdd$1(ON_LINES, patch.requireSetup)
|
|
43411
|
+
});
|
|
43412
|
+
return files;
|
|
43413
|
+
}
|
|
43414
|
+
const handlersAdded = patch.handlers === "added";
|
|
43415
|
+
if (patch.kind === "turned-on") {
|
|
43255
43416
|
files.changed.push({
|
|
43256
43417
|
path: patch.file,
|
|
43257
|
-
added: [`${
|
|
43418
|
+
added: [...patch.turnedOn.map((option) => `${option}: true`), ...handlersAdded ? [...HANDLER_SETTINGS] : []]
|
|
43258
43419
|
});
|
|
43259
|
-
const
|
|
43260
|
-
files.notices = [`
|
|
43261
|
-
}
|
|
43420
|
+
const done = [...patch.turnedOn.length > 0 ? [`turned ${patch.turnedOn.join(" and ")} on`] : [], ...handlersAdded ? ["installed Nextly's cache handlers"] : []].join(" and ");
|
|
43421
|
+
files.notices = [`In ${patch.file}, ${done}: Nextly runs with Cache Components and Partial Prefetching on. What each setting does: ${CACHE_COMPONENTS_DOCS_URL}`];
|
|
43422
|
+
}
|
|
43423
|
+
if (typeof patch.handlers === "object") files.skipped.push({
|
|
43262
43424
|
path: patch.file,
|
|
43263
|
-
why: `Nextly
|
|
43264
|
-
toAdd: ` ${
|
|
43425
|
+
why: `Nextly's cache handlers were not added: ${patch.handlers.skipped}. Without them a page read right after a write can show the previous version once, and in production every cached public read refreshes within a minute of a visit, since a write from another instance, the CLI or a job reaches no other cache (${CACHE_COMPONENTS_DOCS_URL}); to use them, put these in place of the config's own cache settings`,
|
|
43426
|
+
toAdd: linesToAdd$1(patch.handlers.lines.map((property) => ` ${property},`), patch.handlers.requireSetup)
|
|
43265
43427
|
});
|
|
43266
43428
|
return files;
|
|
43267
43429
|
}
|
|
43268
43430
|
/**
|
|
43269
|
-
*
|
|
43270
|
-
*
|
|
43271
|
-
* On Next.js 14 the package list and the instrumentation opt-in are keys of
|
|
43272
|
-
* one `experimental` object, so they are given as one: two `experimental`
|
|
43273
|
-
* properties, pasted as given, leave only the second. The instrumentation
|
|
43274
|
-
* hook Nextly starts from needs the opt-in there, and no other step says so.
|
|
43275
|
-
* `wrap` turns each spelling into what to paste: the key itself, or a whole
|
|
43276
|
-
* config file when the app has none.
|
|
43277
|
-
*/
|
|
43278
|
-
function optionText(packages, spelling, wrap = (option) => option) {
|
|
43279
|
-
const list = packages.map((name) => JSON.stringify(name)).join(", ");
|
|
43280
|
-
const current = wrap(` ${PROPERTY}: [${list}],`);
|
|
43281
|
-
const legacy = wrap(` experimental: { instrumentationHook: true, serverComponentsExternalPackages: [${list}] },`);
|
|
43282
|
-
if (spelling === "legacy") return legacy;
|
|
43283
|
-
return spelling === "either" ? `On Next.js 15 or later:\n\n${current}\n\nOn Next.js 14:\n\n${legacy}` : current;
|
|
43284
|
-
}
|
|
43285
|
-
/**
|
|
43286
|
-
* A whole config holding `option`, for an app that has none. ESM in `.mjs`,
|
|
43287
|
-
* which loads whatever the package's own "type" says.
|
|
43431
|
+
* The config lines to add by hand. An ES module config with no `require`
|
|
43432
|
+
* also gets the lines that define it for the handlers, at the top of the file.
|
|
43288
43433
|
*/
|
|
43289
|
-
function
|
|
43290
|
-
|
|
43434
|
+
function linesToAdd$1(lines, requireSetup) {
|
|
43435
|
+
const config = lines.join("\n");
|
|
43436
|
+
return requireSetup ? `// At the top of the file:\n${requireSetup.replace("\n\n", "\n")}\n\n// In the config object:\n${config}` : config;
|
|
43437
|
+
}
|
|
43438
|
+
/** What to add by hand: the list, as the config's top-level option. */
|
|
43439
|
+
function optionText(packages) {
|
|
43440
|
+
const list = packages.map((name) => JSON.stringify(name)).join(", ");
|
|
43441
|
+
return ` ${PROPERTY}: [${list}],`;
|
|
43291
43442
|
}
|
|
43292
43443
|
/**
|
|
43293
43444
|
* What a patch did, in the shape every existing-app step reports in: the
|
|
@@ -43311,109 +43462,14 @@ function nextConfigFiles(patch) {
|
|
|
43311
43462
|
files.written.push(patch.file);
|
|
43312
43463
|
break;
|
|
43313
43464
|
case "unchanged": break;
|
|
43314
|
-
case "manual": files.skipped.push(
|
|
43465
|
+
case "manual": files.skipped.push({
|
|
43315
43466
|
path: patch.file,
|
|
43316
|
-
why: `Nextly's packages were not added: ${patch.reason}, and the
|
|
43317
|
-
toAdd: optionText(patch.packages
|
|
43318
|
-
} : {
|
|
43319
|
-
path: patch.file,
|
|
43320
|
-
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`,
|
|
43321
|
-
toAdd: optionText(patch.packages, patch.spelling)
|
|
43467
|
+
why: patch.reason === LEGACY_LIST_REASON ? `Nextly's packages were not added: ${patch.reason}. Move the packages under experimental.${LEGACY_PROPERTY} into a top-level ${PROPERTY} list, add Nextly's to it, and delete experimental.${LEGACY_PROPERTY}, keeping the config's other experimental keys` : `Nextly's packages were not added: ${patch.reason}. Add them to the config object it exports, keeping any packages and experimental keys already there`,
|
|
43468
|
+
toAdd: optionText(patch.packages)
|
|
43322
43469
|
});
|
|
43323
43470
|
}
|
|
43324
43471
|
return files;
|
|
43325
43472
|
}
|
|
43326
|
-
// src/generators/page-extensions.ts
|
|
43327
|
-
/**
|
|
43328
|
-
* The page extension to name a new module file with, a hook or a route: a
|
|
43329
|
-
* TypeScript one first, since Nextly's files are TypeScript, then a
|
|
43330
|
-
* JavaScript one; never a content extension such as `mdx`, which Next.js
|
|
43331
|
-
* would compile as content, and never `cjs`, since the files are ES modules
|
|
43332
|
-
* and a `.cjs` file cannot hold one. Null when the list holds no extension a
|
|
43333
|
-
* module can be written in.
|
|
43334
|
-
*/
|
|
43335
|
-
function moduleExtension(extensions) {
|
|
43336
|
-
const usable = writable(extensions);
|
|
43337
|
-
return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
|
|
43338
|
-
}
|
|
43339
|
-
/**
|
|
43340
|
-
* The extensions a file with code in it can be written under: not a
|
|
43341
|
-
* declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
|
|
43342
|
-
* types only and refuses code in.
|
|
43343
|
-
*/
|
|
43344
|
-
function writable(extensions) {
|
|
43345
|
-
return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
|
|
43346
|
-
}
|
|
43347
|
-
/**
|
|
43348
|
-
* The `pageExtensions` the app's Next.js config sets, as every existing-app
|
|
43349
|
-
* step that names a file Next.js routes reads it: the list; `"absent"` when
|
|
43350
|
-
* the config sets none, or there is no config; or null when it is set in a
|
|
43351
|
-
* way that cannot be read. Read from the config the app exports, as the Next
|
|
43352
|
-
* config step reads it, so a commented-out or nested `pageExtensions` sets
|
|
43353
|
-
* nothing. `configName` is the file it was read from, for the report.
|
|
43354
|
-
*/
|
|
43355
|
-
async function appPageExtensions(cwd) {
|
|
43356
|
-
const nextConfig = await readNextConfig(cwd);
|
|
43357
|
-
return {
|
|
43358
|
-
configName: nextConfig?.name ?? "The Next.js config",
|
|
43359
|
-
extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
|
|
43360
|
-
};
|
|
43361
|
-
}
|
|
43362
|
-
/**
|
|
43363
|
-
* The text of the app's Next.js config, or null when it has none, from the
|
|
43364
|
-
* file the Next config step reads and extends ({@link findConfigFile}).
|
|
43365
|
-
*/
|
|
43366
|
-
async function readNextConfig(cwd) {
|
|
43367
|
-
const name = await findConfigFile(cwd);
|
|
43368
|
-
return name === null ? null : {
|
|
43369
|
-
name,
|
|
43370
|
-
text: await import_lib$1.default.readFile(path.join(cwd, name), "utf-8")
|
|
43371
|
-
};
|
|
43372
|
-
}
|
|
43373
|
-
/**
|
|
43374
|
-
* The page extension for a file of TypeScript with JSX, such as a page or a
|
|
43375
|
-
* layout: one ending in `tsx`. Null when the list holds none.
|
|
43376
|
-
*/
|
|
43377
|
-
function tsxExtension(extensions) {
|
|
43378
|
-
return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
|
|
43379
|
-
}
|
|
43380
|
-
/** A JavaScript or TypeScript module without JSX, such as a route. */
|
|
43381
|
-
const MODULE_FILE = {
|
|
43382
|
-
pick: moduleExtension,
|
|
43383
|
-
holds: "a JavaScript or TypeScript extension"
|
|
43384
|
-
};
|
|
43385
|
-
/** TypeScript with JSX, such as a page or a layout. */
|
|
43386
|
-
const TSX_FILE = {
|
|
43387
|
-
pick: tsxExtension,
|
|
43388
|
-
holds: "an extension ending in tsx"
|
|
43389
|
-
};
|
|
43390
|
-
/**
|
|
43391
|
-
* The report for a file an app that sets `pageExtensions` gets in place of
|
|
43392
|
-
* the file: the name to create it under and its content. Next.js routes a
|
|
43393
|
-
* file there only under a name with one of the app's extensions, and the app
|
|
43394
|
-
* may already have the file under such a name, which the existing-app steps
|
|
43395
|
-
* do not look for, so nothing is written. A list that cannot be read, or that
|
|
43396
|
-
* holds no extension the content needs, gives a placeholder name and says
|
|
43397
|
-
* which.
|
|
43398
|
-
*
|
|
43399
|
-
* @param base the file's path without its extension, as reported, such as
|
|
43400
|
-
* `app/admin/[[...params]]/page`
|
|
43401
|
-
*/
|
|
43402
|
-
function pageExtensionsSkip(base, text, pages, need) {
|
|
43403
|
-
const { configName, extensions } = pages;
|
|
43404
|
-
const extension = extensions === null ? null : need.pick(extensions);
|
|
43405
|
-
const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
|
|
43406
|
-
if (extensions !== null && extension === null) return {
|
|
43407
|
-
path: `${base}.<${need.holds} added to pageExtensions>`,
|
|
43408
|
-
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`,
|
|
43409
|
-
toAdd: text
|
|
43410
|
-
};
|
|
43411
|
-
return {
|
|
43412
|
-
path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
|
|
43413
|
-
why: `${sets}; unless the app already has this file, create it under that name with this content`,
|
|
43414
|
-
toAdd: text
|
|
43415
|
-
};
|
|
43416
|
-
}
|
|
43417
43473
|
// src/utils/detect-pm-from-user-agent.ts
|
|
43418
43474
|
/**
|
|
43419
43475
|
* Detect the package manager that invoked `create-nextly-app` by reading
|
|
@@ -45644,6 +45700,45 @@ function getDefaultApproach(name) {
|
|
|
45644
45700
|
function shouldUseBundledTemplate(projectType, options) {
|
|
45645
45701
|
return BUNDLED_TEMPLATES.some((name) => name === projectType) && !options.localTemplatePath && !options.useYalc && options.branch === void 0;
|
|
45646
45702
|
}
|
|
45703
|
+
// src/supported-versions.ts
|
|
45704
|
+
/**
|
|
45705
|
+
* The Next.js and React this release of Nextly supports, in one place.
|
|
45706
|
+
*
|
|
45707
|
+
* Two facts, deliberately kept apart:
|
|
45708
|
+
*
|
|
45709
|
+
* - `REVIEWED_VERSIONS` is the newest release of each package this Nextly
|
|
45710
|
+
* release was reviewed against. A new app declares it as a `~` range
|
|
45711
|
+
* ({@link scaffoldRange}): patch releases of that minor, never a newer minor
|
|
45712
|
+
* or major that nobody has checked, and never whatever npm's `latest` names
|
|
45713
|
+
* on the day of the scaffold.
|
|
45714
|
+
* - `SUPPORTED_RANGES` is every release Nextly works with: the `nextly`
|
|
45715
|
+
* package's own peer ranges, inlined at build, so they are declared once.
|
|
45716
|
+
* Every other package declares the same as its peers. Adding Nextly to an
|
|
45717
|
+
* existing app refuses one whose Next.js, React or React DOM falls outside
|
|
45718
|
+
* them, and a scaffolded plugin declares them as its own peers.
|
|
45719
|
+
*
|
|
45720
|
+
* Only a release review moves either: a newer Next.js or React is supported
|
|
45721
|
+
* once a Nextly release has been checked against it, not before.
|
|
45722
|
+
*/
|
|
45723
|
+
const REVIEWED_VERSIONS = {
|
|
45724
|
+
next: "16.4.2",
|
|
45725
|
+
"eslint-config-next": "16.4.2",
|
|
45726
|
+
react: "19.3.1",
|
|
45727
|
+
"react-dom": "19.3.1"
|
|
45728
|
+
};
|
|
45729
|
+
const SUPPORTED_RANGES = {
|
|
45730
|
+
"next": "^16.4.1",
|
|
45731
|
+
"react": "^19.3.0",
|
|
45732
|
+
"react-dom": "^19.3.0"
|
|
45733
|
+
};
|
|
45734
|
+
/**
|
|
45735
|
+
* The range a new project declares for `pkg`: patch releases of the reviewed
|
|
45736
|
+
* minor only. Derived here rather than written out, so the reviewed version is
|
|
45737
|
+
* the one thing a release review edits.
|
|
45738
|
+
*/
|
|
45739
|
+
function scaffoldRange(pkg) {
|
|
45740
|
+
return `~${REVIEWED_VERSIONS[pkg]}`;
|
|
45741
|
+
}
|
|
45647
45742
|
// src/utils/package-manager-commands.ts
|
|
45648
45743
|
/**
|
|
45649
45744
|
* The prefix that runs a package.json script.
|
|
@@ -45671,37 +45766,35 @@ function binaryRunner(packageManager) {
|
|
|
45671
45766
|
case "bun": return "bunx";
|
|
45672
45767
|
}
|
|
45673
45768
|
}
|
|
45674
|
-
// src/utils/
|
|
45675
|
-
/**
|
|
45676
|
-
* The Node versions a scaffolded plugin supports, and the check that refuses
|
|
45677
|
-
* to scaffold one on a Node it cannot build on.
|
|
45678
|
-
*
|
|
45679
|
-
* A plugin builds with tsdown and tests with Vitest, and each declares the
|
|
45680
|
-
* Node it runs on; the plugin's floor is where both agree. The repository's
|
|
45681
|
-
* packages have a lower floor, because only a plugin author runs these tools.
|
|
45682
|
-
* `scaffold-plugin.test.ts` derives this range from the installed tools' own
|
|
45683
|
-
* `engines`, so a version bump that moves either one fails there.
|
|
45684
|
-
*/
|
|
45685
|
-
const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
|
|
45769
|
+
// src/utils/version-range.ts
|
|
45686
45770
|
function parseVersion$1(text) {
|
|
45687
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)
|
|
45688
|
-
if (!match) throw new Error(`not a
|
|
45689
|
-
return [
|
|
45771
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/.exec(text.trim());
|
|
45772
|
+
if (!match) throw new Error(`not a version: ${text}`);
|
|
45773
|
+
return Object.assign([
|
|
45690
45774
|
Number(match[1]),
|
|
45691
45775
|
Number(match[2]),
|
|
45692
45776
|
Number(match[3])
|
|
45693
|
-
];
|
|
45777
|
+
], { prerelease: match[4] !== void 0 });
|
|
45694
45778
|
}
|
|
45779
|
+
/** Older first; of two with the same numbers, a prerelease comes before its release, as in semver. */
|
|
45695
45780
|
function compare(a, b) {
|
|
45696
|
-
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
45781
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || Number(Boolean(b.prerelease)) - Number(Boolean(a.prerelease));
|
|
45697
45782
|
}
|
|
45698
45783
|
/**
|
|
45699
|
-
*
|
|
45700
|
-
*
|
|
45701
|
-
|
|
45784
|
+
* Negative when `a` is older than `b`, positive when newer, 0 when equal.
|
|
45785
|
+
* A prerelease is older than its release: `16.4.1-canary.1` is below `16.4.1`.
|
|
45786
|
+
*/
|
|
45787
|
+
function compareVersions(a, b) {
|
|
45788
|
+
return compare(parseVersion$1(a), parseVersion$1(b));
|
|
45789
|
+
}
|
|
45790
|
+
/**
|
|
45791
|
+
* Whether `version` satisfies one comparator. Only the two forms this CLI
|
|
45792
|
+
* writes are understood, `^X.Y.Z` and `>=X.Y.Z`; any other is refused rather
|
|
45793
|
+
* than read approximately, so a range that starts using another form fails
|
|
45702
45794
|
* the test that reads it.
|
|
45703
45795
|
*/
|
|
45704
45796
|
function satisfiesComparator(version, comparator) {
|
|
45797
|
+
if (version.prerelease) return false;
|
|
45705
45798
|
const caret = /^\^(\d+\.\d+\.\d+)$/.exec(comparator);
|
|
45706
45799
|
if (caret) {
|
|
45707
45800
|
const floor = parseVersion$1(caret[1]);
|
|
@@ -45709,21 +45802,83 @@ function satisfiesComparator(version, comparator) {
|
|
|
45709
45802
|
}
|
|
45710
45803
|
const atLeast = /^>=(\d+\.\d+\.\d+)$/.exec(comparator);
|
|
45711
45804
|
if (atLeast) return compare(version, parseVersion$1(atLeast[1])) >= 0;
|
|
45712
|
-
throw new Error(`unsupported
|
|
45805
|
+
throw new Error(`unsupported range comparator: ${comparator}`);
|
|
45713
45806
|
}
|
|
45714
45807
|
/** Whether `version` satisfies a `||` list of `^X.Y.Z` / `>=X.Y.Z` comparators. */
|
|
45715
|
-
function
|
|
45808
|
+
function satisfiesRange(version, range) {
|
|
45716
45809
|
const parsed = parseVersion$1(version);
|
|
45717
45810
|
return range.split("||").map((part) => part.trim()).some((comparator) => satisfiesComparator(parsed, comparator));
|
|
45718
45811
|
}
|
|
45719
45812
|
/**
|
|
45720
|
-
*
|
|
45721
|
-
* `undefined` when
|
|
45722
|
-
*
|
|
45813
|
+
* The lowest version a package.json dependency spec admits, as `X.Y.Z`, or
|
|
45814
|
+
* `undefined` when the spec names no lower bound this can read: a dist-tag
|
|
45815
|
+
* (`latest`, `canary`), a protocol (`workspace:*`, `file:`, a URL), `*`, or an
|
|
45816
|
+
* upper bound alone (`<17`). An exact version is its own lowest; `^`, `~`, `=`
|
|
45817
|
+
* and `>=` start at the version they name; a missing or wildcard minor or
|
|
45818
|
+
* patch (`15`, `15.x`) counts as 0; across `||` the lowest alternative wins.
|
|
45723
45819
|
*/
|
|
45724
|
-
function
|
|
45725
|
-
|
|
45726
|
-
|
|
45820
|
+
function lowestVersionIn(spec) {
|
|
45821
|
+
let lowest;
|
|
45822
|
+
for (const part of spec.split("||")) {
|
|
45823
|
+
const match = /^\s*(?:\^|~|>=|=)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?/.exec(part);
|
|
45824
|
+
if (!match) continue;
|
|
45825
|
+
const number = (text) => text === void 0 || !/^\d+$/.test(text) ? 0 : Number(text);
|
|
45826
|
+
const found = [
|
|
45827
|
+
Number(match[1]),
|
|
45828
|
+
number(match[2]),
|
|
45829
|
+
number(match[3])
|
|
45830
|
+
];
|
|
45831
|
+
if (!lowest || compare(found, lowest) < 0) lowest = found;
|
|
45832
|
+
}
|
|
45833
|
+
return lowest ? lowest.join(".") : void 0;
|
|
45834
|
+
}
|
|
45835
|
+
/**
|
|
45836
|
+
* The highest major a package.json dependency spec admits, `Infinity` when it
|
|
45837
|
+
* has no upper bound (`>=16.4.1`, `*`, `x`), or `undefined` when no
|
|
45838
|
+
* alternative names one this can read (a dist-tag, a protocol). Across `||`
|
|
45839
|
+
* the highest alternative wins, since an install resolves the newest version
|
|
45840
|
+
* any of them admits. Within one alternative an upper bound (`<17`, `<=17.2`)
|
|
45841
|
+
* decides; otherwise the version a `^`, `~`, `=` or bare spec names.
|
|
45842
|
+
*/
|
|
45843
|
+
function highestMajorIn(spec) {
|
|
45844
|
+
let highest;
|
|
45845
|
+
for (const part of spec.split("||")) {
|
|
45846
|
+
const found = majorReachedBy(part.trim());
|
|
45847
|
+
if (found !== void 0 && (highest === void 0 || found > highest)) highest = found;
|
|
45848
|
+
}
|
|
45849
|
+
return highest;
|
|
45850
|
+
}
|
|
45851
|
+
/** The highest major one `||` alternative reaches, as `highestMajorIn` reads it. */
|
|
45852
|
+
function majorReachedBy(part) {
|
|
45853
|
+
const upper = /<(=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(part);
|
|
45854
|
+
if (upper) {
|
|
45855
|
+
const [, inclusive, major, minor, patch] = upper;
|
|
45856
|
+
return inclusive !== void 0 || Number(minor ?? 0) > 0 || Number(patch ?? 0) > 0 ? Number(major) : Number(major) - 1;
|
|
45857
|
+
}
|
|
45858
|
+
if (part === "" || /^(\*|[xX])$/.test(part) || part.startsWith(">")) return Infinity;
|
|
45859
|
+
const named = /^(?:\^|~|=)?\s*v?(\d+)/.exec(part);
|
|
45860
|
+
return named ? Number(named[1]) : void 0;
|
|
45861
|
+
}
|
|
45862
|
+
// src/utils/plugin-node.ts
|
|
45863
|
+
/**
|
|
45864
|
+
* The Node versions a scaffolded plugin supports, and the check that refuses
|
|
45865
|
+
* to scaffold one on a Node it cannot build on.
|
|
45866
|
+
*
|
|
45867
|
+
* A plugin builds with tsdown and tests with Vitest, and each declares the
|
|
45868
|
+
* Node it runs on; the plugin's floor is where both agree. The repository's
|
|
45869
|
+
* packages have a lower floor, because only a plugin author runs these tools.
|
|
45870
|
+
* `scaffold-plugin.test.ts` derives this range from the installed tools' own
|
|
45871
|
+
* `engines`, so a version bump that moves either one fails there.
|
|
45872
|
+
*/
|
|
45873
|
+
const PLUGIN_NODE_ENGINES = "^22.18.0 || ^24.11.0 || >=26.0.0";
|
|
45874
|
+
/**
|
|
45875
|
+
* Why the plugin template cannot be scaffolded on `nodeVersion`, or
|
|
45876
|
+
* `undefined` when it can. Checked before anything is written, since a plugin
|
|
45877
|
+
* scaffolded on an older Node would install and then fail its first build.
|
|
45878
|
+
*/
|
|
45879
|
+
function pluginNodeRefusal(nodeVersion) {
|
|
45880
|
+
if (satisfiesRange(nodeVersion, "^22.18.0 || ^24.11.0 || >=26.0.0")) return void 0;
|
|
45881
|
+
return `The plugin template builds with tools that need Node ${PLUGIN_NODE_ENGINES}, and this is Node ${nodeVersion}. Install a supported Node (for example \`nvm install 24\`) and run create-nextly-app again.`;
|
|
45727
45882
|
}
|
|
45728
45883
|
// src/utils/template.ts
|
|
45729
45884
|
/**
|
|
@@ -45818,41 +45973,56 @@ const SEARCH_INDEX_SCRIPT = path.join("scripts", "build-search-index.mjs");
|
|
|
45818
45973
|
async function projectHasSearchIndexScript(targetDir) {
|
|
45819
45974
|
return import_lib$1.default.pathExists(path.join(targetDir, SEARCH_INDEX_SCRIPT));
|
|
45820
45975
|
}
|
|
45821
|
-
/**
|
|
45822
|
-
|
|
45823
|
-
* imports from a `@nextlyhq/plugin-*` package (the SDK aside) that a
|
|
45824
|
-
* `plugins: [...]` list in it names, however deep in an element. Read with
|
|
45825
|
-
* the parser the Next config step uses, so nested brackets, either quote and
|
|
45826
|
-
* default or aliased imports are read as the compiler reads them. First-party
|
|
45827
|
-
* plugins register under their package name, which is what
|
|
45828
|
-
* `nextly plugins install` takes. Throws for a config that does not parse,
|
|
45829
|
-
* since every template's must.
|
|
45830
|
-
*
|
|
45831
|
-
* Exported for the test that holds every template's config to its build.
|
|
45832
|
-
*/
|
|
45833
|
-
function configuredPlugins(configText, file = "nextly.config.ts") {
|
|
45834
|
-
let program;
|
|
45976
|
+
/** Parses a config, naming the file when it does not parse. */
|
|
45977
|
+
function parseConfigProgram(configText, file) {
|
|
45835
45978
|
try {
|
|
45836
|
-
|
|
45979
|
+
return (0, import_lib.parse)(configText, {
|
|
45837
45980
|
sourceType: "module",
|
|
45838
45981
|
plugins: ["typescript", "jsx"]
|
|
45839
45982
|
}).program;
|
|
45840
45983
|
} catch (error) {
|
|
45841
45984
|
throw new Error(`${file} cannot be parsed (${error.message}), so the plugins its build must install cannot be read.`);
|
|
45842
45985
|
}
|
|
45843
|
-
|
|
45986
|
+
}
|
|
45987
|
+
/** Each binding imported from a module `matches` accepts, to that module's name. */
|
|
45988
|
+
function bindingsImportedFrom(program, matches) {
|
|
45989
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
45844
45990
|
for (const statement of program.body) {
|
|
45845
45991
|
if (statement.type !== "ImportDeclaration") continue;
|
|
45846
|
-
|
|
45847
|
-
|
|
45848
|
-
for (const specifier of statement.specifiers) fromPackage.set(specifier.local.name, pkg);
|
|
45992
|
+
if (!matches(statement.source.value)) continue;
|
|
45993
|
+
for (const specifier of statement.specifiers) bindings.set(specifier.local.name, statement.source.value);
|
|
45849
45994
|
}
|
|
45850
|
-
|
|
45995
|
+
return bindings;
|
|
45996
|
+
}
|
|
45997
|
+
const isPluginPackage = (source) => source.startsWith("@nextlyhq/plugin-") && source !== "@nextlyhq/plugin-sdk";
|
|
45998
|
+
/** What a config's `plugins` lists hold: the packages, and whether a binding from its own source is among them. */
|
|
45999
|
+
function readPluginLists(configText, file) {
|
|
46000
|
+
const program = parseConfigProgram(configText, file);
|
|
46001
|
+
const fromPackage = bindingsImportedFrom(program, isPluginPackage);
|
|
46002
|
+
const packages = /* @__PURE__ */ new Set();
|
|
45851
46003
|
for (const name of identifiersInPluginLists(program, file)) {
|
|
45852
46004
|
const pkg = fromPackage.get(name);
|
|
45853
|
-
if (pkg !== void 0
|
|
46005
|
+
if (pkg !== void 0) packages.add(pkg);
|
|
45854
46006
|
}
|
|
45855
|
-
|
|
46007
|
+
const local = bindingsImportedFrom(program, (source) => source.startsWith("."));
|
|
46008
|
+
const ownPlugin = namesInPluginEntries(program, file).some((name) => local.has(name) && !fromPackage.has(name));
|
|
46009
|
+
return {
|
|
46010
|
+
packages: [...packages],
|
|
46011
|
+
ownPlugin
|
|
46012
|
+
};
|
|
46013
|
+
}
|
|
46014
|
+
/**
|
|
46015
|
+
* The `nextly plugins install` steps a project's build runs, read from its
|
|
46016
|
+
* config. A plugin package is installed by name, and the build says which. A
|
|
46017
|
+
* plugin the site wrote itself has no package name to give, and a production
|
|
46018
|
+
* boot refuses it like any other when nothing has installed it, so a config
|
|
46019
|
+
* that lists one installs with `--all`, which reads the names from the config.
|
|
46020
|
+
* Exported for the test that holds every template's config to its build.
|
|
46021
|
+
*/
|
|
46022
|
+
function pluginInstallSteps(configText, file = "nextly.config.ts") {
|
|
46023
|
+
const { packages, ownPlugin } = readPluginLists(configText, file);
|
|
46024
|
+
if (ownPlugin) return ["nextly plugins install --all"];
|
|
46025
|
+
return packages.map((pkg) => `nextly plugins install ${pkg}`);
|
|
45856
46026
|
}
|
|
45857
46027
|
/** Every identifier inside the value of each `plugins: [...]` property, in source order. */
|
|
45858
46028
|
function identifiersInPluginLists(program, file) {
|
|
@@ -45866,6 +46036,20 @@ function identifiersInPluginLists(program, file) {
|
|
|
45866
46036
|
return names;
|
|
45867
46037
|
}
|
|
45868
46038
|
/**
|
|
46039
|
+
* The binding each entry of a `plugins: [...]` list is, as written: `seo` for
|
|
46040
|
+
* `seo` or `seo({ ... })`. Narrower than {@link identifiersInPluginLists},
|
|
46041
|
+
* which also sees the identifiers inside an entry's options.
|
|
46042
|
+
*/
|
|
46043
|
+
function namesInPluginEntries(program, file) {
|
|
46044
|
+
const names = [];
|
|
46045
|
+
walkNodes(program, (node) => {
|
|
46046
|
+
if (!isPluginList(node, file) || node.value.type !== "ArrayExpression") return;
|
|
46047
|
+
for (const element of node.value.elements) if (element?.type === "Identifier") names.push(element.name);
|
|
46048
|
+
else if (element?.type === "CallExpression" && element.callee.type === "Identifier") names.push(element.callee.name);
|
|
46049
|
+
});
|
|
46050
|
+
return names;
|
|
46051
|
+
}
|
|
46052
|
+
/**
|
|
45869
46053
|
* A `plugins: [...]` property, by an identifier or a string key. A `plugins`
|
|
45870
46054
|
* property written any other way, such as a variable holding the list, a call
|
|
45871
46055
|
* or a spread inside it, is
|
|
@@ -45897,16 +46081,15 @@ function isNode(value) {
|
|
|
45897
46081
|
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
45898
46082
|
}
|
|
45899
46083
|
/**
|
|
45900
|
-
* The
|
|
45901
|
-
* finished project as {@link projectHasSearchIndexScript} is: a template
|
|
45902
|
-
* offers more than one config
|
|
45903
|
-
*
|
|
45904
|
-
* no project on disk to ask.
|
|
46084
|
+
* The plugin install steps the scaffolded PROJECT's build needs, read from
|
|
46085
|
+
* the finished project as {@link projectHasSearchIndexScript} is: a template
|
|
46086
|
+
* offers more than one config, so only the file the copy wrote can say. None
|
|
46087
|
+
* when there is no config, or no project on disk to ask.
|
|
45905
46088
|
*/
|
|
45906
|
-
async function
|
|
46089
|
+
async function projectPluginInstalls(targetDir) {
|
|
45907
46090
|
if (targetDir === void 0) return [];
|
|
45908
46091
|
const config = path.join(targetDir, "nextly.config.ts");
|
|
45909
|
-
return await import_lib$1.default.pathExists(config) ?
|
|
46092
|
+
return await import_lib$1.default.pathExists(config) ? pluginInstallSteps(await import_lib$1.default.readFile(config, "utf-8"), config) : [];
|
|
45910
46093
|
}
|
|
45911
46094
|
/**
|
|
45912
46095
|
* Every file under `root` that could carry an import specifier.
|
|
@@ -46367,14 +46550,12 @@ async function replacePlaceholders(dir, placeholders, skip = /* @__PURE__ */ new
|
|
|
46367
46550
|
/**
|
|
46368
46551
|
* Pinned dependency versions for generated projects.
|
|
46369
46552
|
*
|
|
46370
|
-
* `next
|
|
46371
|
-
*
|
|
46372
|
-
*
|
|
46373
|
-
* wide semver ranges that rarely need updating.
|
|
46553
|
+
* `next`, `eslint-config-next`, `react` and `react-dom` are not here: they
|
|
46554
|
+
* come from `supported-versions`, the reviewed release of each, so a project
|
|
46555
|
+
* never declares a Next.js or React no Nextly release was checked against.
|
|
46556
|
+
* The remaining versions use wide semver ranges that rarely need updating.
|
|
46374
46557
|
*/
|
|
46375
46558
|
const PINNED_VERSIONS = {
|
|
46376
|
-
react: "^19.3.1",
|
|
46377
|
-
"react-dom": "^19.3.1",
|
|
46378
46559
|
typescript: "^5",
|
|
46379
46560
|
"@types/node": "^22",
|
|
46380
46561
|
"@types/react": "^19",
|
|
@@ -46392,11 +46573,6 @@ const PINNED_VERSIONS = {
|
|
|
46392
46573
|
*/
|
|
46393
46574
|
const CONFIG_TOOLING_DEV_DEPENDENCIES = [`esbuild@${PINNED_VERSIONS.esbuild}`];
|
|
46394
46575
|
/**
|
|
46395
|
-
* Packages whose latest version is fetched from the npm registry at
|
|
46396
|
-
* runtime so the CLI always scaffolds with the newest release.
|
|
46397
|
-
*/
|
|
46398
|
-
const RUNTIME_RESOLVED_PACKAGES = ["next", "eslint-config-next"];
|
|
46399
|
-
/**
|
|
46400
46576
|
* @nextlyhq packages whose latest version is fetched from npm at runtime.
|
|
46401
46577
|
* This avoids having to republish create-nextly-app every time a
|
|
46402
46578
|
* dependency package is updated.
|
|
@@ -46420,9 +46596,10 @@ const resolvedNextlyVersions = /* @__PURE__ */ new Map();
|
|
|
46420
46596
|
* On any failure (non-OK response, timeout, missing tag, thrown error) it
|
|
46421
46597
|
* returns the requested dist-tag NAME itself (e.g. "alpha") — a valid npm
|
|
46422
46598
|
* install spec — so a project on the alpha line never silently drops to
|
|
46423
|
-
* `latest`.
|
|
46599
|
+
* `latest`. The channel has no default: only the Nextly packages are read
|
|
46600
|
+
* from the registry, always on a tag their caller names.
|
|
46424
46601
|
*/
|
|
46425
|
-
async function fetchLatestVersion(pkg, channel
|
|
46602
|
+
async function fetchLatestVersion(pkg, channel) {
|
|
46426
46603
|
try {
|
|
46427
46604
|
const res = await fetch(`https://registry.npmjs.org/-/package/${encodeURIComponent(pkg)}/dist-tags`, { signal: AbortSignal.timeout(5e3) });
|
|
46428
46605
|
if (!res.ok) return channel;
|
|
@@ -46456,26 +46633,6 @@ async function nextlyVersions(source) {
|
|
|
46456
46633
|
if (source.kind === "release") return Object.fromEntries(NEXTLY_PACKAGES.map((pkg) => [pkg, source.cliVersion]));
|
|
46457
46634
|
return resolveNextlyVersions("alpha");
|
|
46458
46635
|
}
|
|
46459
|
-
/** Cache for runtime-resolved package versions (next, eslint-config-next). */
|
|
46460
|
-
let resolvedRuntimeVersions = null;
|
|
46461
|
-
/**
|
|
46462
|
-
* Resolve latest versions for Next.js ecosystem packages in parallel.
|
|
46463
|
-
* Falls back to a safe semver range if the registry is unreachable.
|
|
46464
|
-
*/
|
|
46465
|
-
async function resolveRuntimeVersions() {
|
|
46466
|
-
if (resolvedRuntimeVersions) return resolvedRuntimeVersions;
|
|
46467
|
-
const NEXT_FLOOR = "^16.4.1";
|
|
46468
|
-
const FALLBACKS = {
|
|
46469
|
-
next: NEXT_FLOOR,
|
|
46470
|
-
"eslint-config-next": NEXT_FLOOR
|
|
46471
|
-
};
|
|
46472
|
-
const entries = await Promise.all(RUNTIME_RESOLVED_PACKAGES.map(async (pkg) => {
|
|
46473
|
-
const version = await fetchLatestVersion(pkg);
|
|
46474
|
-
return [pkg, version === "latest" ? FALLBACKS[pkg] : version];
|
|
46475
|
-
}));
|
|
46476
|
-
resolvedRuntimeVersions = Object.fromEntries(entries);
|
|
46477
|
-
return resolvedRuntimeVersions;
|
|
46478
|
-
}
|
|
46479
46636
|
/**
|
|
46480
46637
|
* Generate a `package.json` string for a fresh Nextly project.
|
|
46481
46638
|
*
|
|
@@ -46494,11 +46651,10 @@ async function resolveRuntimeVersions() {
|
|
|
46494
46651
|
*/
|
|
46495
46652
|
async function generatePackageJson(projectName, database, source = nextlySource({ cliVersion: packageVersion() }), projectType = "blank", templateDirs = [], targetDir) {
|
|
46496
46653
|
if (projectType === "plugin") return generatePluginPackageJson(projectName, source);
|
|
46497
|
-
const runtimeVersions = await resolveRuntimeVersions();
|
|
46498
46654
|
const dependencies = {
|
|
46499
|
-
next:
|
|
46500
|
-
react:
|
|
46501
|
-
"react-dom":
|
|
46655
|
+
next: scaffoldRange("next"),
|
|
46656
|
+
react: scaffoldRange("react"),
|
|
46657
|
+
"react-dom": scaffoldRange("react-dom")
|
|
46502
46658
|
};
|
|
46503
46659
|
dependencies["@tanstack/react-query"] = "^5.62.0";
|
|
46504
46660
|
dependencies["lucide-react"] = "^0.544.0";
|
|
@@ -46522,11 +46678,11 @@ async function generatePackageJson(projectName, database, source = nextlySource(
|
|
|
46522
46678
|
"@tailwindcss/postcss": PINNED_VERSIONS["@tailwindcss/postcss"],
|
|
46523
46679
|
tailwindcss: PINNED_VERSIONS.tailwindcss,
|
|
46524
46680
|
eslint: PINNED_VERSIONS.eslint,
|
|
46525
|
-
"eslint-config-next":
|
|
46681
|
+
"eslint-config-next": scaffoldRange("eslint-config-next"),
|
|
46526
46682
|
esbuild: PINNED_VERSIONS.esbuild
|
|
46527
46683
|
};
|
|
46528
46684
|
const shipsSearchIndex = targetDir ? await projectHasSearchIndexScript(targetDir) : false;
|
|
46529
|
-
const
|
|
46685
|
+
const pluginInstalls = await projectPluginInstalls(targetDir);
|
|
46530
46686
|
if (shipsSearchIndex) devDependencies.pagefind = "^1.1.0";
|
|
46531
46687
|
const pkg = {
|
|
46532
46688
|
name: projectName,
|
|
@@ -46536,7 +46692,7 @@ async function generatePackageJson(projectName, database, source = nextlySource(
|
|
|
46536
46692
|
dev: "next dev --turbopack",
|
|
46537
46693
|
build: [
|
|
46538
46694
|
"nextly migrate",
|
|
46539
|
-
...
|
|
46695
|
+
...pluginInstalls,
|
|
46540
46696
|
"next build",
|
|
46541
46697
|
...shipsSearchIndex ? ["node scripts/build-search-index.mjs"] : []
|
|
46542
46698
|
].join(" && "),
|
|
@@ -46598,7 +46754,6 @@ async function resolvePluginNextlyRange(source) {
|
|
|
46598
46754
|
*/
|
|
46599
46755
|
async function generatePluginPackageJson(projectName, source) {
|
|
46600
46756
|
const versions = await nextlyVersions(source);
|
|
46601
|
-
const runtimeVersions = await resolveRuntimeVersions();
|
|
46602
46757
|
const range = (pkg) => versions[pkg] ?? "alpha";
|
|
46603
46758
|
const peerRange = (pkg) => pluginPeerRange(range(pkg), source.cliVersion);
|
|
46604
46759
|
const peerDependencies = {
|
|
@@ -46606,8 +46761,8 @@ async function generatePluginPackageJson(projectName, source) {
|
|
|
46606
46761
|
"@nextlyhq/admin": peerRange("@nextlyhq/admin"),
|
|
46607
46762
|
"@nextlyhq/plugin-sdk": peerRange("@nextlyhq/plugin-sdk"),
|
|
46608
46763
|
"@nextlyhq/ui": peerRange("@nextlyhq/ui"),
|
|
46609
|
-
react:
|
|
46610
|
-
"react-dom":
|
|
46764
|
+
react: SUPPORTED_RANGES.react,
|
|
46765
|
+
"react-dom": SUPPORTED_RANGES["react-dom"]
|
|
46611
46766
|
};
|
|
46612
46767
|
const devDependencies = {
|
|
46613
46768
|
nextly: range("nextly"),
|
|
@@ -46616,9 +46771,9 @@ async function generatePluginPackageJson(projectName, source) {
|
|
|
46616
46771
|
"@nextlyhq/plugin-sdk": range("@nextlyhq/plugin-sdk"),
|
|
46617
46772
|
"@nextlyhq/adapter-drizzle": range("@nextlyhq/adapter-drizzle"),
|
|
46618
46773
|
"@nextlyhq/adapter-sqlite": range("@nextlyhq/adapter-sqlite"),
|
|
46619
|
-
next:
|
|
46620
|
-
react:
|
|
46621
|
-
"react-dom":
|
|
46774
|
+
next: scaffoldRange("next"),
|
|
46775
|
+
react: scaffoldRange("react"),
|
|
46776
|
+
"react-dom": scaffoldRange("react-dom"),
|
|
46622
46777
|
"better-sqlite3": "^12.0.0",
|
|
46623
46778
|
"@types/node": PINNED_VERSIONS["@types/node"],
|
|
46624
46779
|
"@types/react": PINNED_VERSIONS["@types/react"],
|
|
@@ -46936,6 +47091,143 @@ async function copyPluginTemplate(opts) {
|
|
|
46936
47091
|
await writeScaffoldNpmrc(targetDir, packageManager);
|
|
46937
47092
|
await replacePlaceholders(targetDir, placeholders, alreadyRendered);
|
|
46938
47093
|
}
|
|
47094
|
+
// src/generators/page-extensions.ts
|
|
47095
|
+
/**
|
|
47096
|
+
* The page extension to name a new module file with, a hook or a route: a
|
|
47097
|
+
* TypeScript one first, since Nextly's files are TypeScript, then a
|
|
47098
|
+
* JavaScript one; never a content extension such as `mdx`, which Next.js
|
|
47099
|
+
* would compile as content, and never `cjs`, since the files are ES modules
|
|
47100
|
+
* and a `.cjs` file cannot hold one. Null when the list holds no extension a
|
|
47101
|
+
* module can be written in.
|
|
47102
|
+
*/
|
|
47103
|
+
function moduleExtension(extensions) {
|
|
47104
|
+
const usable = writable(extensions);
|
|
47105
|
+
return usable.find((extension) => /(^|\.)tsx?$/.test(extension)) ?? usable.find((extension) => /(^|\.)(js|jsx|mjs)$/.test(extension)) ?? null;
|
|
47106
|
+
}
|
|
47107
|
+
/**
|
|
47108
|
+
* The extensions a file with code in it can be written under: not a
|
|
47109
|
+
* declaration file's (`d.ts`, `d.mts`, `d.cts`), which TypeScript reads as
|
|
47110
|
+
* types only and refuses code in.
|
|
47111
|
+
*/
|
|
47112
|
+
function writable(extensions) {
|
|
47113
|
+
return extensions.filter((extension) => !/(^|\.)d\.[cm]?tsx?$/.test(extension));
|
|
47114
|
+
}
|
|
47115
|
+
/**
|
|
47116
|
+
* The `pageExtensions` the app's Next.js config sets, as every existing-app
|
|
47117
|
+
* step that names a file Next.js routes reads it: the list; `"absent"` when
|
|
47118
|
+
* the config sets none, or there is no config; or null when it is set in a
|
|
47119
|
+
* way that cannot be read. Read from the config the app exports, as the Next
|
|
47120
|
+
* config step reads it, so a commented-out or nested `pageExtensions` sets
|
|
47121
|
+
* nothing. `configName` is the file it was read from, for the report.
|
|
47122
|
+
*/
|
|
47123
|
+
async function appPageExtensions(cwd) {
|
|
47124
|
+
const nextConfig = await readNextConfig(cwd);
|
|
47125
|
+
return {
|
|
47126
|
+
configName: nextConfig?.name ?? "The Next.js config",
|
|
47127
|
+
extensions: nextConfig ? configStringList(nextConfig.text, nextConfig.name, "pageExtensions") : "absent"
|
|
47128
|
+
};
|
|
47129
|
+
}
|
|
47130
|
+
/**
|
|
47131
|
+
* The text of the app's Next.js config, or null when it has none, from the
|
|
47132
|
+
* file the Next config step reads and extends ({@link findConfigFile}).
|
|
47133
|
+
*/
|
|
47134
|
+
async function readNextConfig(cwd) {
|
|
47135
|
+
const name = await findConfigFile(cwd);
|
|
47136
|
+
return name === null ? null : {
|
|
47137
|
+
name,
|
|
47138
|
+
text: await import_lib$1.default.readFile(path.join(cwd, name), "utf-8")
|
|
47139
|
+
};
|
|
47140
|
+
}
|
|
47141
|
+
/**
|
|
47142
|
+
* The page extension for a file of TypeScript with JSX, such as a page or a
|
|
47143
|
+
* layout: one ending in `tsx`. Null when the list holds none.
|
|
47144
|
+
*/
|
|
47145
|
+
function tsxExtension(extensions) {
|
|
47146
|
+
return writable(extensions).find((extension) => /(^|\.)tsx$/.test(extension)) ?? null;
|
|
47147
|
+
}
|
|
47148
|
+
/** A JavaScript or TypeScript module without JSX, such as a route. */
|
|
47149
|
+
const MODULE_FILE = {
|
|
47150
|
+
pick: moduleExtension,
|
|
47151
|
+
holds: "a JavaScript or TypeScript extension"
|
|
47152
|
+
};
|
|
47153
|
+
/** TypeScript with JSX, such as a page or a layout. */
|
|
47154
|
+
const TSX_FILE = {
|
|
47155
|
+
pick: tsxExtension,
|
|
47156
|
+
holds: "an extension ending in tsx"
|
|
47157
|
+
};
|
|
47158
|
+
/**
|
|
47159
|
+
* The report for a file an app that sets `pageExtensions` gets in place of
|
|
47160
|
+
* the file: the name to create it under and its content. Next.js routes a
|
|
47161
|
+
* file there only under a name with one of the app's extensions, and the app
|
|
47162
|
+
* may already have the file under such a name, which the existing-app steps
|
|
47163
|
+
* do not look for, so nothing is written. A list that cannot be read, or that
|
|
47164
|
+
* holds no extension the content needs, gives a placeholder name and says
|
|
47165
|
+
* which.
|
|
47166
|
+
*
|
|
47167
|
+
* @param base the file's path without its extension, as reported, such as
|
|
47168
|
+
* `app/admin/[[...params]]/page`
|
|
47169
|
+
*/
|
|
47170
|
+
function pageExtensionsSkip(base, text, pages, need) {
|
|
47171
|
+
const { configName, extensions } = pages;
|
|
47172
|
+
const extension = extensions === null ? null : need.pick(extensions);
|
|
47173
|
+
const sets = `${configName} sets pageExtensions, and Next.js routes a file there only under a name with one of them, so none was written`;
|
|
47174
|
+
if (extensions !== null && extension === null) return {
|
|
47175
|
+
path: `${base}.<${need.holds} added to pageExtensions>`,
|
|
47176
|
+
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`,
|
|
47177
|
+
toAdd: text
|
|
47178
|
+
};
|
|
47179
|
+
return {
|
|
47180
|
+
path: `${base}.${extension ?? "<one of your pageExtensions>"}`,
|
|
47181
|
+
why: `${sets}; unless the app already has this file, create it under that name with this content`,
|
|
47182
|
+
toAdd: text
|
|
47183
|
+
};
|
|
47184
|
+
}
|
|
47185
|
+
// src/generators/write-new.ts
|
|
47186
|
+
/**
|
|
47187
|
+
* Writes `text` to `relative` (a project-relative path, written with `/`)
|
|
47188
|
+
* only where nothing is, and records which it was.
|
|
47189
|
+
*
|
|
47190
|
+
* The file is created exclusively (`wx`), so a file the app already has, a
|
|
47191
|
+
* link included, is never replaced, whatever appeared since any earlier
|
|
47192
|
+
* check. One that is there is reported as skipped, with why and, when given,
|
|
47193
|
+
* what to add to it by hand. `wx` guards only the file's own name, so a
|
|
47194
|
+
* folder on its path that is a symbolic link, which would take the write
|
|
47195
|
+
* wherever it points, is found first, as the media routes find it, and
|
|
47196
|
+
* nothing is written through it.
|
|
47197
|
+
*/
|
|
47198
|
+
async function writeNew(cwd, relative, text, files, ifThere) {
|
|
47199
|
+
const file = path.join(cwd, ...relative.split("/"));
|
|
47200
|
+
const link = await linkedFolder(cwd, file);
|
|
47201
|
+
if (link !== null) {
|
|
47202
|
+
const folder = path.relative(cwd, link.folder).split(path.sep).join("/");
|
|
47203
|
+
files.skipped.push({
|
|
47204
|
+
path: relative,
|
|
47205
|
+
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}`,
|
|
47206
|
+
toAdd: ifThere.toAdd
|
|
47207
|
+
});
|
|
47208
|
+
return;
|
|
47209
|
+
}
|
|
47210
|
+
await import_lib$1.default.ensureDir(path.dirname(file));
|
|
47211
|
+
try {
|
|
47212
|
+
await import_lib$1.default.writeFile(file, text, {
|
|
47213
|
+
encoding: "utf-8",
|
|
47214
|
+
flag: "wx"
|
|
47215
|
+
});
|
|
47216
|
+
files.written.push(relative);
|
|
47217
|
+
} catch (error) {
|
|
47218
|
+
if (error.code !== "EEXIST") throw error;
|
|
47219
|
+
files.skipped.push({
|
|
47220
|
+
path: relative,
|
|
47221
|
+
...ifThere
|
|
47222
|
+
});
|
|
47223
|
+
}
|
|
47224
|
+
}
|
|
47225
|
+
/** A step's report before it has done anything. */
|
|
47226
|
+
const nothingYet = () => ({
|
|
47227
|
+
written: [],
|
|
47228
|
+
changed: [],
|
|
47229
|
+
skipped: []
|
|
47230
|
+
});
|
|
46939
47231
|
// src/generators/routes.ts
|
|
46940
47232
|
const API_ROUTE_TEMPLATE = `// Imported from \`nextly/runtime\` (not the package root) so the
|
|
46941
47233
|
// Next.js-coupled handler factory stays out of Node-only code paths
|
|
@@ -47046,8 +47338,31 @@ const UPLOADS_ROUTE = {
|
|
|
47046
47338
|
importsConfig: true,
|
|
47047
47339
|
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`
|
|
47048
47340
|
};
|
|
47049
|
-
/**
|
|
47050
|
-
|
|
47341
|
+
/**
|
|
47342
|
+
* The admin's layout, which `generateAdminPage` writes beside the admin page.
|
|
47343
|
+
* Read with the routes, from the template a new project gets, so an app Nextly
|
|
47344
|
+
* is added to renders its admin the same way: its shell prerendered, and its
|
|
47345
|
+
* request work beneath a Suspense boundary.
|
|
47346
|
+
*/
|
|
47347
|
+
const ADMIN_LAYOUT = {
|
|
47348
|
+
path: "admin/[[...params]]/layout.tsx",
|
|
47349
|
+
importsConfig: true
|
|
47350
|
+
};
|
|
47351
|
+
/**
|
|
47352
|
+
* The admin component map's two generated files, under the app's source root,
|
|
47353
|
+
* as the base template commits them: the admin layout imports the list, and
|
|
47354
|
+
* the admin page imports the map that registers the components. Read with the
|
|
47355
|
+
* routes, so an app Nextly is added to starts from the template's own empty
|
|
47356
|
+
* map rather than a second one written out here, and `next dev` rewrites them
|
|
47357
|
+
* as plugins are added.
|
|
47358
|
+
*/
|
|
47359
|
+
const ADMIN_COMPONENT_MAP_FILES = ["types/generated/plugin-admin-components.generated.ts", "types/generated/plugin-admin-imports.generated.ts"];
|
|
47360
|
+
/** Every file the existing-app path writes from the templates, read together. */
|
|
47361
|
+
const ALL_ROUTES = [
|
|
47362
|
+
...ROUTES,
|
|
47363
|
+
UPLOADS_ROUTE,
|
|
47364
|
+
ADMIN_LAYOUT
|
|
47365
|
+
];
|
|
47051
47366
|
/**
|
|
47052
47367
|
* The import that brings in the Nextly config: the template spells it through
|
|
47053
47368
|
* the `@nextly-config` alias or a path relative to its own layout, neither of
|
|
@@ -47147,6 +47462,15 @@ async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
|
|
|
47147
47462
|
if (route.importsConfig && !CONFIG_IMPORT.test(text)) throw new Error(`The template's ${route.path} does not import the Nextly config, so no route that finds it can be written. Nothing was installed or written.`);
|
|
47148
47463
|
texts.set(route.path, text);
|
|
47149
47464
|
}
|
|
47465
|
+
for (const file of ADMIN_COMPONENT_MAP_FILES) {
|
|
47466
|
+
const relative = `base/src/${file}`;
|
|
47467
|
+
try {
|
|
47468
|
+
texts.set(file, await import_lib$1.default.readFile(path.join(templatesDir, relative), "utf-8"));
|
|
47469
|
+
} catch (error) {
|
|
47470
|
+
const code = error.code ?? "unreadable";
|
|
47471
|
+
throw new Error(`The templates at ${templatesDir} have no readable ${relative} (${code}), so Nextly's admin component map cannot be added. Nothing was installed or written; check the --local-template path if one was given.`);
|
|
47472
|
+
}
|
|
47473
|
+
}
|
|
47150
47474
|
return texts;
|
|
47151
47475
|
}
|
|
47152
47476
|
/**
|
|
@@ -47597,56 +47921,71 @@ function skipped(cwd, left, target, text, role, reason) {
|
|
|
47597
47921
|
function toPosix(p) {
|
|
47598
47922
|
return p.split(path.sep).join("/");
|
|
47599
47923
|
}
|
|
47600
|
-
// src/generators/
|
|
47924
|
+
// src/generators/types.ts
|
|
47601
47925
|
/**
|
|
47602
|
-
*
|
|
47603
|
-
*
|
|
47604
|
-
*
|
|
47605
|
-
* The file is created exclusively (`wx`), so a file the app already has, a
|
|
47606
|
-
* link included, is never replaced, whatever appeared since any earlier
|
|
47607
|
-
* check. One that is there is reported as skipped, with why and, when given,
|
|
47608
|
-
* what to add to it by hand. `wx` guards only the file's own name, so a
|
|
47609
|
-
* folder on its path that is a symbolic link, which would take the write
|
|
47610
|
-
* wherever it points, is found first, as the media routes find it, and
|
|
47611
|
-
* nothing is written through it.
|
|
47926
|
+
* Where the generated types file goes, relative to the project root: under
|
|
47927
|
+
* src/ when the app keeps its code there, at the root when it does not. The
|
|
47928
|
+
* config names this path and the types step creates it, so both read it here.
|
|
47612
47929
|
*/
|
|
47613
|
-
|
|
47614
|
-
|
|
47615
|
-
|
|
47616
|
-
|
|
47617
|
-
|
|
47618
|
-
|
|
47619
|
-
|
|
47620
|
-
|
|
47621
|
-
|
|
47622
|
-
|
|
47623
|
-
|
|
47624
|
-
|
|
47625
|
-
|
|
47626
|
-
|
|
47627
|
-
|
|
47628
|
-
|
|
47629
|
-
|
|
47630
|
-
|
|
47631
|
-
|
|
47632
|
-
|
|
47633
|
-
|
|
47634
|
-
|
|
47635
|
-
|
|
47636
|
-
|
|
47637
|
-
|
|
47930
|
+
function typesOutputFile(projectInfo) {
|
|
47931
|
+
return `${projectInfo.srcDir ? "./src" : "."}/types/generated/nextly-types.ts`;
|
|
47932
|
+
}
|
|
47933
|
+
/**
|
|
47934
|
+
* The folder the generated files go in, relative to the project root, as
|
|
47935
|
+
* {@link typesOutputFile} names it: the admin layout and page import the
|
|
47936
|
+
* component map from here.
|
|
47937
|
+
*/
|
|
47938
|
+
function typesOutputFolder(projectInfo) {
|
|
47939
|
+
return path.posix.dirname(path.posix.normalize(typesOutputFile(projectInfo)));
|
|
47940
|
+
}
|
|
47941
|
+
/**
|
|
47942
|
+
* Create the types directory structure for generated TypeScript types.
|
|
47943
|
+
* This mirrors what `nextly init` does - creates the directory and a placeholder file.
|
|
47944
|
+
* The actual types are generated when running `next dev` (which boots Nextly
|
|
47945
|
+
* and emits types lazily) or `npx nextly generate:types`.
|
|
47946
|
+
*/
|
|
47947
|
+
async function generateTypesDirectory(cwd, projectInfo, templates) {
|
|
47948
|
+
const files = nothingYet();
|
|
47949
|
+
const typesFile = path.posix.normalize(typesOutputFile(projectInfo));
|
|
47950
|
+
const typesDir = typesOutputFolder(projectInfo);
|
|
47951
|
+
await writeNew(cwd, `${typesDir}/.gitkeep`, "", files, { why: "it is already there" });
|
|
47952
|
+
await writeNew(cwd, typesFile, `/**
|
|
47953
|
+
* Nextly Generated Types
|
|
47954
|
+
*
|
|
47955
|
+
* This file will be auto-generated when you run:
|
|
47956
|
+
* next dev (Nextly emits types on first boot)
|
|
47957
|
+
* npx nextly generate:types
|
|
47958
|
+
*
|
|
47959
|
+
* Do not edit this file manually - it will be overwritten.
|
|
47960
|
+
*/
|
|
47961
|
+
|
|
47962
|
+
// Types will be generated here after running next dev
|
|
47963
|
+
export {};
|
|
47964
|
+
`, files, { why: "it is already there; Nextly writes the types into it when they are generated" });
|
|
47965
|
+
for (const file of ADMIN_COMPONENT_MAP_FILES) {
|
|
47966
|
+
const text = templates.get(file);
|
|
47967
|
+
if (text === void 0) throw new Error(`The templates read for this app hold no ${file}, so Nextly's admin component map cannot be written.`);
|
|
47968
|
+
await writeNew(cwd, `${typesDir}/${path.posix.basename(file)}`, text, files, { why: "it is already there; `next dev` and `nextly generate:types` keep it current" });
|
|
47638
47969
|
}
|
|
47970
|
+
return files;
|
|
47639
47971
|
}
|
|
47640
|
-
/** A step's report before it has done anything. */
|
|
47641
|
-
const nothingYet = () => ({
|
|
47642
|
-
written: [],
|
|
47643
|
-
changed: [],
|
|
47644
|
-
skipped: []
|
|
47645
|
-
});
|
|
47646
47972
|
// src/generators/admin.ts
|
|
47647
|
-
|
|
47973
|
+
/**
|
|
47974
|
+
* The generated component map's two files, by the name a module imports them
|
|
47975
|
+
* as: the list the layout reads, and the map the page loads to register the
|
|
47976
|
+
* components.
|
|
47977
|
+
*/
|
|
47978
|
+
const COMPONENT_LIST = "plugin-admin-components.generated";
|
|
47979
|
+
const COMPONENT_MAP = "plugin-admin-imports.generated";
|
|
47980
|
+
/** How the file at `target` imports `name` from the app's generated types folder. */
|
|
47981
|
+
function generatedImport(cwd, target, projectInfo, name) {
|
|
47982
|
+
const relative = path.relative(path.dirname(target), path.join(cwd, typesOutputFolder(projectInfo), name)).split(path.sep).join("/");
|
|
47983
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
47984
|
+
}
|
|
47985
|
+
const adminPageTemplate = (componentMap) => `"use client";
|
|
47648
47986
|
|
|
47649
47987
|
import "@nextlyhq/admin/style.css";
|
|
47988
|
+
import "${componentMap}";
|
|
47650
47989
|
import { RootLayout, QueryProvider, ErrorBoundary } from "@nextlyhq/admin";
|
|
47651
47990
|
|
|
47652
47991
|
export default function AdminPage() {
|
|
@@ -47664,92 +48003,42 @@ export default function AdminPage() {
|
|
|
47664
48003
|
}
|
|
47665
48004
|
`;
|
|
47666
48005
|
/**
|
|
47667
|
-
*
|
|
47668
|
-
*
|
|
47669
|
-
*
|
|
47670
|
-
*
|
|
47671
|
-
*
|
|
48006
|
+
* The admin layout, as the base template holds it, with its config import
|
|
48007
|
+
* pointing at the project's `nextly.config`, and its component-list import at
|
|
48008
|
+
* the app's generated types folder, from where the layout is written. One
|
|
48009
|
+
* source with a new project's layout, so an app Nextly is added to gets the
|
|
48010
|
+
* same per-request admin.
|
|
47672
48011
|
*/
|
|
47673
|
-
function
|
|
47674
|
-
|
|
47675
|
-
|
|
47676
|
-
|
|
47677
|
-
|
|
47678
|
-
const brandingCss = getBrandingCss(config.admin?.branding);
|
|
47679
|
-
|
|
47680
|
-
export default function AdminLayout({
|
|
47681
|
-
children,
|
|
47682
|
-
}: {
|
|
47683
|
-
children: React.ReactNode;
|
|
47684
|
-
}) {
|
|
47685
|
-
return (
|
|
47686
|
-
<>
|
|
47687
|
-
{brandingCss && (
|
|
47688
|
-
<style dangerouslySetInnerHTML={{ __html: brandingCss }} />
|
|
47689
|
-
)}
|
|
47690
|
-
{children}
|
|
47691
|
-
</>
|
|
47692
|
-
);
|
|
47693
|
-
}
|
|
47694
|
-
`;
|
|
48012
|
+
function adminLayout(cwd, adminDir, projectInfo, templates) {
|
|
48013
|
+
const text = templates.get(ADMIN_LAYOUT.path);
|
|
48014
|
+
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.`);
|
|
48015
|
+
const target = path.join(cwd, adminDir, "layout.tsx");
|
|
48016
|
+
return withRelativeConfigImport(text, cwd, target, ADMIN_LAYOUT.path).replace(new RegExp(`"[^"]*/${COMPONENT_LIST.replaceAll(".", "\\.")}"`), `"${generatedImport(cwd, target, projectInfo, COMPONENT_LIST)}"`);
|
|
47695
48017
|
}
|
|
47696
48018
|
/**
|
|
47697
48019
|
* Generate the admin panel page and layout.
|
|
47698
48020
|
* Uses [[...params]] to match the expected route pattern.
|
|
47699
48021
|
*/
|
|
47700
|
-
async function generateAdminPage(cwd, projectInfo) {
|
|
48022
|
+
async function generateAdminPage(cwd, projectInfo, templates) {
|
|
47701
48023
|
const files = nothingYet();
|
|
47702
48024
|
const adminDir = `${projectInfo.appDir}/admin/[[...params]]`;
|
|
47703
|
-
const layout =
|
|
48025
|
+
const layout = adminLayout(cwd, adminDir, projectInfo, templates);
|
|
48026
|
+
const page = adminPageTemplate(generatedImport(cwd, path.join(cwd, adminDir, "page.tsx"), projectInfo, COMPONENT_MAP));
|
|
47704
48027
|
const pages = await appPageExtensions(cwd);
|
|
47705
48028
|
if (pages.extensions !== "absent") {
|
|
47706
48029
|
const { configName, extensions } = pages;
|
|
47707
|
-
for (const [name, text] of [["page",
|
|
48030
|
+
for (const [name, text] of [["page", page], ["layout", layout]]) files.skipped.push(pageExtensionsSkip(`${adminDir}/${name}`, text, {
|
|
47708
48031
|
configName,
|
|
47709
48032
|
extensions
|
|
47710
48033
|
}, TSX_FILE));
|
|
47711
48034
|
return files;
|
|
47712
48035
|
}
|
|
47713
|
-
for (const [name, text] of [["page.tsx",
|
|
48036
|
+
for (const [name, text] of [["page.tsx", page], ["layout.tsx", layout]]) await writeNew(cwd, `${adminDir}/${name}`, text, files, {
|
|
47714
48037
|
why: `the app already has one; Nextly's admin needs its own ${name}, which is`,
|
|
47715
48038
|
toAdd: text
|
|
47716
48039
|
});
|
|
47717
48040
|
return files;
|
|
47718
48041
|
}
|
|
47719
|
-
// src/generators/types.ts
|
|
47720
|
-
/**
|
|
47721
|
-
* Where the generated types file goes, relative to the project root: under
|
|
47722
|
-
* src/ when the app keeps its code there, at the root when it does not. The
|
|
47723
|
-
* config names this path and the types step creates it, so both read it here.
|
|
47724
|
-
*/
|
|
47725
|
-
function typesOutputFile(projectInfo) {
|
|
47726
|
-
return `${projectInfo.srcDir ? "./src" : "."}/types/generated/nextly-types.ts`;
|
|
47727
|
-
}
|
|
47728
|
-
/**
|
|
47729
|
-
* Create the types directory structure for generated TypeScript types.
|
|
47730
|
-
* This mirrors what `nextly init` does - creates the directory and a placeholder file.
|
|
47731
|
-
* The actual types are generated when running `next dev` (which boots Nextly
|
|
47732
|
-
* and emits types lazily) or `npx nextly generate:types`.
|
|
47733
|
-
*/
|
|
47734
|
-
async function generateTypesDirectory(cwd, projectInfo) {
|
|
47735
|
-
const files = nothingYet();
|
|
47736
|
-
const typesFile = path.posix.normalize(typesOutputFile(projectInfo));
|
|
47737
|
-
await writeNew(cwd, `${path.posix.dirname(typesFile)}/.gitkeep`, "", files, { why: "it is already there" });
|
|
47738
|
-
await writeNew(cwd, typesFile, `/**
|
|
47739
|
-
* Nextly Generated Types
|
|
47740
|
-
*
|
|
47741
|
-
* This file will be auto-generated when you run:
|
|
47742
|
-
* next dev (Nextly emits types on first boot)
|
|
47743
|
-
* npx nextly generate:types
|
|
47744
|
-
*
|
|
47745
|
-
* Do not edit this file manually - it will be overwritten.
|
|
47746
|
-
*/
|
|
47747
|
-
|
|
47748
|
-
// Types will be generated here after running next dev
|
|
47749
|
-
export {};
|
|
47750
|
-
`, files, { why: "it is already there; Nextly writes the types into it when they are generated" });
|
|
47751
|
-
return files;
|
|
47752
|
-
}
|
|
47753
48042
|
// src/generators/config.ts
|
|
47754
48043
|
/**
|
|
47755
48044
|
* Base templates for project types that use inline config generation.
|
|
@@ -47830,8 +48119,9 @@ function startNextly(configImport) {
|
|
|
47830
48119
|
const { createRegister } = await import("nextly");
|
|
47831
48120
|
const { default: config } = await import("${configImport}");
|
|
47832
48121
|
// Next.js writes its Cache Components setting into this file as it builds
|
|
47833
|
-
// it. Nextly, loaded from node_modules, cannot read that for itself
|
|
47834
|
-
// warns
|
|
48122
|
+
// it. Nextly, loaded from node_modules, cannot read that for itself. With
|
|
48123
|
+
// it off, Nextly warns at start; with it on, a cached read that finds no
|
|
48124
|
+
// "use cache" read function says so.
|
|
47835
48125
|
await createRegister(config, {
|
|
47836
48126
|
cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
|
|
47837
48127
|
})();`;
|
|
@@ -47929,10 +48219,7 @@ function pageExtensionsAdvice(dir, configImport, configName, extensions) {
|
|
|
47929
48219
|
* never rewritten; it is reported as skipped with what to add. So is the case
|
|
47930
48220
|
* where the app sets \`pageExtensions\`: Next.js then reads the hook only under a
|
|
47931
48221
|
* name with one of those extensions, which this does not guess. The config file
|
|
47932
|
-
* itself is never edited here.
|
|
47933
|
-
* `experimental.instrumentationHook`, which the Next config step gives in the
|
|
47934
|
-
* same `experimental` object as Nextly's packages (`patchNextConfig`), so two
|
|
47935
|
-
* steps never hand the developer two objects for one key.
|
|
48222
|
+
* itself is never edited here.
|
|
47936
48223
|
*/
|
|
47937
48224
|
async function generateInstrumentation(cwd, projectInfo) {
|
|
47938
48225
|
const dir = projectInfo.srcDir ? "src" : "";
|
|
@@ -47980,13 +48267,13 @@ async function generateIntoExistingApp(cwd, projectType, projectInfo, database,
|
|
|
47980
48267
|
return [
|
|
47981
48268
|
await generateConfig(cwd, projectType, projectInfo),
|
|
47982
48269
|
await generateRoutes(cwd, projectInfo),
|
|
47983
|
-
await generateAdminPage(cwd, projectInfo),
|
|
48270
|
+
await generateAdminPage(cwd, projectInfo, templates),
|
|
47984
48271
|
await generateMediaRoutes(cwd, projectInfo, templates),
|
|
47985
48272
|
await generateUploadsRoute(cwd, projectInfo, templates),
|
|
47986
48273
|
await generateInstrumentation(cwd, projectInfo),
|
|
47987
|
-
await generateTypesDirectory(cwd, projectInfo),
|
|
48274
|
+
await generateTypesDirectory(cwd, projectInfo, templates),
|
|
47988
48275
|
nextConfigFiles(await patchNextConfig(cwd, database)),
|
|
47989
|
-
cacheComponentsFiles(await
|
|
48276
|
+
cacheComponentsFiles(await turnOnCacheComponents(cwd))
|
|
47990
48277
|
];
|
|
47991
48278
|
}
|
|
47992
48279
|
// src/generators/report.ts
|
|
@@ -50643,7 +50930,7 @@ var hr = Symbol("entry");
|
|
|
50643
50930
|
var cs = Symbol("entryOpt");
|
|
50644
50931
|
var ui = Symbol("writeEntryClass");
|
|
50645
50932
|
var lr = Symbol("write");
|
|
50646
|
-
var fs$
|
|
50933
|
+
var fs$7 = Symbol("ondrain");
|
|
50647
50934
|
var wt = class extends A {
|
|
50648
50935
|
sync = !1;
|
|
50649
50936
|
opt;
|
|
@@ -50677,8 +50964,8 @@ var wt = class extends A {
|
|
|
50677
50964
|
if ((t.gzip ? 1 : 0) + (t.brotli ? 1 : 0) + (t.zstd ? 1 : 0) > 1) throw new TypeError("gzip, brotli, zstd are mutually exclusive");
|
|
50678
50965
|
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");
|
|
50679
50966
|
let e = this.zip;
|
|
50680
|
-
e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$
|
|
50681
|
-
} else this.on("drain", this[fs$
|
|
50967
|
+
e.on("data", (i) => super.write(i)), e.on("end", () => super.end()), e.on("drain", () => this[fs$7]()), this.on("resume", () => e.resume());
|
|
50968
|
+
} else this.on("drain", this[fs$7]);
|
|
50682
50969
|
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;
|
|
50683
50970
|
}
|
|
50684
50971
|
[lr](t) {
|
|
@@ -50806,7 +51093,7 @@ var wt = class extends A {
|
|
|
50806
51093
|
this.emit("error", e);
|
|
50807
51094
|
}
|
|
50808
51095
|
}
|
|
50809
|
-
[fs$
|
|
51096
|
+
[fs$7]() {
|
|
50810
51097
|
this[Et] && this[Et].entry && this[Et].entry.resume();
|
|
50811
51098
|
}
|
|
50812
51099
|
[di](t) {
|
|
@@ -51873,6 +52160,126 @@ async function cleanupDownload(source) {
|
|
|
51873
52160
|
await import_lib$1.default.remove(extractionRoot).catch(() => {});
|
|
51874
52161
|
}
|
|
51875
52162
|
}
|
|
52163
|
+
// src/lib/framework-versions.ts
|
|
52164
|
+
/**
|
|
52165
|
+
* Whether an existing app's Next.js, React and React DOM are ones this
|
|
52166
|
+
* release of Nextly supports, and what to change when they are not.
|
|
52167
|
+
*
|
|
52168
|
+
* Nextly is reviewed against one Next.js major and one React major
|
|
52169
|
+
* (`SUPPORTED_RANGES`). An app below the floor or on a newer major would
|
|
52170
|
+
* install Nextly and then fail at build or at run time, so adding Nextly to
|
|
52171
|
+
* it stops before anything is written, saying what was found and the way on.
|
|
52172
|
+
*/
|
|
52173
|
+
/** The first spec the app's package.json gives `name`, in field order. */
|
|
52174
|
+
function declaredSpec(manifest, name) {
|
|
52175
|
+
const fields = ["optionalDependencies", ...DEPENDENCY_FIELDS$1.filter((field) => field !== "optionalDependencies")];
|
|
52176
|
+
for (const field of fields) {
|
|
52177
|
+
const deps = manifest[field];
|
|
52178
|
+
if (!deps || typeof deps !== "object") continue;
|
|
52179
|
+
const spec = deps[name];
|
|
52180
|
+
if (typeof spec === "string") return spec;
|
|
52181
|
+
}
|
|
52182
|
+
}
|
|
52183
|
+
/**
|
|
52184
|
+
* The version an app is headed for, the one judged against the range: the
|
|
52185
|
+
* installed copy, unless the app's declaration points past it.
|
|
52186
|
+
*/
|
|
52187
|
+
function judgedVersion(installed, declared, supported) {
|
|
52188
|
+
const lowestDeclared = declared !== void 0 ? lowestVersionIn(declared) : void 0;
|
|
52189
|
+
const floor = lowestVersionIn(supported);
|
|
52190
|
+
if (floor === void 0) return installed ?? lowestDeclared;
|
|
52191
|
+
if (lowestDeclared !== void 0 && compareVersions(lowestDeclared, floor) > 0 && !satisfiesRange(lowestDeclared, supported)) return lowestDeclared;
|
|
52192
|
+
const highest = declared !== void 0 ? highestMajorIn(declared) : void 0;
|
|
52193
|
+
if (installed === void 0 && highest !== void 0 && highest > major(floor)) return `${major(floor) + 1}.0.0`;
|
|
52194
|
+
return installed ?? lowestDeclared;
|
|
52195
|
+
}
|
|
52196
|
+
/**
|
|
52197
|
+
* Too new only on a later major than the range covers; anything else outside
|
|
52198
|
+
* it (below the floor, or a prerelease within the major) needs a supported
|
|
52199
|
+
* release of the same major. A range without a floor leaves nothing to
|
|
52200
|
+
* compare, and the version is then reported as too new.
|
|
52201
|
+
*/
|
|
52202
|
+
function directionOf(judged, supported) {
|
|
52203
|
+
const floor = lowestVersionIn(supported);
|
|
52204
|
+
return floor === void 0 || major(judged) > major(floor) ? "too-new" : "too-old";
|
|
52205
|
+
}
|
|
52206
|
+
/**
|
|
52207
|
+
* Every supported framework package the app at `cwd` has outside its range.
|
|
52208
|
+
*
|
|
52209
|
+
* The version judged is the one the app is headed for (`judgedVersion`): the
|
|
52210
|
+
* installed copy, read from the `node_modules` Node would load it from,
|
|
52211
|
+
* unless the app's package.json declaration points past it; with none
|
|
52212
|
+
* installed, what an install of that declaration could resolve to. A package
|
|
52213
|
+
* the app neither has installed nor declares in a form that names a version
|
|
52214
|
+
* (a dist-tag, `workspace:*`) is not judged.
|
|
52215
|
+
*/
|
|
52216
|
+
async function frameworkVersionProblems(cwd) {
|
|
52217
|
+
const manifest = await import_lib$1.default.readJson(path.join(cwd, "package.json")).catch(() => ({}));
|
|
52218
|
+
const problems = [];
|
|
52219
|
+
for (const name of Object.keys(SUPPORTED_RANGES)) {
|
|
52220
|
+
const supported = SUPPORTED_RANGES[name];
|
|
52221
|
+
const installed = await installedVersion(cwd, name);
|
|
52222
|
+
const declared = declaredSpec(manifest, name);
|
|
52223
|
+
const judged = judgedVersion(installed, declared, supported);
|
|
52224
|
+
if (judged === void 0 || satisfiesRange(judged, supported)) continue;
|
|
52225
|
+
const direction = directionOf(judged, supported);
|
|
52226
|
+
problems.push({
|
|
52227
|
+
name,
|
|
52228
|
+
installed,
|
|
52229
|
+
declared,
|
|
52230
|
+
supported,
|
|
52231
|
+
judged,
|
|
52232
|
+
direction
|
|
52233
|
+
});
|
|
52234
|
+
}
|
|
52235
|
+
return problems;
|
|
52236
|
+
}
|
|
52237
|
+
const LABEL = {
|
|
52238
|
+
next: "Next.js",
|
|
52239
|
+
react: "React",
|
|
52240
|
+
"react-dom": "React"
|
|
52241
|
+
};
|
|
52242
|
+
const major = (version) => Number(version.split(".")[0]);
|
|
52243
|
+
/** What `fix` reads from a problem: the supported major and the version found. */
|
|
52244
|
+
function majorsOf(problem) {
|
|
52245
|
+
const floor = lowestVersionIn(problem.supported) ?? problem.supported;
|
|
52246
|
+
return {
|
|
52247
|
+
supportedMajor: major(floor),
|
|
52248
|
+
foundMajor: major(problem.judged)
|
|
52249
|
+
};
|
|
52250
|
+
}
|
|
52251
|
+
/** A version past the supported major: install that major, or wait for Nextly. */
|
|
52252
|
+
function tooNewFix(problem, add) {
|
|
52253
|
+
const { supportedMajor, foundMajor } = majorsOf(problem);
|
|
52254
|
+
const label = LABEL[problem.name];
|
|
52255
|
+
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}`}`;
|
|
52256
|
+
}
|
|
52257
|
+
/** A version below the supported floor: the codemod across a Next.js major, an install otherwise. */
|
|
52258
|
+
function tooOldFix(problem, add) {
|
|
52259
|
+
const { supportedMajor, foundMajor } = majorsOf(problem);
|
|
52260
|
+
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}`;
|
|
52261
|
+
const install = problem.name === "next" ? `next@${problem.supported}` : `react@${SUPPORTED_RANGES.react} react-dom@${SUPPORTED_RANGES["react-dom"]}`;
|
|
52262
|
+
return `Upgrade ${LABEL[problem.name]} to ${problem.supported}:\n ${add} ${install}`;
|
|
52263
|
+
}
|
|
52264
|
+
/** The one step that brings `problem` into range, as a sentence and a command. */
|
|
52265
|
+
function fix(problem, pm) {
|
|
52266
|
+
const add = ADD_COMMAND[pm];
|
|
52267
|
+
return problem.direction === "too-new" ? tooNewFix(problem, add) : tooOldFix(problem, add);
|
|
52268
|
+
}
|
|
52269
|
+
/**
|
|
52270
|
+
* Why the run stopped: each package found, the range it falls outside, and
|
|
52271
|
+
* the change that brings it in. React and React DOM move together, so a
|
|
52272
|
+
* problem with both gives one command.
|
|
52273
|
+
*/
|
|
52274
|
+
function frameworkVersionMessage(problems, pm) {
|
|
52275
|
+
const lines = problems.map(({ name, installed, declared, supported }) => {
|
|
52276
|
+
return ` ${name}: ${[installed !== void 0 ? `installed ${installed}` : void 0, declared !== void 0 ? `declared ${declared}` : void 0].filter(Boolean).join(", ")}; supported ${supported}`;
|
|
52277
|
+
});
|
|
52278
|
+
const fixes = [...new Set(problems.map((problem) => fix(problem, pm)))];
|
|
52279
|
+
return `This app's Next.js or React is outside what this release of Nextly supports:
|
|
52280
|
+
${lines.join("\n")}\n\nNothing was changed. Change the app as follows, then run create-nextly-app again:
|
|
52281
|
+
` + fixes.map((line) => ` ${line}`).join("\n");
|
|
52282
|
+
}
|
|
51876
52283
|
// src/prompts/approach.ts
|
|
51877
52284
|
/**
|
|
51878
52285
|
* Labels shown in the CLI select prompt for each approach.
|
|
@@ -52313,6 +52720,16 @@ async function createNextly(options = {}) {
|
|
|
52313
52720
|
const existingProject = options.projectType !== "plugin" && await isExistingNextProject(cwd);
|
|
52314
52721
|
if (existingProject) {
|
|
52315
52722
|
R.success(`${import_picocolors.default.green("Next.js")} project detected`);
|
|
52723
|
+
const frameworkProblems = await frameworkVersionProblems(cwd);
|
|
52724
|
+
if (frameworkProblems.length > 0) {
|
|
52725
|
+
capture("scaffold_failed", {
|
|
52726
|
+
stage: "detect",
|
|
52727
|
+
error_code: "unknown",
|
|
52728
|
+
duration_ms: Date.now() - overallStartedAt
|
|
52729
|
+
});
|
|
52730
|
+
Nt$1(frameworkVersionMessage(frameworkProblems, await detectAppPackageManager(cwd)));
|
|
52731
|
+
process.exit(1);
|
|
52732
|
+
}
|
|
52316
52733
|
if (!defaults) {
|
|
52317
52734
|
const shouldInstall = await Rt$1({
|
|
52318
52735
|
message: "Install Nextly in this project?",
|
|
@@ -52655,4 +53072,4 @@ async function createNextly(options = {}) {
|
|
|
52655
53072
|
}
|
|
52656
53073
|
export { getAvailableTemplateNames as a, init as c, __require as d, __toESM$1 as f, validateTemplateName as i, shutdown as l, resolveProjectArg as n, packageVersion as o, validateProjectName as r, capture as s, createNextly as t, __commonJSMin$1 as u };
|
|
52657
53074
|
|
|
52658
|
-
//# sourceMappingURL=create-nextly-
|
|
53075
|
+
//# sourceMappingURL=create-nextly-ChHdzG83.mjs.map
|