@monkeyplus/flow 6.0.6 → 6.0.8

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 (57) hide show
  1. package/modules/content/module.mjs +17 -2
  2. package/modules/content/query.d.ts +25 -0
  3. package/modules/content/query.mjs +105 -19
  4. package/modules/content/runtime/client.d.ts +2 -0
  5. package/modules/content/runtime/client.mjs +1 -0
  6. package/modules/images/ipx.d.ts +2 -0
  7. package/modules/images/ipx.mjs +55 -0
  8. package/modules/images/module.d.ts +4 -0
  9. package/modules/images/module.mjs +146 -0
  10. package/modules/images/runtime/build.d.ts +6 -0
  11. package/modules/images/runtime/build.mjs +174 -0
  12. package/modules/images/runtime/helpers.d.ts +16 -0
  13. package/modules/images/runtime/helpers.mjs +45 -0
  14. package/modules/images/runtime/image.d.ts +7 -0
  15. package/modules/images/runtime/image.mjs +252 -0
  16. package/modules/images/runtime/renames.d.ts +2 -0
  17. package/modules/images/runtime/renames.mjs +79 -0
  18. package/modules/images/runtime/server.d.ts +3 -0
  19. package/modules/images/runtime/server.mjs +80 -0
  20. package/modules/images/runtime/types.d.ts +79 -0
  21. package/modules/images/runtime/types.mjs +0 -0
  22. package/modules/sitemap/handler.mjs +6 -7
  23. package/modules/sitemap/module.mjs +236 -22
  24. package/modules/sitemap/xml.d.ts +7 -0
  25. package/modules/sitemap/xml.mjs +87 -0
  26. package/package.json +7 -1
  27. package/server/lib/pages.mjs +20 -21
  28. package/server/lib/render.mjs +16 -0
  29. package/server/renderer.d.ts +1 -1
  30. package/server/renderer.mjs +2 -1
  31. package/src/public/components.d.ts +3 -0
  32. package/src/public/components.mjs +3 -0
  33. package/src/public/index.d.ts +5 -3
  34. package/src/public/index.mjs +1 -0
  35. package/src/public/modules/images.d.ts +2 -0
  36. package/src/public/modules/images.mjs +1 -0
  37. package/src/public/nitro.mjs +4 -1
  38. package/src/public/query-content.d.ts +7 -0
  39. package/src/public/query-content.mjs +130 -0
  40. package/src/public/vite.mjs +18 -2
  41. package/src/runtime/components/MkImage.d.ts +188 -0
  42. package/src/runtime/components/MkImage.mjs +131 -0
  43. package/src/runtime/components/MkLink.d.ts +22 -0
  44. package/src/runtime/components/MkLink.mjs +72 -0
  45. package/src/runtime/components/MkPicture.d.ts +199 -0
  46. package/src/runtime/components/MkPicture.mjs +172 -0
  47. package/src/runtime/components/image-shared.d.ts +28 -0
  48. package/src/runtime/components/image-shared.mjs +68 -0
  49. package/src/runtime/config.d.ts +22 -0
  50. package/src/runtime/config.mjs +5 -2
  51. package/src/runtime/locale-routing.d.ts +12 -0
  52. package/src/runtime/locale-routing.mjs +93 -0
  53. package/src/runtime/modules.mjs +1 -0
  54. package/src/runtime/page-discovery.mjs +8 -15
  55. package/src/runtime/pages.d.ts +16 -0
  56. package/src/runtime/virtual.d.ts +17 -0
  57. package/src/runtime/vue.mjs +6 -0
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
- import { defineFlowModule } from "../../src/runtime/config.mjs";
3
2
  import { resolvePackageFile, resolvePackagePath } from "../../src/public/shared.mjs";
3
+ import { defineFlowModule } from "../../src/runtime/config.mjs";
4
4
  export default defineFlowModule({
5
5
  meta: {
6
6
  name: "content",
@@ -12,21 +12,36 @@ export default defineFlowModule({
12
12
  },
13
13
  setup(options, context) {
14
14
  const contentDir = resolve(context.projectRoot, options.dir);
15
+ const publicConfig = {
16
+ apiBase: options.apiBase
17
+ };
15
18
  const queryHandlerPath = context.projectRoot === resolvePackagePath() ? resolve(context.projectRoot, "modules/content/query.ts") : resolvePackageFile("modules/content/query.ts", "modules/content/query.mjs", "modules/content/query.js");
16
19
  context.nitro.handlers.push({
17
20
  method: "get",
18
21
  route: `${options.apiBase}/query`,
19
22
  handler: queryHandlerPath
20
23
  });
24
+ context.nitro.handlers.push({
25
+ method: "get",
26
+ route: `${options.apiBase}/tree`,
27
+ handler: queryHandlerPath
28
+ });
21
29
  context.nitro.routeRules[`${options.apiBase}/**`] = {
22
30
  cors: true
23
31
  };
24
32
  context.nitro.runtimeConfig.flow = {
25
33
  ...typeof context.nitro.runtimeConfig.flow === "object" && context.nitro.runtimeConfig.flow ? context.nitro.runtimeConfig.flow : {},
26
34
  content: {
27
- apiBase: options.apiBase,
35
+ ...publicConfig,
28
36
  dir: contentDir
29
37
  }
30
38
  };
39
+ context.nitro.runtimeConfig.public = {
40
+ ...typeof context.nitro.runtimeConfig.public === "object" && context.nitro.runtimeConfig.public ? context.nitro.runtimeConfig.public : {},
41
+ flow: {
42
+ ...typeof context.nitro.runtimeConfig.public?.flow === "object" && context.nitro.runtimeConfig.public.flow ? context.nitro.runtimeConfig.public.flow : {},
43
+ content: publicConfig
44
+ }
45
+ };
31
46
  }
32
47
  });
@@ -1,2 +1,27 @@
1
+ export interface ContentEntry {
2
+ path: string;
3
+ stem: string;
4
+ dir: string;
5
+ name: string;
6
+ extension: string;
7
+ title?: string;
8
+ body: string;
9
+ data: Record<string, unknown>;
10
+ }
11
+ export interface ContentDirectoryNode {
12
+ kind: 'directory';
13
+ name: string;
14
+ path: string;
15
+ stem: string;
16
+ children: ContentTreeNode[];
17
+ }
18
+ export interface ContentFileNode extends ContentEntry {
19
+ kind: 'file';
20
+ }
21
+ export type ContentTreeNode = ContentDirectoryNode | ContentFileNode;
22
+ export declare function findContentEntries(entries: ContentEntry[], path?: string): ContentEntry[];
23
+ export declare function readContentEntries(contentDir: string): ContentEntry[];
24
+ export declare function buildContentTree(entries: ContentEntry[]): ContentTreeNode[];
25
+ export declare function findContentTree(tree: ContentTreeNode[], path?: string): ContentTreeNode[];
1
26
  declare const _default: any;
2
27
  export default _default;
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync, readdirSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { extname, relative, resolve } from "node:path";
3
- import { defineEventHandler, getQuery } from "nitro/h3";
3
+ import { defineEventHandler, getQuery, getRequestURL } from "nitro/h3";
4
4
  import { useRuntimeConfig } from "nitro/runtime-config";
5
5
  function collectFiles(rootDir, currentDir = rootDir) {
6
6
  const entries = readdirSync(currentDir, { withFileTypes: true });
@@ -22,9 +22,30 @@ function collectFiles(rootDir, currentDir = rootDir) {
22
22
  }
23
23
  return files.sort((left, right) => left.localeCompare(right));
24
24
  }
25
+ function normalizeQueryPath(path) {
26
+ if (!path) {
27
+ return "/";
28
+ }
29
+ const normalized = `/${path}`.replace(/\/+/g, "/");
30
+ return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
31
+ }
32
+ export function findContentEntries(entries, path) {
33
+ const normalizedPath = normalizeQueryPath(path);
34
+ if (normalizedPath === "/") {
35
+ return entries;
36
+ }
37
+ return entries.filter((entry) => entry.path === normalizedPath || entry.path.startsWith(`${normalizedPath}/`));
38
+ }
25
39
  function normalizeContentPath(rootDir, filePath) {
26
40
  const shortPath = relative(rootDir, filePath).replaceAll("\\", "/");
27
- return `/${shortPath.replace(/\.(md|json|ya?ml|txt)$/i, "")}`;
41
+ const stem = shortPath.replace(/\.(md|json|ya?ml|txt)$/i, "");
42
+ const segments = stem.split("/").filter(Boolean);
43
+ return {
44
+ path: `/${stem}`,
45
+ stem,
46
+ dir: segments.length > 1 ? `/${segments.slice(0, -1).join("/")}` : "/",
47
+ name: segments[segments.length - 1] || stem
48
+ };
28
49
  }
29
50
  function parseKeyValueBlock(block) {
30
51
  return block.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).reduce((data, line) => {
@@ -41,26 +62,21 @@ function parseKeyValueBlock(block) {
41
62
  function parseContentFile(rootDir, filePath) {
42
63
  const raw = readFileSync(filePath, "utf8");
43
64
  const extension = extname(filePath).toLowerCase();
44
- const path = normalizeContentPath(rootDir, filePath);
65
+ const normalizedPath = normalizeContentPath(rootDir, filePath);
45
66
  if (extension === ".json") {
46
67
  const parsed = JSON.parse(raw);
47
68
  return {
48
- path,
69
+ ...normalizedPath,
49
70
  extension,
50
71
  title: typeof parsed.title === "string" ? parsed.title : void 0,
51
- body: JSON.stringify(parsed, null, 2),
52
- data: Object.entries(parsed).reduce((result, [key, value]) => {
53
- if (typeof value === "string") {
54
- result[key] = value;
55
- }
56
- return result;
57
- }, {})
72
+ body: raw,
73
+ data: parsed
58
74
  };
59
75
  }
60
76
  if ((extension === ".yml" || extension === ".yaml") && raw.trim()) {
61
77
  const data = parseKeyValueBlock(raw);
62
78
  return {
63
- path,
79
+ ...normalizedPath,
64
80
  extension,
65
81
  title: data.title,
66
82
  body: raw,
@@ -74,7 +90,7 @@ function parseContentFile(rootDir, filePath) {
74
90
  const body = raw.slice(end + 5).trim();
75
91
  const data = parseKeyValueBlock(frontmatter);
76
92
  return {
77
- path,
93
+ ...normalizedPath,
78
94
  extension,
79
95
  title: data.title,
80
96
  body,
@@ -83,22 +99,92 @@ function parseContentFile(rootDir, filePath) {
83
99
  }
84
100
  }
85
101
  return {
86
- path,
102
+ ...normalizedPath,
87
103
  extension,
88
104
  body: raw,
89
105
  data: {}
90
106
  };
91
107
  }
108
+ export function readContentEntries(contentDir) {
109
+ if (!contentDir || !existsSync(contentDir)) {
110
+ return [];
111
+ }
112
+ return collectFiles(contentDir).map((filePath) => parseContentFile(contentDir, filePath));
113
+ }
114
+ function sortTree(nodes) {
115
+ nodes.sort((left, right) => {
116
+ if (left.kind !== right.kind) {
117
+ return left.kind === "directory" ? -1 : 1;
118
+ }
119
+ return left.name.localeCompare(right.name);
120
+ });
121
+ for (const node of nodes) {
122
+ if (node.kind === "directory") {
123
+ sortTree(node.children);
124
+ }
125
+ }
126
+ return nodes;
127
+ }
128
+ export function buildContentTree(entries) {
129
+ const roots = [];
130
+ const directories = /* @__PURE__ */ new Map();
131
+ for (const entry of entries) {
132
+ const segments = entry.stem.split("/").filter(Boolean);
133
+ const dirSegments = segments.slice(0, -1);
134
+ let siblings = roots;
135
+ for (let index = 0; index < dirSegments.length; index += 1) {
136
+ const stem = dirSegments.slice(0, index + 1).join("/");
137
+ let directory = directories.get(stem);
138
+ if (!directory) {
139
+ directory = {
140
+ kind: "directory",
141
+ name: dirSegments[index],
142
+ path: `/${stem}`,
143
+ stem,
144
+ children: []
145
+ };
146
+ directories.set(stem, directory);
147
+ siblings.push(directory);
148
+ }
149
+ siblings = directory.children;
150
+ }
151
+ siblings.push({
152
+ kind: "file",
153
+ ...entry
154
+ });
155
+ }
156
+ return sortTree(roots);
157
+ }
158
+ export function findContentTree(tree, path) {
159
+ const normalizedPath = normalizeQueryPath(path);
160
+ if (normalizedPath === "/") {
161
+ return tree;
162
+ }
163
+ const stack = [...tree];
164
+ while (stack.length) {
165
+ const node = stack.shift();
166
+ if (node.path === normalizedPath) {
167
+ return [node];
168
+ }
169
+ if (node.kind === "directory") {
170
+ stack.unshift(...node.children);
171
+ }
172
+ }
173
+ return [];
174
+ }
92
175
  export default defineEventHandler((event) => {
93
176
  const runtimeConfig = useRuntimeConfig();
94
177
  const query = getQuery(event);
95
178
  const contentDir = runtimeConfig.flow?.content?.dir;
96
- if (!contentDir || !existsSync(contentDir)) {
97
- return [];
179
+ const requestUrl = getRequestURL(event);
180
+ const isTreeRequest = requestUrl.pathname.endsWith("/tree") || query.tree === true || query.tree === "true" || query.tree === "1";
181
+ const entries = readContentEntries(contentDir || "");
182
+ if (isTreeRequest) {
183
+ const tree = buildContentTree(entries);
184
+ return findContentTree(tree, query.path);
98
185
  }
99
- const entries = collectFiles(contentDir).map((filePath) => parseContentFile(contentDir, filePath));
100
186
  if (query.path) {
101
- return entries.filter((entry) => entry.path === query.path || entry.path.startsWith(`${query.path}/`));
187
+ return findContentEntries(entries, query.path);
102
188
  }
103
189
  return entries;
104
190
  });
@@ -0,0 +1,2 @@
1
+ export { queryContent } from '../../../src/public/query-content.ts';
2
+ export type { QueryContentBuilder } from '../../../src/public/query-content.ts';
@@ -0,0 +1 @@
1
+ export { queryContent } from "../../../src/public/query-content.mjs";
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,55 @@
1
+ import process from "node:process";
2
+ import { createIPX, createIPXH3Handler, ipxFSStorage, ipxHttpStorage } from "ipx";
3
+ import { defineHandler } from "nitro";
4
+ import { getRequestURL } from "nitro/h3";
5
+ import { useRuntimeConfig } from "nitro/runtime-config";
6
+ let cachedDir = "";
7
+ let cachedDomainsKey = "";
8
+ let cachedHandler;
9
+ function getEnvFlowImagesConfig() {
10
+ const raw = process.env.FLOW_IMAGES_RUNTIME_CONFIG;
11
+ if (!raw) {
12
+ return void 0;
13
+ }
14
+ try {
15
+ return JSON.parse(raw);
16
+ } catch {
17
+ return void 0;
18
+ }
19
+ }
20
+ function resolveHandler() {
21
+ const envConfig = getEnvFlowImagesConfig();
22
+ const runtimeConfig = useRuntimeConfig();
23
+ const config = runtimeConfig.flow?.images || envConfig;
24
+ const publicDir = config?.publicDir;
25
+ const domains = Object.keys(config?.options?.domains || {});
26
+ const domainsKey = domains.join("|");
27
+ if (!publicDir) {
28
+ return void 0;
29
+ }
30
+ if (!cachedHandler || cachedDir !== publicDir || cachedDomainsKey !== domainsKey) {
31
+ cachedDir = publicDir;
32
+ cachedDomainsKey = domainsKey;
33
+ const ipx = createIPX({
34
+ storage: ipxFSStorage({ dir: publicDir }),
35
+ ...domains.length ? {
36
+ httpStorage: ipxHttpStorage({ domains })
37
+ } : {}
38
+ });
39
+ cachedHandler = createIPXH3Handler(ipx);
40
+ }
41
+ return cachedHandler;
42
+ }
43
+ export default defineHandler(async (event) => {
44
+ if (!getRequestURL(event)?.pathname.startsWith("/_ipx")) {
45
+ console.log("route", event.req.url.toString());
46
+ return;
47
+ }
48
+ const handler = resolveHandler();
49
+ const path = event.path.replace(/^\/_ipx/, "") || "/";
50
+ return await handler?.({
51
+ ...event,
52
+ path,
53
+ node: event.runtime?.node
54
+ });
55
+ });
@@ -0,0 +1,4 @@
1
+ import type { FlowImagesModuleOptions } from './runtime/types.ts';
2
+ export type { FlowImagesModuleOptions as ImagesModuleOptions } from './runtime/types.ts';
3
+ declare const _default: import("../../src/runtime/config.ts").FlowModuleDefinition<FlowImagesModuleOptions>;
4
+ export default _default;
@@ -0,0 +1,146 @@
1
+ import { rmSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import process from "node:process";
4
+ import { createIPX, createIPXNodeServer, ipxFSStorage, ipxHttpStorage } from "ipx";
5
+ import { resolvePackageFile, resolvePackagePath } from "../../src/public/shared.mjs";
6
+ import { defineFlowModule } from "../../src/runtime/config.mjs";
7
+ import { materializeGeneratedImages } from "./runtime/build.mjs";
8
+ import { screens } from "./runtime/helpers.mjs";
9
+ import { resetFlowImageRuntimeState } from "./runtime/server.mjs";
10
+ function withoutTrailingSlash(value) {
11
+ return value.replace(/\/+$/, "");
12
+ }
13
+ function resolveStrapiDomains(flowConfig, dirImages) {
14
+ const strapi = flowConfig.strapi;
15
+ if (!strapi || typeof strapi.url !== "string" || !strapi.url) {
16
+ return {};
17
+ }
18
+ try {
19
+ const url = new URL(strapi.url);
20
+ const target = dirImages;
21
+ const values = /* @__PURE__ */ new Set([
22
+ withoutTrailingSlash(url.origin),
23
+ withoutTrailingSlash(url.toString())
24
+ ]);
25
+ if (url.pathname && url.pathname !== "/") {
26
+ values.add(withoutTrailingSlash(`${url.origin}${url.pathname}`));
27
+ }
28
+ return [...values].reduce((result, value) => {
29
+ if (value) {
30
+ result[value] = target;
31
+ }
32
+ return result;
33
+ }, {});
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ function createIpxDevServerPlugin(imagesRuntimeConfig) {
39
+ const domains = Object.keys(imagesRuntimeConfig.options.domains || {});
40
+ let middleware;
41
+ return {
42
+ name: "flow:images-ipx-dev",
43
+ apply: "serve",
44
+ configureServer(server) {
45
+ if (!middleware) {
46
+ const ipx = createIPX({
47
+ storage: ipxFSStorage({ dir: imagesRuntimeConfig.publicDir }),
48
+ ...domains.length ? { httpStorage: ipxHttpStorage({ domains }) } : {}
49
+ });
50
+ middleware = createIPXNodeServer(ipx);
51
+ }
52
+ server.middlewares.use("/_ipx", (req, res, next) => {
53
+ try {
54
+ const result = middleware?.(req, res);
55
+ if (result && typeof result.then === "function") {
56
+ void result.catch(next);
57
+ }
58
+ } catch (error) {
59
+ next(error);
60
+ }
61
+ });
62
+ }
63
+ };
64
+ }
65
+ export default defineFlowModule({
66
+ meta: {
67
+ name: "images",
68
+ configKey: "images"
69
+ },
70
+ defaults: {
71
+ dirRenames: "shared/seo_images",
72
+ dirFiles: ["images", "media"],
73
+ buildBatchSize: 8,
74
+ lazy: true,
75
+ screens,
76
+ baseURL: "/_ipx",
77
+ dirImages: "/images",
78
+ domains: {},
79
+ presets: {}
80
+ },
81
+ setup(options, context) {
82
+ const resolvedDomains = {
83
+ ...resolveStrapiDomains(context.flowConfig, options.dirImages),
84
+ ...options.domains || {}
85
+ };
86
+ const isSsg = context.flowConfig.build.preset === "ssg";
87
+ const shouldGenerateOutput = isSsg && process.env.NODE_ENV === "production";
88
+ const isNetlify = !!process.env.NETLIFY;
89
+ const publicDir = resolve(context.projectRoot, "public");
90
+ const renameDir = resolve(context.projectRoot, options.dirRenames);
91
+ const imagesCacheRoot = resolve(context.projectRoot, "node_modules/.cache-images");
92
+ const generatedManifestPath = resolve(imagesCacheRoot, "generated-images.jsonl");
93
+ const generatedCacheDir = isNetlify ? resolve(imagesCacheRoot, "netlify") : void 0;
94
+ const generatedCacheManifestPath = generatedCacheDir ? resolve(generatedCacheDir, "manifest.json") : void 0;
95
+ const ipxHandlerPath = context.projectRoot === resolvePackagePath() ? resolve(context.projectRoot, "modules/images/ipx.ts") : resolvePackageFile("modules/images/ipx.ts", "modules/images/ipx.mjs", "modules/images/ipx.js");
96
+ const imagesRuntimeConfig = {
97
+ dirRenames: renameDir,
98
+ dirFiles: options.dirFiles,
99
+ buildBatchSize: options.buildBatchSize,
100
+ publicDir,
101
+ outputDir: resolve(context.projectRoot, ".output/public"),
102
+ generatedManifestPath,
103
+ generatedCacheDir,
104
+ generatedCacheManifestPath,
105
+ generate: shouldGenerateOutput,
106
+ netlifyCache: isNetlify,
107
+ options: {
108
+ lazy: options.lazy,
109
+ screens: options.screens,
110
+ domains: resolvedDomains,
111
+ presets: options.presets,
112
+ baseURL: options.baseURL,
113
+ dirImages: options.dirImages
114
+ }
115
+ };
116
+ process.env.FLOW_IMAGES_RUNTIME_CONFIG = JSON.stringify(imagesRuntimeConfig);
117
+ if (shouldGenerateOutput) {
118
+ rmSync(generatedManifestPath, { force: true });
119
+ const existingPrerenderDone = context.nitro.hooks["prerender:done"];
120
+ context.nitro.hooks["prerender:done"] = async (...args) => {
121
+ if (typeof existingPrerenderDone === "function") {
122
+ await existingPrerenderDone(...args);
123
+ }
124
+ const result = await materializeGeneratedImages(imagesRuntimeConfig);
125
+ if (result.total > 0) {
126
+ console.log(
127
+ `[flow:images] materialized ${result.total} images (${result.generated} generated, ${result.cacheHits} cache hits${imagesRuntimeConfig.netlifyCache && process.env.NETLIFY ? ", netlify cache enabled" : ""})`
128
+ );
129
+ }
130
+ resetFlowImageRuntimeState();
131
+ };
132
+ }
133
+ context.nitro.handlers.push({
134
+ route: "/_ipx/**",
135
+ handler: ipxHandlerPath
136
+ // middleware: true,
137
+ });
138
+ context.vite.plugins.push(createIpxDevServerPlugin(imagesRuntimeConfig));
139
+ context.nitro.runtimeConfig.flow = {
140
+ ...typeof context.nitro.runtimeConfig.flow === "object" && context.nitro.runtimeConfig.flow ? context.nitro.runtimeConfig.flow : {},
141
+ images: {
142
+ ...imagesRuntimeConfig
143
+ }
144
+ };
145
+ }
146
+ });
@@ -0,0 +1,6 @@
1
+ import type { FlowImagesRuntimeConfig } from './types.ts';
2
+ export declare function materializeGeneratedImages(config: FlowImagesRuntimeConfig): Promise<{
3
+ cacheHits: number;
4
+ generated: number;
5
+ total: number;
6
+ }>;
@@ -0,0 +1,174 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import process from "node:process";
6
+ import { createIPX, ipxFSStorage, ipxHttpStorage } from "ipx";
7
+ const DEFAULT_BUILD_BATCH_SIZE = 8;
8
+ function getGeneratedImageKey(entry) {
9
+ return `${entry.url}:${entry.generate}`;
10
+ }
11
+ function sortRecord(value) {
12
+ return Object.keys(value).sort().reduce((result, key) => {
13
+ result[key] = value[key];
14
+ return result;
15
+ }, {});
16
+ }
17
+ function hashRecord(value) {
18
+ return createHash("sha1").update(JSON.stringify(sortRecord(value))).digest("hex");
19
+ }
20
+ function normalizeOutputPath(path) {
21
+ return path.startsWith("/") ? path.slice(1) : path;
22
+ }
23
+ function outputFilePath(baseDir, path) {
24
+ return resolve(baseDir, normalizeOutputPath(path));
25
+ }
26
+ function createBatches(items, batchSize) {
27
+ const batches = [];
28
+ for (let index = 0; index < items.length; index += batchSize) {
29
+ batches.push(items.slice(index, index + batchSize));
30
+ }
31
+ return batches;
32
+ }
33
+ function resolveBuildBatchSize() {
34
+ const configuredValue = Number(process.env.FLOW_IMAGES_BUILD_BATCH_SIZE);
35
+ if (!Number.isFinite(configuredValue) || configuredValue < 1) {
36
+ return DEFAULT_BUILD_BATCH_SIZE;
37
+ }
38
+ return Math.floor(configuredValue);
39
+ }
40
+ function resolveConfiguredBatchSize(configuredBatchSize) {
41
+ if (Number.isFinite(configuredBatchSize) && configuredBatchSize && configuredBatchSize > 0) {
42
+ return Math.floor(configuredBatchSize);
43
+ }
44
+ return resolveBuildBatchSize();
45
+ }
46
+ async function ensureParentDir(path) {
47
+ await mkdir(dirname(path), { recursive: true });
48
+ }
49
+ async function readJson(path, fallback) {
50
+ try {
51
+ return JSON.parse(await readFile(path, "utf8"));
52
+ } catch {
53
+ return fallback;
54
+ }
55
+ }
56
+ async function readGeneratedManifest(path) {
57
+ if (!path || !existsSync(path)) {
58
+ return [];
59
+ }
60
+ const raw = await readFile(path, "utf8");
61
+ const collection = /* @__PURE__ */ new Map();
62
+ for (const line of raw.split(/\r?\n/)) {
63
+ if (!line.trim()) {
64
+ continue;
65
+ }
66
+ const parsed = JSON.parse(line);
67
+ collection.set(getGeneratedImageKey(parsed), parsed);
68
+ }
69
+ return [...collection.values()];
70
+ }
71
+ async function createSignature(entry, publicDir) {
72
+ if (entry.src.startsWith("http://") || entry.src.startsWith("https://")) {
73
+ return hashRecord({
74
+ generate: entry.generate,
75
+ modifiers: entry.modifiers,
76
+ src: entry.src
77
+ });
78
+ }
79
+ const sourcePath = resolve(publicDir, entry.src.replace(/^\//, ""));
80
+ const sourceStat = await stat(sourcePath);
81
+ return hashRecord({
82
+ generate: entry.generate,
83
+ modifiers: entry.modifiers,
84
+ mtimeMs: sourceStat.mtimeMs,
85
+ size: sourceStat.size,
86
+ src: entry.src
87
+ });
88
+ }
89
+ async function walkFiles(rootDir, currentDir = rootDir, files = []) {
90
+ if (!existsSync(currentDir)) {
91
+ return files;
92
+ }
93
+ for (const entry of await readdir(currentDir, { withFileTypes: true })) {
94
+ const entryPath = join(currentDir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ await walkFiles(rootDir, entryPath, files);
97
+ continue;
98
+ }
99
+ if (entry.isFile()) {
100
+ files.push(entryPath);
101
+ }
102
+ }
103
+ return files;
104
+ }
105
+ async function pruneCache(cacheDir, activeFiles) {
106
+ for (const filePath of await walkFiles(cacheDir)) {
107
+ const relativePath = normalizeOutputPath(filePath.replace(`${cacheDir}/`, "").replace(`${cacheDir}\\`, ""));
108
+ if (!activeFiles.has(relativePath)) {
109
+ await rm(filePath, { force: true });
110
+ }
111
+ }
112
+ }
113
+ export async function materializeGeneratedImages(config) {
114
+ if (!config.generate) {
115
+ return { cacheHits: 0, generated: 0, total: 0 };
116
+ }
117
+ const images = await readGeneratedManifest(config.generatedManifestPath);
118
+ if (!images.length) {
119
+ return { cacheHits: 0, generated: 0, total: 0 };
120
+ }
121
+ const domains = Object.keys(config.options.domains || {});
122
+ const ipx = createIPX({
123
+ storage: ipxFSStorage({ dir: config.publicDir }),
124
+ ...domains.length ? { httpStorage: ipxHttpStorage({ domains }) } : {}
125
+ });
126
+ const previousManifest = config.netlifyCache && config.generatedCacheManifestPath ? await readJson(config.generatedCacheManifestPath, {}) : {};
127
+ const nextManifest = {};
128
+ const activeCacheFiles = /* @__PURE__ */ new Set();
129
+ let cacheHits = 0;
130
+ let generated = 0;
131
+ const batchSize = resolveConfiguredBatchSize(config.buildBatchSize);
132
+ for (const batch of createBatches(images, batchSize)) {
133
+ const results = await Promise.all(batch.map(async (image) => {
134
+ const signature = await createSignature(image, config.publicDir);
135
+ const imageKey = getGeneratedImageKey(image);
136
+ nextManifest[imageKey] = {
137
+ ...image,
138
+ signature
139
+ };
140
+ const outputPath = outputFilePath(config.outputDir, image.generate);
141
+ const cacheRelativePath = normalizeOutputPath(image.generate);
142
+ const cachePath = config.generatedCacheDir ? outputFilePath(config.generatedCacheDir, cacheRelativePath) : void 0;
143
+ activeCacheFiles.add(cacheRelativePath);
144
+ await ensureParentDir(outputPath);
145
+ if (config.netlifyCache && cachePath && previousManifest[imageKey]?.signature === signature && existsSync(cachePath)) {
146
+ await copyFile(cachePath, outputPath);
147
+ return { cacheHit: 1, generated: 0 };
148
+ }
149
+ const processed = await ipx(image.src, image.modifiers).process();
150
+ const data = typeof processed.data === "string" ? Buffer.from(processed.data) : processed.data;
151
+ await writeFile(outputPath, data);
152
+ if (config.netlifyCache && cachePath) {
153
+ await ensureParentDir(cachePath);
154
+ await writeFile(cachePath, data);
155
+ }
156
+ return { cacheHit: 0, generated: 1 };
157
+ }));
158
+ for (const result of results) {
159
+ cacheHits += result.cacheHit;
160
+ generated += result.generated;
161
+ }
162
+ }
163
+ if (config.netlifyCache && config.generatedCacheDir && config.generatedCacheManifestPath) {
164
+ await mkdir(config.generatedCacheDir, { recursive: true });
165
+ await pruneCache(config.generatedCacheDir, activeCacheFiles);
166
+ await writeFile(config.generatedCacheManifestPath, `${JSON.stringify(nextManifest, null, 2)}
167
+ `, "utf8");
168
+ }
169
+ return {
170
+ cacheHits,
171
+ generated,
172
+ total: images.length
173
+ };
174
+ }
@@ -0,0 +1,16 @@
1
+ import type { FlowImageOptions, ImageOptions } from './types.ts';
2
+ export declare const screens: {
3
+ xs: number;
4
+ sm: number;
5
+ md: number;
6
+ lg: number;
7
+ xl: number;
8
+ xxl: number;
9
+ };
10
+ export declare function parseSize(input?: string | number | undefined): number | undefined;
11
+ export declare function getNormalName(originalName: string): string;
12
+ export declare function getFileExtension(url?: string): string;
13
+ export declare function guessExt(input?: string): string;
14
+ export declare function getPreset(ctx: {
15
+ options: FlowImageOptions;
16
+ }, name?: string): ImageOptions;