create-nextly-app 0.0.2-alpha.73 → 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.
@@ -45986,41 +45986,56 @@ const SEARCH_INDEX_SCRIPT = path.default.join("scripts", "build-search-index.mjs
45986
45986
  async function projectHasSearchIndexScript(targetDir) {
45987
45987
  return import_lib$1.default.pathExists(path.default.join(targetDir, SEARCH_INDEX_SCRIPT));
45988
45988
  }
45989
- /**
45990
- * The plugins a Nextly config registers, by package name: each binding it
45991
- * imports from a `@nextlyhq/plugin-*` package (the SDK aside) that a
45992
- * `plugins: [...]` list in it names, however deep in an element. Read with
45993
- * the parser the Next config step uses, so nested brackets, either quote and
45994
- * default or aliased imports are read as the compiler reads them. First-party
45995
- * plugins register under their package name, which is what
45996
- * `nextly plugins install` takes. Throws for a config that does not parse,
45997
- * since every template's must.
45998
- *
45999
- * Exported for the test that holds every template's config to its build.
46000
- */
46001
- function configuredPlugins(configText, file = "nextly.config.ts") {
46002
- let program;
45989
+ /** Parses a config, naming the file when it does not parse. */
45990
+ function parseConfigProgram(configText, file) {
46003
45991
  try {
46004
- program = (0, import_lib.parse)(configText, {
45992
+ return (0, import_lib.parse)(configText, {
46005
45993
  sourceType: "module",
46006
45994
  plugins: ["typescript", "jsx"]
46007
45995
  }).program;
46008
45996
  } catch (error) {
46009
45997
  throw new Error(`${file} cannot be parsed (${error.message}), so the plugins its build must install cannot be read.`);
46010
45998
  }
46011
- const fromPackage = /* @__PURE__ */ new Map();
45999
+ }
46000
+ /** Each binding imported from a module `matches` accepts, to that module's name. */
46001
+ function bindingsImportedFrom(program, matches) {
46002
+ const bindings = /* @__PURE__ */ new Map();
46012
46003
  for (const statement of program.body) {
46013
46004
  if (statement.type !== "ImportDeclaration") continue;
46014
- const pkg = statement.source.value;
46015
- if (!pkg.startsWith("@nextlyhq/plugin-") || pkg === "@nextlyhq/plugin-sdk") continue;
46016
- for (const specifier of statement.specifiers) fromPackage.set(specifier.local.name, pkg);
46005
+ if (!matches(statement.source.value)) continue;
46006
+ for (const specifier of statement.specifiers) bindings.set(specifier.local.name, statement.source.value);
46017
46007
  }
46018
- const found = [];
46008
+ return bindings;
46009
+ }
46010
+ const isPluginPackage = (source) => source.startsWith("@nextlyhq/plugin-") && source !== "@nextlyhq/plugin-sdk";
46011
+ /** What a config's `plugins` lists hold: the packages, and whether a binding from its own source is among them. */
46012
+ function readPluginLists(configText, file) {
46013
+ const program = parseConfigProgram(configText, file);
46014
+ const fromPackage = bindingsImportedFrom(program, isPluginPackage);
46015
+ const packages = /* @__PURE__ */ new Set();
46019
46016
  for (const name of identifiersInPluginLists(program, file)) {
46020
46017
  const pkg = fromPackage.get(name);
46021
- if (pkg !== void 0 && !found.includes(pkg)) found.push(pkg);
46018
+ if (pkg !== void 0) packages.add(pkg);
46022
46019
  }
46023
- return found;
46020
+ const local = bindingsImportedFrom(program, (source) => source.startsWith("."));
46021
+ const ownPlugin = namesInPluginEntries(program, file).some((name) => local.has(name) && !fromPackage.has(name));
46022
+ return {
46023
+ packages: [...packages],
46024
+ ownPlugin
46025
+ };
46026
+ }
46027
+ /**
46028
+ * The `nextly plugins install` steps a project's build runs, read from its
46029
+ * config. A plugin package is installed by name, and the build says which. A
46030
+ * plugin the site wrote itself has no package name to give, and a production
46031
+ * boot refuses it like any other when nothing has installed it, so a config
46032
+ * that lists one installs with `--all`, which reads the names from the config.
46033
+ * Exported for the test that holds every template's config to its build.
46034
+ */
46035
+ function pluginInstallSteps(configText, file = "nextly.config.ts") {
46036
+ const { packages, ownPlugin } = readPluginLists(configText, file);
46037
+ if (ownPlugin) return ["nextly plugins install --all"];
46038
+ return packages.map((pkg) => `nextly plugins install ${pkg}`);
46024
46039
  }
46025
46040
  /** Every identifier inside the value of each `plugins: [...]` property, in source order. */
46026
46041
  function identifiersInPluginLists(program, file) {
@@ -46034,6 +46049,20 @@ function identifiersInPluginLists(program, file) {
46034
46049
  return names;
46035
46050
  }
46036
46051
  /**
46052
+ * The binding each entry of a `plugins: [...]` list is, as written: `seo` for
46053
+ * `seo` or `seo({ ... })`. Narrower than {@link identifiersInPluginLists},
46054
+ * which also sees the identifiers inside an entry's options.
46055
+ */
46056
+ function namesInPluginEntries(program, file) {
46057
+ const names = [];
46058
+ walkNodes(program, (node) => {
46059
+ if (!isPluginList(node, file) || node.value.type !== "ArrayExpression") return;
46060
+ for (const element of node.value.elements) if (element?.type === "Identifier") names.push(element.name);
46061
+ else if (element?.type === "CallExpression" && element.callee.type === "Identifier") names.push(element.callee.name);
46062
+ });
46063
+ return names;
46064
+ }
46065
+ /**
46037
46066
  * A `plugins: [...]` property, by an identifier or a string key. A `plugins`
46038
46067
  * property written any other way, such as a variable holding the list, a call
46039
46068
  * or a spread inside it, is
@@ -46065,16 +46094,15 @@ function isNode(value) {
46065
46094
  return typeof value === "object" && value !== null && typeof value.type === "string";
46066
46095
  }
46067
46096
  /**
46068
- * The plugins the scaffolded PROJECT's config registers, read from the
46069
- * finished project as {@link projectHasSearchIndexScript} is: a template
46070
- * offers more than one config (the blog's visual one registers none), so
46071
- * only the file the copy wrote can say. None when there is no config, or
46072
- * no project on disk to ask.
46097
+ * The plugin install steps the scaffolded PROJECT's build needs, read from
46098
+ * the finished project as {@link projectHasSearchIndexScript} is: a template
46099
+ * offers more than one config, so only the file the copy wrote can say. None
46100
+ * when there is no config, or no project on disk to ask.
46073
46101
  */
46074
- async function projectPlugins(targetDir) {
46102
+ async function projectPluginInstalls(targetDir) {
46075
46103
  if (targetDir === void 0) return [];
46076
46104
  const config = path.default.join(targetDir, "nextly.config.ts");
46077
- return await import_lib$1.default.pathExists(config) ? configuredPlugins(await import_lib$1.default.readFile(config, "utf-8"), config) : [];
46105
+ return await import_lib$1.default.pathExists(config) ? pluginInstallSteps(await import_lib$1.default.readFile(config, "utf-8"), config) : [];
46078
46106
  }
46079
46107
  /**
46080
46108
  * Every file under `root` that could carry an import specifier.
@@ -46667,7 +46695,7 @@ async function generatePackageJson(projectName, database, source = nextlySource(
46667
46695
  esbuild: PINNED_VERSIONS.esbuild
46668
46696
  };
46669
46697
  const shipsSearchIndex = targetDir ? await projectHasSearchIndexScript(targetDir) : false;
46670
- const plugins = await projectPlugins(targetDir);
46698
+ const pluginInstalls = await projectPluginInstalls(targetDir);
46671
46699
  if (shipsSearchIndex) devDependencies.pagefind = "^1.1.0";
46672
46700
  const pkg = {
46673
46701
  name: projectName,
@@ -46677,7 +46705,7 @@ async function generatePackageJson(projectName, database, source = nextlySource(
46677
46705
  dev: "next dev --turbopack",
46678
46706
  build: [
46679
46707
  "nextly migrate",
46680
- ...plugins.map((plugin) => `nextly plugins install ${plugin}`),
46708
+ ...pluginInstalls,
46681
46709
  "next build",
46682
46710
  ...shipsSearchIndex ? ["node scripts/build-search-index.mjs"] : []
46683
46711
  ].join(" && "),
@@ -47333,6 +47361,15 @@ const ADMIN_LAYOUT = {
47333
47361
  path: "admin/[[...params]]/layout.tsx",
47334
47362
  importsConfig: true
47335
47363
  };
47364
+ /**
47365
+ * The admin component map's two generated files, under the app's source root,
47366
+ * as the base template commits them: the admin layout imports the list, and
47367
+ * the admin page imports the map that registers the components. Read with the
47368
+ * routes, so an app Nextly is added to starts from the template's own empty
47369
+ * map rather than a second one written out here, and `next dev` rewrites them
47370
+ * as plugins are added.
47371
+ */
47372
+ const ADMIN_COMPONENT_MAP_FILES = ["types/generated/plugin-admin-components.generated.ts", "types/generated/plugin-admin-imports.generated.ts"];
47336
47373
  /** Every file the existing-app path writes from the templates, read together. */
47337
47374
  const ALL_ROUTES = [
47338
47375
  ...ROUTES,
@@ -47438,6 +47475,15 @@ async function readMediaRouteTemplates(templatesDir = resolveTemplatePath()) {
47438
47475
  if (route.importsConfig && !CONFIG_IMPORT.test(text)) throw new Error(`The template's ${route.path} does not import the Nextly config, so no route that finds it can be written. Nothing was installed or written.`);
47439
47476
  texts.set(route.path, text);
47440
47477
  }
47478
+ for (const file of ADMIN_COMPONENT_MAP_FILES) {
47479
+ const relative = `base/src/${file}`;
47480
+ try {
47481
+ texts.set(file, await import_lib$1.default.readFile(path.default.join(templatesDir, relative), "utf-8"));
47482
+ } catch (error) {
47483
+ const code = error.code ?? "unreadable";
47484
+ throw new Error(`The templates at ${templatesDir} have no readable ${relative} (${code}), so Nextly's admin component map cannot be added. Nothing was installed or written; check the --local-template path if one was given.`);
47485
+ }
47486
+ }
47441
47487
  return texts;
47442
47488
  }
47443
47489
  /**
@@ -47888,10 +47934,71 @@ function skipped(cwd, left, target, text, role, reason) {
47888
47934
  function toPosix(p) {
47889
47935
  return p.split(path.default.sep).join("/");
47890
47936
  }
47937
+ // src/generators/types.ts
47938
+ /**
47939
+ * Where the generated types file goes, relative to the project root: under
47940
+ * src/ when the app keeps its code there, at the root when it does not. The
47941
+ * config names this path and the types step creates it, so both read it here.
47942
+ */
47943
+ function typesOutputFile(projectInfo) {
47944
+ return `${projectInfo.srcDir ? "./src" : "."}/types/generated/nextly-types.ts`;
47945
+ }
47946
+ /**
47947
+ * The folder the generated files go in, relative to the project root, as
47948
+ * {@link typesOutputFile} names it: the admin layout and page import the
47949
+ * component map from here.
47950
+ */
47951
+ function typesOutputFolder(projectInfo) {
47952
+ return path.default.posix.dirname(path.default.posix.normalize(typesOutputFile(projectInfo)));
47953
+ }
47954
+ /**
47955
+ * Create the types directory structure for generated TypeScript types.
47956
+ * This mirrors what `nextly init` does - creates the directory and a placeholder file.
47957
+ * The actual types are generated when running `next dev` (which boots Nextly
47958
+ * and emits types lazily) or `npx nextly generate:types`.
47959
+ */
47960
+ async function generateTypesDirectory(cwd, projectInfo, templates) {
47961
+ const files = nothingYet();
47962
+ const typesFile = path.default.posix.normalize(typesOutputFile(projectInfo));
47963
+ const typesDir = typesOutputFolder(projectInfo);
47964
+ await writeNew(cwd, `${typesDir}/.gitkeep`, "", files, { why: "it is already there" });
47965
+ await writeNew(cwd, typesFile, `/**
47966
+ * Nextly Generated Types
47967
+ *
47968
+ * This file will be auto-generated when you run:
47969
+ * next dev (Nextly emits types on first boot)
47970
+ * npx nextly generate:types
47971
+ *
47972
+ * Do not edit this file manually - it will be overwritten.
47973
+ */
47974
+
47975
+ // Types will be generated here after running next dev
47976
+ export {};
47977
+ `, files, { why: "it is already there; Nextly writes the types into it when they are generated" });
47978
+ for (const file of ADMIN_COMPONENT_MAP_FILES) {
47979
+ const text = templates.get(file);
47980
+ if (text === void 0) throw new Error(`The templates read for this app hold no ${file}, so Nextly's admin component map cannot be written.`);
47981
+ await writeNew(cwd, `${typesDir}/${path.default.posix.basename(file)}`, text, files, { why: "it is already there; `next dev` and `nextly generate:types` keep it current" });
47982
+ }
47983
+ return files;
47984
+ }
47891
47985
  // src/generators/admin.ts
47892
- const ADMIN_PAGE_TEMPLATE = `"use client";
47986
+ /**
47987
+ * The generated component map's two files, by the name a module imports them
47988
+ * as: the list the layout reads, and the map the page loads to register the
47989
+ * components.
47990
+ */
47991
+ const COMPONENT_LIST = "plugin-admin-components.generated";
47992
+ const COMPONENT_MAP = "plugin-admin-imports.generated";
47993
+ /** How the file at `target` imports `name` from the app's generated types folder. */
47994
+ function generatedImport(cwd, target, projectInfo, name) {
47995
+ const relative = path.default.relative(path.default.dirname(target), path.default.join(cwd, typesOutputFolder(projectInfo), name)).split(path.default.sep).join("/");
47996
+ return relative.startsWith(".") ? relative : `./${relative}`;
47997
+ }
47998
+ const adminPageTemplate = (componentMap) => `"use client";
47893
47999
 
47894
48000
  import "@nextlyhq/admin/style.css";
48001
+ import "${componentMap}";
47895
48002
  import { RootLayout, QueryProvider, ErrorBoundary } from "@nextlyhq/admin";
47896
48003
 
47897
48004
  export default function AdminPage() {
@@ -47910,14 +48017,16 @@ export default function AdminPage() {
47910
48017
  `;
47911
48018
  /**
47912
48019
  * The admin layout, as the base template holds it, with its config import
47913
- * pointing at the project's `nextly.config` from where the layout is written.
47914
- * One source with a new project's layout, so an app Nextly is added to gets
47915
- * the same per-request admin.
48020
+ * pointing at the project's `nextly.config`, and its component-list import at
48021
+ * the app's generated types folder, from where the layout is written. One
48022
+ * source with a new project's layout, so an app Nextly is added to gets the
48023
+ * same per-request admin.
47916
48024
  */
47917
- function adminLayout(cwd, adminDir, templates) {
48025
+ function adminLayout(cwd, adminDir, projectInfo, templates) {
47918
48026
  const text = templates.get(ADMIN_LAYOUT.path);
47919
48027
  if (text === void 0) throw new Error(`The templates read for this app hold no ${ADMIN_LAYOUT.path}, so Nextly's admin layout cannot be written.`);
47920
- return withRelativeConfigImport(text, cwd, path.default.join(cwd, adminDir, "layout.tsx"), ADMIN_LAYOUT.path);
48028
+ const target = path.default.join(cwd, adminDir, "layout.tsx");
48029
+ return withRelativeConfigImport(text, cwd, target, ADMIN_LAYOUT.path).replace(new RegExp(`"[^"]*/${COMPONENT_LIST.replaceAll(".", "\\.")}"`), `"${generatedImport(cwd, target, projectInfo, COMPONENT_LIST)}"`);
47921
48030
  }
47922
48031
  /**
47923
48032
  * Generate the admin panel page and layout.
@@ -47926,56 +48035,23 @@ function adminLayout(cwd, adminDir, templates) {
47926
48035
  async function generateAdminPage(cwd, projectInfo, templates) {
47927
48036
  const files = nothingYet();
47928
48037
  const adminDir = `${projectInfo.appDir}/admin/[[...params]]`;
47929
- const layout = adminLayout(cwd, adminDir, templates);
48038
+ const layout = adminLayout(cwd, adminDir, projectInfo, templates);
48039
+ const page = adminPageTemplate(generatedImport(cwd, path.default.join(cwd, adminDir, "page.tsx"), projectInfo, COMPONENT_MAP));
47930
48040
  const pages = await appPageExtensions(cwd);
47931
48041
  if (pages.extensions !== "absent") {
47932
48042
  const { configName, extensions } = pages;
47933
- for (const [name, text] of [["page", ADMIN_PAGE_TEMPLATE], ["layout", layout]]) files.skipped.push(pageExtensionsSkip(`${adminDir}/${name}`, text, {
48043
+ for (const [name, text] of [["page", page], ["layout", layout]]) files.skipped.push(pageExtensionsSkip(`${adminDir}/${name}`, text, {
47934
48044
  configName,
47935
48045
  extensions
47936
48046
  }, TSX_FILE));
47937
48047
  return files;
47938
48048
  }
47939
- for (const [name, text] of [["page.tsx", ADMIN_PAGE_TEMPLATE], ["layout.tsx", layout]]) await writeNew(cwd, `${adminDir}/${name}`, text, files, {
48049
+ for (const [name, text] of [["page.tsx", page], ["layout.tsx", layout]]) await writeNew(cwd, `${adminDir}/${name}`, text, files, {
47940
48050
  why: `the app already has one; Nextly's admin needs its own ${name}, which is`,
47941
48051
  toAdd: text
47942
48052
  });
47943
48053
  return files;
47944
48054
  }
47945
- // src/generators/types.ts
47946
- /**
47947
- * Where the generated types file goes, relative to the project root: under
47948
- * src/ when the app keeps its code there, at the root when it does not. The
47949
- * config names this path and the types step creates it, so both read it here.
47950
- */
47951
- function typesOutputFile(projectInfo) {
47952
- return `${projectInfo.srcDir ? "./src" : "."}/types/generated/nextly-types.ts`;
47953
- }
47954
- /**
47955
- * Create the types directory structure for generated TypeScript types.
47956
- * This mirrors what `nextly init` does - creates the directory and a placeholder file.
47957
- * The actual types are generated when running `next dev` (which boots Nextly
47958
- * and emits types lazily) or `npx nextly generate:types`.
47959
- */
47960
- async function generateTypesDirectory(cwd, projectInfo) {
47961
- const files = nothingYet();
47962
- const typesFile = path.default.posix.normalize(typesOutputFile(projectInfo));
47963
- await writeNew(cwd, `${path.default.posix.dirname(typesFile)}/.gitkeep`, "", files, { why: "it is already there" });
47964
- await writeNew(cwd, typesFile, `/**
47965
- * Nextly Generated Types
47966
- *
47967
- * This file will be auto-generated when you run:
47968
- * next dev (Nextly emits types on first boot)
47969
- * npx nextly generate:types
47970
- *
47971
- * Do not edit this file manually - it will be overwritten.
47972
- */
47973
-
47974
- // Types will be generated here after running next dev
47975
- export {};
47976
- `, files, { why: "it is already there; Nextly writes the types into it when they are generated" });
47977
- return files;
47978
- }
47979
48055
  // src/generators/config.ts
47980
48056
  /**
47981
48057
  * Base templates for project types that use inline config generation.
@@ -48208,7 +48284,7 @@ async function generateIntoExistingApp(cwd, projectType, projectInfo, database,
48208
48284
  await generateMediaRoutes(cwd, projectInfo, templates),
48209
48285
  await generateUploadsRoute(cwd, projectInfo, templates),
48210
48286
  await generateInstrumentation(cwd, projectInfo),
48211
- await generateTypesDirectory(cwd, projectInfo),
48287
+ await generateTypesDirectory(cwd, projectInfo, templates),
48212
48288
  nextConfigFiles(await patchNextConfig(cwd, database)),
48213
48289
  cacheComponentsFiles(await turnOnCacheComponents(cwd))
48214
48290
  ];
@@ -53074,4 +53150,4 @@ Object.defineProperty(exports, "validateTemplateName", {
53074
53150
  }
53075
53151
  });
53076
53152
 
53077
- //# sourceMappingURL=create-nextly-Q62KZJib.cjs.map
53153
+ //# sourceMappingURL=create-nextly-Decg8dye.cjs.map