@monkeyplus/flow 6.0.6 → 6.0.7
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 +5 -0
- package/modules/images/ipx.mjs +31 -0
- package/modules/images/module.d.ts +4 -0
- package/modules/images/module.mjs +96 -0
- package/modules/images/plugin.d.ts +2 -0
- package/modules/images/plugin.mjs +19 -0
- package/modules/images/runtime/build.d.ts +6 -0
- package/modules/images/runtime/build.mjs +142 -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 +235 -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 +68 -0
- package/modules/images/runtime/types.d.ts +77 -0
- package/modules/images/runtime/types.mjs +0 -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/query-content.d.ts +7 -0
- package/src/public/query-content.mjs +127 -0
- package/src/public/vite.mjs +18 -2
- package/src/runtime/components/MkImage.d.ts +188 -0
- package/src/runtime/components/MkImage.mjs +130 -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 +171 -0
- package/src/runtime/components/image-shared.d.ts +27 -0
- package/src/runtime/components/image-shared.mjs +51 -0
- package/src/runtime/config.d.ts +18 -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/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,31 @@
|
|
|
1
|
+
import { createIPX, createIPXH3Handler, ipxFSStorage, ipxHttpStorage } from "ipx";
|
|
2
|
+
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
3
|
+
let cachedDir = "";
|
|
4
|
+
let cachedDomainsKey = "";
|
|
5
|
+
let cachedHandler;
|
|
6
|
+
function resolveHandler() {
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const publicDir = config.flow?.images?.publicDir;
|
|
9
|
+
const domains = Object.keys(config.flow?.images?.options?.domains || {});
|
|
10
|
+
const domainsKey = domains.join("|");
|
|
11
|
+
if (!publicDir) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
if (!cachedHandler || cachedDir !== publicDir || cachedDomainsKey !== domainsKey) {
|
|
15
|
+
cachedDir = publicDir;
|
|
16
|
+
cachedDomainsKey = domainsKey;
|
|
17
|
+
const ipx = createIPX({
|
|
18
|
+
storage: ipxFSStorage({ dir: publicDir }),
|
|
19
|
+
...domains.length ? { httpStorage: ipxHttpStorage({ domains }) } : {}
|
|
20
|
+
});
|
|
21
|
+
cachedHandler = createIPXH3Handler(ipx);
|
|
22
|
+
}
|
|
23
|
+
return cachedHandler;
|
|
24
|
+
}
|
|
25
|
+
export default function ipxHandler(event) {
|
|
26
|
+
const handler = resolveHandler();
|
|
27
|
+
if (!handler) {
|
|
28
|
+
return new Response("Not Found", { status: 404 });
|
|
29
|
+
}
|
|
30
|
+
return handler(event);
|
|
31
|
+
}
|
|
@@ -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,96 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { joinURL } from "ufo";
|
|
4
|
+
import { resolvePackageFile, resolvePackagePath } from "../../src/public/shared.mjs";
|
|
5
|
+
import { defineFlowModule } from "../../src/runtime/config.mjs";
|
|
6
|
+
import { screens } from "./runtime/helpers.mjs";
|
|
7
|
+
function withoutTrailingSlash(value) {
|
|
8
|
+
return value.replace(/\/+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function resolveStrapiDomains(flowConfig, dirImages) {
|
|
11
|
+
const strapi = flowConfig.strapi;
|
|
12
|
+
if (!strapi || typeof strapi.url !== "string" || !strapi.url) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(strapi.url);
|
|
17
|
+
const target = joinURL(dirImages, "strapi");
|
|
18
|
+
const values = /* @__PURE__ */ new Set([
|
|
19
|
+
withoutTrailingSlash(url.origin),
|
|
20
|
+
withoutTrailingSlash(url.toString())
|
|
21
|
+
]);
|
|
22
|
+
if (url.pathname && url.pathname !== "/") {
|
|
23
|
+
values.add(withoutTrailingSlash(`${url.origin}${url.pathname}`));
|
|
24
|
+
}
|
|
25
|
+
return [...values].reduce((result, value) => {
|
|
26
|
+
if (value) {
|
|
27
|
+
result[value] = target;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}, {});
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export default defineFlowModule({
|
|
36
|
+
meta: {
|
|
37
|
+
name: "images",
|
|
38
|
+
configKey: "images"
|
|
39
|
+
},
|
|
40
|
+
defaults: {
|
|
41
|
+
dirRenames: "shared/seo_images",
|
|
42
|
+
dirFiles: ["images", "media"],
|
|
43
|
+
lazy: true,
|
|
44
|
+
screens,
|
|
45
|
+
baseURL: "/_ipx",
|
|
46
|
+
dirImages: "/images",
|
|
47
|
+
domains: {},
|
|
48
|
+
presets: {}
|
|
49
|
+
},
|
|
50
|
+
setup(options, context) {
|
|
51
|
+
const resolvedDomains = {
|
|
52
|
+
...resolveStrapiDomains(context.flowConfig, options.dirImages),
|
|
53
|
+
...options.domains || {}
|
|
54
|
+
};
|
|
55
|
+
const isSsg = context.flowConfig.build.preset === "ssg";
|
|
56
|
+
const isNetlify = !!process.env.NETLIFY;
|
|
57
|
+
const publicDir = resolve(context.projectRoot, "public");
|
|
58
|
+
const renameDir = resolve(context.projectRoot, options.dirRenames);
|
|
59
|
+
const imagesCacheRoot = resolve(context.projectRoot, "node_modules/.cache-images");
|
|
60
|
+
const generatedManifestPath = resolve(imagesCacheRoot, "generated-images.jsonl");
|
|
61
|
+
const generatedCacheDir = isNetlify ? resolve(imagesCacheRoot, "netlify") : void 0;
|
|
62
|
+
const generatedCacheManifestPath = generatedCacheDir ? resolve(generatedCacheDir, "manifest.json") : void 0;
|
|
63
|
+
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");
|
|
64
|
+
const buildPluginPath = context.projectRoot === resolvePackagePath() ? resolve(context.projectRoot, "modules/images/plugin.ts") : resolvePackageFile("modules/images/plugin.ts", "modules/images/plugin.mjs", "modules/images/plugin.js");
|
|
65
|
+
if (isSsg) {
|
|
66
|
+
rmSync(generatedManifestPath, { force: true });
|
|
67
|
+
context.nitro.plugins.push(buildPluginPath);
|
|
68
|
+
}
|
|
69
|
+
context.nitro.handlers.push({
|
|
70
|
+
route: "/_ipx/**",
|
|
71
|
+
handler: ipxHandlerPath
|
|
72
|
+
});
|
|
73
|
+
context.nitro.runtimeConfig.flow = {
|
|
74
|
+
...typeof context.nitro.runtimeConfig.flow === "object" && context.nitro.runtimeConfig.flow ? context.nitro.runtimeConfig.flow : {},
|
|
75
|
+
images: {
|
|
76
|
+
dirRenames: renameDir,
|
|
77
|
+
dirFiles: options.dirFiles,
|
|
78
|
+
publicDir,
|
|
79
|
+
outputDir: resolve(context.projectRoot, ".output/public"),
|
|
80
|
+
generatedManifestPath,
|
|
81
|
+
generatedCacheDir,
|
|
82
|
+
generatedCacheManifestPath,
|
|
83
|
+
generate: isSsg,
|
|
84
|
+
netlifyCache: isNetlify,
|
|
85
|
+
options: {
|
|
86
|
+
lazy: options.lazy,
|
|
87
|
+
screens: options.screens,
|
|
88
|
+
domains: resolvedDomains,
|
|
89
|
+
presets: options.presets,
|
|
90
|
+
baseURL: options.baseURL,
|
|
91
|
+
dirImages: options.dirImages
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { defineNitroPlugin } from "nitro/runtime";
|
|
3
|
+
import { materializeGeneratedImages } from "./runtime/build.mjs";
|
|
4
|
+
import { resetFlowImageRuntimeState } from "./runtime/server.mjs";
|
|
5
|
+
export default defineNitroPlugin((nitro) => {
|
|
6
|
+
nitro.hooks.hook("prerender:done", async () => {
|
|
7
|
+
const imagesConfig = nitro.options.runtimeConfig.flow?.images;
|
|
8
|
+
if (!imagesConfig?.generate) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const result = await materializeGeneratedImages(imagesConfig);
|
|
12
|
+
if (result.total > 0) {
|
|
13
|
+
console.log(
|
|
14
|
+
`[flow:images] materialized ${result.total} images (${result.generated} generated, ${result.cacheHits} cache hits${imagesConfig.netlifyCache && process.env.NETLIFY ? ", netlify cache enabled" : ""})`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
resetFlowImageRuntimeState();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { createIPX, ipxFSStorage, ipxHttpStorage } from "ipx";
|
|
6
|
+
function sortRecord(value) {
|
|
7
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
8
|
+
result[key] = value[key];
|
|
9
|
+
return result;
|
|
10
|
+
}, {});
|
|
11
|
+
}
|
|
12
|
+
function hashRecord(value) {
|
|
13
|
+
return createHash("sha1").update(JSON.stringify(sortRecord(value))).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
function normalizeOutputPath(path) {
|
|
16
|
+
return path.startsWith("/") ? path.slice(1) : path;
|
|
17
|
+
}
|
|
18
|
+
function outputFilePath(baseDir, path) {
|
|
19
|
+
return resolve(baseDir, normalizeOutputPath(path));
|
|
20
|
+
}
|
|
21
|
+
async function ensureParentDir(path) {
|
|
22
|
+
await mkdir(dirname(path), { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
async function readJson(path, fallback) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function readGeneratedManifest(path) {
|
|
32
|
+
if (!path || !existsSync(path)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const raw = await readFile(path, "utf8");
|
|
36
|
+
const collection = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
38
|
+
if (!line.trim()) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const parsed = JSON.parse(line);
|
|
42
|
+
collection.set(parsed.url, parsed);
|
|
43
|
+
}
|
|
44
|
+
return [...collection.values()];
|
|
45
|
+
}
|
|
46
|
+
async function createSignature(entry, publicDir) {
|
|
47
|
+
if (entry.src.startsWith("http://") || entry.src.startsWith("https://")) {
|
|
48
|
+
return hashRecord({
|
|
49
|
+
generate: entry.generate,
|
|
50
|
+
modifiers: entry.modifiers,
|
|
51
|
+
src: entry.src
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const sourcePath = resolve(publicDir, entry.src.replace(/^\//, ""));
|
|
55
|
+
const sourceStat = await stat(sourcePath);
|
|
56
|
+
return hashRecord({
|
|
57
|
+
generate: entry.generate,
|
|
58
|
+
modifiers: entry.modifiers,
|
|
59
|
+
mtimeMs: sourceStat.mtimeMs,
|
|
60
|
+
size: sourceStat.size,
|
|
61
|
+
src: entry.src
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function walkFiles(rootDir, currentDir = rootDir, files = []) {
|
|
65
|
+
if (!existsSync(currentDir)) {
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of await readdir(currentDir, { withFileTypes: true })) {
|
|
69
|
+
const entryPath = join(currentDir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
await walkFiles(rootDir, entryPath, files);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (entry.isFile()) {
|
|
75
|
+
files.push(entryPath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
async function pruneCache(cacheDir, activeFiles) {
|
|
81
|
+
for (const filePath of await walkFiles(cacheDir)) {
|
|
82
|
+
const relativePath = normalizeOutputPath(filePath.replace(`${cacheDir}/`, "").replace(`${cacheDir}\\`, ""));
|
|
83
|
+
if (!activeFiles.has(relativePath)) {
|
|
84
|
+
await rm(filePath, { force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function materializeGeneratedImages(config) {
|
|
89
|
+
if (!config.generate) {
|
|
90
|
+
return { cacheHits: 0, generated: 0, total: 0 };
|
|
91
|
+
}
|
|
92
|
+
const images = await readGeneratedManifest(config.generatedManifestPath);
|
|
93
|
+
if (!images.length) {
|
|
94
|
+
return { cacheHits: 0, generated: 0, total: 0 };
|
|
95
|
+
}
|
|
96
|
+
const domains = Object.keys(config.options.domains || {});
|
|
97
|
+
const ipx = createIPX({
|
|
98
|
+
storage: ipxFSStorage({ dir: config.publicDir }),
|
|
99
|
+
...domains.length ? { httpStorage: ipxHttpStorage({ domains }) } : {}
|
|
100
|
+
});
|
|
101
|
+
const previousManifest = config.netlifyCache && config.generatedCacheManifestPath ? await readJson(config.generatedCacheManifestPath, {}) : {};
|
|
102
|
+
const nextManifest = {};
|
|
103
|
+
const activeCacheFiles = /* @__PURE__ */ new Set();
|
|
104
|
+
let cacheHits = 0;
|
|
105
|
+
let generated = 0;
|
|
106
|
+
for (const image of images) {
|
|
107
|
+
const signature = await createSignature(image, config.publicDir);
|
|
108
|
+
nextManifest[image.url] = {
|
|
109
|
+
...image,
|
|
110
|
+
signature
|
|
111
|
+
};
|
|
112
|
+
const outputPath = outputFilePath(config.outputDir, image.generate);
|
|
113
|
+
const cacheRelativePath = normalizeOutputPath(image.generate);
|
|
114
|
+
const cachePath = config.generatedCacheDir ? outputFilePath(config.generatedCacheDir, cacheRelativePath) : void 0;
|
|
115
|
+
activeCacheFiles.add(cacheRelativePath);
|
|
116
|
+
await ensureParentDir(outputPath);
|
|
117
|
+
if (config.netlifyCache && cachePath && previousManifest[image.url]?.signature === signature && existsSync(cachePath)) {
|
|
118
|
+
await copyFile(cachePath, outputPath);
|
|
119
|
+
cacheHits += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const processed = await ipx(image.src, image.modifiers).process();
|
|
123
|
+
const data = typeof processed.data === "string" ? Buffer.from(processed.data) : processed.data;
|
|
124
|
+
await writeFile(outputPath, data);
|
|
125
|
+
if (config.netlifyCache && cachePath) {
|
|
126
|
+
await ensureParentDir(cachePath);
|
|
127
|
+
await writeFile(cachePath, data);
|
|
128
|
+
}
|
|
129
|
+
generated += 1;
|
|
130
|
+
}
|
|
131
|
+
if (config.netlifyCache && config.generatedCacheDir && config.generatedCacheManifestPath) {
|
|
132
|
+
await mkdir(config.generatedCacheDir, { recursive: true });
|
|
133
|
+
await pruneCache(config.generatedCacheDir, activeCacheFiles);
|
|
134
|
+
await writeFile(config.generatedCacheManifestPath, `${JSON.stringify(nextManifest, null, 2)}
|
|
135
|
+
`, "utf8");
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
cacheHits,
|
|
139
|
+
generated,
|
|
140
|
+
total: images.length
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function extname(value) {
|
|
2
|
+
const fileName = value.split("/").pop() || value;
|
|
3
|
+
const index = fileName.lastIndexOf(".");
|
|
4
|
+
return index > 0 ? fileName.slice(index) : "";
|
|
5
|
+
}
|
|
6
|
+
function basename(value) {
|
|
7
|
+
return value.split("/").pop() || value;
|
|
8
|
+
}
|
|
9
|
+
export const screens = {
|
|
10
|
+
xs: 320,
|
|
11
|
+
sm: 640,
|
|
12
|
+
md: 768,
|
|
13
|
+
lg: 1024,
|
|
14
|
+
xl: 1280,
|
|
15
|
+
xxl: 1536
|
|
16
|
+
};
|
|
17
|
+
export function parseSize(input = "") {
|
|
18
|
+
if (typeof input === "number") {
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
if (typeof input === "string" && input.replace("px", "").match(/^\d+$/g)) {
|
|
22
|
+
return parseInt(input, 10);
|
|
23
|
+
}
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
export function getNormalName(originalName) {
|
|
27
|
+
const name = basename(originalName).replace(extname(originalName), "");
|
|
28
|
+
return name.replace(/-/g, " ");
|
|
29
|
+
}
|
|
30
|
+
export function getFileExtension(url = "") {
|
|
31
|
+
return url.split(/[?#]/).shift().split("/").pop().split(".").pop();
|
|
32
|
+
}
|
|
33
|
+
export function guessExt(input = "") {
|
|
34
|
+
const ext = input.split(".").pop()?.split("?")[0];
|
|
35
|
+
if (ext && /^[\w0-9]+$/.test(ext)) {
|
|
36
|
+
return `.${ext}`;
|
|
37
|
+
}
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
export function getPreset(ctx, name) {
|
|
41
|
+
if (!name) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
return ctx.options?.presets?.[name] || {};
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowImagesState, FlowImageOptions, GeneratedImageEntry, GetImageFunction } from './types.ts';
|
|
2
|
+
export declare function createImageResolver(imagesOptions: FlowImageOptions, stateImages: FlowImagesState, runtime?: {
|
|
3
|
+
generateOutput?: boolean;
|
|
4
|
+
onGenerate?: (image: GeneratedImageEntry) => void;
|
|
5
|
+
}): {
|
|
6
|
+
getImage: GetImageFunction;
|
|
7
|
+
};
|