@monkeyplus/flow 6.0.7 → 6.0.9
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/images/ipx.d.ts +2 -5
- package/modules/images/ipx.mjs +34 -10
- package/modules/images/module.mjs +72 -22
- package/modules/images/runtime/build.mjs +57 -25
- package/modules/images/runtime/image.d.ts +1 -1
- package/modules/images/runtime/image.mjs +24 -7
- package/modules/images/runtime/server.d.ts +3 -1
- package/modules/images/runtime/server.mjs +26 -3
- package/modules/images/runtime/types.d.ts +3 -1
- 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 +1 -1
- package/server/lib/render.mjs +3 -2
- package/src/public/nitro.mjs +4 -1
- package/src/public/query-content.mjs +3 -0
- package/src/runtime/boot.d.ts +7 -0
- package/src/runtime/components/MkImage.d.ts +2 -2
- package/src/runtime/components/MkImage.mjs +2 -1
- package/src/runtime/components/MkPicture.d.ts +2 -2
- package/src/runtime/components/MkPicture.mjs +15 -14
- package/src/runtime/components/image-shared.d.ts +3 -0
- package/src/runtime/components/image-shared.mjs +40 -1
- package/src/runtime/config.d.ts +4 -0
- package/src/runtime/islands.mjs +3 -0
- package/src/runtime/modules.mjs +1 -0
- package/modules/images/plugin.d.ts +0 -2
- package/modules/images/plugin.mjs +0 -19
package/modules/images/ipx.d.ts
CHANGED
package/modules/images/ipx.mjs
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import process from "node:process";
|
|
1
2
|
import { createIPX, createIPXH3Handler, ipxFSStorage, ipxHttpStorage } from "ipx";
|
|
3
|
+
import { defineHandler } from "nitro";
|
|
4
|
+
import { getRequestURL } from "nitro/h3";
|
|
2
5
|
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
3
6
|
let cachedDir = "";
|
|
4
7
|
let cachedDomainsKey = "";
|
|
5
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
|
+
}
|
|
6
20
|
function resolveHandler() {
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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 || {});
|
|
10
26
|
const domainsKey = domains.join("|");
|
|
11
27
|
if (!publicDir) {
|
|
12
28
|
return void 0;
|
|
@@ -16,16 +32,24 @@ function resolveHandler() {
|
|
|
16
32
|
cachedDomainsKey = domainsKey;
|
|
17
33
|
const ipx = createIPX({
|
|
18
34
|
storage: ipxFSStorage({ dir: publicDir }),
|
|
19
|
-
...domains.length ? {
|
|
35
|
+
...domains.length ? {
|
|
36
|
+
httpStorage: ipxHttpStorage({ domains })
|
|
37
|
+
} : {}
|
|
20
38
|
});
|
|
21
39
|
cachedHandler = createIPXH3Handler(ipx);
|
|
22
40
|
}
|
|
23
41
|
return cachedHandler;
|
|
24
42
|
}
|
|
25
|
-
export default
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
43
|
+
export default defineHandler(async (event) => {
|
|
44
|
+
if (!getRequestURL(event)?.pathname.startsWith("/_ipx")) {
|
|
45
|
+
console.log("route", event.req.url.toString());
|
|
46
|
+
return;
|
|
29
47
|
}
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { rmSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { createIPX, createIPXNodeServer, ipxFSStorage, ipxHttpStorage } from "ipx";
|
|
4
5
|
import { resolvePackageFile, resolvePackagePath } from "../../src/public/shared.mjs";
|
|
5
6
|
import { defineFlowModule } from "../../src/runtime/config.mjs";
|
|
7
|
+
import { materializeGeneratedImages } from "./runtime/build.mjs";
|
|
6
8
|
import { screens } from "./runtime/helpers.mjs";
|
|
9
|
+
import { resetFlowImageRuntimeState } from "./runtime/server.mjs";
|
|
7
10
|
function withoutTrailingSlash(value) {
|
|
8
11
|
return value.replace(/\/+$/, "");
|
|
9
12
|
}
|
|
@@ -14,7 +17,7 @@ function resolveStrapiDomains(flowConfig, dirImages) {
|
|
|
14
17
|
}
|
|
15
18
|
try {
|
|
16
19
|
const url = new URL(strapi.url);
|
|
17
|
-
const target =
|
|
20
|
+
const target = dirImages;
|
|
18
21
|
const values = /* @__PURE__ */ new Set([
|
|
19
22
|
withoutTrailingSlash(url.origin),
|
|
20
23
|
withoutTrailingSlash(url.toString())
|
|
@@ -32,6 +35,33 @@ function resolveStrapiDomains(flowConfig, dirImages) {
|
|
|
32
35
|
return {};
|
|
33
36
|
}
|
|
34
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
|
+
}
|
|
35
65
|
export default defineFlowModule({
|
|
36
66
|
meta: {
|
|
37
67
|
name: "images",
|
|
@@ -40,6 +70,7 @@ export default defineFlowModule({
|
|
|
40
70
|
defaults: {
|
|
41
71
|
dirRenames: "shared/seo_images",
|
|
42
72
|
dirFiles: ["images", "media"],
|
|
73
|
+
buildBatchSize: 8,
|
|
43
74
|
lazy: true,
|
|
44
75
|
screens,
|
|
45
76
|
baseURL: "/_ipx",
|
|
@@ -53,6 +84,7 @@ export default defineFlowModule({
|
|
|
53
84
|
...options.domains || {}
|
|
54
85
|
};
|
|
55
86
|
const isSsg = context.flowConfig.build.preset === "ssg";
|
|
87
|
+
const shouldGenerateOutput = isSsg && process.env.NODE_ENV === "production";
|
|
56
88
|
const isNetlify = !!process.env.NETLIFY;
|
|
57
89
|
const publicDir = resolve(context.projectRoot, "public");
|
|
58
90
|
const renameDir = resolve(context.projectRoot, options.dirRenames);
|
|
@@ -61,35 +93,53 @@ export default defineFlowModule({
|
|
|
61
93
|
const generatedCacheDir = isNetlify ? resolve(imagesCacheRoot, "netlify") : void 0;
|
|
62
94
|
const generatedCacheManifestPath = generatedCacheDir ? resolve(generatedCacheDir, "manifest.json") : void 0;
|
|
63
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");
|
|
64
|
-
const
|
|
65
|
-
|
|
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) {
|
|
66
118
|
rmSync(generatedManifestPath, { force: true });
|
|
67
|
-
context.nitro.
|
|
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
|
+
};
|
|
68
132
|
}
|
|
69
133
|
context.nitro.handlers.push({
|
|
70
134
|
route: "/_ipx/**",
|
|
71
135
|
handler: ipxHandlerPath
|
|
136
|
+
// middleware: true,
|
|
72
137
|
});
|
|
138
|
+
context.vite.plugins.push(createIpxDevServerPlugin(imagesRuntimeConfig));
|
|
73
139
|
context.nitro.runtimeConfig.flow = {
|
|
74
140
|
...typeof context.nitro.runtimeConfig.flow === "object" && context.nitro.runtimeConfig.flow ? context.nitro.runtimeConfig.flow : {},
|
|
75
141
|
images: {
|
|
76
|
-
|
|
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
|
-
}
|
|
142
|
+
...imagesRuntimeConfig
|
|
93
143
|
}
|
|
94
144
|
};
|
|
95
145
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
5
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
|
+
}
|
|
6
11
|
function sortRecord(value) {
|
|
7
12
|
return Object.keys(value).sort().reduce((result, key) => {
|
|
8
13
|
result[key] = value[key];
|
|
@@ -18,6 +23,26 @@ function normalizeOutputPath(path) {
|
|
|
18
23
|
function outputFilePath(baseDir, path) {
|
|
19
24
|
return resolve(baseDir, normalizeOutputPath(path));
|
|
20
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
|
+
}
|
|
21
46
|
async function ensureParentDir(path) {
|
|
22
47
|
await mkdir(dirname(path), { recursive: true });
|
|
23
48
|
}
|
|
@@ -39,7 +64,7 @@ async function readGeneratedManifest(path) {
|
|
|
39
64
|
continue;
|
|
40
65
|
}
|
|
41
66
|
const parsed = JSON.parse(line);
|
|
42
|
-
collection.set(parsed
|
|
67
|
+
collection.set(getGeneratedImageKey(parsed), parsed);
|
|
43
68
|
}
|
|
44
69
|
return [...collection.values()];
|
|
45
70
|
}
|
|
@@ -103,30 +128,37 @@ export async function materializeGeneratedImages(config) {
|
|
|
103
128
|
const activeCacheFiles = /* @__PURE__ */ new Set();
|
|
104
129
|
let cacheHits = 0;
|
|
105
130
|
let generated = 0;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await
|
|
127
|
-
|
|
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;
|
|
128
161
|
}
|
|
129
|
-
generated += 1;
|
|
130
162
|
}
|
|
131
163
|
if (config.netlifyCache && config.generatedCacheDir && config.generatedCacheManifestPath) {
|
|
132
164
|
await mkdir(config.generatedCacheDir, { recursive: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FlowImageOptions, FlowImagesState, GeneratedImageEntry, GetImageFunction } from './types.ts';
|
|
2
2
|
export declare function createImageResolver(imagesOptions: FlowImageOptions, stateImages: FlowImagesState, runtime?: {
|
|
3
3
|
generateOutput?: boolean;
|
|
4
4
|
onGenerate?: (image: GeneratedImageEntry) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encodeParam, encodePath, joinURL } from "ufo";
|
|
2
2
|
import { getNormalName, getPreset, guessExt, parseSize } from "./helpers.mjs";
|
|
3
3
|
const modifierKeyMap = {
|
|
4
4
|
background: "b",
|
|
@@ -47,6 +47,24 @@ function getDirectory(value) {
|
|
|
47
47
|
const lastSlash = pathname.lastIndexOf("/");
|
|
48
48
|
return lastSlash >= 0 ? pathname.slice(0, lastSlash) : "";
|
|
49
49
|
}
|
|
50
|
+
function getRelativePath(value, baseDir) {
|
|
51
|
+
const pathname = getPathname(value).split(/[?#]/, 1)[0] || "";
|
|
52
|
+
const normalizedBaseDir = baseDir.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
53
|
+
const normalizedPath = pathname.replace(/^\/+/, "");
|
|
54
|
+
if (!normalizedBaseDir) {
|
|
55
|
+
return normalizedPath;
|
|
56
|
+
}
|
|
57
|
+
if (normalizedPath === normalizedBaseDir) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
if (normalizedPath.startsWith(`${normalizedBaseDir}/`)) {
|
|
61
|
+
return normalizedPath.slice(normalizedBaseDir.length + 1);
|
|
62
|
+
}
|
|
63
|
+
return normalizedPath;
|
|
64
|
+
}
|
|
65
|
+
function getModifierSegment(modifiers = {}) {
|
|
66
|
+
return Object.entries(modifiers).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${encodeParam(modifierKeyMap[key] || key)}_${encodeParam(String(value))}`).join(",") || "_";
|
|
67
|
+
}
|
|
50
68
|
function resolveRemoteRename(input, domains = {}) {
|
|
51
69
|
if (!isRemoteUrl(input)) {
|
|
52
70
|
return void 0;
|
|
@@ -129,10 +147,9 @@ export function createImageResolver(imagesOptions, stateImages, runtime = {}) {
|
|
|
129
147
|
if (isRemoteUrl(input) && replacedPath.startsWith("/")) {
|
|
130
148
|
baseDir = getBaseDir(replacedPath, imagesOptions.dirImages);
|
|
131
149
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
150
|
+
const modifierSegment = getModifierSegment(image.modifiers);
|
|
151
|
+
const relativePath = getRelativePath(replacedPath, baseDir);
|
|
152
|
+
image.generate = joinURL(`/${baseDir}`, modifierSegment === "_" ? "" : modifierSegment, relativePath);
|
|
136
153
|
if (originalExt) {
|
|
137
154
|
image.generate = image.generate.replace(originalExt, image.ext);
|
|
138
155
|
} else if (!image.generate.includes(".") && image.ext) {
|
|
@@ -179,7 +196,7 @@ export function createImageResolver(imagesOptions, stateImages, runtime = {}) {
|
|
|
179
196
|
Object.assign(sizes, opts.sizes);
|
|
180
197
|
}
|
|
181
198
|
for (const key in sizes) {
|
|
182
|
-
const screenMaxWidth = imagesOptions.screens?.[key] || parseInt(key, 10);
|
|
199
|
+
const screenMaxWidth = imagesOptions.screens?.[key] || Number.parseInt(key, 10);
|
|
183
200
|
let size = String(sizes[key]);
|
|
184
201
|
const isFluid = size.endsWith("vw");
|
|
185
202
|
if (!isFluid && /^\d+$/.test(size)) {
|
|
@@ -188,7 +205,7 @@ export function createImageResolver(imagesOptions, stateImages, runtime = {}) {
|
|
|
188
205
|
if (!isFluid && !size.endsWith("px")) {
|
|
189
206
|
continue;
|
|
190
207
|
}
|
|
191
|
-
let calculatedWidth = parseInt(size, 10);
|
|
208
|
+
let calculatedWidth = Number.parseInt(size, 10);
|
|
192
209
|
if (!screenMaxWidth || !calculatedWidth) {
|
|
193
210
|
continue;
|
|
194
211
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FlowImageBootPayload } from '../../../src/runtime/boot.ts';
|
|
2
|
+
import type { FlowImageRuntimeUtils, FlowImagesRuntimeConfig } from './types.ts';
|
|
3
|
+
export declare function getFlowImageBootPayload(config?: FlowImagesRuntimeConfig | undefined): FlowImageBootPayload | undefined;
|
|
2
4
|
export declare function getFlowImageRuntimeUtils(config?: FlowImagesRuntimeConfig | undefined): FlowImageRuntimeUtils | undefined;
|
|
3
5
|
export declare function resetFlowImageRuntimeState(): void;
|
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
|
-
import {
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
5
|
import { createImageResolver } from "./image.mjs";
|
|
6
|
+
import { loadImageRenames } from "./renames.mjs";
|
|
6
7
|
let runtimeConfigRequire;
|
|
7
8
|
let cachedKey;
|
|
8
9
|
let cachedUtils;
|
|
9
10
|
let generatedEntryKeys = /* @__PURE__ */ new Set();
|
|
11
|
+
function getEnvFlowImagesRuntimeConfig() {
|
|
12
|
+
const raw = process.env.FLOW_IMAGES_RUNTIME_CONFIG;
|
|
13
|
+
if (!raw) {
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
} catch {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
function getFlowImagesRuntimeConfig() {
|
|
11
23
|
try {
|
|
12
24
|
runtimeConfigRequire ??= createRequire(import.meta.url);
|
|
13
25
|
const runtime = runtimeConfigRequire("nitro/runtime-config");
|
|
14
|
-
return runtime.useRuntimeConfig?.().flow?.images;
|
|
26
|
+
return runtime.useRuntimeConfig?.().flow?.images || getEnvFlowImagesRuntimeConfig();
|
|
15
27
|
} catch {
|
|
28
|
+
return getEnvFlowImagesRuntimeConfig();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function getFlowImageBootPayload(config = getFlowImagesRuntimeConfig()) {
|
|
32
|
+
if (!config) {
|
|
16
33
|
return void 0;
|
|
17
34
|
}
|
|
35
|
+
const renameSources = [config.dirRenames, config.publicDir].filter(Boolean);
|
|
36
|
+
return {
|
|
37
|
+
all: loadImageRenames(renameSources),
|
|
38
|
+
options: { ...config.options },
|
|
39
|
+
generateOutput: !!config.generate
|
|
40
|
+
};
|
|
18
41
|
}
|
|
19
42
|
export function getFlowImageRuntimeUtils(config = getFlowImagesRuntimeConfig()) {
|
|
20
43
|
if (!config) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface FlowImagesModuleOptions {
|
|
2
2
|
dirRenames: string;
|
|
3
3
|
dirFiles: string[];
|
|
4
|
+
buildBatchSize: number;
|
|
4
5
|
lazy: boolean;
|
|
5
6
|
screens: Record<string, number>;
|
|
6
7
|
domains?: Record<string, string>;
|
|
@@ -8,7 +9,7 @@ export interface FlowImagesModuleOptions {
|
|
|
8
9
|
baseURL: string;
|
|
9
10
|
dirImages: string;
|
|
10
11
|
}
|
|
11
|
-
export type FlowImageOptions = Omit<FlowImagesModuleOptions, 'dirRenames' | 'dirFiles'>;
|
|
12
|
+
export type FlowImageOptions = Omit<FlowImagesModuleOptions, 'dirRenames' | 'dirFiles' | 'buildBatchSize'>;
|
|
12
13
|
export interface FlowImageMeta {
|
|
13
14
|
rename?: string;
|
|
14
15
|
name: string;
|
|
@@ -61,6 +62,7 @@ export interface FlowImagesState {
|
|
|
61
62
|
export interface FlowImagesRuntimeConfig {
|
|
62
63
|
dirRenames: string;
|
|
63
64
|
dirFiles: string[];
|
|
65
|
+
buildBatchSize?: number;
|
|
64
66
|
publicDir: string;
|
|
65
67
|
outputDir: string;
|
|
66
68
|
generatedManifestPath: string;
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { defineEventHandler, getRequestURL, setHeader } from "nitro/h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
3
3
|
import { getUrls } from "../../server/lib/pages.mjs";
|
|
4
|
-
|
|
5
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6
|
-
}
|
|
4
|
+
import { buildSitemapXml } from "./xml.mjs";
|
|
7
5
|
export default defineEventHandler(async (event) => {
|
|
8
6
|
const urls = await getUrls(true, true);
|
|
9
7
|
const runtimeConfig = useRuntimeConfig();
|
|
10
8
|
const origin = runtimeConfig.flow?.siteUrl || getRequestURL(event).origin;
|
|
11
|
-
const xml =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const xml = buildSitemapXml(urls, origin, {
|
|
10
|
+
locales: runtimeConfig.flow?.locale?.locales || ["es-ec"],
|
|
11
|
+
language: runtimeConfig.flow?.locale?.language || "es",
|
|
12
|
+
location: runtimeConfig.flow?.locale?.location || "ec"
|
|
13
|
+
});
|
|
15
14
|
setHeader(event, "content-type", "application/xml; charset=utf-8");
|
|
16
15
|
return xml;
|
|
17
16
|
});
|
|
@@ -2,13 +2,14 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { resolvePackagePath } from "../../src/public/shared.mjs";
|
|
4
4
|
import { defineFlowModule } from "../../src/runtime/config.mjs";
|
|
5
|
-
function createGeneratedSitemapHandler(siteUrl,
|
|
5
|
+
function createGeneratedSitemapHandler(siteUrl, localeConfig) {
|
|
6
6
|
return `
|
|
7
7
|
import pageDefinitions from 'virtual:flow/pages';
|
|
8
8
|
|
|
9
9
|
const dynamicRouteCache = new Map();
|
|
10
10
|
const configuredSiteUrl = ${JSON.stringify(siteUrl || "")};
|
|
11
|
-
const
|
|
11
|
+
const localeConfig = ${JSON.stringify(localeConfig)};
|
|
12
|
+
const enabledLocales = localeConfig.locales;
|
|
12
13
|
|
|
13
14
|
function escapeXml(value) {
|
|
14
15
|
return value
|
|
@@ -27,6 +28,26 @@ function normalizePath(value) {
|
|
|
27
28
|
return value.length > 1 && value.endsWith('/') ? value.slice(0, -1) : value;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function ensureLeadingSlash(value) {
|
|
32
|
+
if (!value) {
|
|
33
|
+
return '/';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value.startsWith('/') ? value : '/' + value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureTrailingSlash(value) {
|
|
40
|
+
if (value === '/' || value.endsWith('/')) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return value + '/';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function preserveTrailingSlash(value, source) {
|
|
48
|
+
return source.endsWith('/') ? ensureTrailingSlash(value) : value;
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
function replacePath(pattern, url) {
|
|
31
52
|
let resolved = pattern;
|
|
32
53
|
|
|
@@ -53,8 +74,198 @@ function toPublicRoutePattern(url) {
|
|
|
53
74
|
return normalizePath(url.replaceAll('*', ''));
|
|
54
75
|
}
|
|
55
76
|
|
|
77
|
+
function trimLeadingSlash(value) {
|
|
78
|
+
let result = value;
|
|
79
|
+
|
|
80
|
+
while (result.startsWith('/')) {
|
|
81
|
+
result = result.slice(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function trimTrailingSlash(value) {
|
|
88
|
+
let result = value;
|
|
89
|
+
|
|
90
|
+
while (result.endsWith('/')) {
|
|
91
|
+
result = result.slice(0, -1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function combineName(name, dynamicName) {
|
|
98
|
+
return trimTrailingSlash(name) + '/' + trimLeadingSlash(dynamicName);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getLanguage(locale) {
|
|
102
|
+
const [language = localeConfig.language || 'es'] = locale.split('-');
|
|
103
|
+
return language;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getLocation(locale) {
|
|
107
|
+
const [, location = localeConfig.location || 'ec'] = locale.split('-');
|
|
108
|
+
return location;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getDefaultLocaleCode() {
|
|
112
|
+
return localeConfig.language + '-' + localeConfig.location;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function routeHasPrefix(route, prefix) {
|
|
116
|
+
return route === prefix || route === prefix + '/' || route.startsWith(prefix + '/');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getLocalePrefixVariants(localeCode) {
|
|
120
|
+
const lang = getLanguage(localeCode);
|
|
121
|
+
const loc = getLocation(localeCode);
|
|
122
|
+
const prefixes = new Set();
|
|
123
|
+
|
|
124
|
+
if (loc === localeConfig.location) {
|
|
125
|
+
prefixes.add('/' + lang);
|
|
126
|
+
return [...prefixes];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
prefixes.add('/' + lang + '-' + loc);
|
|
130
|
+
prefixes.add('/' + lang + '/' + loc);
|
|
131
|
+
|
|
132
|
+
return [...prefixes];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getLocalePrefix(localeCode) {
|
|
136
|
+
if (localeConfig.prefixStrategy === 'manual') {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (localeCode === getDefaultLocaleCode()) {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const lang = getLanguage(localeCode);
|
|
145
|
+
const loc = getLocation(localeCode);
|
|
146
|
+
|
|
147
|
+
if (loc === localeConfig.location) {
|
|
148
|
+
return '/' + lang;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (localeConfig.prefixFormat === 'nested') {
|
|
152
|
+
return '/' + lang + '/' + loc;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return '/' + lang + '-' + loc;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function localizeRoutePattern(localeCode, route) {
|
|
159
|
+
const normalizedRoute = ensureLeadingSlash(route || '/');
|
|
160
|
+
const prefix = getLocalePrefix(localeCode);
|
|
161
|
+
|
|
162
|
+
if (!prefix) {
|
|
163
|
+
return normalizedRoute;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (getLocalePrefixVariants(localeCode).some(candidate => routeHasPrefix(normalizedRoute, candidate))) {
|
|
167
|
+
return normalizedRoute;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (normalizedRoute === '/') {
|
|
171
|
+
return ensureTrailingSlash(prefix);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return preserveTrailingSlash(normalizePath(prefix) + normalizedRoute, normalizedRoute);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function toPublicRoute(localeCode, route) {
|
|
178
|
+
const localizedRoute = localizeRoutePattern(localeCode, route);
|
|
179
|
+
const publicRoute = localizedRoute.replaceAll('*', '');
|
|
180
|
+
|
|
181
|
+
return preserveTrailingSlash(normalizePath(publicRoute), localizedRoute);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toAbsoluteUrl(origin, url) {
|
|
185
|
+
return new URL(url, origin).toString();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildAlternateLink(hreflang, origin, url) {
|
|
189
|
+
return '<xhtml:link rel="alternate" hreflang="' + escapeXml(hreflang) + '" href="' + escapeXml(toAbsoluteUrl(origin, url)) + '"/>';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildUrl(origin, url, lastMod, alternates = []) {
|
|
193
|
+
return [
|
|
194
|
+
' <url>',
|
|
195
|
+
' <loc>' + escapeXml(toAbsoluteUrl(origin, url)) + '</loc>',
|
|
196
|
+
' <lastmod>' + escapeXml(lastMod) + '</lastmod>',
|
|
197
|
+
...alternates.map(alternate => ' ' + alternate),
|
|
198
|
+
' </url>',
|
|
199
|
+
].join('\\n');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function groupByName(entries) {
|
|
203
|
+
const grouped = new Map();
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const group = grouped.get(entry.name) || [];
|
|
207
|
+
group.push(entry);
|
|
208
|
+
grouped.set(entry.name, group);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return [...grouped.values()];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildLocalizedUrls(entries, origin, lastMod) {
|
|
215
|
+
const defaultLocale = getDefaultLocaleCode();
|
|
216
|
+
|
|
217
|
+
return groupByName(entries).map((pages) => {
|
|
218
|
+
const pagesByLanguage = pages.reduce((result, page) => {
|
|
219
|
+
const language = getLanguage(page.locale);
|
|
220
|
+
const group = result.get(language) || [];
|
|
221
|
+
group.push(page);
|
|
222
|
+
result.set(language, group);
|
|
223
|
+
return result;
|
|
224
|
+
}, new Map());
|
|
225
|
+
const defaultEntry = pages.find(page => page.locale === defaultLocale) || pages[0];
|
|
226
|
+
const alternates = [buildAlternateLink('x-default', origin, defaultEntry.url)];
|
|
227
|
+
|
|
228
|
+
for (const [language, localizedPages] of [...pagesByLanguage.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
229
|
+
const sortedPages = [...localizedPages].sort((left, right) => left.locale.localeCompare(right.locale));
|
|
230
|
+
|
|
231
|
+
if (sortedPages.length > 1) {
|
|
232
|
+
const languageDefault = sortedPages.find(page => getLocation(page.locale) === localeConfig.location) || sortedPages[0];
|
|
233
|
+
alternates.push(buildAlternateLink(language, origin, languageDefault.url));
|
|
234
|
+
|
|
235
|
+
for (const page of sortedPages) {
|
|
236
|
+
alternates.push(buildAlternateLink(page.locale, origin, page.url));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
alternates.push(buildAlternateLink(language, origin, sortedPages[0].url));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return buildUrl(origin, defaultEntry.url, lastMod, alternates);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildSitemapXml(entries, origin) {
|
|
250
|
+
const normalizedEntries = entries
|
|
251
|
+
.map((entry) => typeof entry === 'string'
|
|
252
|
+
? { url: entry, locale: localeConfig.language + '-' + localeConfig.location, name: entry }
|
|
253
|
+
: entry)
|
|
254
|
+
.sort((left, right) => left.url.localeCompare(right.url));
|
|
255
|
+
const lastMod = new Date().toISOString().split('T')[0];
|
|
256
|
+
const isMultiple = localeConfig.locales.length > 1;
|
|
257
|
+
const urls = isMultiple
|
|
258
|
+
? buildLocalizedUrls(normalizedEntries, origin, lastMod)
|
|
259
|
+
: normalizedEntries.map(entry => buildUrl(origin, entry.url, lastMod));
|
|
260
|
+
const alternateNamespace = isMultiple
|
|
261
|
+
? ' xmlns:xhtml="http://www.w3.org/1999/xhtml"'
|
|
262
|
+
: '';
|
|
263
|
+
|
|
264
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' + alternateNamespace + '>\\n' + urls.join('\\n') + '\\n</urlset>';
|
|
265
|
+
}
|
|
266
|
+
|
|
56
267
|
function createLocale(code) {
|
|
57
|
-
const [lang = 'es', loc = 'ec'] = code.split('-');
|
|
268
|
+
const [lang = localeConfig.language || 'es', loc = localeConfig.location || 'ec'] = code.split('-');
|
|
58
269
|
|
|
59
270
|
return {
|
|
60
271
|
code,
|
|
@@ -91,7 +302,7 @@ async function getUrl(namePage, localeCode, options = {}) {
|
|
|
91
302
|
|
|
92
303
|
if (localePage.dynamic && options.dynamicName) {
|
|
93
304
|
const locale = createLocale(code);
|
|
94
|
-
const ctx = createContext(definition, locale, localePage, localePage.url, {});
|
|
305
|
+
const ctx = createContext(definition, locale, localePage, toPublicRoute(code, localePage.url), {});
|
|
95
306
|
const entries = await getDynamicEntries(definition, code, ctx);
|
|
96
307
|
const entry = entries.find(candidate => candidate.name === options.dynamicName);
|
|
97
308
|
|
|
@@ -99,10 +310,10 @@ async function getUrl(namePage, localeCode, options = {}) {
|
|
|
99
310
|
return undefined;
|
|
100
311
|
}
|
|
101
312
|
|
|
102
|
-
return replacePath(localePage.url, entry.url);
|
|
313
|
+
return replacePath(localizeRoutePattern(code, localePage.url), entry.url);
|
|
103
314
|
}
|
|
104
315
|
|
|
105
|
-
return
|
|
316
|
+
return toPublicRoute(code, localePage.url);
|
|
106
317
|
}
|
|
107
318
|
}
|
|
108
319
|
|
|
@@ -125,19 +336,19 @@ async function getUrls(withLocale = false, omitNoPublish = false) {
|
|
|
125
336
|
|
|
126
337
|
if (localePage.dynamic) {
|
|
127
338
|
const locale = createLocale(localeCode);
|
|
128
|
-
const ctx = createContext(definition, locale, localePage, localePage.url, {});
|
|
339
|
+
const ctx = createContext(definition, locale, localePage, toPublicRoute(localeCode, localePage.url), {});
|
|
129
340
|
const entries = await getDynamicEntries(definition, localeCode, ctx);
|
|
130
341
|
|
|
131
342
|
for (const entry of entries) {
|
|
132
|
-
const url = replacePath(localePage.url, entry.url);
|
|
133
|
-
urls.push(withLocale ? { url, locale: localeCode } : url);
|
|
343
|
+
const url = replacePath(localizeRoutePattern(localeCode, localePage.url), entry.url);
|
|
344
|
+
urls.push(withLocale ? { url, locale: localeCode, name: combineName(definition.name, entry.name) } : url);
|
|
134
345
|
}
|
|
135
346
|
|
|
136
347
|
continue;
|
|
137
348
|
}
|
|
138
349
|
|
|
139
|
-
const url =
|
|
140
|
-
urls.push(withLocale ? { url, locale: localeCode } : url);
|
|
350
|
+
const url = toPublicRoute(localeCode, localePage.url);
|
|
351
|
+
urls.push(withLocale ? { url, locale: localeCode, name: definition.name } : url);
|
|
141
352
|
}
|
|
142
353
|
}
|
|
143
354
|
|
|
@@ -201,12 +412,7 @@ export default async function sitemapHandler(event) {
|
|
|
201
412
|
? new URL(event.req.url, configuredSiteUrl || 'http://localhost')
|
|
202
413
|
: new URL(configuredSiteUrl || 'http://localhost');
|
|
203
414
|
const origin = configuredSiteUrl || requestUrl.origin;
|
|
204
|
-
|
|
205
|
-
const xml = '<?xml version="1.0" encoding="UTF-8"?>\\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\\n' + urls
|
|
206
|
-
.map((entry) => typeof entry === 'string'
|
|
207
|
-
? ' <url><loc>' + escapeXml(new URL(entry, origin).toString()) + '</loc></url>'
|
|
208
|
-
: ' <url><loc>' + escapeXml(new URL(entry.url, origin).toString()) + '</loc><xhtml:link rel="alternate" hreflang="' + escapeXml(entry.locale) + '" href="' + escapeXml(new URL(entry.url, origin).toString()) + '" xmlns:xhtml="http://www.w3.org/1999/xhtml" /></url>')
|
|
209
|
-
.join('\\n') + '\\n</urlset>';
|
|
415
|
+
const xml = buildSitemapXml(urls, origin);
|
|
210
416
|
|
|
211
417
|
return new Response(xml, {
|
|
212
418
|
headers: {
|
|
@@ -216,12 +422,18 @@ export default async function sitemapHandler(event) {
|
|
|
216
422
|
}
|
|
217
423
|
`;
|
|
218
424
|
}
|
|
219
|
-
function ensureGeneratedSitemapHandler(projectRoot,
|
|
425
|
+
function ensureGeneratedSitemapHandler(projectRoot, flowConfig) {
|
|
220
426
|
const handlerPath = resolve(projectRoot, ".flow/generated/modules/sitemap/handler.mjs");
|
|
221
427
|
mkdirSync(dirname(handlerPath), { recursive: true });
|
|
222
428
|
writeFileSync(handlerPath, createGeneratedSitemapHandler(
|
|
223
|
-
typeof
|
|
224
|
-
|
|
429
|
+
typeof flowConfig.siteUrl === "string" ? flowConfig.siteUrl : void 0,
|
|
430
|
+
{
|
|
431
|
+
locales: Array.isArray(flowConfig.locale?.locales) ? flowConfig.locale.locales : ["es-ec"],
|
|
432
|
+
language: typeof flowConfig.locale?.language === "string" ? flowConfig.locale.language : "es",
|
|
433
|
+
location: typeof flowConfig.locale?.location === "string" ? flowConfig.locale.location : "ec",
|
|
434
|
+
prefixStrategy: flowConfig.locale?.prefixStrategy === "manual" ? "manual" : "auto",
|
|
435
|
+
prefixFormat: flowConfig.locale?.prefixFormat === "nested" ? "nested" : "compact"
|
|
436
|
+
}
|
|
225
437
|
), "utf8");
|
|
226
438
|
return handlerPath;
|
|
227
439
|
}
|
|
@@ -235,9 +447,11 @@ export default defineFlowModule({
|
|
|
235
447
|
prerender: true
|
|
236
448
|
},
|
|
237
449
|
setup(options, context) {
|
|
238
|
-
const contextRuntimeConfig = typeof context.nitro.runtimeConfig.flow === "object" && context.nitro.runtimeConfig.flow ? context.nitro.runtimeConfig.flow : {};
|
|
239
450
|
const localHandlerPath = resolve(context.projectRoot, "modules/sitemap/handler.ts");
|
|
240
|
-
const handlerPath = context.projectRoot === resolvePackagePath() ? localHandlerPath : ensureGeneratedSitemapHandler(context.projectRoot,
|
|
451
|
+
const handlerPath = context.projectRoot === resolvePackagePath() ? localHandlerPath : ensureGeneratedSitemapHandler(context.projectRoot, {
|
|
452
|
+
siteUrl: context.flowConfig.siteUrl,
|
|
453
|
+
locale: context.flowConfig.locale
|
|
454
|
+
});
|
|
241
455
|
context.nitro.handlers.push({
|
|
242
456
|
method: "get",
|
|
243
457
|
route: options.route,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PageUrlInfo } from '../../src/runtime/pages.ts';
|
|
2
|
+
export interface SitemapLocaleConfig {
|
|
3
|
+
locales: string[];
|
|
4
|
+
language: string;
|
|
5
|
+
location: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function buildSitemapXml(entries: Array<string | PageUrlInfo>, origin: string, locale: SitemapLocaleConfig): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
function escapeXml(value) {
|
|
2
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3
|
+
}
|
|
4
|
+
function getLanguage(locale) {
|
|
5
|
+
const [language = "es"] = locale.split("-");
|
|
6
|
+
return language;
|
|
7
|
+
}
|
|
8
|
+
function getLocation(locale) {
|
|
9
|
+
const [, location = "ec"] = locale.split("-");
|
|
10
|
+
return location;
|
|
11
|
+
}
|
|
12
|
+
function toAbsoluteUrl(origin, url) {
|
|
13
|
+
return new URL(url, origin).toString();
|
|
14
|
+
}
|
|
15
|
+
function buildAlternateLink(hreflang, origin, url) {
|
|
16
|
+
return `<xhtml:link rel="alternate" hreflang="${escapeXml(hreflang)}" href="${escapeXml(toAbsoluteUrl(origin, url))}"/>`;
|
|
17
|
+
}
|
|
18
|
+
function buildUrl(origin, url, lastMod, alternates = []) {
|
|
19
|
+
const lines = [
|
|
20
|
+
" <url>",
|
|
21
|
+
` <loc>${escapeXml(toAbsoluteUrl(origin, url))}</loc>`,
|
|
22
|
+
` <lastmod>${escapeXml(lastMod)}</lastmod>`,
|
|
23
|
+
...alternates.map((alternate) => ` ${alternate}`),
|
|
24
|
+
" </url>"
|
|
25
|
+
];
|
|
26
|
+
return lines.join("\n");
|
|
27
|
+
}
|
|
28
|
+
function normalizeEntries(entries, locale) {
|
|
29
|
+
const defaultLocale = `${locale.language}-${locale.location}`;
|
|
30
|
+
return entries.map((entry) => {
|
|
31
|
+
if (typeof entry === "string") {
|
|
32
|
+
return {
|
|
33
|
+
url: entry,
|
|
34
|
+
locale: defaultLocale,
|
|
35
|
+
name: entry
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
}).sort((left, right) => left.url.localeCompare(right.url));
|
|
40
|
+
}
|
|
41
|
+
function groupByName(entries) {
|
|
42
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const group = grouped.get(entry.name) || [];
|
|
45
|
+
group.push(entry);
|
|
46
|
+
grouped.set(entry.name, group);
|
|
47
|
+
}
|
|
48
|
+
return [...grouped.values()];
|
|
49
|
+
}
|
|
50
|
+
function buildLocalizedUrls(entries, origin, locale, lastMod) {
|
|
51
|
+
const defaultLocale = `${locale.language}-${locale.location}`;
|
|
52
|
+
return groupByName(entries).map((pages) => {
|
|
53
|
+
const pagesByLanguage = pages.reduce((result, page) => {
|
|
54
|
+
const language = getLanguage(page.locale);
|
|
55
|
+
const group = result.get(language) || [];
|
|
56
|
+
group.push(page);
|
|
57
|
+
result.set(language, group);
|
|
58
|
+
return result;
|
|
59
|
+
}, /* @__PURE__ */ new Map());
|
|
60
|
+
const defaultEntry = pages.find((page) => page.locale === defaultLocale) || pages[0];
|
|
61
|
+
const alternates = [buildAlternateLink("x-default", origin, defaultEntry.url)];
|
|
62
|
+
for (const [language, localizedPages] of [...pagesByLanguage.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
63
|
+
const sortedPages = [...localizedPages].sort((left, right) => left.locale.localeCompare(right.locale));
|
|
64
|
+
if (sortedPages.length > 1) {
|
|
65
|
+
const languageDefault = sortedPages.find((page) => getLocation(page.locale) === locale.location) || sortedPages[0];
|
|
66
|
+
alternates.push(buildAlternateLink(language, origin, languageDefault.url));
|
|
67
|
+
for (const page of sortedPages) {
|
|
68
|
+
alternates.push(buildAlternateLink(page.locale, origin, page.url));
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
alternates.push(buildAlternateLink(language, origin, sortedPages[0].url));
|
|
73
|
+
}
|
|
74
|
+
return buildUrl(origin, defaultEntry.url, lastMod, alternates);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export function buildSitemapXml(entries, origin, locale) {
|
|
78
|
+
const normalizedEntries = normalizeEntries(entries, locale);
|
|
79
|
+
const lastMod = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
80
|
+
const isMultiple = locale.locales.length > 1;
|
|
81
|
+
const urls = isMultiple ? buildLocalizedUrls(normalizedEntries, origin, locale, lastMod) : normalizedEntries.map((entry) => buildUrl(origin, entry.url, lastMod));
|
|
82
|
+
const alternateNamespace = isMultiple ? ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' : "";
|
|
83
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
84
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${alternateNamespace}>
|
|
85
|
+
${urls.join("\n")}
|
|
86
|
+
</urlset>`;
|
|
87
|
+
}
|
package/package.json
CHANGED
package/server/lib/render.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import bases from "virtual:flow/bases";
|
|
|
5
5
|
import layouts from "virtual:flow/layouts";
|
|
6
6
|
import templates from "virtual:flow/templates";
|
|
7
7
|
import { createSSRApp, defineComponent, h } from "vue";
|
|
8
|
-
import { getFlowImageRuntimeUtils } from "../../modules/images/runtime/server.mjs";
|
|
8
|
+
import { getFlowImageBootPayload, getFlowImageRuntimeUtils } from "../../modules/images/runtime/server.mjs";
|
|
9
9
|
import { installFlowVuePlugins } from "../../src/runtime/vue";
|
|
10
10
|
import { getUrl, getUrls } from "./pages.mjs";
|
|
11
11
|
function escapeHtml(value) {
|
|
@@ -152,7 +152,8 @@ export async function renderDocument(page, clientAssets) {
|
|
|
152
152
|
template: page.definition.view.template,
|
|
153
153
|
title: fallbackTitle,
|
|
154
154
|
locale: page.locale,
|
|
155
|
-
mode
|
|
155
|
+
mode,
|
|
156
|
+
images: getFlowImageBootPayload()
|
|
156
157
|
};
|
|
157
158
|
const rendered = await renderBody(page);
|
|
158
159
|
const body = stripVueFragmentMarkers(rendered.body);
|
package/src/public/nitro.mjs
CHANGED
|
@@ -16,7 +16,10 @@ export function createFlowNitroConfig(options = {}) {
|
|
|
16
16
|
const flowConfig = resolveFlowConfig(options.userFlowConfig || {});
|
|
17
17
|
const flowModules = loadFlowModules(projectRoot, flowConfig);
|
|
18
18
|
const flowNitroConfig = { ...flowConfig.nitro || {} };
|
|
19
|
-
const flowNitroHooks =
|
|
19
|
+
const flowNitroHooks = {
|
|
20
|
+
...flowNitroConfig.hooks || {},
|
|
21
|
+
...flowModules.nitro.hooks
|
|
22
|
+
};
|
|
20
23
|
const configuredNoExternals = Array.isArray(flowNitroConfig.noExternals) ? flowNitroConfig.noExternals : [];
|
|
21
24
|
const flowPackagePattern = /^@monkeyplus\/flow(?:\/.*)?$/;
|
|
22
25
|
const flowRuntimeConfig = typeof flowModules.nitro.runtimeConfig.flow === "object" && flowModules.nitro.runtimeConfig.flow ? flowModules.nitro.runtimeConfig.flow : {};
|
|
@@ -82,6 +82,7 @@ async function fetchContent(route, path) {
|
|
|
82
82
|
const localFetch = getGlobalFetch();
|
|
83
83
|
const nitroFetch = await getNitroFetch();
|
|
84
84
|
if (typeof window === "undefined" && localFetch) {
|
|
85
|
+
console.debug(`[Flow Content] Fetching content from ${joinUrl(apiBase, route)} using global fetch`);
|
|
85
86
|
return await localFetch(joinUrl(apiBase, route), {
|
|
86
87
|
headers: {
|
|
87
88
|
accept: "application/json"
|
|
@@ -90,6 +91,7 @@ async function fetchContent(route, path) {
|
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
if (typeof window === "undefined" && nitroFetch) {
|
|
94
|
+
console.debug(`[Flow Content] Fetching content from ${joinUrl(apiBase, route)} using Nitro fetch`);
|
|
93
95
|
return await nitroFetch(joinUrl(apiBase, route), {
|
|
94
96
|
headers: {
|
|
95
97
|
accept: "application/json"
|
|
@@ -99,6 +101,7 @@ async function fetchContent(route, path) {
|
|
|
99
101
|
}
|
|
100
102
|
const absoluteBase = await resolveAbsoluteContentApiBase();
|
|
101
103
|
const target = withQuery(joinUrl(absoluteBase, route), query);
|
|
104
|
+
console.debug(`[Flow Content] Fetching content from ${target}`);
|
|
102
105
|
const response = await fetch(target, {
|
|
103
106
|
headers: {
|
|
104
107
|
accept: "application/json"
|
package/src/runtime/boot.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import type { FlowImageMeta, FlowImageOptions } from '../../modules/images/runtime/types.ts';
|
|
1
2
|
import type { FlowHydrationMode, FlowLocale } from './pages';
|
|
3
|
+
export interface FlowImageBootPayload {
|
|
4
|
+
all: Record<string, FlowImageMeta>;
|
|
5
|
+
options: FlowImageOptions;
|
|
6
|
+
generateOutput: boolean;
|
|
7
|
+
}
|
|
2
8
|
export interface FlowBootPayload {
|
|
3
9
|
path: string;
|
|
4
10
|
bundle: string;
|
|
@@ -6,4 +12,5 @@ export interface FlowBootPayload {
|
|
|
6
12
|
title: string;
|
|
7
13
|
locale: FlowLocale;
|
|
8
14
|
mode: FlowHydrationMode;
|
|
15
|
+
images?: FlowImageBootPayload;
|
|
9
16
|
}
|
|
@@ -173,14 +173,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
173
173
|
preset: string;
|
|
174
174
|
modifiers: Record<string, any>;
|
|
175
175
|
sizes: string | Record<string, any>;
|
|
176
|
+
background: string;
|
|
177
|
+
quality: string | number;
|
|
176
178
|
alt: string;
|
|
177
179
|
referrerpolicy: string;
|
|
178
180
|
usemap: string;
|
|
179
181
|
longdesc: string;
|
|
180
182
|
ismap: boolean;
|
|
181
183
|
loading: string;
|
|
182
|
-
quality: string | number;
|
|
183
|
-
background: string;
|
|
184
184
|
title: string;
|
|
185
185
|
sync: boolean;
|
|
186
186
|
thumbnail: string | boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, defineComponent, h } from "vue";
|
|
2
2
|
import { parseSize } from "../../../modules/images/runtime/helpers.mjs";
|
|
3
|
-
import { useImage } from "./image-shared.mjs";
|
|
3
|
+
import { useImage, useLazySizes } from "./image-shared.mjs";
|
|
4
4
|
function resolveImageSource(value, fallback) {
|
|
5
5
|
if (typeof value === "string" && value.length) {
|
|
6
6
|
return value;
|
|
@@ -110,6 +110,7 @@ export default defineComponent({
|
|
|
110
110
|
thumbnail
|
|
111
111
|
};
|
|
112
112
|
});
|
|
113
|
+
useLazySizes(() => !isRuntimeLambda() && !!nSrc.value.thumbnail);
|
|
113
114
|
return () => {
|
|
114
115
|
const compatibilityThumb = !isRuntimeLambda() && nSrc.value.thumbnail;
|
|
115
116
|
return h("img", {
|
|
@@ -183,14 +183,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
183
183
|
preset: string;
|
|
184
184
|
modifiers: Record<string, any>;
|
|
185
185
|
sizes: string | Record<string, any>;
|
|
186
|
+
background: string;
|
|
187
|
+
quality: string | number;
|
|
186
188
|
alt: string;
|
|
187
189
|
referrerpolicy: string;
|
|
188
190
|
usemap: string;
|
|
189
191
|
longdesc: string;
|
|
190
192
|
ismap: boolean;
|
|
191
193
|
loading: string;
|
|
192
|
-
quality: string | number;
|
|
193
|
-
background: string;
|
|
194
194
|
title: string;
|
|
195
195
|
sync: boolean;
|
|
196
196
|
thumbnail: string | boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, defineComponent, h } from "vue";
|
|
2
2
|
import { getFileExtension, screens } from "../../../modules/images/runtime/helpers.mjs";
|
|
3
|
-
import { useImage } from "./image-shared.mjs";
|
|
3
|
+
import { useImage, useLazySizes } from "./image-shared.mjs";
|
|
4
4
|
function getLocalSource(src) {
|
|
5
5
|
return src.startsWith("http") ? "" : src;
|
|
6
6
|
}
|
|
@@ -118,16 +118,17 @@ export default defineComponent({
|
|
|
118
118
|
}, nOption.value);
|
|
119
119
|
return typeof thumbnail === "string" ? thumbnail : props.src;
|
|
120
120
|
});
|
|
121
|
+
useLazySizes(() => !isRuntimeLambda() && !!nThumbnail.value);
|
|
121
122
|
return () => {
|
|
122
123
|
if (isRuntimeLambda()) {
|
|
123
124
|
return h("img", {
|
|
124
125
|
...attrs,
|
|
125
126
|
...nImgAttrs.value,
|
|
126
|
-
src: props.src,
|
|
127
|
-
alt: props.alt || image.value.alt,
|
|
128
|
-
title: props.title || image.value.title,
|
|
129
|
-
width: props.eWidth || nImgAttrs.value.width,
|
|
130
|
-
height: props.eHeight || nImgAttrs.value.height,
|
|
127
|
+
"src": props.src,
|
|
128
|
+
"alt": props.alt || image.value.alt,
|
|
129
|
+
"title": props.title || image.value.title,
|
|
130
|
+
"width": props.eWidth || nImgAttrs.value.width,
|
|
131
|
+
"height": props.eHeight || nImgAttrs.value.height,
|
|
131
132
|
"x-src": getLocalSource(props.src)
|
|
132
133
|
});
|
|
133
134
|
}
|
|
@@ -139,18 +140,18 @@ export default defineComponent({
|
|
|
139
140
|
const imgAttrs = {
|
|
140
141
|
...attrs,
|
|
141
142
|
...nImgAttrs.value,
|
|
142
|
-
src: compatibilityThumb || primarySource.src,
|
|
143
|
-
srcset: compatibilityThumb ? void 0 : primarySource.srcset,
|
|
144
|
-
sizes: compatibilityThumb ? void 0 : primarySource.sizes,
|
|
143
|
+
"src": compatibilityThumb || primarySource.src,
|
|
144
|
+
"srcset": compatibilityThumb ? void 0 : primarySource.srcset,
|
|
145
|
+
"sizes": compatibilityThumb ? void 0 : primarySource.sizes,
|
|
145
146
|
"data-src": compatibilityThumb ? primarySource.src : void 0,
|
|
146
147
|
"data-srcset": compatibilityThumb ? primarySource.srcset : void 0,
|
|
147
148
|
"data-sizes": compatibilityThumb ? primarySource.sizes : void 0,
|
|
148
149
|
"data-thumb": compatibilityThumb,
|
|
149
|
-
alt: props.alt || image.value.alt,
|
|
150
|
-
title: props.title || image.value.title,
|
|
151
|
-
width: props.eWidth || nImgAttrs.value.width,
|
|
152
|
-
height: props.eHeight || nImgAttrs.value.height,
|
|
153
|
-
class: compatibilityThumb ? [attrs.class, "lazyload", props.classImg] : [attrs.class, props.classImg],
|
|
150
|
+
"alt": props.alt || image.value.alt,
|
|
151
|
+
"title": props.title || image.value.title,
|
|
152
|
+
"width": props.eWidth || nImgAttrs.value.width,
|
|
153
|
+
"height": props.eHeight || nImgAttrs.value.height,
|
|
154
|
+
"class": compatibilityThumb ? [attrs.class, "lazyload", props.classImg] : [attrs.class, props.classImg],
|
|
154
155
|
"x-src": getLocalSource(props.src)
|
|
155
156
|
};
|
|
156
157
|
if (options.value.lazy && !props.sync && !imgAttrs.loading) {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { FlowImageMeta, FlowImageOptions, GetImageFunction } from '../../../modules/images/runtime/types.ts';
|
|
2
|
+
import type { FlowBootPayload } from '../boot.ts';
|
|
2
3
|
export interface InjectedImageUtils {
|
|
3
4
|
getImage?: GetImageFunction;
|
|
4
5
|
getImageMeta?: (src: string) => FlowImageMeta | undefined;
|
|
5
6
|
getImageOptions?: () => FlowImageOptions | undefined;
|
|
6
7
|
}
|
|
7
8
|
export declare function useInjectedImageUtils(): InjectedImageUtils;
|
|
9
|
+
export declare function useLazySizes(enabled: () => unknown): void;
|
|
10
|
+
export declare function createBootImageUtils(boot?: FlowBootPayload): InjectedImageUtils;
|
|
8
11
|
export declare function useImage(props: Record<string, any>): {
|
|
9
12
|
imageUtils: InjectedImageUtils;
|
|
10
13
|
image: import("vue").ComputedRef<FlowImageMeta>;
|
|
@@ -1,8 +1,47 @@
|
|
|
1
|
-
import { computed, inject } from "vue";
|
|
1
|
+
import { computed, inject, watchEffect } from "vue";
|
|
2
2
|
import { getNormalName, parseSize } from "../../../modules/images/runtime/helpers.mjs";
|
|
3
|
+
import { createImageResolver } from "../../../modules/images/runtime/image.mjs";
|
|
4
|
+
let lazySizesLoadPromise;
|
|
5
|
+
function loadLazySizes() {
|
|
6
|
+
if (lazySizesLoadPromise) {
|
|
7
|
+
return lazySizesLoadPromise;
|
|
8
|
+
}
|
|
9
|
+
lazySizesLoadPromise = import("lazysizes");
|
|
10
|
+
return lazySizesLoadPromise;
|
|
11
|
+
}
|
|
3
12
|
export function useInjectedImageUtils() {
|
|
4
13
|
return inject("utils", {});
|
|
5
14
|
}
|
|
15
|
+
export function useLazySizes(enabled) {
|
|
16
|
+
watchEffect(() => {
|
|
17
|
+
if (typeof window === "undefined" || !enabled()) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
void loadLazySizes();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function createBootImageUtils(boot) {
|
|
24
|
+
const snapshot = boot?.images;
|
|
25
|
+
if (!snapshot) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
const stateImages = {
|
|
29
|
+
all: snapshot.all || {},
|
|
30
|
+
generate: {}
|
|
31
|
+
};
|
|
32
|
+
const resolver = createImageResolver(snapshot.options, stateImages, {
|
|
33
|
+
generateOutput: snapshot.generateOutput
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
getImage: resolver.getImage,
|
|
37
|
+
getImageMeta(src) {
|
|
38
|
+
return stateImages.all[src];
|
|
39
|
+
},
|
|
40
|
+
getImageOptions() {
|
|
41
|
+
return { ...snapshot.options };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
6
45
|
export function useImage(props) {
|
|
7
46
|
const imageUtils = useInjectedImageUtils();
|
|
8
47
|
const nImgAttrs = computed(() => ({
|
package/src/runtime/config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Options as AutoimportOptions } from 'unplugin-auto-import/types';
|
|
2
2
|
import type { Options as ComponentOptions } from 'unplugin-vue-components/types';
|
|
3
|
+
import type { FlowImagesModuleOptions } from '../../modules/images/runtime/types.ts';
|
|
3
4
|
export interface FlowDirEntry {
|
|
4
5
|
dir: string;
|
|
5
6
|
}
|
|
@@ -45,6 +46,7 @@ export interface FlowConfig {
|
|
|
45
46
|
};
|
|
46
47
|
build: FlowBuildConfig;
|
|
47
48
|
locale: FlowLocaleConfig;
|
|
49
|
+
images?: Partial<FlowImagesModuleOptions>;
|
|
48
50
|
server?: FlowServerConfig;
|
|
49
51
|
siteUrl?: string;
|
|
50
52
|
nitro?: Record<string, unknown>;
|
|
@@ -56,12 +58,14 @@ export type UserFlowConfig = Partial<FlowConfig> & Record<string, unknown> & {
|
|
|
56
58
|
pages?: FlowDirEntry[];
|
|
57
59
|
};
|
|
58
60
|
locale?: Partial<FlowLocaleConfig>;
|
|
61
|
+
images?: Partial<FlowImagesModuleOptions>;
|
|
59
62
|
server?: FlowServerConfig;
|
|
60
63
|
siteUrl?: string;
|
|
61
64
|
components?: Partial<ComponentOptions>;
|
|
62
65
|
autoImport?: Partial<AutoimportOptions>;
|
|
63
66
|
};
|
|
64
67
|
export interface FlowModuleNitroConfig extends Record<string, unknown> {
|
|
68
|
+
hooks: Record<string, unknown>;
|
|
65
69
|
plugins: string[];
|
|
66
70
|
handlers: Array<Record<string, unknown>>;
|
|
67
71
|
routeRules: Record<string, Record<string, unknown>>;
|
package/src/runtime/islands.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import islands from "virtual:flow/islands";
|
|
2
2
|
import { createSSRApp } from "vue";
|
|
3
|
+
import { createBootImageUtils } from "./components/image-shared.mjs";
|
|
3
4
|
import { getClientHead } from "./head.mjs";
|
|
4
5
|
import { installFlowVuePlugins } from "./vue.mjs";
|
|
5
6
|
function parseIslandProps(value) {
|
|
@@ -39,7 +40,9 @@ async function hydrateIsland(element, boot) {
|
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
const app = createSSRApp(mod.default, props);
|
|
43
|
+
const imageUtils = createBootImageUtils(boot);
|
|
42
44
|
app.use(getClientHead());
|
|
45
|
+
app.provide("utils", imageUtils);
|
|
43
46
|
installFlowVuePlugins(app);
|
|
44
47
|
app.mount(element);
|
|
45
48
|
element.dataset.flowIslandHydrated = "true";
|
package/src/runtime/modules.mjs
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
});
|