@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.
- package/modules/content/module.mjs +17 -2
- package/modules/content/query.d.ts +25 -0
- package/modules/content/query.mjs +105 -19
- package/modules/content/runtime/client.d.ts +2 -0
- package/modules/content/runtime/client.mjs +1 -0
- package/modules/images/ipx.d.ts +2 -0
- package/modules/images/ipx.mjs +55 -0
- package/modules/images/module.d.ts +4 -0
- package/modules/images/module.mjs +146 -0
- package/modules/images/runtime/build.d.ts +6 -0
- package/modules/images/runtime/build.mjs +174 -0
- package/modules/images/runtime/helpers.d.ts +16 -0
- package/modules/images/runtime/helpers.mjs +45 -0
- package/modules/images/runtime/image.d.ts +7 -0
- package/modules/images/runtime/image.mjs +252 -0
- package/modules/images/runtime/renames.d.ts +2 -0
- package/modules/images/runtime/renames.mjs +79 -0
- package/modules/images/runtime/server.d.ts +3 -0
- package/modules/images/runtime/server.mjs +80 -0
- package/modules/images/runtime/types.d.ts +79 -0
- package/modules/images/runtime/types.mjs +0 -0
- package/modules/sitemap/handler.mjs +6 -7
- package/modules/sitemap/module.mjs +236 -22
- package/modules/sitemap/xml.d.ts +7 -0
- package/modules/sitemap/xml.mjs +87 -0
- package/package.json +7 -1
- package/server/lib/pages.mjs +20 -21
- package/server/lib/render.mjs +16 -0
- package/server/renderer.d.ts +1 -1
- package/server/renderer.mjs +2 -1
- package/src/public/components.d.ts +3 -0
- package/src/public/components.mjs +3 -0
- package/src/public/index.d.ts +5 -3
- package/src/public/index.mjs +1 -0
- package/src/public/modules/images.d.ts +2 -0
- package/src/public/modules/images.mjs +1 -0
- package/src/public/nitro.mjs +4 -1
- package/src/public/query-content.d.ts +7 -0
- package/src/public/query-content.mjs +130 -0
- package/src/public/vite.mjs +18 -2
- package/src/runtime/components/MkImage.d.ts +188 -0
- package/src/runtime/components/MkImage.mjs +131 -0
- package/src/runtime/components/MkLink.d.ts +22 -0
- package/src/runtime/components/MkLink.mjs +72 -0
- package/src/runtime/components/MkPicture.d.ts +199 -0
- package/src/runtime/components/MkPicture.mjs +172 -0
- package/src/runtime/components/image-shared.d.ts +28 -0
- package/src/runtime/components/image-shared.mjs +68 -0
- package/src/runtime/config.d.ts +22 -0
- package/src/runtime/config.mjs +5 -2
- package/src/runtime/locale-routing.d.ts +12 -0
- package/src/runtime/locale-routing.mjs +93 -0
- package/src/runtime/modules.mjs +1 -0
- package/src/runtime/page-discovery.mjs +8 -15
- package/src/runtime/pages.d.ts +16 -0
- package/src/runtime/virtual.d.ts +17 -0
- 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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
65
|
+
const normalizedPath = normalizeContentPath(rootDir, filePath);
|
|
45
66
|
if (extension === ".json") {
|
|
46
67
|
const parsed = JSON.parse(raw);
|
|
47
68
|
return {
|
|
48
|
-
|
|
69
|
+
...normalizedPath,
|
|
49
70
|
extension,
|
|
50
71
|
title: typeof parsed.title === "string" ? parsed.title : void 0,
|
|
51
|
-
body:
|
|
52
|
-
data:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
187
|
+
return findContentEntries(entries, query.path);
|
|
102
188
|
}
|
|
103
189
|
return entries;
|
|
104
190
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { queryContent } from "../../../src/public/query-content.mjs";
|
|
@@ -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,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;
|