@lukoweb/apitogo 0.2.60 → 0.2.61

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.
Files changed (43) hide show
  1. package/dist/cli/cli.js +893 -527
  2. package/docs/cli/commands.md +6 -6
  3. package/docs/cli/go-live.md +12 -12
  4. package/docs/cli/overview.md +26 -27
  5. package/docs/cli/setup.md +38 -46
  6. package/docs/cli/shape.md +32 -35
  7. package/docs/cli/walkthrough.md +19 -19
  8. package/docs/components/slot.mdx +116 -116
  9. package/docs/configuration/api-reference.md +1 -1
  10. package/docs/configuration/authentication.md +3 -3
  11. package/docs/configuration/footer.mdx +89 -89
  12. package/docs/configuration/gateway.md +8 -8
  13. package/docs/configuration/make-config.mdx +20 -10
  14. package/docs/configuration/overview.md +3 -3
  15. package/docs/configuration/plans.md +5 -5
  16. package/docs/configuration/portal.md +36 -0
  17. package/docs/customization/colors-theme.mdx +255 -255
  18. package/docs/customization/fonts.md +105 -105
  19. package/docs/customization/landing-page.md +1 -1
  20. package/docs/deploy/apitogo.md +3 -3
  21. package/docs/deployment.md +3 -3
  22. package/docs/guides/custom-pages.md +3 -3
  23. package/docs/guides/customer-account.md +1 -1
  24. package/docs/guides/stripe-setup.mdx +9 -9
  25. package/docs/markdown/frontmatter.md +2 -2
  26. package/docs/mcp/go-live.md +7 -7
  27. package/docs/mcp/overview.md +58 -58
  28. package/docs/mcp/setup.md +6 -6
  29. package/docs/mcp/shape.md +2 -2
  30. package/docs/mcp/tools.md +57 -57
  31. package/docs/quickstart.md +6 -4
  32. package/docs/writing.mdx +68 -68
  33. package/package.json +1 -1
  34. package/src/config/apitogo-config-core.ts +11 -1
  35. package/src/config/v2/resolve.ts +19 -0
  36. package/src/config/v2/rules.ts +88 -0
  37. package/src/config/v2/schema.ts +28 -0
  38. package/src/config/v2/wrapper.ts +76 -13
  39. package/src/vite/config.ts +278 -273
  40. package/src/vite/markdown-output-path.ts +48 -0
  41. package/src/vite/plugin-markdown-export.ts +210 -214
  42. package/src/vite/prerender/prerender.ts +4 -4
  43. package/src/vite/watch-ignore.ts +19 -0
@@ -1,273 +1,278 @@
1
- import path from "node:path";
2
- import dotenv from "dotenv";
3
- import colors from "picocolors";
4
- import {
5
- type ConfigEnv,
6
- type InlineConfig,
7
- type LogLevel,
8
- loadConfigFromFile,
9
- mergeConfig,
10
- } from "vite";
11
- import packageJson from "../../package.json" with { type: "json" };
12
- import { ZuploEnv } from "../app/env.js";
13
- import { logger } from "../cli/common/logger.js";
14
- import { getZudokuRootDir } from "../cli/common/package-json.js";
15
- import { loadZudokuConfig } from "../config/loader.js";
16
- import { CdnUrlSchema } from "../config/validators/ZudokuConfig.js";
17
- import { joinUrl } from "../lib/util/joinUrl.js";
18
- import { loadDevPortalViteDefines } from "./dev-portal-env.js";
19
- import { findPackageRoot } from "./package-root.js";
20
- import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
21
- import vitePlugin from "./plugin.js";
22
- import { devServerAllow, stateIgnore } from "./watch-ignore.js";
23
- import { getZuploSystemConfigurations } from "./zuplo.js";
24
-
25
- /** Resolved absolute public directory (Vite default: `<root>/public`). */
26
- const resolveMergedPublicDir = (
27
- rootDir: string,
28
- merged: InlineConfig,
29
- ): string | undefined => {
30
- if (merged.publicDir === false) return undefined;
31
- const rel =
32
- typeof merged.publicDir === "string" ? merged.publicDir : "public";
33
- return path.resolve(rootDir, rel);
34
- };
35
-
36
- export type ZudokuConfigEnv = ConfigEnv & {
37
- mode: "development" | "production";
38
- };
39
-
40
- export const getAppClientEntryPath = () =>
41
- path.posix.join(getZudokuRootDir(), "src/app/entry.client.tsx");
42
-
43
- export const getAppServerEntryPath = () =>
44
- path.posix.join(getZudokuRootDir(), "src/app/entry.server.tsx");
45
-
46
- // the vite config gets loaded multiple times, so we only log the CDN info once
47
- let hasLoggedCdnInfo = false;
48
- const MEDIA_REGEX =
49
- /\.(a?png|jpe?g|gif|bmp|svg|webp|tiff|ico|webm|ogg|mp3|wav|m4a|avif|mp4)/i;
50
-
51
- const defineEnvVars = (vars: string[]) =>
52
- Object.fromEntries(
53
- vars.flatMap((v) => [
54
- [`process.env.${v}`, JSON.stringify(process.env[v])],
55
- [`import.meta.env.${v}`, JSON.stringify(process.env[v])],
56
- ]),
57
- );
58
-
59
- export async function getViteConfig(
60
- dir: string,
61
- configEnv: ZudokuConfigEnv,
62
- ): Promise<InlineConfig> {
63
- const { config, publicEnv, envPrefix } = await loadZudokuConfig(
64
- configEnv,
65
- dir,
66
- );
67
-
68
- const cdnUrl = CdnUrlSchema.parse(config.cdnUrl);
69
-
70
- const base = cdnUrl?.base
71
- ? joinUrl(cdnUrl.base, config.basePath)
72
- : config.basePath;
73
-
74
- if (cdnUrl && !hasLoggedCdnInfo) {
75
- logger.info(colors.blue(`Using CDN URL:`));
76
- logger.info(colors.blue(` base: `) + colors.dim(cdnUrl.base));
77
- logger.info(colors.blue(` media: `) + colors.dim(cdnUrl.media));
78
- hasLoggedCdnInfo = true;
79
- }
80
-
81
- // We define public env vars as `process.env` vars because Vite only exposes them as `import.meta.env` vars
82
- const publicVarsProcessEnvDefine = Object.fromEntries(
83
- Object.entries(publicEnv).map(([key, value]) => [
84
- `process.env.${key}`,
85
- value,
86
- ]),
87
- );
88
-
89
- // The workspace of a config version 2 portal links back to its own project; the watcher must not follow that.
90
- const ignoreState = stateIgnore(dir);
91
-
92
- if (ZuploEnv.isZuplo) {
93
- dotenv.config({
94
- path: path.resolve(config.__meta.rootDir, "../.env.zuplo"),
95
- quiet: true,
96
- });
97
- }
98
-
99
- const deploymentName =
100
- ZuploEnv.buildConfig?.deploymentName ||
101
- getZuploSystemConfigurations(process.env.ZUPLO_SYSTEM_CONFIGURATIONS)
102
- ?.__ZUPLO_DEPLOYMENT_NAME;
103
-
104
- const devPortalDefines = await loadDevPortalViteDefines(dir, configEnv.mode);
105
-
106
- const viteConfig: InlineConfig = {
107
- root: dir,
108
- base,
109
- appType: "custom",
110
- configFile: false,
111
- clearScreen: false,
112
- logLevel: (process.env.LOG_LEVEL ?? "info") as LogLevel,
113
- customLogger: logger,
114
- envPrefix,
115
- resolve: {
116
- dedupe: ["react", "react-dom"],
117
- alias: {
118
- "@mdx-js/react": import.meta.resolve("@mdx-js/react"),
119
- },
120
- },
121
- define: {
122
- "process.env.ZUDOKU_VERSION": JSON.stringify(packageJson.version),
123
- "process.env.IS_ZUPLO": ZuploEnv.isZuplo,
124
- "import.meta.env.IS_ZUPLO": ZuploEnv.isZuplo,
125
- "import.meta.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME":
126
- JSON.stringify(deploymentName),
127
- ...defineEnvVars([
128
- "SENTRY_DSN",
129
- "ZUPLO_BUILD_ID",
130
- "ZUPLO_BUILD_CONFIG",
131
- "ZUPLO_ENVIRONMENT_TYPE",
132
- "ZUPLO_SERVER_URL",
133
- "ZUPLO_GATEWAY_SERVICE_URL",
134
- ]),
135
- ...publicVarsProcessEnvDefine,
136
- ...devPortalDefines,
137
- },
138
- ssr: {
139
- target: "node",
140
- noExternal: [/apitogo/, "@mdx-js/react"],
141
- external: ["@shikijs/themes", "@shikijs/langs"],
142
- },
143
- server: {
144
- middlewareMode: true,
145
- open: true,
146
- fs: {
147
- allow: devServerAllow(dir, config.__meta.moduleDir),
148
- },
149
- watch: {
150
- ignored: [
151
- `${dir}/dist`,
152
- `${dir}/lib`,
153
- `${dir}/.git`,
154
- `${config.__meta.moduleDir}/src/vite`,
155
- `${config.__meta.moduleDir}/src/cli`,
156
- ...(ignoreState ? [ignoreState] : []),
157
- ],
158
- },
159
- },
160
- build: {
161
- ssr: configEnv.isSsrBuild,
162
- sourcemap: true,
163
- target: "es2022",
164
- outDir: path.resolve(
165
- path.join(
166
- dir,
167
- "dist",
168
- config.basePath ?? "",
169
- configEnv.isSsrBuild ? "server" : "",
170
- ),
171
- ),
172
- emptyOutDir: true,
173
- rollupOptions: {
174
- input:
175
- configEnv.command === "build"
176
- ? configEnv.isSsrBuild
177
- ? [
178
- "@lukoweb/apitogo/app/entry.server.tsx",
179
- config.__meta.configPath,
180
- ]
181
- : "@lukoweb/apitogo/app/entry.client.tsx"
182
- : undefined,
183
- external: [joinUrl(config.basePath, "/pagefind/pagefind.js")],
184
- },
185
- chunkSizeWarningLimit: 1500,
186
- },
187
- experimental: {
188
- renderBuiltUrl(filename) {
189
- if (cdnUrl?.base && [".js", ".css"].includes(path.extname(filename))) {
190
- return joinUrl(cdnUrl.base, filename);
191
- }
192
-
193
- if (cdnUrl?.media && MEDIA_REGEX.test(filename)) {
194
- return joinUrl(cdnUrl.media, filename);
195
- }
196
-
197
- return { relative: true };
198
- },
199
- },
200
- optimizeDeps: {
201
- esbuildOptions: {
202
- target: "es2022",
203
- },
204
- entries: [path.posix.join(getZudokuRootDir(), "src/{app,lib}/**")],
205
- exclude: [
206
- "@lukoweb/apitogo",
207
- "@lukoweb/apitogo-plugin-dev-portal-billing",
208
- ],
209
- include: [
210
- "react-dom/client",
211
- "@lukoweb/apitogo/icons",
212
- ...(process.env.SENTRY_DSN ? ["@sentry/react"] : []),
213
- ],
214
- },
215
- assetsInclude: [
216
- // Workaround for Pre-transform error for "virtual" file: https://github.com/vitejs/vite/issues/15374
217
- "/__z/entry.client.tsx",
218
- "**/pagefind.js",
219
- ],
220
- plugins: [vitePlugin()],
221
- future: {
222
- removeServerModuleGraph: "warn",
223
- removeSsrLoadModule: "warn",
224
- removeServerTransformRequest: "warn",
225
- removePluginHookHandleHotUpdate: "warn",
226
- removePluginHookSsrArgument: "warn",
227
- removeServerHot: "warn",
228
- },
229
- };
230
-
231
- // Merge vite configs from plugin directories and user's project
232
- const configRoots = await Promise.all(
233
- (config.__pluginDirs ?? []).map(findPackageRoot),
234
- );
235
- configRoots.push(dir);
236
-
237
- let mergedViteConfig = viteConfig;
238
-
239
- for (const root of configRoots) {
240
- if (!root) continue;
241
- const loaded = await loadConfigFromFile(
242
- configEnv,
243
- undefined,
244
- root,
245
- undefined,
246
- undefined,
247
- "runner",
248
- );
249
- if (!loaded) continue;
250
-
251
- mergedViteConfig = mergeConfig(mergedViteConfig, loaded.config, true);
252
-
253
- if (root === dir) {
254
- logger.info(colors.blue(`merged with custom user Vite config`), {
255
- timestamp: true,
256
- });
257
- }
258
- }
259
-
260
- // Create pagefind stub before Vite starts. In development, `optimizeDeps.entries` crawls all of
261
- // `src/lib/**`, which includes PagefindSearch's dev import of `/pagefind/pagefind.js`; esbuild
262
- // must be able to open that path even when search is not configured. Pagefind users still get
263
- // the stub until a real index is written (dev index or production prerender).
264
- const publicDir = resolveMergedPublicDir(dir, mergedViteConfig);
265
- if (
266
- publicDir &&
267
- (configEnv.mode === "development" || config.search?.type === "pagefind")
268
- ) {
269
- await ensurePagefindDevStub(publicDir);
270
- }
271
-
272
- return mergedViteConfig;
273
- }
1
+ import path from "node:path";
2
+ import dotenv from "dotenv";
3
+ import colors from "picocolors";
4
+ import {
5
+ type ConfigEnv,
6
+ type InlineConfig,
7
+ type LogLevel,
8
+ loadConfigFromFile,
9
+ mergeConfig,
10
+ } from "vite";
11
+ import packageJson from "../../package.json" with { type: "json" };
12
+ import { ZuploEnv } from "../app/env.js";
13
+ import { logger } from "../cli/common/logger.js";
14
+ import { getZudokuRootDir } from "../cli/common/package-json.js";
15
+ import { loadZudokuConfig } from "../config/loader.js";
16
+ import { CdnUrlSchema } from "../config/validators/ZudokuConfig.js";
17
+ import { joinUrl } from "../lib/util/joinUrl.js";
18
+ import { loadDevPortalViteDefines } from "./dev-portal-env.js";
19
+ import { findPackageRoot } from "./package-root.js";
20
+ import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
21
+ import vitePlugin from "./plugin.js";
22
+ import {
23
+ devServerAllow,
24
+ nestedStateIgnore,
25
+ stateIgnore,
26
+ } from "./watch-ignore.js";
27
+ import { getZuploSystemConfigurations } from "./zuplo.js";
28
+
29
+ /** Resolved absolute public directory (Vite default: `<root>/public`). */
30
+ const resolveMergedPublicDir = (
31
+ rootDir: string,
32
+ merged: InlineConfig,
33
+ ): string | undefined => {
34
+ if (merged.publicDir === false) return undefined;
35
+ const rel =
36
+ typeof merged.publicDir === "string" ? merged.publicDir : "public";
37
+ return path.resolve(rootDir, rel);
38
+ };
39
+
40
+ export type ZudokuConfigEnv = ConfigEnv & {
41
+ mode: "development" | "production";
42
+ };
43
+
44
+ export const getAppClientEntryPath = () =>
45
+ path.posix.join(getZudokuRootDir(), "src/app/entry.client.tsx");
46
+
47
+ export const getAppServerEntryPath = () =>
48
+ path.posix.join(getZudokuRootDir(), "src/app/entry.server.tsx");
49
+
50
+ // the vite config gets loaded multiple times, so we only log the CDN info once
51
+ let hasLoggedCdnInfo = false;
52
+ const MEDIA_REGEX =
53
+ /\.(a?png|jpe?g|gif|bmp|svg|webp|tiff|ico|webm|ogg|mp3|wav|m4a|avif|mp4)/i;
54
+
55
+ const defineEnvVars = (vars: string[]) =>
56
+ Object.fromEntries(
57
+ vars.flatMap((v) => [
58
+ [`process.env.${v}`, JSON.stringify(process.env[v])],
59
+ [`import.meta.env.${v}`, JSON.stringify(process.env[v])],
60
+ ]),
61
+ );
62
+
63
+ export async function getViteConfig(
64
+ dir: string,
65
+ configEnv: ZudokuConfigEnv,
66
+ ): Promise<InlineConfig> {
67
+ const { config, publicEnv, envPrefix } = await loadZudokuConfig(
68
+ configEnv,
69
+ dir,
70
+ );
71
+
72
+ const cdnUrl = CdnUrlSchema.parse(config.cdnUrl);
73
+
74
+ const base = cdnUrl?.base
75
+ ? joinUrl(cdnUrl.base, config.basePath)
76
+ : config.basePath;
77
+
78
+ if (cdnUrl && !hasLoggedCdnInfo) {
79
+ logger.info(colors.blue(`Using CDN URL:`));
80
+ logger.info(colors.blue(` base: `) + colors.dim(cdnUrl.base));
81
+ logger.info(colors.blue(` media: `) + colors.dim(cdnUrl.media));
82
+ hasLoggedCdnInfo = true;
83
+ }
84
+
85
+ // We define public env vars as `process.env` vars because Vite only exposes them as `import.meta.env` vars
86
+ const publicVarsProcessEnvDefine = Object.fromEntries(
87
+ Object.entries(publicEnv).map(([key, value]) => [
88
+ `process.env.${key}`,
89
+ value,
90
+ ]),
91
+ );
92
+
93
+ // The workspace of a config version 2 portal links back to its own project; the watcher must not follow that.
94
+ const ignoreState = stateIgnore(dir);
95
+
96
+ if (ZuploEnv.isZuplo) {
97
+ dotenv.config({
98
+ path: path.resolve(config.__meta.rootDir, "../.env.zuplo"),
99
+ quiet: true,
100
+ });
101
+ }
102
+
103
+ const deploymentName =
104
+ ZuploEnv.buildConfig?.deploymentName ||
105
+ getZuploSystemConfigurations(process.env.ZUPLO_SYSTEM_CONFIGURATIONS)
106
+ ?.__ZUPLO_DEPLOYMENT_NAME;
107
+
108
+ const devPortalDefines = await loadDevPortalViteDefines(dir, configEnv.mode);
109
+
110
+ const viteConfig: InlineConfig = {
111
+ root: dir,
112
+ base,
113
+ appType: "custom",
114
+ configFile: false,
115
+ clearScreen: false,
116
+ logLevel: (process.env.LOG_LEVEL ?? "info") as LogLevel,
117
+ customLogger: logger,
118
+ envPrefix,
119
+ resolve: {
120
+ dedupe: ["react", "react-dom"],
121
+ alias: {
122
+ "@mdx-js/react": import.meta.resolve("@mdx-js/react"),
123
+ },
124
+ },
125
+ define: {
126
+ "process.env.ZUDOKU_VERSION": JSON.stringify(packageJson.version),
127
+ "process.env.IS_ZUPLO": ZuploEnv.isZuplo,
128
+ "import.meta.env.IS_ZUPLO": ZuploEnv.isZuplo,
129
+ "import.meta.env.ZUPLO_PUBLIC_DEPLOYMENT_NAME":
130
+ JSON.stringify(deploymentName),
131
+ ...defineEnvVars([
132
+ "SENTRY_DSN",
133
+ "ZUPLO_BUILD_ID",
134
+ "ZUPLO_BUILD_CONFIG",
135
+ "ZUPLO_ENVIRONMENT_TYPE",
136
+ "ZUPLO_SERVER_URL",
137
+ "ZUPLO_GATEWAY_SERVICE_URL",
138
+ ]),
139
+ ...publicVarsProcessEnvDefine,
140
+ ...devPortalDefines,
141
+ },
142
+ ssr: {
143
+ target: "node",
144
+ noExternal: [/apitogo/, "@mdx-js/react"],
145
+ external: ["@shikijs/themes", "@shikijs/langs"],
146
+ },
147
+ server: {
148
+ middlewareMode: true,
149
+ open: true,
150
+ fs: {
151
+ allow: devServerAllow(dir, config.__meta.moduleDir),
152
+ },
153
+ watch: {
154
+ ignored: [
155
+ `${dir}/dist`,
156
+ `${dir}/lib`,
157
+ `${dir}/.git`,
158
+ `${config.__meta.moduleDir}/src/vite`,
159
+ `${config.__meta.moduleDir}/src/cli`,
160
+ nestedStateIgnore(dir),
161
+ ...(ignoreState ? [ignoreState] : []),
162
+ ],
163
+ },
164
+ },
165
+ build: {
166
+ ssr: configEnv.isSsrBuild,
167
+ sourcemap: true,
168
+ target: "es2022",
169
+ outDir: path.resolve(
170
+ path.join(
171
+ dir,
172
+ "dist",
173
+ config.basePath ?? "",
174
+ configEnv.isSsrBuild ? "server" : "",
175
+ ),
176
+ ),
177
+ emptyOutDir: true,
178
+ rollupOptions: {
179
+ input:
180
+ configEnv.command === "build"
181
+ ? configEnv.isSsrBuild
182
+ ? [
183
+ "@lukoweb/apitogo/app/entry.server.tsx",
184
+ config.__meta.configPath,
185
+ ]
186
+ : "@lukoweb/apitogo/app/entry.client.tsx"
187
+ : undefined,
188
+ external: [joinUrl(config.basePath, "/pagefind/pagefind.js")],
189
+ },
190
+ chunkSizeWarningLimit: 1500,
191
+ },
192
+ experimental: {
193
+ renderBuiltUrl(filename) {
194
+ if (cdnUrl?.base && [".js", ".css"].includes(path.extname(filename))) {
195
+ return joinUrl(cdnUrl.base, filename);
196
+ }
197
+
198
+ if (cdnUrl?.media && MEDIA_REGEX.test(filename)) {
199
+ return joinUrl(cdnUrl.media, filename);
200
+ }
201
+
202
+ return { relative: true };
203
+ },
204
+ },
205
+ optimizeDeps: {
206
+ esbuildOptions: {
207
+ target: "es2022",
208
+ },
209
+ entries: [path.posix.join(getZudokuRootDir(), "src/{app,lib}/**")],
210
+ exclude: [
211
+ "@lukoweb/apitogo",
212
+ "@lukoweb/apitogo-plugin-dev-portal-billing",
213
+ ],
214
+ include: [
215
+ "react-dom/client",
216
+ "@lukoweb/apitogo/icons",
217
+ ...(process.env.SENTRY_DSN ? ["@sentry/react"] : []),
218
+ ],
219
+ },
220
+ assetsInclude: [
221
+ // Workaround for Pre-transform error for "virtual" file: https://github.com/vitejs/vite/issues/15374
222
+ "/__z/entry.client.tsx",
223
+ "**/pagefind.js",
224
+ ],
225
+ plugins: [vitePlugin()],
226
+ future: {
227
+ removeServerModuleGraph: "warn",
228
+ removeSsrLoadModule: "warn",
229
+ removeServerTransformRequest: "warn",
230
+ removePluginHookHandleHotUpdate: "warn",
231
+ removePluginHookSsrArgument: "warn",
232
+ removeServerHot: "warn",
233
+ },
234
+ };
235
+
236
+ // Merge vite configs from plugin directories and user's project
237
+ const configRoots = await Promise.all(
238
+ (config.__pluginDirs ?? []).map(findPackageRoot),
239
+ );
240
+ configRoots.push(dir);
241
+
242
+ let mergedViteConfig = viteConfig;
243
+
244
+ for (const root of configRoots) {
245
+ if (!root) continue;
246
+ const loaded = await loadConfigFromFile(
247
+ configEnv,
248
+ undefined,
249
+ root,
250
+ undefined,
251
+ undefined,
252
+ "runner",
253
+ );
254
+ if (!loaded) continue;
255
+
256
+ mergedViteConfig = mergeConfig(mergedViteConfig, loaded.config, true);
257
+
258
+ if (root === dir) {
259
+ logger.info(colors.blue(`merged with custom user Vite config`), {
260
+ timestamp: true,
261
+ });
262
+ }
263
+ }
264
+
265
+ // Create pagefind stub before Vite starts. In development, `optimizeDeps.entries` crawls all of
266
+ // `src/lib/**`, which includes PagefindSearch's dev import of `/pagefind/pagefind.js`; esbuild
267
+ // must be able to open that path even when search is not configured. Pagefind users still get
268
+ // the stub until a real index is written (dev index or production prerender).
269
+ const publicDir = resolveMergedPublicDir(dir, mergedViteConfig);
270
+ if (
271
+ publicDir &&
272
+ (configEnv.mode === "development" || config.search?.type === "pagefind")
273
+ ) {
274
+ await ensurePagefindDevStub(publicDir);
275
+ }
276
+
277
+ return mergedViteConfig;
278
+ }
@@ -0,0 +1,48 @@
1
+ import { rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Where the build writes the exported Markdown copy of the page at `routePath`: `<distDir>/<route>.md`, or
6
+ * `<distDir>/index.md` for `/`. The markdown export plugin writes it and prerender removes it again when the copies
7
+ * were only made for llms.txt, so both must use this one function.
8
+ */
9
+ export const markdownOutputPath = (
10
+ distDir: string,
11
+ routePath: string,
12
+ ): string => {
13
+ const segments =
14
+ routePath === "/" ? ["index"] : routePath.split("/").filter(Boolean);
15
+ return `${path.join(distDir, ...segments)}.md`;
16
+ };
17
+
18
+ /**
19
+ * The exported copy to remove after the build, or `undefined` when the path would leave `distDir` (a route with
20
+ * `..` in it, say). Prerender must only ever delete what the build wrote, never a source page.
21
+ */
22
+ export const removableMarkdownOutput = (
23
+ distDir: string,
24
+ routePath: string,
25
+ ): string | undefined => {
26
+ const output = path.resolve(markdownOutputPath(distDir, routePath));
27
+ const relative = path.relative(path.resolve(distDir), output);
28
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
29
+ return undefined;
30
+ }
31
+ return output;
32
+ };
33
+
34
+ /**
35
+ * Remove the exported `.md` copies of these pages from `distDir`. Only the route decides what is removed; the source
36
+ * file (`filePath` in markdown-info.json) is never touched.
37
+ */
38
+ export const removeExportedMarkdown = async (
39
+ distDir: string,
40
+ pages: ReadonlyArray<{ routePath: string }>,
41
+ ): Promise<void> => {
42
+ await Promise.all(
43
+ pages
44
+ .map((page) => removableMarkdownOutput(distDir, page.routePath))
45
+ .filter((output) => output !== undefined)
46
+ .map((output) => rm(output, { force: true }).catch(() => {})),
47
+ );
48
+ };