@lukoweb/apitogo 0.1.15 → 0.1.16

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/cli.js CHANGED
@@ -3822,7 +3822,7 @@ import {
3822
3822
  // package.json
3823
3823
  var package_default = {
3824
3824
  name: "@lukoweb/apitogo",
3825
- version: "0.1.15",
3825
+ version: "0.1.16",
3826
3826
  type: "module",
3827
3827
  sideEffects: [
3828
3828
  "**/*.css",
@@ -8006,7 +8006,9 @@ var prerender = async ({
8006
8006
  }
8007
8007
  if (pagefindWriteResult?.outputPath) {
8008
8008
  logger.info(
8009
- colors7.blue(`\u2713 pagefind index built: ${pagefindWriteResult.outputPath}`)
8009
+ colors7.blue(
8010
+ `\u2713 pagefind index built: ${pagefindWriteResult.outputPath}`
8011
+ )
8010
8012
  );
8011
8013
  }
8012
8014
  }
@@ -8455,8 +8457,8 @@ var build_default = {
8455
8457
  };
8456
8458
 
8457
8459
  // src/cli/configure-github-workflow/handler.ts
8458
- import { access, mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
8459
8460
  import { constants } from "node:fs";
8461
+ import { access, mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
8460
8462
  import path25 from "node:path";
8461
8463
  var APITOGO_DEPLOY_WORKFLOW_YAML = `name: Build and Deploy
8462
8464
 
@@ -8602,9 +8604,7 @@ function isDeployJsonOnlyArgv() {
8602
8604
  if (deployIdx === -1) {
8603
8605
  return false;
8604
8606
  }
8605
- return args.slice(deployIdx).some(
8606
- (a) => a === "--json-only" || a.startsWith("--json-only=")
8607
- );
8607
+ return args.slice(deployIdx).some((a) => a === "--json-only" || a.startsWith("--json-only="));
8608
8608
  }
8609
8609
  function isJsonOnlyDeployEnv() {
8610
8610
  return process.env.APITOGO_JSON_ONLY === "1";
@@ -9144,29 +9144,7 @@ var dev_default = {
9144
9144
  }
9145
9145
  };
9146
9146
 
9147
- // src/cli/cmds/preview.ts
9148
- var previewCommand = {
9149
- desc: "Preview production build",
9150
- command: "preview",
9151
- builder: (yargs2) => yargs2.option("dir", {
9152
- type: "string",
9153
- describe: "The directory containing your project",
9154
- default: ".",
9155
- normalize: true,
9156
- hidden: true
9157
- }).option("port", {
9158
- type: "number",
9159
- describe: "The port to run the local server on"
9160
- }),
9161
- handler: async (argv) => {
9162
- process.env.NODE_ENV = "production";
9163
- await captureEvent({ argv, event: "apitogo preview" });
9164
- await preview(argv);
9165
- }
9166
- };
9167
- var preview_default = previewCommand;
9168
-
9169
- // src/cli/sync-config/handler.ts
9147
+ // src/cli/make-config/handler.ts
9170
9148
  import { readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
9171
9149
  import path30 from "node:path";
9172
9150
  import { glob as glob4 } from "glob";
@@ -9182,6 +9160,7 @@ var CONFIG_FILENAMES = [
9182
9160
  "zudoku.config.js",
9183
9161
  "zudoku.config.mjs"
9184
9162
  ];
9163
+ var DEFAULT_NEW_CONFIG_FILENAME = "apitogo.config.tsx";
9185
9164
  var createTreeNode = () => ({ docs: [], children: /* @__PURE__ */ new Map() });
9186
9165
  var toPosixPath2 = (value) => value.split(path30.sep).join(path30.posix.sep);
9187
9166
  var toRoutePath = (relativeFile, pagesDir) => {
@@ -9349,19 +9328,78 @@ var findMatchingBracket = (source, startIndex) => {
9349
9328
  }
9350
9329
  throw new Error("Could not find matching closing bracket.");
9351
9330
  };
9352
- var replaceArrayProperty = (source, propertyName, replacement) => {
9353
- const propertyIndex = source.indexOf(`${propertyName}:`);
9354
- if (propertyIndex === -1) {
9331
+ var findPropertyArrayBounds = (source, propertyName) => {
9332
+ const re = new RegExp(`\\b${propertyName}\\s*:`);
9333
+ const match = source.match(re);
9334
+ if (!match || match.index === void 0) {
9355
9335
  throw new Error(`Could not find '${propertyName}' in config file.`);
9356
9336
  }
9357
- const arrayStart = source.indexOf("[", propertyIndex);
9337
+ const arrayStart = source.indexOf("[", match.index);
9358
9338
  if (arrayStart === -1) {
9359
- throw new Error(`Could not find array start for '${propertyName}'.`);
9339
+ throw new Error(
9340
+ `Could not find array value for '${propertyName}' (expected [...]).`
9341
+ );
9360
9342
  }
9361
9343
  const arrayEnd = findMatchingBracket(source, arrayStart);
9344
+ return { arrayStart, arrayEnd };
9345
+ };
9346
+ var replaceArrayProperty = (source, propertyName, replacement) => {
9347
+ const { arrayStart, arrayEnd } = findPropertyArrayBounds(
9348
+ source,
9349
+ propertyName
9350
+ );
9362
9351
  return `${source.slice(0, arrayStart)}${replacement}${source.slice(arrayEnd + 1)}`;
9363
9352
  };
9364
- var findConfigPath = async (dir) => {
9353
+ var insertNavigationAndRedirectsKeys = (source) => {
9354
+ const m = source.match(/(const\s+config\s*:\s*ApitogoConfig\s*=\s*\{)/);
9355
+ if (!m || m.index === void 0) {
9356
+ throw new Error(
9357
+ "Could not find 'const config: ApitogoConfig = {' \u2014 add navigation and redirects arrays manually."
9358
+ );
9359
+ }
9360
+ const insertAt = m.index + m[0].length;
9361
+ return `${source.slice(0, insertAt)}
9362
+ navigation: [],
9363
+ redirects: [],${source.slice(insertAt)}`;
9364
+ };
9365
+ var insertRedirectsAfterNavigation = (source) => {
9366
+ if (/\bredirects\s*:/.test(source)) return source;
9367
+ const { arrayEnd } = findPropertyArrayBounds(source, "navigation");
9368
+ let i = arrayEnd + 1;
9369
+ while (i < source.length && /\s/.test(source[i])) i++;
9370
+ if (source[i] === ",") {
9371
+ return `${source.slice(0, i + 1)}
9372
+ redirects: [],${source.slice(i + 1)}`;
9373
+ }
9374
+ return `${source.slice(0, arrayEnd + 1)},
9375
+ redirects: [],${source.slice(arrayEnd + 1)}`;
9376
+ };
9377
+ var ensureNavigationAndRedirectsForUpdate = (source) => {
9378
+ const hasNav = /\bnavigation\s*:/.test(source);
9379
+ const hasRedirects = /\bredirects\s*:/.test(source);
9380
+ if (hasNav && hasRedirects) return source;
9381
+ if (!hasNav && !hasRedirects) return insertNavigationAndRedirectsKeys(source);
9382
+ if (hasNav && !hasRedirects) return insertRedirectsAfterNavigation(source);
9383
+ throw new Error(
9384
+ "Found 'redirects' but not 'navigation'. Add a navigation: [...] array manually."
9385
+ );
9386
+ };
9387
+ var buildNewConfigContents = (pagesDir) => {
9388
+ const filesGlob = `/${pagesDir}/**/*.mdx`;
9389
+ return `import type { ApitogoConfig } from "@lukoweb/apitogo";
9390
+
9391
+ const config: ApitogoConfig = {
9392
+ docs: {
9393
+ files: "${filesGlob}",
9394
+ },
9395
+ navigation: [],
9396
+ redirects: [],
9397
+ };
9398
+
9399
+ export default config;
9400
+ `;
9401
+ };
9402
+ var findExistingConfigPath = async (dir) => {
9365
9403
  for (const filename of CONFIG_FILENAMES) {
9366
9404
  const fullPath = path30.join(dir, filename);
9367
9405
  try {
@@ -9370,15 +9408,22 @@ var findConfigPath = async (dir) => {
9370
9408
  } catch {
9371
9409
  }
9372
9410
  }
9373
- throw new Error("No APIToGo config file found in the project root.");
9411
+ return null;
9374
9412
  };
9375
- async function syncConfig(argv) {
9413
+ async function makeConfig(argv) {
9376
9414
  const dir = path30.resolve(process.cwd(), argv.dir);
9377
9415
  const pagesDir = toPosixPath2(
9378
9416
  argv.pagesDir.replace(/^[./]+/, "").replace(/\/+$/, "")
9379
9417
  );
9380
9418
  try {
9381
- const configPath = await findConfigPath(dir);
9419
+ let configPath = await findExistingConfigPath(dir);
9420
+ let createdNew = false;
9421
+ if (!configPath) {
9422
+ configPath = path30.join(dir, DEFAULT_NEW_CONFIG_FILENAME);
9423
+ await writeFile7(configPath, buildNewConfigContents(pagesDir), "utf8");
9424
+ createdNew = true;
9425
+ printDiagnosticsToConsole(`Created ${DEFAULT_NEW_CONFIG_FILENAME}`);
9426
+ }
9382
9427
  const pageFiles = await glob4(`${pagesDir}/**/*.{md,mdx}`, {
9383
9428
  cwd: dir,
9384
9429
  ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
@@ -9390,13 +9435,12 @@ async function syncConfig(argv) {
9390
9435
  if (routePaths.length === 0) {
9391
9436
  throw new Error(`No .md or .mdx files found under '${pagesDir}'.`);
9392
9437
  }
9393
- const tree = buildTree(routePaths);
9394
- const navigationItems = treeToNavigationItems(tree);
9395
- const originalConfig = await readFile5(configPath, "utf8");
9438
+ let originalConfig = await readFile5(configPath, "utf8");
9439
+ originalConfig = ensureNavigationAndRedirectsForUpdate(originalConfig);
9396
9440
  const withNavigation = replaceArrayProperty(
9397
9441
  originalConfig,
9398
9442
  "navigation",
9399
- buildNavigationCode(navigationItems)
9443
+ buildNavigationCode(treeToNavigationItems(buildTree(routePaths)))
9400
9444
  );
9401
9445
  const withRedirects = replaceArrayProperty(
9402
9446
  withNavigation,
@@ -9404,8 +9448,9 @@ async function syncConfig(argv) {
9404
9448
  buildRedirectsCode(routePaths[0])
9405
9449
  );
9406
9450
  await writeFile7(configPath, withRedirects, "utf8");
9451
+ const action = createdNew ? "Initialized" : "Updated";
9407
9452
  printResultToConsole(
9408
- `Updated ${path30.basename(configPath)} from ${pagesDir}/`
9453
+ `${action} ${path30.basename(configPath)} from ${pagesDir}/ (navigation and redirects only; other settings unchanged)`
9409
9454
  );
9410
9455
  } catch (error) {
9411
9456
  await printCriticalFailureToConsoleAndExit(
@@ -9414,10 +9459,10 @@ async function syncConfig(argv) {
9414
9459
  }
9415
9460
  }
9416
9461
 
9417
- // src/cli/cmds/sync-config.ts
9418
- var sync_config_default = {
9419
- desc: "Regenerate navigation from the pages folder",
9420
- command: "sync-config",
9462
+ // src/cli/cmds/make-config.ts
9463
+ var make_config_default = {
9464
+ desc: "Create or update config navigation from the pages folder (other settings preserved)",
9465
+ command: "make-config",
9421
9466
  builder: (yargs2) => yargs2.option("dir", {
9422
9467
  type: "string",
9423
9468
  describe: "The directory containing your project",
@@ -9430,11 +9475,33 @@ var sync_config_default = {
9430
9475
  default: "pages"
9431
9476
  }),
9432
9477
  handler: async (argv) => {
9433
- await captureEvent({ argv, event: "apitogo sync-config" });
9434
- await syncConfig(argv);
9478
+ await captureEvent({ argv, event: "apitogo make-config" });
9479
+ await makeConfig(argv);
9435
9480
  }
9436
9481
  };
9437
9482
 
9483
+ // src/cli/cmds/preview.ts
9484
+ var previewCommand = {
9485
+ desc: "Preview production build",
9486
+ command: "preview",
9487
+ builder: (yargs2) => yargs2.option("dir", {
9488
+ type: "string",
9489
+ describe: "The directory containing your project",
9490
+ default: ".",
9491
+ normalize: true,
9492
+ hidden: true
9493
+ }).option("port", {
9494
+ type: "number",
9495
+ describe: "The port to run the local server on"
9496
+ }),
9497
+ handler: async (argv) => {
9498
+ process.env.NODE_ENV = "production";
9499
+ await captureEvent({ argv, event: "apitogo preview" });
9500
+ await preview(argv);
9501
+ }
9502
+ };
9503
+ var preview_default = previewCommand;
9504
+
9438
9505
  // src/cli/common/outdated.ts
9439
9506
  import { existsSync as existsSync2, mkdirSync } from "node:fs";
9440
9507
  import { readFile as readFile6, writeFile as writeFile8 } from "node:fs/promises";
@@ -9679,7 +9746,7 @@ var cli = yargs(hideBin(process.argv)).option("zuplo", {
9679
9746
  process.env.ZUPLO = "1";
9680
9747
  printDiagnosticsToConsole("Running in Zuplo mode");
9681
9748
  }
9682
- }).middleware(warnPackageVersionMismatch).command(build_default).command(configure_github_workflow_default).command(dev_default).command(deploy_default).command(preview_default).command(sync_config_default).demandCommand().strictCommands().version(packageJson?.version).fail(false).help();
9749
+ }).middleware(warnPackageVersionMismatch).command(build_default).command(configure_github_workflow_default).command(dev_default).command(deploy_default).command(preview_default).command(make_config_default).demandCommand().strictCommands().version(packageJson?.version).fail(false).help();
9683
9750
  try {
9684
9751
  void warnIfOutdatedVersion(packageJson?.version);
9685
9752
  await cli.argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
package/src/vite/build.ts CHANGED
@@ -1,237 +1,237 @@
1
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { build as esbuild } from "esbuild";
4
- import type { Rollup } from "vite";
5
- import { build as viteBuild, mergeConfig } from "vite";
6
- import { ZuploEnv } from "../app/env.js";
7
- import { getZudokuRootDir } from "../cli/common/package-json.js";
8
- import {
9
- findOutputPathOfServerConfig,
10
- loadZudokuConfig,
11
- } from "../config/loader.js";
12
- import { getIssuer } from "../lib/auth/issuer.js";
13
- import invariant from "../lib/util/invariant.js";
14
- import { joinUrl } from "../lib/util/joinUrl.js";
15
- import { getViteConfig } from "./config.js";
16
- import { getBuildHtml } from "./html.js";
17
- import { writeOutput } from "./output.js";
18
- import { prerender } from "./prerender/prerender.js";
19
-
20
- const DIST_DIR = "dist";
21
-
22
- const extractAssets = (result: Rollup.RollupOutput) => {
23
- const jsEntry = result.output.find(
24
- (o) => "isEntry" in o && o.isEntry,
25
- )?.fileName;
26
- const cssEntries = result.output
27
- .filter((o) => o.fileName.endsWith(".css"))
28
- .map((o) => o.fileName);
29
-
30
- if (!jsEntry || cssEntries.length === 0) {
31
- throw new Error("Build failed. No js or css assets found");
32
- }
33
-
34
- return { jsEntry, cssEntries };
35
- };
36
-
37
- export type BuildOptions = {
38
- dir: string;
39
- ssr?: boolean;
40
- adapter?: "node" | "cloudflare" | "vercel";
41
- /** Suppress Vite build table and most build logs (used with `apitogo deploy --json-only`). */
42
- jsonOnly?: boolean;
43
- };
44
-
45
- export async function runBuild(options: BuildOptions) {
46
- const { dir, ssr, adapter = "node", jsonOnly = false } = options;
47
-
48
- // Build client and server bundles
49
- const viteClientConfig = await getViteConfig(dir, {
50
- mode: "production",
51
- command: "build",
52
- });
53
- const viteServerConfig = await getViteConfig(dir, {
54
- mode: "production",
55
- command: "build",
56
- isSsrBuild: true,
57
- });
58
-
59
- const clientResult = await viteBuild(
60
- jsonOnly
61
- ? mergeConfig(viteClientConfig, { logLevel: "silent" as const })
62
- : viteClientConfig,
63
- );
64
- const serverResult = await viteBuild({
65
- ...viteServerConfig,
66
- logLevel: "silent",
67
- });
68
-
69
- if (Array.isArray(clientResult) || !("output" in clientResult)) {
70
- throw new Error("Client build failed");
71
- }
72
- if (Array.isArray(serverResult) || !("output" in serverResult)) {
73
- throw new Error("Server build failed");
74
- }
75
-
76
- const { config } = await loadZudokuConfig(
77
- { mode: "production", command: "build" },
78
- dir,
79
- );
80
-
81
- const { jsEntry, cssEntries } = extractAssets(clientResult);
82
-
83
- const html = getBuildHtml({
84
- jsEntry: joinUrl(viteClientConfig.base, jsEntry),
85
- cssEntries: cssEntries.map((css) => joinUrl(viteClientConfig.base, css)),
86
- dir: config.site?.dir,
87
- });
88
-
89
- invariant(viteClientConfig.build?.outDir, "Client build outDir is missing");
90
- invariant(viteServerConfig.build?.outDir, "Server build outDir is missing");
91
-
92
- const clientOutDir = viteClientConfig.build.outDir;
93
- const serverOutDir = viteServerConfig.build.outDir;
94
- const serverConfigFilename = findOutputPathOfServerConfig(serverResult);
95
-
96
- if (ssr) {
97
- // SSR: bundle entry.js and remove index.html
98
- await bundleSSREntry({
99
- dir,
100
- adapter,
101
- serverOutDir,
102
- serverConfigFilename,
103
- html,
104
- basePath: config.basePath,
105
- });
106
- await rm(path.join(clientOutDir, "index.html"), { force: true });
107
- } else {
108
- // SSG: prerender and clean up server
109
- await runPrerender({
110
- dir,
111
- config,
112
- html,
113
- clientOutDir,
114
- serverOutDir,
115
- serverResult,
116
- });
117
- }
118
- }
119
-
120
- type PrerenderOptions = {
121
- dir: string;
122
- config: Awaited<ReturnType<typeof loadZudokuConfig>>["config"];
123
- html: string;
124
- clientOutDir: string;
125
- serverOutDir: string;
126
- serverResult: Rollup.RollupOutput;
127
- };
128
-
129
- const runPrerender = async (options: PrerenderOptions) => {
130
- const { dir, config, html, clientOutDir, serverOutDir, serverResult } =
131
- options;
132
- const issuer = await getIssuer(config);
133
- const serverConfigFilename = findOutputPathOfServerConfig(serverResult);
134
-
135
- try {
136
- const { workerResults, rewrites } = await prerender({
137
- html,
138
- dir,
139
- basePath: config.basePath,
140
- serverConfigFilename,
141
- writeRedirects: process.env.VERCEL === undefined,
142
- });
143
-
144
- const indexHtml = path.join(clientOutDir, "index.html");
145
- if (!workerResults.find((r) => r.outputPath === indexHtml)) {
146
- await writeFile(indexHtml, html, "utf-8");
147
- }
148
-
149
- // Move status pages (400, 404, 500) to root path
150
- const statusPages = workerResults.flatMap((r) =>
151
- /400|404|500\.html$/.test(r.outputPath) ? r.outputPath : [],
152
- );
153
- for (const statusPage of statusPages) {
154
- await rename(
155
- statusPage,
156
- path.join(dir, DIST_DIR, path.basename(statusPage)),
157
- );
158
- }
159
-
160
- // Delete server build - not needed after prerender
161
- await rm(serverOutDir, { recursive: true, force: true });
162
-
163
- if (process.env.VERCEL) {
164
- await mkdir(path.join(dir, ".vercel/output/static"), { recursive: true });
165
- await rename(
166
- path.join(dir, DIST_DIR),
167
- path.join(dir, ".vercel/output/static"),
168
- );
169
- }
170
-
171
- await writeOutput(dir, {
172
- config,
173
- redirects: workerResults.flatMap((r) => r.redirect ?? []),
174
- rewrites,
175
- });
176
-
177
- if (ZuploEnv.isZuplo && issuer) {
178
- await writeFile(
179
- path.join(dir, DIST_DIR, ".output/zuplo.json"),
180
- JSON.stringify({ issuer }, null, 2),
181
- "utf-8",
182
- );
183
- }
184
- } catch (e) {
185
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
186
- console.error(e);
187
- throw e;
188
- }
189
- };
190
-
191
- type SSREntryOptions = {
192
- dir: string;
193
- adapter: "node" | "cloudflare" | "vercel";
194
- serverOutDir: string;
195
- serverConfigFilename: string;
196
- html: string;
197
- basePath?: string;
198
- };
199
-
200
- const bundleSSREntry = async (options: SSREntryOptions) => {
201
- const { dir, adapter, serverOutDir, serverConfigFilename, html, basePath } =
202
- options;
203
- const tempEntryPath = path.join(dir, "__ssr-entry.ts");
204
-
205
- const packageRoot = getZudokuRootDir();
206
-
207
- const templateContent = await readFile(
208
- path.join(packageRoot, "src/vite/ssr-templates", `${adapter}.ts`),
209
- "utf-8",
210
- );
211
-
212
- const entryContent = templateContent
213
- .replace('"__TEMPLATE__"', JSON.stringify(html))
214
- .replace('"__CONFIG_FILE__"', JSON.stringify(`./${serverConfigFilename}`))
215
- .replace(
216
- '"__BASE_PATH__"',
217
- basePath ? JSON.stringify(basePath) : "undefined",
218
- );
219
-
220
- await writeFile(tempEntryPath, entryContent, "utf-8");
221
-
222
- try {
223
- await esbuild({
224
- entryPoints: [tempEntryPath],
225
- bundle: true,
226
- platform: adapter === "node" ? "node" : "neutral",
227
- target: "es2022",
228
- format: "esm",
229
- outfile: path.join(serverOutDir, "entry.js"),
230
- external: ["./entry.server.js", `./${serverConfigFilename}`],
231
- nodePaths: [path.join(packageRoot, "node_modules")],
232
- banner: { js: "// Bundled SSR entry" },
233
- });
234
- } finally {
235
- await rm(tempEntryPath, { force: true });
236
- }
237
- };
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { build as esbuild } from "esbuild";
4
+ import type { Rollup } from "vite";
5
+ import { build as viteBuild, mergeConfig } from "vite";
6
+ import { ZuploEnv } from "../app/env.js";
7
+ import { getZudokuRootDir } from "../cli/common/package-json.js";
8
+ import {
9
+ findOutputPathOfServerConfig,
10
+ loadZudokuConfig,
11
+ } from "../config/loader.js";
12
+ import { getIssuer } from "../lib/auth/issuer.js";
13
+ import invariant from "../lib/util/invariant.js";
14
+ import { joinUrl } from "../lib/util/joinUrl.js";
15
+ import { getViteConfig } from "./config.js";
16
+ import { getBuildHtml } from "./html.js";
17
+ import { writeOutput } from "./output.js";
18
+ import { prerender } from "./prerender/prerender.js";
19
+
20
+ const DIST_DIR = "dist";
21
+
22
+ const extractAssets = (result: Rollup.RollupOutput) => {
23
+ const jsEntry = result.output.find(
24
+ (o) => "isEntry" in o && o.isEntry,
25
+ )?.fileName;
26
+ const cssEntries = result.output
27
+ .filter((o) => o.fileName.endsWith(".css"))
28
+ .map((o) => o.fileName);
29
+
30
+ if (!jsEntry || cssEntries.length === 0) {
31
+ throw new Error("Build failed. No js or css assets found");
32
+ }
33
+
34
+ return { jsEntry, cssEntries };
35
+ };
36
+
37
+ export type BuildOptions = {
38
+ dir: string;
39
+ ssr?: boolean;
40
+ adapter?: "node" | "cloudflare" | "vercel";
41
+ /** Suppress Vite build table and most build logs (used with `apitogo deploy --json-only`). */
42
+ jsonOnly?: boolean;
43
+ };
44
+
45
+ export async function runBuild(options: BuildOptions) {
46
+ const { dir, ssr, adapter = "node", jsonOnly = false } = options;
47
+
48
+ // Build client and server bundles
49
+ const viteClientConfig = await getViteConfig(dir, {
50
+ mode: "production",
51
+ command: "build",
52
+ });
53
+ const viteServerConfig = await getViteConfig(dir, {
54
+ mode: "production",
55
+ command: "build",
56
+ isSsrBuild: true,
57
+ });
58
+
59
+ const clientResult = await viteBuild(
60
+ jsonOnly
61
+ ? mergeConfig(viteClientConfig, { logLevel: "silent" as const })
62
+ : viteClientConfig,
63
+ );
64
+ const serverResult = await viteBuild({
65
+ ...viteServerConfig,
66
+ logLevel: "silent",
67
+ });
68
+
69
+ if (Array.isArray(clientResult) || !("output" in clientResult)) {
70
+ throw new Error("Client build failed");
71
+ }
72
+ if (Array.isArray(serverResult) || !("output" in serverResult)) {
73
+ throw new Error("Server build failed");
74
+ }
75
+
76
+ const { config } = await loadZudokuConfig(
77
+ { mode: "production", command: "build" },
78
+ dir,
79
+ );
80
+
81
+ const { jsEntry, cssEntries } = extractAssets(clientResult);
82
+
83
+ const html = getBuildHtml({
84
+ jsEntry: joinUrl(viteClientConfig.base, jsEntry),
85
+ cssEntries: cssEntries.map((css) => joinUrl(viteClientConfig.base, css)),
86
+ dir: config.site?.dir,
87
+ });
88
+
89
+ invariant(viteClientConfig.build?.outDir, "Client build outDir is missing");
90
+ invariant(viteServerConfig.build?.outDir, "Server build outDir is missing");
91
+
92
+ const clientOutDir = viteClientConfig.build.outDir;
93
+ const serverOutDir = viteServerConfig.build.outDir;
94
+ const serverConfigFilename = findOutputPathOfServerConfig(serverResult);
95
+
96
+ if (ssr) {
97
+ // SSR: bundle entry.js and remove index.html
98
+ await bundleSSREntry({
99
+ dir,
100
+ adapter,
101
+ serverOutDir,
102
+ serverConfigFilename,
103
+ html,
104
+ basePath: config.basePath,
105
+ });
106
+ await rm(path.join(clientOutDir, "index.html"), { force: true });
107
+ } else {
108
+ // SSG: prerender and clean up server
109
+ await runPrerender({
110
+ dir,
111
+ config,
112
+ html,
113
+ clientOutDir,
114
+ serverOutDir,
115
+ serverResult,
116
+ });
117
+ }
118
+ }
119
+
120
+ type PrerenderOptions = {
121
+ dir: string;
122
+ config: Awaited<ReturnType<typeof loadZudokuConfig>>["config"];
123
+ html: string;
124
+ clientOutDir: string;
125
+ serverOutDir: string;
126
+ serverResult: Rollup.RollupOutput;
127
+ };
128
+
129
+ const runPrerender = async (options: PrerenderOptions) => {
130
+ const { dir, config, html, clientOutDir, serverOutDir, serverResult } =
131
+ options;
132
+ const issuer = await getIssuer(config);
133
+ const serverConfigFilename = findOutputPathOfServerConfig(serverResult);
134
+
135
+ try {
136
+ const { workerResults, rewrites } = await prerender({
137
+ html,
138
+ dir,
139
+ basePath: config.basePath,
140
+ serverConfigFilename,
141
+ writeRedirects: process.env.VERCEL === undefined,
142
+ });
143
+
144
+ const indexHtml = path.join(clientOutDir, "index.html");
145
+ if (!workerResults.find((r) => r.outputPath === indexHtml)) {
146
+ await writeFile(indexHtml, html, "utf-8");
147
+ }
148
+
149
+ // Move status pages (400, 404, 500) to root path
150
+ const statusPages = workerResults.flatMap((r) =>
151
+ /400|404|500\.html$/.test(r.outputPath) ? r.outputPath : [],
152
+ );
153
+ for (const statusPage of statusPages) {
154
+ await rename(
155
+ statusPage,
156
+ path.join(dir, DIST_DIR, path.basename(statusPage)),
157
+ );
158
+ }
159
+
160
+ // Delete server build - not needed after prerender
161
+ await rm(serverOutDir, { recursive: true, force: true });
162
+
163
+ if (process.env.VERCEL) {
164
+ await mkdir(path.join(dir, ".vercel/output/static"), { recursive: true });
165
+ await rename(
166
+ path.join(dir, DIST_DIR),
167
+ path.join(dir, ".vercel/output/static"),
168
+ );
169
+ }
170
+
171
+ await writeOutput(dir, {
172
+ config,
173
+ redirects: workerResults.flatMap((r) => r.redirect ?? []),
174
+ rewrites,
175
+ });
176
+
177
+ if (ZuploEnv.isZuplo && issuer) {
178
+ await writeFile(
179
+ path.join(dir, DIST_DIR, ".output/zuplo.json"),
180
+ JSON.stringify({ issuer }, null, 2),
181
+ "utf-8",
182
+ );
183
+ }
184
+ } catch (e) {
185
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
186
+ console.error(e);
187
+ throw e;
188
+ }
189
+ };
190
+
191
+ type SSREntryOptions = {
192
+ dir: string;
193
+ adapter: "node" | "cloudflare" | "vercel";
194
+ serverOutDir: string;
195
+ serverConfigFilename: string;
196
+ html: string;
197
+ basePath?: string;
198
+ };
199
+
200
+ const bundleSSREntry = async (options: SSREntryOptions) => {
201
+ const { dir, adapter, serverOutDir, serverConfigFilename, html, basePath } =
202
+ options;
203
+ const tempEntryPath = path.join(dir, "__ssr-entry.ts");
204
+
205
+ const packageRoot = getZudokuRootDir();
206
+
207
+ const templateContent = await readFile(
208
+ path.join(packageRoot, "src/vite/ssr-templates", `${adapter}.ts`),
209
+ "utf-8",
210
+ );
211
+
212
+ const entryContent = templateContent
213
+ .replace('"__TEMPLATE__"', JSON.stringify(html))
214
+ .replace('"__CONFIG_FILE__"', JSON.stringify(`./${serverConfigFilename}`))
215
+ .replace(
216
+ '"__BASE_PATH__"',
217
+ basePath ? JSON.stringify(basePath) : "undefined",
218
+ );
219
+
220
+ await writeFile(tempEntryPath, entryContent, "utf-8");
221
+
222
+ try {
223
+ await esbuild({
224
+ entryPoints: [tempEntryPath],
225
+ bundle: true,
226
+ platform: adapter === "node" ? "node" : "neutral",
227
+ target: "es2022",
228
+ format: "esm",
229
+ outfile: path.join(serverOutDir, "entry.js"),
230
+ external: ["./entry.server.js", `./${serverConfigFilename}`],
231
+ nodePaths: [path.join(packageRoot, "node_modules")],
232
+ banner: { js: "// Bundled SSR entry" },
233
+ });
234
+ } finally {
235
+ await rm(tempEntryPath, { force: true });
236
+ }
237
+ };
package/src/vite/llms.ts CHANGED
@@ -1,99 +1,99 @@
1
- import { writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import colors from "picocolors";
4
- import { joinUrl } from "../lib/util/joinUrl.js";
5
- import type { MarkdownFileInfo } from "./plugin-markdown-export.js";
6
- export async function generateLlmsTxtFiles({
7
- markdownFileInfos,
8
- outputUrls,
9
- baseOutputDir,
10
- basePath,
11
- siteName,
12
- llmsTxt,
13
- llmsTxtFull,
14
- redirectUrls,
15
- }: {
16
- markdownFileInfos: MarkdownFileInfo[];
17
- basePath: string | undefined;
18
- outputUrls: string[];
19
- baseOutputDir: string;
20
- siteName?: string;
21
- llmsTxt?: boolean;
22
- llmsTxtFull?: boolean;
23
- redirectUrls: Set<string>;
24
- }) {
25
- const nonRedirectUrls = outputUrls.filter((url) => !redirectUrls.has(url));
26
-
27
- const baseUrl = basePath ?? "";
28
- const title = siteName ?? "Documentation";
29
-
30
- const markdownMap = new Map(
31
- markdownFileInfos.map((info) => [info.routePath, info]),
32
- );
33
-
34
- // Generate llms.txt
35
- if (llmsTxt) {
36
- const llmsTxtParts: string[] = [];
37
-
38
- llmsTxtParts.push(`# ${title}\n`);
39
- llmsTxtParts.push("> Documentation files for Large Language Models\n");
40
-
41
- // Add documentation section with links to all pages (matching sitemap structure)
42
- llmsTxtParts.push("## Documentation\n");
43
-
44
- for (const url of nonRedirectUrls) {
45
- // Skip error pages
46
- if (/(400|404|500)$/.test(url)) continue;
47
-
48
- const mdInfo = markdownMap.get(url);
49
-
50
- // Only include pages that have markdown content
51
- if (mdInfo) {
52
- // If we have markdown for this page, link to the .md file
53
- const mdUrl = joinUrl(baseUrl, `${url}.md`);
54
- const linkTitle = mdInfo.title ?? url;
55
- const description = mdInfo.description ? `: ${mdInfo.description}` : "";
56
- llmsTxtParts.push(`- [${linkTitle}](${mdUrl})${description}`);
57
- }
58
- }
59
-
60
- const llmsTxt = llmsTxtParts.join("\n");
61
- await writeFile(path.join(baseOutputDir, "llms.txt"), llmsTxt, "utf-8");
62
-
63
- if (process.env.APITOGO_JSON_ONLY !== "1") {
64
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
65
- console.log(colors.blue("✓ generated llms.txt"));
66
- }
67
- }
68
-
69
- // Generate llms-full.txt (full content of all markdown documents)
70
- if (llmsTxtFull) {
71
- const llmsFullParts: string[] = [];
72
-
73
- llmsFullParts.push(`# ${title}\n`);
74
- llmsFullParts.push("> Complete documentation for Large Language Models\n");
75
-
76
- // Add each markdown document's full content
77
- for (const info of markdownFileInfos) {
78
- llmsFullParts.push(`\n---\n`);
79
- llmsFullParts.push(`## Document: ${info.title ?? info.routePath}\n`);
80
- if (info.description) {
81
- llmsFullParts.push(`${info.description}\n`);
82
- }
83
- llmsFullParts.push(`URL: ${joinUrl(baseUrl, info.routePath)}\n`);
84
- llmsFullParts.push(`\n${info.content}\n`);
85
- }
86
-
87
- const llmsFull = llmsFullParts.join("\n");
88
- await writeFile(
89
- path.join(baseOutputDir, "llms-full.txt"),
90
- llmsFull,
91
- "utf-8",
92
- );
93
-
94
- if (process.env.APITOGO_JSON_ONLY !== "1") {
95
- // biome-ignore lint/suspicious/noConsole: Allowed here
96
- console.log(colors.blue("✓ generated llms-full.txt"));
97
- }
98
- }
99
- }
1
+ import { writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import colors from "picocolors";
4
+ import { joinUrl } from "../lib/util/joinUrl.js";
5
+ import type { MarkdownFileInfo } from "./plugin-markdown-export.js";
6
+ export async function generateLlmsTxtFiles({
7
+ markdownFileInfos,
8
+ outputUrls,
9
+ baseOutputDir,
10
+ basePath,
11
+ siteName,
12
+ llmsTxt,
13
+ llmsTxtFull,
14
+ redirectUrls,
15
+ }: {
16
+ markdownFileInfos: MarkdownFileInfo[];
17
+ basePath: string | undefined;
18
+ outputUrls: string[];
19
+ baseOutputDir: string;
20
+ siteName?: string;
21
+ llmsTxt?: boolean;
22
+ llmsTxtFull?: boolean;
23
+ redirectUrls: Set<string>;
24
+ }) {
25
+ const nonRedirectUrls = outputUrls.filter((url) => !redirectUrls.has(url));
26
+
27
+ const baseUrl = basePath ?? "";
28
+ const title = siteName ?? "Documentation";
29
+
30
+ const markdownMap = new Map(
31
+ markdownFileInfos.map((info) => [info.routePath, info]),
32
+ );
33
+
34
+ // Generate llms.txt
35
+ if (llmsTxt) {
36
+ const llmsTxtParts: string[] = [];
37
+
38
+ llmsTxtParts.push(`# ${title}\n`);
39
+ llmsTxtParts.push("> Documentation files for Large Language Models\n");
40
+
41
+ // Add documentation section with links to all pages (matching sitemap structure)
42
+ llmsTxtParts.push("## Documentation\n");
43
+
44
+ for (const url of nonRedirectUrls) {
45
+ // Skip error pages
46
+ if (/(400|404|500)$/.test(url)) continue;
47
+
48
+ const mdInfo = markdownMap.get(url);
49
+
50
+ // Only include pages that have markdown content
51
+ if (mdInfo) {
52
+ // If we have markdown for this page, link to the .md file
53
+ const mdUrl = joinUrl(baseUrl, `${url}.md`);
54
+ const linkTitle = mdInfo.title ?? url;
55
+ const description = mdInfo.description ? `: ${mdInfo.description}` : "";
56
+ llmsTxtParts.push(`- [${linkTitle}](${mdUrl})${description}`);
57
+ }
58
+ }
59
+
60
+ const llmsTxt = llmsTxtParts.join("\n");
61
+ await writeFile(path.join(baseOutputDir, "llms.txt"), llmsTxt, "utf-8");
62
+
63
+ if (process.env.APITOGO_JSON_ONLY !== "1") {
64
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
65
+ console.log(colors.blue("✓ generated llms.txt"));
66
+ }
67
+ }
68
+
69
+ // Generate llms-full.txt (full content of all markdown documents)
70
+ if (llmsTxtFull) {
71
+ const llmsFullParts: string[] = [];
72
+
73
+ llmsFullParts.push(`# ${title}\n`);
74
+ llmsFullParts.push("> Complete documentation for Large Language Models\n");
75
+
76
+ // Add each markdown document's full content
77
+ for (const info of markdownFileInfos) {
78
+ llmsFullParts.push(`\n---\n`);
79
+ llmsFullParts.push(`## Document: ${info.title ?? info.routePath}\n`);
80
+ if (info.description) {
81
+ llmsFullParts.push(`${info.description}\n`);
82
+ }
83
+ llmsFullParts.push(`URL: ${joinUrl(baseUrl, info.routePath)}\n`);
84
+ llmsFullParts.push(`\n${info.content}\n`);
85
+ }
86
+
87
+ const llmsFull = llmsFullParts.join("\n");
88
+ await writeFile(
89
+ path.join(baseOutputDir, "llms-full.txt"),
90
+ llmsFull,
91
+ "utf-8",
92
+ );
93
+
94
+ if (process.env.APITOGO_JSON_ONLY !== "1") {
95
+ // biome-ignore lint/suspicious/noConsole: Allowed here
96
+ console.log(colors.blue("✓ generated llms-full.txt"));
97
+ }
98
+ }
99
+ }
@@ -181,7 +181,9 @@ export const prerender = async ({
181
181
  }
182
182
  if (pagefindWriteResult?.outputPath) {
183
183
  logger.info(
184
- colors.blue(`✓ pagefind index built: ${pagefindWriteResult.outputPath}`),
184
+ colors.blue(
185
+ `✓ pagefind index built: ${pagefindWriteResult.outputPath}`,
186
+ ),
185
187
  );
186
188
  }
187
189
  }