@ecopages/image-processor 0.2.0-beta.2 → 0.2.0-beta.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/image-processor",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.20",
4
4
  "description": "Image processor, transform and optimize images for web",
5
5
  "keywords": [
6
6
  "image",
@@ -11,14 +11,14 @@
11
11
  "type": "module",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "https://github.com/ecopages/ecopages.git",
14
+ "url": "git+https://github.com/ecopages/ecopages.git",
15
15
  "directory": "packages/processors/image-processor"
16
16
  },
17
17
  "peerDependencies": {
18
- "@ecopages/core": "0.2.0-beta.2"
18
+ "@ecopages/core": "0.2.0-beta.20"
19
19
  },
20
20
  "dependencies": {
21
- "@ecopages/file-system": "0.2.0-beta.2",
21
+ "@ecopages/file-system": "0.2.0-beta.20",
22
22
  "@ecopages/logger": "^0.2.3",
23
23
  "react": "^19.2.6",
24
24
  "react-dom": "^19.2.6",
@@ -37,10 +37,10 @@ class ImageProcessor {
37
37
  if (this.config.cacheEnabled) {
38
38
  const cached = await this.cacheManager.readCache(cacheKey);
39
39
  if (cached) {
40
- const mainFilePath = path.join(process.cwd(), cached.attributes.src);
40
+ const mainFilePath = path.join(this.config.outputDir, path.basename(cached.attributes.src));
41
41
  const mainFileExists = fileSystem.exists(mainFilePath);
42
42
  const variantsExist = cached.variants.every(
43
- (v) => fileSystem.exists(path.join(process.cwd(), v.src))
43
+ (variant) => fileSystem.exists(path.join(this.config.outputDir, path.basename(variant.src)))
44
44
  );
45
45
  if (mainFileExists && variantsExist) {
46
46
  appLogger.debug(`Cache hit for ${imagePath}`);
@@ -0,0 +1,20 @@
1
+ import type { ImageSpecifications } from './types.js';
2
+ export type ImageMap = Record<string, ImageSpecifications>;
3
+ export type ImageProcessorDiskConfig = {
4
+ sourceDir: string;
5
+ outputDir: string;
6
+ acceptedFormats?: string[];
7
+ };
8
+ /**
9
+ * Parses export bindings from the generated image virtual-module source file.
10
+ */
11
+ export declare function parseVirtualModuleExports(content: string): Record<string, ImageSpecifications> | null;
12
+ export declare function getSourceImagePaths(resolvedConfig: ImageProcessorDiskConfig): Promise<string[]>;
13
+ /**
14
+ * Restores processed image state from generated dist artifacts when every source
15
+ * image has a matching virtual-module export and output files on disk.
16
+ */
17
+ export declare function loadProcessedImagesFromDisk(options: {
18
+ resolvedConfig: ImageProcessorDiskConfig;
19
+ runtimeVirtualModulePath: string;
20
+ }): Promise<ImageMap | null>;
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ import { anyCaseToCamelCase } from "./utils.js";
4
+ function parseVirtualModuleExports(content) {
5
+ const exports = {};
6
+ const pattern = /export const (\w+) = ([\s\S]*?) as const;/g;
7
+ let match;
8
+ while ((match = pattern.exec(content)) !== null) {
9
+ try {
10
+ exports[match[1]] = JSON.parse(match[2]);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ return exports;
16
+ }
17
+ async function getSourceImagePaths(resolvedConfig) {
18
+ const acceptedFormats = resolvedConfig.acceptedFormats ?? ["jpg", "jpeg", "png", "webp"];
19
+ return fileSystem.glob([`${resolvedConfig.sourceDir}/**/*.{${acceptedFormats.join(",")}}`]);
20
+ }
21
+ function getGeneratedOutputPath(outputDir, src) {
22
+ return path.join(outputDir, path.basename(src));
23
+ }
24
+ async function loadProcessedImagesFromDisk(options) {
25
+ const sourceImages = await getSourceImagePaths(options.resolvedConfig);
26
+ if (sourceImages.length === 0) {
27
+ return {};
28
+ }
29
+ if (!fileSystem.exists(options.runtimeVirtualModulePath)) {
30
+ return null;
31
+ }
32
+ const exportsByName = parseVirtualModuleExports(fileSystem.readFileSync(options.runtimeVirtualModulePath));
33
+ if (!exportsByName) {
34
+ return null;
35
+ }
36
+ const imageMap = {};
37
+ for (const file of sourceImages) {
38
+ const basename = path.basename(file);
39
+ const exportName = anyCaseToCamelCase(basename);
40
+ const spec = exportsByName[exportName];
41
+ if (!spec) {
42
+ return null;
43
+ }
44
+ const outputPaths = [spec.attributes.src, ...spec.variants.map((variant) => variant.src)];
45
+ if (!outputPaths.every(
46
+ (src) => fileSystem.exists(getGeneratedOutputPath(options.resolvedConfig.outputDir, src))
47
+ )) {
48
+ return null;
49
+ }
50
+ imageMap[basename] = spec;
51
+ }
52
+ return imageMap;
53
+ }
54
+ export {
55
+ getSourceImagePaths,
56
+ loadProcessedImagesFromDisk,
57
+ parseVirtualModuleExports
58
+ };
package/src/plugin.d.ts CHANGED
@@ -32,7 +32,7 @@ export interface ImageProcessorConfig {
32
32
  * ImageMap
33
33
  * This is the representation of the image map in the virtual module
34
34
  */
35
- export type ImageMap = Record<string, ImageSpecifications>;
35
+ export type { ImageMap } from './image-runtime-state.js';
36
36
  /**
37
37
  * ImageProcessorPlugin
38
38
  * A Processor for optimizing images.
@@ -54,22 +54,29 @@ export declare class ImageProcessorPlugin extends Processor<ImageProcessorConfig
54
54
  */
55
55
  private replaceProcessedImages;
56
56
  /**
57
- * Prepares the image virtual-module state before config build seals the app
58
- * manifest.
57
+ * Resolves processor config and creates the sharp-backed worker instance.
58
+ */
59
+ private initializeProcessor;
60
+ /**
61
+ * Prepares build-facing processor state before config build seals the app manifest.
62
+ *
63
+ * @remarks
64
+ * Image processing stays in {@link setup}; this hook only initializes config and
65
+ * restores any previously generated virtual-module state from disk.
59
66
  */
60
67
  prepareBuildContributions(): Promise<void>;
61
68
  private getRuntimeVirtualModulePath;
62
69
  private getGeneratedOutputPath;
63
- private hasGeneratedOutputs;
64
- private rehydrateGeneratedOutputs;
65
70
  /**
66
- * Generate dependencies for processor.
67
- * It is ossible to define which one should be included in the final bundle based on the environment.
68
- * @returns
71
+ * Restores in-memory image state from previously generated dist artifacts.
72
+ *
73
+ * @returns `true` when state was restored without invoking sharp.
69
74
  */
70
- private generateDependencies;
75
+ private syncProcessedImagesFromDisk;
76
+ private ensureGeneratedOutputs;
77
+ private writeGeneratedFile;
71
78
  /**
72
- * Prepares build contributions if not already done and rehydrates previously generated image outputs.
79
+ * Ensures optimized image outputs and virtual-module state exist on disk.
73
80
  */
74
81
  setup(): Promise<void>;
75
82
  /**
package/src/plugin.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import { Logger } from "@ecopages/logger";
9
9
  import { createImagePlugin, createImagePluginBundler } from "./image-plugins.js";
10
10
  import { ImageProcessor } from "./image-processor.js";
11
+ import { getSourceImagePaths, loadProcessedImagesFromDisk } from "./image-runtime-state.js";
11
12
  import { anyCaseToCamelCase } from "./utils.js";
12
13
  function resolveGeneratedPath(type, options) {
13
14
  const { root, module, subPath } = options;
@@ -110,11 +111,10 @@ class ImageProcessorPlugin extends Processor {
110
111
  Object.assign(this.processedImages, images);
111
112
  }
112
113
  /**
113
- * Prepares the image virtual-module state before config build seals the app
114
- * manifest.
114
+ * Resolves processor config and creates the sharp-backed worker instance.
115
115
  */
116
- async prepareBuildContributions() {
117
- if (this.buildContributionsPrepared) {
116
+ initializeProcessor() {
117
+ if (this.processor && this.resolvedConfig) {
118
118
  return;
119
119
  }
120
120
  if (!this.context) {
@@ -138,12 +138,27 @@ class ImageProcessorPlugin extends Processor {
138
138
  readCache: (key) => this.readCache(key),
139
139
  writeCache: (key, data) => this.writeCache(key, data)
140
140
  });
141
- this.replaceProcessedImages(await this.processor.processDirectory());
142
141
  if (this.watchConfig) {
143
142
  this.watchConfig.paths = [config.sourceDir];
144
143
  }
145
- this.dependencies = this.generateDependencies();
146
- this.generateTypes();
144
+ }
145
+ /**
146
+ * Prepares build-facing processor state before config build seals the app manifest.
147
+ *
148
+ * @remarks
149
+ * Image processing stays in {@link setup}; this hook only initializes config and
150
+ * restores any previously generated virtual-module state from disk.
151
+ */
152
+ async prepareBuildContributions() {
153
+ if (this.buildContributionsPrepared) {
154
+ return;
155
+ }
156
+ if (process.env.ECOPAGES_LIT_STATIC_RENDER_WORKER === "true") {
157
+ this.buildContributionsPrepared = true;
158
+ return;
159
+ }
160
+ this.initializeProcessor();
161
+ await this.syncProcessedImagesFromDisk();
147
162
  this.buildContributionsPrepared = true;
148
163
  }
149
164
  getRuntimeVirtualModulePath() {
@@ -162,46 +177,58 @@ class ImageProcessorPlugin extends Processor {
162
177
  }
163
178
  return path.join(this.resolvedConfig.outputDir, path.basename(src));
164
179
  }
165
- hasGeneratedOutputs() {
180
+ /**
181
+ * Restores in-memory image state from previously generated dist artifacts.
182
+ *
183
+ * @returns `true` when state was restored without invoking sharp.
184
+ */
185
+ async syncProcessedImagesFromDisk() {
166
186
  if (!this.resolvedConfig) {
167
187
  return false;
168
188
  }
169
- if (!fileSystem.exists(this.resolvedConfig.outputDir) || !fileSystem.exists(this.getRuntimeVirtualModulePath())) {
189
+ const sourceImages = await getSourceImagePaths(this.resolvedConfig);
190
+ const imageMap = await loadProcessedImagesFromDisk({
191
+ resolvedConfig: this.resolvedConfig,
192
+ runtimeVirtualModulePath: this.getRuntimeVirtualModulePath()
193
+ });
194
+ if (imageMap === null) {
170
195
  return false;
171
196
  }
172
- return Object.values(this.processedImages).every((image) => {
173
- const outputPaths = [image.attributes.src, ...image.variants.map((variant) => variant.src)];
174
- return outputPaths.every((src) => fileSystem.exists(this.getGeneratedOutputPath(src)));
175
- });
197
+ if (sourceImages.length > 0 && Object.keys(imageMap).length !== sourceImages.length) {
198
+ return false;
199
+ }
200
+ this.replaceProcessedImages(imageMap);
201
+ if (Object.keys(imageMap).length > 0) {
202
+ this.generateTypes();
203
+ }
204
+ return true;
176
205
  }
177
- async rehydrateGeneratedOutputs() {
206
+ async ensureGeneratedOutputs() {
178
207
  if (!this.processor) {
179
208
  throw new Error("ImageProcessor not initialized");
180
209
  }
181
- if (this.hasGeneratedOutputs()) {
210
+ if (await this.syncProcessedImagesFromDisk()) {
182
211
  return;
183
212
  }
184
213
  this.replaceProcessedImages(await this.processor.processDirectory());
185
- this.dependencies = this.generateDependencies();
186
214
  this.generateTypes();
187
215
  }
188
- /**
189
- * Generate dependencies for processor.
190
- * It is ossible to define which one should be included in the final bundle based on the environment.
191
- * @returns
192
- */
193
- generateDependencies() {
194
- const deps = [];
195
- if (process.env.NODE_ENV === "development") {
216
+ writeGeneratedFile(filePath, content) {
217
+ fileSystem.ensureDir(path.dirname(filePath));
218
+ if (fileSystem.exists(filePath) && fileSystem.readFileSync(filePath) === content) {
219
+ return;
196
220
  }
197
- return deps;
221
+ fileSystem.write(filePath, content);
198
222
  }
199
223
  /**
200
- * Prepares build contributions if not already done and rehydrates previously generated image outputs.
224
+ * Ensures optimized image outputs and virtual-module state exist on disk.
201
225
  */
202
226
  async setup() {
203
- await this.prepareBuildContributions();
204
- await this.rehydrateGeneratedOutputs();
227
+ if (process.env.ECOPAGES_LIT_STATIC_RENDER_WORKER === "true") {
228
+ return;
229
+ }
230
+ this.initializeProcessor();
231
+ await this.ensureGeneratedOutputs();
205
232
  }
206
233
  /**
207
234
  * Process images.
@@ -288,8 +315,7 @@ declare module "ecopages:images" {
288
315
  module: this.name,
289
316
  subPath: "virtual-module.d.ts"
290
317
  });
291
- fileSystem.ensureDir(path.dirname(typesDir));
292
- fileSystem.write(typesDir, content);
318
+ this.writeGeneratedFile(typesDir, content);
293
319
  logger.debug("Generated types for virtual module", { typesDir });
294
320
  const indexTypesDir = resolveGeneratedPath("types", {
295
321
  root: this.context.rootDir,
@@ -297,7 +323,7 @@ declare module "ecopages:images" {
297
323
  subPath: "index.d.ts"
298
324
  });
299
325
  const indexContent = 'import "./virtual-module.d.js";';
300
- fileSystem.write(indexTypesDir, indexContent);
326
+ this.writeGeneratedFile(indexTypesDir, indexContent);
301
327
  logger.debug("Generated index types for virtual module", { indexTypesDir });
302
328
  const runtimeVirtualModulePath = resolveGeneratedPath("cache", {
303
329
  root: this.context.distDir,
@@ -307,8 +333,7 @@ declare module "ecopages:images" {
307
333
  const runtimeModuleContent = Object.entries(this.processedImages).map(([key, value]) => {
308
334
  return `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value, null, 2)} as const;`;
309
335
  }).join("\n\n");
310
- fileSystem.ensureDir(path.dirname(runtimeVirtualModulePath));
311
- fileSystem.write(runtimeVirtualModulePath, runtimeModuleContent);
336
+ this.writeGeneratedFile(runtimeVirtualModulePath, runtimeModuleContent);
312
337
  logger.debug("Generated runtime virtual module for images", { runtimeVirtualModulePath });
313
338
  }
314
339
  /**