@monkeyplus/flow 6.0.7 → 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.
@@ -1,5 +1,2 @@
1
- export default function ipxHandler(event: any): Promise<string | void | Buffer<ArrayBufferLike> | {
2
- error: {
3
- message: string;
4
- };
5
- }> | Response;
1
+ declare const _default: any;
2
+ export default _default;
@@ -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 config = useRuntimeConfig();
8
- const publicDir = config.flow?.images?.publicDir;
9
- const domains = Object.keys(config.flow?.images?.options?.domains || {});
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 ? { httpStorage: ipxHttpStorage({ domains }) } : {}
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 function ipxHandler(event) {
26
- const handler = resolveHandler();
27
- if (!handler) {
28
- return new Response("Not Found", { status: 404 });
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
- return handler(event);
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 { joinURL } from "ufo";
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 = joinURL(dirImages, "strapi");
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 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) {
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.plugins.push(buildPluginPath);
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
- 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
- }
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.url, 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
- 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);
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 { FlowImagesState, FlowImageOptions, GeneratedImageEntry, GetImageFunction } from './types.ts';
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 { joinURL, encodeParam, encodePath } from "ufo";
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
- image.generate = decodeURIComponent(image.url).replace(decodeURIComponent(input), replacedPath).replace(`/${baseDir}`, "").replace("_ipx", baseDir).split("?")[0];
133
- if (image.generate.startsWith("/")) {
134
- image.generate = image.generate.replace(/\/+/g, "/");
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,3 @@
1
- import type { FlowImagesRuntimeConfig, FlowImageRuntimeUtils } from './types.ts';
1
+ import type { FlowImageRuntimeUtils, FlowImagesRuntimeConfig } from './types.ts';
2
2
  export declare function getFlowImageRuntimeUtils(config?: FlowImagesRuntimeConfig | undefined): FlowImageRuntimeUtils | undefined;
3
3
  export declare function resetFlowImageRuntimeState(): void;
@@ -1,19 +1,31 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
- import { dirname } from "node:path";
3
2
  import { createRequire } from "node:module";
4
- import { loadImageRenames } from "./renames.mjs";
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 {
16
- return void 0;
28
+ return getEnvFlowImagesRuntimeConfig();
17
29
  }
18
30
  }
19
31
  export function getFlowImageRuntimeUtils(config = getFlowImagesRuntimeConfig()) {
@@ -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
- function escapeXml(value) {
5
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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 = `<?xml version="1.0" encoding="UTF-8"?>
12
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
13
- ${urls.map((entry) => typeof entry === "string" ? ` <url><loc>${escapeXml(new URL(entry, origin).toString())}</loc></url>` : ` <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>`).join("\n")}
14
- </urlset>`;
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, locales) {
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 enabledLocales = ${JSON.stringify(locales)};
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 toPublicRoutePattern(localePage.url);
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 = toPublicRoutePattern(localePage.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, flowRuntimeConfig) {
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 flowRuntimeConfig.siteUrl === "string" ? flowRuntimeConfig.siteUrl : void 0,
224
- Array.isArray(flowRuntimeConfig.locale?.locales) ? flowRuntimeConfig.locale.locales : []
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, contextRuntimeConfig);
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monkeyplus/flow",
3
- "version": "6.0.7",
3
+ "version": "6.0.8",
4
4
  "description": "@monkeyplus/flow package-first runtime with Vite, Nitro, Vue and a workspace playground.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -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 = flowNitroConfig.hooks || {};
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"
@@ -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", {
@@ -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) {
@@ -5,6 +5,7 @@ export interface InjectedImageUtils {
5
5
  getImageOptions?: () => FlowImageOptions | undefined;
6
6
  }
7
7
  export declare function useInjectedImageUtils(): InjectedImageUtils;
8
+ export declare function useLazySizes(enabled: () => unknown): void;
8
9
  export declare function useImage(props: Record<string, any>): {
9
10
  imageUtils: InjectedImageUtils;
10
11
  image: import("vue").ComputedRef<FlowImageMeta>;
@@ -1,8 +1,25 @@
1
1
  import { computed, inject } from "vue";
2
+ import { watchEffect } from "vue";
2
3
  import { getNormalName, parseSize } from "../../../modules/images/runtime/helpers.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
+ }
6
23
  export function useImage(props) {
7
24
  const imageUtils = useInjectedImageUtils();
8
25
  const nImgAttrs = computed(() => ({
@@ -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>>;
@@ -2,6 +2,7 @@ import { dirname, resolve } from "node:path";
2
2
  import { createJiti } from "jiti";
3
3
  function createNitroConfig() {
4
4
  return {
5
+ hooks: {},
5
6
  plugins: [],
6
7
  handlers: [],
7
8
  routeRules: {},
@@ -1,2 +0,0 @@
1
- declare const _default: any;
2
- export default _default;
@@ -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
- });