@place-framework/place-block-image 1.0.0
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/README.md +117 -0
- package/dist/constants/index.d.ts +9 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +17 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/react-jsx.d.ts +2 -0
- package/dist/templates/react-jsx.d.ts.map +1 -0
- package/dist/templates/react-jsx.js +29 -0
- package/dist/templates/react-jsx.js.map +1 -0
- package/dist/templates/react-tsx.d.ts +2 -0
- package/dist/templates/react-tsx.d.ts.map +1 -0
- package/dist/templates/react-tsx.js +35 -0
- package/dist/templates/react-tsx.js.map +1 -0
- package/dist/templates/shared/index.d.ts +6 -0
- package/dist/templates/shared/index.d.ts.map +1 -0
- package/dist/templates/shared/index.js +49 -0
- package/dist/templates/shared/index.js.map +1 -0
- package/dist/templates/shared/react.d.ts +10 -0
- package/dist/templates/shared/react.d.ts.map +1 -0
- package/dist/templates/shared/react.js +48 -0
- package/dist/templates/shared/react.js.map +1 -0
- package/dist/templates/vue.d.ts +2 -0
- package/dist/templates/vue.d.ts.map +1 -0
- package/dist/templates/vue.js +100 -0
- package/dist/templates/vue.js.map +1 -0
- package/dist/templates.d.ts +5 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +32 -0
- package/dist/templates.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +32 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/webpack-plugin.d.ts +49 -0
- package/dist/webpack-plugin.d.ts.map +1 -0
- package/dist/webpack-plugin.js +259 -0
- package/dist/webpack-plugin.js.map +1 -0
- package/package.json +49 -0
- package/src/constants/index.ts +14 -0
- package/src/index.ts +4 -0
- package/src/templates/react-jsx.ts +27 -0
- package/src/templates/react-tsx.ts +33 -0
- package/src/templates/shared/index.ts +47 -0
- package/src/templates/shared/react.ts +51 -0
- package/src/templates/vue.ts +98 -0
- package/src/templates.ts +29 -0
- package/src/utils/index.ts +35 -0
- package/src/webpack-plugin.ts +273 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webpack-plugin.d.ts","sourceRoot":"","sources":["../src/webpack-plugin.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IAEjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IACtC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,OAAO,CAAyD;IACxE,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,kBAAkB,CAAa;gBAE3B,OAAO,EAAE,4BAA4B;IASjD;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;YACW,UAAU;IA8BxB;;OAEG;IACH,OAAO,CAAC,YAAY;IAmCpB;;OAEG;YACW,aAAa;IAY3B;;OAEG;YACW,kBAAkB;IA0BhC;;OAEG;YACW,gBAAgB;IAyB9B,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;YA4BjB,mBAAmB;CA8ClC;AAED,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.PlaceBlockImagePlugin = void 0;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const glob_1 = require("glob");
|
|
43
|
+
const image_size_1 = __importDefault(require("image-size"));
|
|
44
|
+
const templates_1 = require("./templates");
|
|
45
|
+
class PlaceBlockImagePlugin {
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.isGenerating = false;
|
|
48
|
+
this.lastGenerationTime = 0;
|
|
49
|
+
this.options = {
|
|
50
|
+
imagePrefix: 'image-',
|
|
51
|
+
generateComponent: true,
|
|
52
|
+
componentType: 'tsx',
|
|
53
|
+
...options
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate CSS class name from filename
|
|
58
|
+
*/
|
|
59
|
+
generateClassName(filename) {
|
|
60
|
+
const name = path.basename(filename, path.extname(filename));
|
|
61
|
+
// Convert to kebab-case and remove special characters
|
|
62
|
+
const cleanName = name
|
|
63
|
+
.toLowerCase()
|
|
64
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
65
|
+
.replace(/-+/g, '-')
|
|
66
|
+
.replace(/^-|-$/g, '');
|
|
67
|
+
return `${this.options.imagePrefix}${cleanName}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scan directory for images and get their dimensions
|
|
71
|
+
*/
|
|
72
|
+
async scanImages() {
|
|
73
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'];
|
|
74
|
+
const pattern = `${this.options.imageDir}/**/*.{${imageExtensions.join(',')}}`;
|
|
75
|
+
const files = await (0, glob_1.glob)(pattern, { nodir: true });
|
|
76
|
+
const results = [];
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
try {
|
|
79
|
+
const dimensions = (0, image_size_1.default)(file);
|
|
80
|
+
if (dimensions.width && dimensions.height) {
|
|
81
|
+
const relativePath = path.relative(this.options.imageDir, file);
|
|
82
|
+
const className = this.generateClassName(relativePath);
|
|
83
|
+
results.push({
|
|
84
|
+
width: dimensions.width,
|
|
85
|
+
height: dimensions.height,
|
|
86
|
+
filename: relativePath,
|
|
87
|
+
className
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.warn(`⚠️ Could not read dimensions for ${file}:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generate SCSS with CSS custom properties and base class
|
|
99
|
+
*/
|
|
100
|
+
generateScss(images) {
|
|
101
|
+
const wrapperClassName = `${this.options.imagePrefix}wrapper`;
|
|
102
|
+
const lines = [
|
|
103
|
+
'// Auto-generated image dimensions',
|
|
104
|
+
'// This file is generated by place-block-image webpack plugin',
|
|
105
|
+
'// Do not edit manually - changes will be overwritten',
|
|
106
|
+
'',
|
|
107
|
+
'// Base wrapper and image styles',
|
|
108
|
+
`.${wrapperClassName} {`,
|
|
109
|
+
' display: block;',
|
|
110
|
+
' aspect-ratio: calc(var(--p-width) / var(--p-height));',
|
|
111
|
+
'',
|
|
112
|
+
' &,',
|
|
113
|
+
' img {',
|
|
114
|
+
' display: block;',
|
|
115
|
+
' }',
|
|
116
|
+
'',
|
|
117
|
+
'// Ensure images fill the wrapper and maintain aspect ratio',
|
|
118
|
+
' img {',
|
|
119
|
+
' width: 100%;',
|
|
120
|
+
' height: 100%;',
|
|
121
|
+
' object-fit: cover;',
|
|
122
|
+
' }',
|
|
123
|
+
'}',
|
|
124
|
+
'',
|
|
125
|
+
'// Image-specific dimensions',
|
|
126
|
+
...images.map(img => `.${wrapperClassName}.${img.className} {\n --p-width: ${img.width}px;\n --p-height: ${img.height}px;\n}`),
|
|
127
|
+
''
|
|
128
|
+
];
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Write SCSS file
|
|
133
|
+
*/
|
|
134
|
+
async writeScssFile(images) {
|
|
135
|
+
const scssContent = this.generateScss(images);
|
|
136
|
+
const outputDir = path.dirname(this.options.scssPath);
|
|
137
|
+
// Ensure output directory exists
|
|
138
|
+
if (!fs.existsSync(outputDir)) {
|
|
139
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
fs.writeFileSync(this.options.scssPath, scssContent);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generate and write component file
|
|
145
|
+
*/
|
|
146
|
+
async writeComponentFile() {
|
|
147
|
+
if (!this.options.generateComponent || !this.options.componentPath || !this.options.componentType) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const componentFileName = `PlaceBlockImage.${this.options.componentType}`;
|
|
151
|
+
const componentFilePath = path.resolve(this.options.componentPath, componentFileName);
|
|
152
|
+
// Check if component already exists
|
|
153
|
+
if (fs.existsSync(componentFilePath)) {
|
|
154
|
+
console.log(`📦 Component already exists, skipping: ${componentFilePath}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const componentContent = (0, templates_1.getTemplate)(this.options.componentType, this.options.imagePrefix);
|
|
158
|
+
const outputDir = path.dirname(componentFilePath);
|
|
159
|
+
// Ensure output directory exists
|
|
160
|
+
if (!fs.existsSync(outputDir)) {
|
|
161
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
fs.writeFileSync(componentFilePath, componentContent);
|
|
164
|
+
console.log(`📦 Generated component: ${componentFilePath}`);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if images have changed since last generation
|
|
168
|
+
*/
|
|
169
|
+
async shouldRegenerate() {
|
|
170
|
+
if (!fs.existsSync(this.options.scssPath)) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
const scssStats = fs.statSync(this.options.scssPath);
|
|
174
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'];
|
|
175
|
+
const pattern = `${this.options.imageDir}/**/*.{${imageExtensions.join(',')}}`;
|
|
176
|
+
try {
|
|
177
|
+
const files = await (0, glob_1.glob)(pattern, { nodir: true });
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const fileStats = fs.statSync(file);
|
|
180
|
+
if (fileStats.mtime > scssStats.mtime) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
apply(compiler) {
|
|
191
|
+
// Use environment hook to run only once at startup
|
|
192
|
+
compiler.hooks.environment.tap('PlaceBlockImagePlugin', () => {
|
|
193
|
+
this.generateImageStyles();
|
|
194
|
+
});
|
|
195
|
+
// Also run on watchRun for file changes during development
|
|
196
|
+
compiler.hooks.watchRun.tapAsync('PlaceBlockImagePlugin', async (compiler, callback) => {
|
|
197
|
+
// Prevent running too frequently
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
if (this.isGenerating || (now - this.lastGenerationTime) < 1000) {
|
|
200
|
+
callback();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const shouldRegenerate = await this.shouldRegenerate();
|
|
205
|
+
if (shouldRegenerate) {
|
|
206
|
+
await this.generateImageStyles();
|
|
207
|
+
}
|
|
208
|
+
callback();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error('❌ PlaceBlockImagePlugin error:', error);
|
|
212
|
+
callback();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async generateImageStyles() {
|
|
217
|
+
if (this.isGenerating) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.isGenerating = true;
|
|
221
|
+
this.lastGenerationTime = Date.now();
|
|
222
|
+
try {
|
|
223
|
+
console.log('🖼️ PlaceBlockImagePlugin: Generating image styles...');
|
|
224
|
+
if (!fs.existsSync(this.options.imageDir)) {
|
|
225
|
+
console.warn(`⚠️ Image directory does not exist: ${this.options.imageDir}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.log(`🔍 Scanning images in: ${this.options.imageDir}`);
|
|
229
|
+
const images = await this.scanImages();
|
|
230
|
+
if (images.length === 0) {
|
|
231
|
+
console.log('⚠️ No images found');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await this.writeScssFile(images);
|
|
235
|
+
await this.writeComponentFile();
|
|
236
|
+
console.log(`✅ Generated CSS custom properties for ${images.length} images`);
|
|
237
|
+
console.log(`📝 Output: ${this.options.scssPath}`);
|
|
238
|
+
// Log some examples
|
|
239
|
+
if (images.length > 0) {
|
|
240
|
+
console.log('📋 Generated classes:');
|
|
241
|
+
images.slice(0, 3).forEach(img => {
|
|
242
|
+
console.log(` .${img.className} { --p-width: ${img.width}; --p-height: ${img.height}; }`);
|
|
243
|
+
});
|
|
244
|
+
if (images.length > 3) {
|
|
245
|
+
console.log(` ... and ${images.length - 3} more`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error('❌ PlaceBlockImagePlugin error:', error);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
this.isGenerating = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
exports.PlaceBlockImagePlugin = PlaceBlockImagePlugin;
|
|
258
|
+
exports.default = PlaceBlockImagePlugin;
|
|
259
|
+
//# sourceMappingURL=webpack-plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webpack-plugin.js","sourceRoot":"","sources":["../src/webpack-plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@place-framework/place-block-image",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A utility package for generating CSS custom properties from image dimensions to prevent layout shift",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"generate": "node dist/generate.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"image",
|
|
15
|
+
"layout-shift",
|
|
16
|
+
"css",
|
|
17
|
+
"custom-properties",
|
|
18
|
+
"react",
|
|
19
|
+
"vue",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"author": "Brian Kelley",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"@types/glob": "^8.1.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"image-size": "^1.0.2",
|
|
31
|
+
"glob": "^10.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"webpack": ">=5.0.0",
|
|
35
|
+
"react": ">=16.8.0",
|
|
36
|
+
"vue": ">=3.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"react": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
42
|
+
"vue": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"webpack": {
|
|
46
|
+
"optional": false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const CLASS_NAMES = {
|
|
2
|
+
// Lazy loading states
|
|
3
|
+
LAZY: 'lazy',
|
|
4
|
+
LOADED: 'loaded',
|
|
5
|
+
|
|
6
|
+
// Wrapper classes
|
|
7
|
+
IMAGE_WRAPPER: 'image-wrapper',
|
|
8
|
+
|
|
9
|
+
// Base image class
|
|
10
|
+
IMAGE_BLOCK: 'image-block'
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export const getWrapperClassName = (imagePrefix: string) => `${imagePrefix}wrapper`;
|
|
14
|
+
export const getImageClassName = (imagePrefix: string, filename: string) => `${imagePrefix}${filename}`;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getSharedReactTemplate } from './shared/react';
|
|
2
|
+
|
|
3
|
+
export function getReactJsxTemplate(imagePrefix: string): string {
|
|
4
|
+
const shared = getSharedReactTemplate(imagePrefix);
|
|
5
|
+
|
|
6
|
+
return `${shared.imports}
|
|
7
|
+
|
|
8
|
+
${shared.comment}
|
|
9
|
+
export const PlaceBlockImage = ({
|
|
10
|
+
src,
|
|
11
|
+
alt,
|
|
12
|
+
lazy = false,
|
|
13
|
+
className = '',
|
|
14
|
+
...props
|
|
15
|
+
}) => {
|
|
16
|
+
${shared.hooks}
|
|
17
|
+
|
|
18
|
+
${shared.getImageClassName}
|
|
19
|
+
|
|
20
|
+
${shared.classNames}
|
|
21
|
+
|
|
22
|
+
${shared.jsx}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
${shared.export}
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getSharedReactTemplate } from './shared/react';
|
|
2
|
+
|
|
3
|
+
export function getReactTsxTemplate(imagePrefix: string): string {
|
|
4
|
+
const shared = getSharedReactTemplate(imagePrefix);
|
|
5
|
+
|
|
6
|
+
return `${shared.imports}
|
|
7
|
+
|
|
8
|
+
interface PlaceBlockImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
9
|
+
src: string;
|
|
10
|
+
alt: string;
|
|
11
|
+
lazy?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
${shared.comment}
|
|
15
|
+
export const PlaceBlockImage: React.FC<PlaceBlockImageProps> = ({
|
|
16
|
+
src,
|
|
17
|
+
alt,
|
|
18
|
+
lazy = false,
|
|
19
|
+
className = '',
|
|
20
|
+
...props
|
|
21
|
+
}) => {
|
|
22
|
+
${shared.hooks}
|
|
23
|
+
|
|
24
|
+
${shared.getImageClassName}
|
|
25
|
+
|
|
26
|
+
${shared.classNames}
|
|
27
|
+
|
|
28
|
+
${shared.jsx}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
${shared.export}
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CLASS_NAMES } from '../../constants';
|
|
2
|
+
|
|
3
|
+
export const getSharedTemplate = (imagePrefix: string) => ({
|
|
4
|
+
// Common comment block
|
|
5
|
+
comment: `/**
|
|
6
|
+
* PlaceBlockImage component that prevents layout shift using CSS custom properties
|
|
7
|
+
* Generated by place-block-image webpack plugin
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* <PlaceBlockImage src="/images/logo.svg" alt="Logo" />
|
|
11
|
+
* <PlaceBlockImage src="/images/hero.jpg" alt="Hero" lazy={true} />
|
|
12
|
+
*
|
|
13
|
+
* This will automatically apply:
|
|
14
|
+
* - .${imagePrefix}wrapper class on picture (for dimensions)
|
|
15
|
+
* - .${imagePrefix}logo class on picture (specific dimensions via CSS custom properties)
|
|
16
|
+
* - ${CLASS_NAMES.LAZY}/${CLASS_NAMES.LAZY}.${CLASS_NAMES.LOADED} classes for lazy loading states
|
|
17
|
+
*/`,
|
|
18
|
+
|
|
19
|
+
// Common filename extraction logic (as string for interpolation)
|
|
20
|
+
getImageClassNameTemplate: `// Extract filename from src to generate class name
|
|
21
|
+
const getImageClassName = (imageSrc: string): string => {
|
|
22
|
+
// Remove /images/ prefix and file extension, convert to kebab-case
|
|
23
|
+
const filename = imageSrc
|
|
24
|
+
.replace(/^.*\\/images\\//, '') // Remove path up to /images/
|
|
25
|
+
.replace(/\\.[^/.]+$/, '') // Remove file extension
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9-]/g, '-') // Convert special chars to hyphens
|
|
28
|
+
.replace(/-+/g, '-') // Remove duplicate hyphens
|
|
29
|
+
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
30
|
+
|
|
31
|
+
return \`${imagePrefix}\${filename}\`;
|
|
32
|
+
};`,
|
|
33
|
+
|
|
34
|
+
// Common intersection observer logic
|
|
35
|
+
intersectionObserverTemplate: `const observer = new IntersectionObserver(
|
|
36
|
+
(entries) => {
|
|
37
|
+
entries.forEach((entry) => {
|
|
38
|
+
if (entry.isIntersecting) {
|
|
39
|
+
setImageSrc(src);
|
|
40
|
+
setIsLoaded(true);
|
|
41
|
+
observer.unobserve(entry.target);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
{ threshold: 0.1 }
|
|
46
|
+
);`
|
|
47
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getSharedTemplate } from './index';
|
|
2
|
+
import { CLASS_NAMES } from '../../constants';
|
|
3
|
+
|
|
4
|
+
export const getSharedReactTemplate = (imagePrefix: string) => {
|
|
5
|
+
const shared = getSharedTemplate(imagePrefix);
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
imports: `import React, { useRef, useEffect, useState } from 'react';`,
|
|
9
|
+
|
|
10
|
+
comment: shared.comment,
|
|
11
|
+
|
|
12
|
+
hooks: ` const imgRef = useRef(null);
|
|
13
|
+
const [imageSrc, setImageSrc] = useState(lazy ? '' : src);
|
|
14
|
+
const [isLoaded, setIsLoaded] = useState(!lazy);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!lazy || isLoaded) return;
|
|
18
|
+
|
|
19
|
+
${shared.intersectionObserverTemplate}
|
|
20
|
+
|
|
21
|
+
if (imgRef.current) {
|
|
22
|
+
observer.observe(imgRef.current);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return () => observer.disconnect();
|
|
26
|
+
}, [src, lazy, isLoaded]);`,
|
|
27
|
+
|
|
28
|
+
getImageClassName: shared.getImageClassNameTemplate,
|
|
29
|
+
|
|
30
|
+
classNames: ` const imageClassName = getImageClassName(src);
|
|
31
|
+
const wrapperClassName = \`${imagePrefix}wrapper \${imageClassName} \${className || ''}\`.trim();
|
|
32
|
+
|
|
33
|
+
// Build img className with lazy states
|
|
34
|
+
const lazyClass = lazy ? (isLoaded ? '${CLASS_NAMES.LAZY} ${CLASS_NAMES.LOADED}' : '${CLASS_NAMES.LAZY}') : '';
|
|
35
|
+
const imgClassName = \`${CLASS_NAMES.IMAGE_BLOCK} \${lazyClass}\`.trim();`,
|
|
36
|
+
|
|
37
|
+
jsx: ` return (
|
|
38
|
+
<picture className={wrapperClassName}>
|
|
39
|
+
<img
|
|
40
|
+
ref={imgRef}
|
|
41
|
+
src={imageSrc}
|
|
42
|
+
alt={alt}
|
|
43
|
+
className={imgClassName}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
</picture>
|
|
47
|
+
);`,
|
|
48
|
+
|
|
49
|
+
export: `export default PlaceBlockImage;`
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { getSharedTemplate } from './shared/';
|
|
2
|
+
import { CLASS_NAMES } from '../constants';
|
|
3
|
+
|
|
4
|
+
export function getVueTemplate(imagePrefix: string): string {
|
|
5
|
+
const shared = getSharedTemplate(imagePrefix);
|
|
6
|
+
|
|
7
|
+
return `<template>
|
|
8
|
+
<picture :class="wrapperClassName">
|
|
9
|
+
<img
|
|
10
|
+
ref="imgRef"
|
|
11
|
+
:src="imageSrc"
|
|
12
|
+
:alt="alt"
|
|
13
|
+
:class="imgClassName"
|
|
14
|
+
v-bind="$attrs"
|
|
15
|
+
/>
|
|
16
|
+
</picture>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
src: string;
|
|
24
|
+
alt: string;
|
|
25
|
+
lazy?: boolean;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
30
|
+
lazy: false,
|
|
31
|
+
class: ''
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
${shared.comment}
|
|
35
|
+
|
|
36
|
+
const imgRef = ref<HTMLImageElement | null>(null);
|
|
37
|
+
const imageSrc = ref(props.lazy ? '' : props.src);
|
|
38
|
+
const isLoaded = ref(!props.lazy);
|
|
39
|
+
let observer: IntersectionObserver | null = null;
|
|
40
|
+
|
|
41
|
+
${shared.getImageClassNameTemplate}
|
|
42
|
+
|
|
43
|
+
const imageClassName = computed(() => getImageClassName(props.src));
|
|
44
|
+
const wrapperClassName = computed(() =>
|
|
45
|
+
\`${imagePrefix}wrapper \${imageClassName.value}\`
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const lazyClass = computed(() =>
|
|
49
|
+
props.lazy ? (isLoaded.value ? '${CLASS_NAMES.LAZY} ${CLASS_NAMES.LOADED}' : '${CLASS_NAMES.LAZY}') : ''
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const imgClassName = computed(() =>
|
|
53
|
+
\`\${props.class} \${lazyClass.value}\`.trim()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const setupLazyLoading = () => {
|
|
57
|
+
if (!props.lazy || isLoaded.value) return;
|
|
58
|
+
|
|
59
|
+
${shared.intersectionObserverTemplate}
|
|
60
|
+
|
|
61
|
+
if (imgRef.value) {
|
|
62
|
+
observer.observe(imgRef.value);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const cleanupObserver = () => {
|
|
67
|
+
if (observer) {
|
|
68
|
+
observer.disconnect();
|
|
69
|
+
observer = null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
onMounted(() => {
|
|
74
|
+
setupLazyLoading();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
onUnmounted(() => {
|
|
78
|
+
cleanupObserver();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
watch(() => props.src, (newSrc) => {
|
|
82
|
+
if (!props.lazy) {
|
|
83
|
+
imageSrc.value = newSrc;
|
|
84
|
+
} else if (!isLoaded.value) {
|
|
85
|
+
cleanupObserver();
|
|
86
|
+
setupLazyLoading();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
watch(isLoaded, (loaded) => {
|
|
91
|
+
if (loaded) {
|
|
92
|
+
imageSrc.value = props.src;
|
|
93
|
+
cleanupObserver();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
`;
|
|
98
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Main template exports
|
|
2
|
+
import { getReactTsxTemplate as getReactTsx } from './templates/react-tsx';
|
|
3
|
+
import { getReactJsxTemplate as getReactJsx } from './templates/react-jsx';
|
|
4
|
+
import { getVueTemplate as getVue } from './templates/vue';
|
|
5
|
+
|
|
6
|
+
export function getReactTsxTemplate(imagePrefix: string): string {
|
|
7
|
+
return getReactTsx(imagePrefix);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getReactJsxTemplate(imagePrefix: string): string {
|
|
11
|
+
return getReactJsx(imagePrefix);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getVueTemplate(imagePrefix: string): string {
|
|
15
|
+
return getVue(imagePrefix);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getTemplate(type: 'tsx' | 'jsx' | 'vue', imagePrefix: string): string {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'tsx':
|
|
21
|
+
return getReactTsxTemplate(imagePrefix);
|
|
22
|
+
case 'jsx':
|
|
23
|
+
return getReactJsxTemplate(imagePrefix);
|
|
24
|
+
case 'vue':
|
|
25
|
+
return getVueTemplate(imagePrefix);
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(`Unsupported component type: ${type}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Shared template utilities
|
|
2
|
+
|
|
3
|
+
export interface TemplateData {
|
|
4
|
+
imagePrefix: string;
|
|
5
|
+
baseClassName: string;
|
|
6
|
+
wrapperClassName: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getTemplateData(imagePrefix: string): TemplateData {
|
|
10
|
+
return {
|
|
11
|
+
imagePrefix,
|
|
12
|
+
baseClassName: `${imagePrefix}block`,
|
|
13
|
+
wrapperClassName: `${imagePrefix}wrapper`
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getSharedLogic(imagePrefix: string): string {
|
|
18
|
+
return `
|
|
19
|
+
// Extract filename from src to generate class name
|
|
20
|
+
const getImageClassName = (imageSrc) => {
|
|
21
|
+
// Remove /images/ prefix and file extension, convert to kebab-case
|
|
22
|
+
const filename = imageSrc
|
|
23
|
+
.replace(/^.*\\/images\\//, '') // Remove path up to /images/
|
|
24
|
+
.replace(/\\.[^/.]+$/, '') // Remove file extension
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9-]/g, '-') // Convert special chars to hyphens
|
|
27
|
+
.replace(/-+/g, '-') // Remove duplicate hyphens
|
|
28
|
+
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
29
|
+
|
|
30
|
+
return \`${imagePrefix}\${filename}\`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const imageClassName = getImageClassName(src);
|
|
34
|
+
const wrapperClassName = \`${imagePrefix}wrapper \${imageClassName}\`;`;
|
|
35
|
+
}
|