@ecopages/image-processor 0.2.0-beta.3 → 0.2.0-beta.31
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 +4 -4
- package/src/image-processor.js +7 -3
- package/src/image-runtime-state.d.ts +20 -0
- package/src/image-runtime-state.js +58 -0
- package/src/plugin.d.ts +17 -10
- package/src/plugin.js +61 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/image-processor",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.31",
|
|
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.
|
|
18
|
+
"@ecopages/core": "0.2.0-beta.31"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@ecopages/file-system": "0.2.0-beta.
|
|
21
|
+
"@ecopages/file-system": "0.2.0-beta.31",
|
|
22
22
|
"@ecopages/logger": "^0.2.3",
|
|
23
23
|
"react": "^19.2.6",
|
|
24
24
|
"react-dom": "^19.2.6",
|
package/src/image-processor.js
CHANGED
|
@@ -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(
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
58
|
-
|
|
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
|
-
*
|
|
67
|
-
*
|
|
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
|
|
75
|
+
private syncProcessedImagesFromDisk;
|
|
76
|
+
private ensureGeneratedOutputs;
|
|
77
|
+
private writeGeneratedFile;
|
|
71
78
|
/**
|
|
72
|
-
*
|
|
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
|
-
*
|
|
114
|
-
* manifest.
|
|
110
|
+
* Resolves processor config and creates the sharp-backed worker instance.
|
|
115
111
|
*/
|
|
116
|
-
|
|
117
|
-
if (this.
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
202
|
+
async ensureGeneratedOutputs() {
|
|
178
203
|
if (!this.processor) {
|
|
179
204
|
throw new Error("ImageProcessor not initialized");
|
|
180
205
|
}
|
|
181
|
-
if (this.
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
217
|
+
fileSystem.write(filePath, content);
|
|
198
218
|
}
|
|
199
219
|
/**
|
|
200
|
-
*
|
|
220
|
+
* Ensures optimized image outputs and virtual-module state exist on disk.
|
|
201
221
|
*/
|
|
202
222
|
async setup() {
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|