@ecopages/image-processor 0.2.0-beta.3 → 0.2.0-beta.30

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.3",
3
+ "version": "0.2.0-beta.30",
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.3"
18
+ "@ecopages/core": "0.2.0-beta.30"
19
19
  },
20
20
  "dependencies": {
21
- "@ecopages/file-system": "0.2.0-beta.3",
21
+ "@ecopages/file-system": "0.2.0-beta.30",
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}`);
@@ -132,7 +132,11 @@ class ImageProcessor {
132
132
  })
133
133
  )).filter(Boolean);
134
134
  appLogger.debugTimeEnd("Processing images");
135
- appLogger.info(`Processed ${results.length} images`);
135
+ if (process.env.ECOPAGES_BENCH === "1" && process.env.ECOPAGES_BENCH_VERBOSE !== "1") {
136
+ appLogger.debug(`Processed ${results.length} images`);
137
+ } else {
138
+ appLogger.info(`Processed ${results.length} images`);
139
+ }
136
140
  return Object.fromEntries(results);
137
141
  }
138
142
  }
@@ -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
@@ -1,19 +1,16 @@
1
1
  import path from "node:path";
2
- import { GENERATED_BASE_PATHS } from "@ecopages/core/constants";
3
2
  import { fileSystem } from "@ecopages/file-system";
4
3
  import {
5
4
  mergeProcessorOptions,
6
- Processor
5
+ Processor,
6
+ resolveGeneratedPath
7
7
  } from "@ecopages/core/plugins/processor";
8
+ import { isLitStaticRenderWorkerThread } from "@ecopages/core/build/lit-static-render-worker-context";
8
9
  import { Logger } from "@ecopages/logger";
9
10
  import { createImagePlugin, createImagePluginBundler } from "./image-plugins.js";
10
11
  import { ImageProcessor } from "./image-processor.js";
12
+ import { getSourceImagePaths, loadProcessedImagesFromDisk } from "./image-runtime-state.js";
11
13
  import { anyCaseToCamelCase } from "./utils.js";
12
- function resolveGeneratedPath(type, options) {
13
- const { root, module, subPath } = options;
14
- const parts = [root, GENERATED_BASE_PATHS[type], module, subPath].filter(Boolean);
15
- return path.join(...parts);
16
- }
17
14
  const logger = new Logger("[@ecopages/image-processor]", {
18
15
  debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
19
16
  });
@@ -110,11 +107,10 @@ class ImageProcessorPlugin extends Processor {
110
107
  Object.assign(this.processedImages, images);
111
108
  }
112
109
  /**
113
- * Prepares the image virtual-module state before config build seals the app
114
- * manifest.
110
+ * Resolves processor config and creates the sharp-backed worker instance.
115
111
  */
116
- async prepareBuildContributions() {
117
- if (this.buildContributionsPrepared) {
112
+ initializeProcessor() {
113
+ if (this.processor && this.resolvedConfig) {
118
114
  return;
119
115
  }
120
116
  if (!this.context) {
@@ -138,12 +134,27 @@ class ImageProcessorPlugin extends Processor {
138
134
  readCache: (key) => this.readCache(key),
139
135
  writeCache: (key, data) => this.writeCache(key, data)
140
136
  });
141
- this.replaceProcessedImages(await this.processor.processDirectory());
142
137
  if (this.watchConfig) {
143
138
  this.watchConfig.paths = [config.sourceDir];
144
139
  }
145
- this.dependencies = this.generateDependencies();
146
- this.generateTypes();
140
+ }
141
+ /**
142
+ * Prepares build-facing processor state before config build seals the app manifest.
143
+ *
144
+ * @remarks
145
+ * Image processing stays in {@link setup}; this hook only initializes config and
146
+ * restores any previously generated virtual-module state from disk.
147
+ */
148
+ async prepareBuildContributions() {
149
+ if (this.buildContributionsPrepared) {
150
+ return;
151
+ }
152
+ if (isLitStaticRenderWorkerThread()) {
153
+ this.buildContributionsPrepared = true;
154
+ return;
155
+ }
156
+ this.initializeProcessor();
157
+ await this.syncProcessedImagesFromDisk();
147
158
  this.buildContributionsPrepared = true;
148
159
  }
149
160
  getRuntimeVirtualModulePath() {
@@ -162,46 +173,58 @@ class ImageProcessorPlugin extends Processor {
162
173
  }
163
174
  return path.join(this.resolvedConfig.outputDir, path.basename(src));
164
175
  }
165
- hasGeneratedOutputs() {
176
+ /**
177
+ * Restores in-memory image state from previously generated dist artifacts.
178
+ *
179
+ * @returns `true` when state was restored without invoking sharp.
180
+ */
181
+ async syncProcessedImagesFromDisk() {
166
182
  if (!this.resolvedConfig) {
167
183
  return false;
168
184
  }
169
- if (!fileSystem.exists(this.resolvedConfig.outputDir) || !fileSystem.exists(this.getRuntimeVirtualModulePath())) {
185
+ const sourceImages = await getSourceImagePaths(this.resolvedConfig);
186
+ const imageMap = await loadProcessedImagesFromDisk({
187
+ resolvedConfig: this.resolvedConfig,
188
+ runtimeVirtualModulePath: this.getRuntimeVirtualModulePath()
189
+ });
190
+ if (imageMap === null) {
170
191
  return false;
171
192
  }
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
- });
193
+ if (sourceImages.length > 0 && Object.keys(imageMap).length !== sourceImages.length) {
194
+ return false;
195
+ }
196
+ this.replaceProcessedImages(imageMap);
197
+ if (Object.keys(imageMap).length > 0) {
198
+ this.generateTypes();
199
+ }
200
+ return true;
176
201
  }
177
- async rehydrateGeneratedOutputs() {
202
+ async ensureGeneratedOutputs() {
178
203
  if (!this.processor) {
179
204
  throw new Error("ImageProcessor not initialized");
180
205
  }
181
- if (this.hasGeneratedOutputs()) {
206
+ if (await this.syncProcessedImagesFromDisk()) {
182
207
  return;
183
208
  }
184
209
  this.replaceProcessedImages(await this.processor.processDirectory());
185
- this.dependencies = this.generateDependencies();
186
210
  this.generateTypes();
187
211
  }
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") {
212
+ writeGeneratedFile(filePath, content) {
213
+ fileSystem.ensureDir(path.dirname(filePath));
214
+ if (fileSystem.exists(filePath) && fileSystem.readFileSync(filePath) === content) {
215
+ return;
196
216
  }
197
- return deps;
217
+ fileSystem.write(filePath, content);
198
218
  }
199
219
  /**
200
- * Prepares build contributions if not already done and rehydrates previously generated image outputs.
220
+ * Ensures optimized image outputs and virtual-module state exist on disk.
201
221
  */
202
222
  async setup() {
203
- await this.prepareBuildContributions();
204
- await this.rehydrateGeneratedOutputs();
223
+ if (isLitStaticRenderWorkerThread()) {
224
+ return;
225
+ }
226
+ this.initializeProcessor();
227
+ await this.ensureGeneratedOutputs();
205
228
  }
206
229
  /**
207
230
  * Process images.
@@ -288,8 +311,7 @@ declare module "ecopages:images" {
288
311
  module: this.name,
289
312
  subPath: "virtual-module.d.ts"
290
313
  });
291
- fileSystem.ensureDir(path.dirname(typesDir));
292
- fileSystem.write(typesDir, content);
314
+ this.writeGeneratedFile(typesDir, content);
293
315
  logger.debug("Generated types for virtual module", { typesDir });
294
316
  const indexTypesDir = resolveGeneratedPath("types", {
295
317
  root: this.context.rootDir,
@@ -297,7 +319,7 @@ declare module "ecopages:images" {
297
319
  subPath: "index.d.ts"
298
320
  });
299
321
  const indexContent = 'import "./virtual-module.d.js";';
300
- fileSystem.write(indexTypesDir, indexContent);
322
+ this.writeGeneratedFile(indexTypesDir, indexContent);
301
323
  logger.debug("Generated index types for virtual module", { indexTypesDir });
302
324
  const runtimeVirtualModulePath = resolveGeneratedPath("cache", {
303
325
  root: this.context.distDir,
@@ -307,8 +329,7 @@ declare module "ecopages:images" {
307
329
  const runtimeModuleContent = Object.entries(this.processedImages).map(([key, value]) => {
308
330
  return `export const ${anyCaseToCamelCase(key)} = ${JSON.stringify(value, null, 2)} as const;`;
309
331
  }).join("\n\n");
310
- fileSystem.ensureDir(path.dirname(runtimeVirtualModulePath));
311
- fileSystem.write(runtimeVirtualModulePath, runtimeModuleContent);
332
+ this.writeGeneratedFile(runtimeVirtualModulePath, runtimeModuleContent);
312
333
  logger.debug("Generated runtime virtual module for images", { runtimeVirtualModulePath });
313
334
  }
314
335
  /**