@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,273 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
import sizeOf from 'image-size';
|
|
5
|
+
import { Compiler } from 'webpack';
|
|
6
|
+
import { getTemplate } from './templates';
|
|
7
|
+
|
|
8
|
+
export interface PlaceBlockImagePluginOptions {
|
|
9
|
+
imagePrefix?: string;
|
|
10
|
+
imageDir: string;
|
|
11
|
+
scssPath: string;
|
|
12
|
+
// Component generation options
|
|
13
|
+
componentPath?: string;
|
|
14
|
+
componentType?: 'tsx' | 'jsx' | 'vue';
|
|
15
|
+
generateComponent?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ImageDimensions {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
filename: string;
|
|
22
|
+
className: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class PlaceBlockImagePlugin {
|
|
26
|
+
private options: PlaceBlockImagePluginOptions & { imagePrefix: string };
|
|
27
|
+
private isGenerating: boolean = false;
|
|
28
|
+
private lastGenerationTime: number = 0;
|
|
29
|
+
|
|
30
|
+
constructor(options: PlaceBlockImagePluginOptions) {
|
|
31
|
+
this.options = {
|
|
32
|
+
imagePrefix: 'image-',
|
|
33
|
+
generateComponent: true,
|
|
34
|
+
componentType: 'tsx',
|
|
35
|
+
...options
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate CSS class name from filename
|
|
41
|
+
*/
|
|
42
|
+
private generateClassName(filename: string): string {
|
|
43
|
+
const name = path.basename(filename, path.extname(filename));
|
|
44
|
+
// Convert to kebab-case and remove special characters
|
|
45
|
+
const cleanName = name
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
48
|
+
.replace(/-+/g, '-')
|
|
49
|
+
.replace(/^-|-$/g, '');
|
|
50
|
+
|
|
51
|
+
return `${this.options.imagePrefix}${cleanName}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Scan directory for images and get their dimensions
|
|
56
|
+
*/
|
|
57
|
+
private async scanImages(): Promise<ImageDimensions[]> {
|
|
58
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'];
|
|
59
|
+
const pattern = `${this.options.imageDir}/**/*.{${imageExtensions.join(',')}}`;
|
|
60
|
+
|
|
61
|
+
const files = await glob(pattern, { nodir: true });
|
|
62
|
+
const results: ImageDimensions[] = [];
|
|
63
|
+
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
try {
|
|
66
|
+
const dimensions = sizeOf(file);
|
|
67
|
+
|
|
68
|
+
if (dimensions.width && dimensions.height) {
|
|
69
|
+
const relativePath = path.relative(this.options.imageDir, file);
|
|
70
|
+
const className = this.generateClassName(relativePath);
|
|
71
|
+
|
|
72
|
+
results.push({
|
|
73
|
+
width: dimensions.width,
|
|
74
|
+
height: dimensions.height,
|
|
75
|
+
filename: relativePath,
|
|
76
|
+
className
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn(`⚠️ Could not read dimensions for ${file}:`, error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate SCSS with CSS custom properties and base class
|
|
89
|
+
*/
|
|
90
|
+
private generateScss(images: ImageDimensions[]): string {
|
|
91
|
+
const wrapperClassName = `${this.options.imagePrefix}wrapper`;
|
|
92
|
+
const lines = [
|
|
93
|
+
'// Auto-generated image dimensions',
|
|
94
|
+
'// This file is generated by place-block-image webpack plugin',
|
|
95
|
+
'// Do not edit manually - changes will be overwritten',
|
|
96
|
+
'',
|
|
97
|
+
'// Base wrapper and image styles',
|
|
98
|
+
`.${wrapperClassName} {`,
|
|
99
|
+
' display: block;',
|
|
100
|
+
' aspect-ratio: calc(var(--p-width) / var(--p-height));',
|
|
101
|
+
'',
|
|
102
|
+
' &,',
|
|
103
|
+
' img {',
|
|
104
|
+
' display: block;',
|
|
105
|
+
' }',
|
|
106
|
+
'',
|
|
107
|
+
'// Ensure images fill the wrapper and maintain aspect ratio',
|
|
108
|
+
' img {',
|
|
109
|
+
' width: 100%;',
|
|
110
|
+
' height: 100%;',
|
|
111
|
+
' object-fit: cover;',
|
|
112
|
+
' }',
|
|
113
|
+
'}',
|
|
114
|
+
'',
|
|
115
|
+
'// Image-specific dimensions',
|
|
116
|
+
...images.map(img =>
|
|
117
|
+
`.${wrapperClassName}.${img.className} {\n --p-width: ${img.width}px;\n --p-height: ${img.height}px;\n}`
|
|
118
|
+
),
|
|
119
|
+
''
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Write SCSS file
|
|
127
|
+
*/
|
|
128
|
+
private async writeScssFile(images: ImageDimensions[]): Promise<void> {
|
|
129
|
+
const scssContent = this.generateScss(images);
|
|
130
|
+
const outputDir = path.dirname(this.options.scssPath);
|
|
131
|
+
|
|
132
|
+
// Ensure output directory exists
|
|
133
|
+
if (!fs.existsSync(outputDir)) {
|
|
134
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fs.writeFileSync(this.options.scssPath, scssContent);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate and write component file
|
|
142
|
+
*/
|
|
143
|
+
private async writeComponentFile(): Promise<void> {
|
|
144
|
+
if (!this.options.generateComponent || !this.options.componentPath || !this.options.componentType) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const componentFileName = `PlaceBlockImage.${this.options.componentType}`;
|
|
149
|
+
const componentFilePath = path.resolve(this.options.componentPath, componentFileName);
|
|
150
|
+
|
|
151
|
+
// Check if component already exists
|
|
152
|
+
if (fs.existsSync(componentFilePath)) {
|
|
153
|
+
console.log(`📦 Component already exists, skipping: ${componentFilePath}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const componentContent = getTemplate(this.options.componentType, this.options.imagePrefix);
|
|
158
|
+
const outputDir = path.dirname(componentFilePath);
|
|
159
|
+
|
|
160
|
+
// Ensure output directory exists
|
|
161
|
+
if (!fs.existsSync(outputDir)) {
|
|
162
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fs.writeFileSync(componentFilePath, componentContent);
|
|
166
|
+
console.log(`📦 Generated component: ${componentFilePath}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if images have changed since last generation
|
|
171
|
+
*/
|
|
172
|
+
private async shouldRegenerate(): Promise<boolean> {
|
|
173
|
+
if (!fs.existsSync(this.options.scssPath)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const scssStats = fs.statSync(this.options.scssPath);
|
|
178
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'bmp', 'ico', 'avif'];
|
|
179
|
+
const pattern = `${this.options.imageDir}/**/*.{${imageExtensions.join(',')}}`;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const files = await glob(pattern, { nodir: true });
|
|
183
|
+
|
|
184
|
+
for (const file of files) {
|
|
185
|
+
const fileStats = fs.statSync(file);
|
|
186
|
+
if (fileStats.mtime > scssStats.mtime) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return false;
|
|
192
|
+
} catch {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
apply(compiler: Compiler): void {
|
|
198
|
+
// Use environment hook to run only once at startup
|
|
199
|
+
compiler.hooks.environment.tap('PlaceBlockImagePlugin', () => {
|
|
200
|
+
this.generateImageStyles();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Also run on watchRun for file changes during development
|
|
204
|
+
compiler.hooks.watchRun.tapAsync('PlaceBlockImagePlugin', async (compiler, callback) => {
|
|
205
|
+
// Prevent running too frequently
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
if (this.isGenerating || (now - this.lastGenerationTime) < 1000) {
|
|
208
|
+
callback();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const shouldRegenerate = await this.shouldRegenerate();
|
|
214
|
+
if (shouldRegenerate) {
|
|
215
|
+
await this.generateImageStyles();
|
|
216
|
+
}
|
|
217
|
+
callback();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('❌ PlaceBlockImagePlugin error:', error);
|
|
220
|
+
callback();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async generateImageStyles(): Promise<void> {
|
|
226
|
+
if (this.isGenerating) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.isGenerating = true;
|
|
231
|
+
this.lastGenerationTime = Date.now();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
console.log('🖼️ PlaceBlockImagePlugin: Generating image styles...');
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(this.options.imageDir)) {
|
|
237
|
+
console.warn(`⚠️ Image directory does not exist: ${this.options.imageDir}`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`🔍 Scanning images in: ${this.options.imageDir}`);
|
|
242
|
+
const images = await this.scanImages();
|
|
243
|
+
|
|
244
|
+
if (images.length === 0) {
|
|
245
|
+
console.log('⚠️ No images found');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await this.writeScssFile(images);
|
|
250
|
+
await this.writeComponentFile();
|
|
251
|
+
|
|
252
|
+
console.log(`✅ Generated CSS custom properties for ${images.length} images`);
|
|
253
|
+
console.log(`📝 Output: ${this.options.scssPath}`);
|
|
254
|
+
|
|
255
|
+
// Log some examples
|
|
256
|
+
if (images.length > 0) {
|
|
257
|
+
console.log('📋 Generated classes:');
|
|
258
|
+
images.slice(0, 3).forEach(img => {
|
|
259
|
+
console.log(` .${img.className} { --p-width: ${img.width}; --p-height: ${img.height}; }`);
|
|
260
|
+
});
|
|
261
|
+
if (images.length > 3) {
|
|
262
|
+
console.log(` ... and ${images.length - 3} more`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('❌ PlaceBlockImagePlugin error:', error);
|
|
267
|
+
} finally {
|
|
268
|
+
this.isGenerating = false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default PlaceBlockImagePlugin;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|