ultimate-jekyll-manager 1.3.11 → 1.4.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/CHANGELOG.md +30 -0
- package/CLAUDE.md +2 -2
- package/README.md +4 -0
- package/dist/assets/js/core/auth.js +14 -0
- package/dist/assets/js/libs/auth.js +62 -2
- package/dist/assets/js/libs/form-manager.js +10 -0
- package/dist/assets/themes/classy/css/components/_forms.scss +25 -1
- package/dist/commands/setup.js +20 -10
- package/dist/defaults/dist/_alternatives/example-competitor.md +2 -2
- package/dist/defaults/dist/_includes/admin/sections/sidebar.json +1 -1
- package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +4 -4
- package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +2 -2
- package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +5 -5
- package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +3 -3
- package/dist/defaults/dist/_layouts/blueprint/legal/cookies.md +6 -6
- package/dist/defaults/dist/_layouts/blueprint/legal/terms.md +1 -1
- package/dist/defaults/dist/_layouts/blueprint/portal/email-preferences.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/backend/pages/dashboard/index.html +5 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/404.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/about.html +3 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +22 -22
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html +11 -11
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html +10 -10
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/app.html +4 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/oauth2.html +3 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/reset.html +3 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +10 -9
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +8 -8
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/categories/category.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/categories/index.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/tags/index.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/tags/tag.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html +13 -13
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +13 -13
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/extension/index.html +7 -7
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/extension/installed.html +1 -1
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +3 -3
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/index.html +29 -29
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html +16 -16
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/confirmation.html +5 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/portal/email-preferences.html +4 -4
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +40 -20
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/status.html +2 -2
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/index.html +10 -10
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/member.html +1 -1
- package/dist/defaults/dist/pages/test/account/dashboard.html +1 -1
- package/dist/defaults/dist/pages/test/components/hero-demo-custom.html +2 -2
- package/dist/defaults/dist/pages/test/components/hero-demo-form.html +4 -4
- package/dist/defaults/dist/pages/test/components/hero-demo-input.html +3 -3
- package/dist/defaults/dist/pages/test/components/hero-demo-side.html +2 -2
- package/dist/defaults/dist/pages/test/components/hero-demo-video.html +2 -2
- package/dist/defaults/dist/pages/test/index.md +2 -2
- package/dist/defaults/dist/pages/test/libraries/appearance.html +6 -6
- package/dist/defaults/dist/pages/test/libraries/bootstrap.html +99 -99
- package/dist/defaults/dist/pages/test/libraries/cover.html +4 -4
- package/dist/defaults/dist/pages/test/libraries/error.html +5 -5
- package/dist/defaults/dist/pages/test/libraries/firestore.html +2 -2
- package/dist/defaults/dist/pages/test/libraries/form-manager.html +9 -9
- package/dist/defaults/dist/pages/test/libraries/lazy-loading.html +9 -9
- package/dist/defaults/dist/pages/test/redirect/external.md +2 -2
- package/dist/defaults/dist/pages/test/redirect/internal.md +2 -2
- package/dist/defaults/dist/pages/test/translation/index.md +3 -3
- package/dist/defaults/src/_includes/backend/sections/sidebar.json +3 -3
- package/dist/defaults/src/_includes/frontend/sections/footer.json +5 -5
- package/dist/defaults/src/_includes/frontend/sections/nav.json +1 -1
- package/dist/defaults/src/_includes/global/sections/account.json +2 -2
- package/dist/gulp/main.js +5 -0
- package/dist/gulp/tasks/imagemin.js +160 -79
- package/dist/utils/attach-log-file.js +78 -0
- package/docs/images.md +27 -0
- package/docs/local-development.md +11 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ const logger = Manager.logger('imagemin');
|
|
|
4
4
|
const { src, dest, watch, series } = require('gulp');
|
|
5
5
|
const glob = require('glob').globSync;
|
|
6
6
|
const responsive = require('gulp-responsive-modern');
|
|
7
|
+
const sharp = require('sharp');
|
|
7
8
|
const path = require('path');
|
|
8
9
|
const jetpack = require('fs-jetpack');
|
|
9
10
|
const GitHubCache = require('./utils/github-cache');
|
|
@@ -15,6 +16,8 @@ const ujmConfig = Manager.getUJMConfig();
|
|
|
15
16
|
// Settings
|
|
16
17
|
const CACHE_DIR = '.temp/cache/imagemin';
|
|
17
18
|
const CACHE_BRANCH = 'cache-uj-imagemin';
|
|
19
|
+
const MAX_SOURCE_DIMENSION = 4096;
|
|
20
|
+
const REWRITE_QUALITY = 80;
|
|
18
21
|
|
|
19
22
|
// Variables
|
|
20
23
|
let githubCache;
|
|
@@ -120,6 +123,15 @@ async function imagemin(complete) {
|
|
|
120
123
|
githubCache.cleanDeletedFromMetadata(meta, files, rootPathProject);
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
// Optionally rewrite oversized source images on disk (opt-in via UJ_IMAGEMIN_REWRITE_SOURCES=true).
|
|
127
|
+
// Caps longest dimension at MAX_SOURCE_DIMENSION so gulp-responsive-modern + sharp don't stall
|
|
128
|
+
// on huge inputs. Runs BEFORE determineFilesToProcess so cached-but-oversized images get
|
|
129
|
+
// rewritten too; the new on-disk content hashes differently than the stored meta hash, so
|
|
130
|
+
// determineFilesToProcess naturally picks the rewritten image up for re-optimization.
|
|
131
|
+
if (process.env.UJ_IMAGEMIN_REWRITE_SOURCES === 'true') {
|
|
132
|
+
await rewriteOversizedSources(files);
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
// Determine what needs processing
|
|
124
136
|
const { filesToProcess, validCachePaths } = await determineFilesToProcess(files, meta, githubCache, stats);
|
|
125
137
|
|
|
@@ -159,87 +171,101 @@ async function imagemin(complete) {
|
|
|
159
171
|
// Progress counter
|
|
160
172
|
let processedOutputs = 0;
|
|
161
173
|
|
|
162
|
-
// Process images
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const allCacheFiles = glob(path.join(CACHE_DIR, '**/*'), { nodir: true });
|
|
211
|
-
|
|
212
|
-
// Push to GitHub with atomic replacement
|
|
213
|
-
await githubCache.pushBranch(allCacheFiles, {
|
|
214
|
-
validFiles: validCachePaths,
|
|
215
|
-
stats: {
|
|
216
|
-
timestamp: new Date().toISOString(),
|
|
217
|
-
sourceCount: files.length,
|
|
218
|
-
cachedCount: allCacheFiles.length - 1,
|
|
219
|
-
processedNow: stats.optimized,
|
|
220
|
-
fromCache: stats.fromCache,
|
|
221
|
-
newlyProcessed: stats.optimized,
|
|
222
|
-
timing: {
|
|
223
|
-
startTime,
|
|
224
|
-
endTime,
|
|
225
|
-
elapsedMs
|
|
226
|
-
},
|
|
227
|
-
imageStats: {
|
|
228
|
-
totalImages: stats.totalImages,
|
|
229
|
-
optimized: stats.optimized,
|
|
230
|
-
skipped: stats.fromCache,
|
|
231
|
-
totalSizeBefore: stats.sizeBefore,
|
|
232
|
-
totalSizeAfter: stats.sizeAfter,
|
|
233
|
-
totalSaved: stats.savedBytes
|
|
234
|
-
},
|
|
235
|
-
details: `Optimized ${stats.optimized} images, ${stats.fromCache} from cache\n\n### Files from cache:\n${stats.cachedFiles.length > 0 ? stats.cachedFiles.map(f => `- ${f}`).join('\n') : 'None'}\n\n### Newly optimized files:\n${stats.optimizedFiles.length > 0 ? stats.optimizedFiles.map(f => `- ${f}`).join('\n') : 'None'}`
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
}
|
|
174
|
+
// Process images.
|
|
175
|
+
//
|
|
176
|
+
// CRITICAL: this function is `async`, which means returning a stream from it yields a
|
|
177
|
+
// Promise<Stream> to gulp — gulp resolves the task immediately on the Promise rather than
|
|
178
|
+
// waiting for the stream's 'finish' event. Downstream tasks (jekyll, audit, etc.) then start
|
|
179
|
+
// before imagemin has actually written its outputs to disk, and the build "succeeds" while
|
|
180
|
+
// silently shipping a partial _site/. We must explicitly await stream completion + cache push
|
|
181
|
+
// before returning, so gulp sees the real completion.
|
|
182
|
+
//
|
|
183
|
+
// This await only ever runs in build mode — dev mode short-circuits via `!Manager.isBuildMode()`
|
|
184
|
+
// above (so `npm start` never blocks on this), letting BrowserSync reload as images land later.
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
src(filesToProcess, { base: 'src/assets/images' })
|
|
187
|
+
.pipe(responsive({
|
|
188
|
+
[`**/${RESPONSIVE_GLOB}`]: responsiveConfigs
|
|
189
|
+
}, {
|
|
190
|
+
quality: 80,
|
|
191
|
+
progressive: true,
|
|
192
|
+
withMetadata: false,
|
|
193
|
+
withoutEnlargement: false,
|
|
194
|
+
skipOnEnlargement: false,
|
|
195
|
+
errorOnUnusedImage: false,
|
|
196
|
+
passThroughUnused: true,
|
|
197
|
+
}))
|
|
198
|
+
.on('error', reject)
|
|
199
|
+
.pipe(dest(output))
|
|
200
|
+
.on('error', reject)
|
|
201
|
+
.on('data', (file) => {
|
|
202
|
+
// Progress tracking
|
|
203
|
+
processedOutputs++;
|
|
204
|
+
const relativePath = path.relative(path.join(rootPathProject, output), file.path);
|
|
205
|
+
logger.log(`🖼️ ${processedOutputs}/${expectedOutputs}: ${relativePath}`);
|
|
206
|
+
|
|
207
|
+
// Save to cache
|
|
208
|
+
const cachePath = path.join(CACHE_DIR, 'images', relativePath);
|
|
209
|
+
jetpack.copy(file.path, cachePath, { overwrite: true });
|
|
210
|
+
|
|
211
|
+
// Track size after optimization
|
|
212
|
+
const fileStats = jetpack.inspect(file.path);
|
|
213
|
+
if (fileStats) {
|
|
214
|
+
stats.sizeAfter += fileStats.size;
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
.on('finish', resolve);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Calculate final statistics
|
|
221
|
+
stats.savedBytes = stats.sizeBefore - stats.sizeAfter;
|
|
239
222
|
|
|
240
|
-
|
|
241
|
-
|
|
223
|
+
// Calculate timing
|
|
224
|
+
const endTime = Date.now();
|
|
225
|
+
const elapsedMs = endTime - startTime;
|
|
226
|
+
|
|
227
|
+
// Log statistics
|
|
228
|
+
logImageStatistics(stats, startTime, endTime);
|
|
229
|
+
|
|
230
|
+
// Save metadata and push cache
|
|
231
|
+
if (githubCache && githubCache.hasCredentials()) {
|
|
232
|
+
githubCache.saveMetadata(metaPath, meta);
|
|
233
|
+
|
|
234
|
+
logger.log(`📊 Updating cache with ${stats.optimized} new optimizations and README stats...`);
|
|
235
|
+
|
|
236
|
+
// Collect all cache files to push (metadata will be auto-included)
|
|
237
|
+
const allCacheFiles = glob(path.join(CACHE_DIR, '**/*'), { nodir: true });
|
|
238
|
+
|
|
239
|
+
// Push to GitHub with atomic replacement
|
|
240
|
+
await githubCache.pushBranch(allCacheFiles, {
|
|
241
|
+
validFiles: validCachePaths,
|
|
242
|
+
stats: {
|
|
243
|
+
timestamp: new Date().toISOString(),
|
|
244
|
+
sourceCount: files.length,
|
|
245
|
+
cachedCount: allCacheFiles.length - 1,
|
|
246
|
+
processedNow: stats.optimized,
|
|
247
|
+
fromCache: stats.fromCache,
|
|
248
|
+
newlyProcessed: stats.optimized,
|
|
249
|
+
timing: {
|
|
250
|
+
startTime,
|
|
251
|
+
endTime,
|
|
252
|
+
elapsedMs
|
|
253
|
+
},
|
|
254
|
+
imageStats: {
|
|
255
|
+
totalImages: stats.totalImages,
|
|
256
|
+
optimized: stats.optimized,
|
|
257
|
+
skipped: stats.fromCache,
|
|
258
|
+
totalSizeBefore: stats.sizeBefore,
|
|
259
|
+
totalSizeAfter: stats.sizeAfter,
|
|
260
|
+
totalSaved: stats.savedBytes
|
|
261
|
+
},
|
|
262
|
+
details: `Optimized ${stats.optimized} images, ${stats.fromCache} from cache\n\n### Files from cache:\n${stats.cachedFiles.length > 0 ? stats.cachedFiles.map(f => `- ${f}`).join('\n') : 'None'}\n\n### Newly optimized files:\n${stats.optimizedFiles.length > 0 ? stats.optimizedFiles.map(f => `- ${f}`).join('\n') : 'None'}`
|
|
263
|
+
}
|
|
242
264
|
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.log('✅ Finished!');
|
|
268
|
+
return complete();
|
|
243
269
|
}
|
|
244
270
|
|
|
245
271
|
// Watcher task
|
|
@@ -273,6 +299,61 @@ module.exports = series(
|
|
|
273
299
|
// Helper Functions
|
|
274
300
|
// ============================================================================
|
|
275
301
|
|
|
302
|
+
// Rewrite oversized source images in place, capping longest dimension at MAX_SOURCE_DIMENSION.
|
|
303
|
+
// Only affects files whose decoded longest side exceeds the cap. Cache invalidation is implicit:
|
|
304
|
+
// the new content hashes differently than the previously-cached entry, so determineFilesToProcess
|
|
305
|
+
// will pick affected files up for re-optimization on its own.
|
|
306
|
+
async function rewriteOversizedSources(files) {
|
|
307
|
+
const responsiveFiles = files.filter(f => RESPONSIVE_EXTENSIONS.has(path.extname(f).slice(1).toLowerCase()));
|
|
308
|
+
if (responsiveFiles.length === 0) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
logger.log(`🔍 Checking ${responsiveFiles.length} source images for oversize (>${MAX_SOURCE_DIMENSION}px longest side)...`);
|
|
313
|
+
|
|
314
|
+
let rewritten = 0;
|
|
315
|
+
for (const file of responsiveFiles) {
|
|
316
|
+
const metadata = await sharp(file).metadata();
|
|
317
|
+
const longest = Math.max(metadata.width || 0, metadata.height || 0);
|
|
318
|
+
|
|
319
|
+
if (longest <= MAX_SOURCE_DIMENSION) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ext = path.extname(file).slice(1).toLowerCase();
|
|
324
|
+
const sizeBefore = jetpack.inspect(file)?.size || 0;
|
|
325
|
+
|
|
326
|
+
// Resize, encode to a buffer (sharp can't write back to its own input file directly), then overwrite.
|
|
327
|
+
const pipeline = sharp(file).resize({
|
|
328
|
+
width: MAX_SOURCE_DIMENSION,
|
|
329
|
+
height: MAX_SOURCE_DIMENSION,
|
|
330
|
+
fit: 'inside',
|
|
331
|
+
withoutEnlargement: true,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const buffer = ext === 'png'
|
|
335
|
+
? await pipeline.png({ quality: REWRITE_QUALITY }).toBuffer()
|
|
336
|
+
: await pipeline.jpeg({ quality: REWRITE_QUALITY, progressive: true, mozjpeg: true }).toBuffer();
|
|
337
|
+
|
|
338
|
+
jetpack.write(file, buffer);
|
|
339
|
+
const sizeAfter = buffer.length;
|
|
340
|
+
|
|
341
|
+
// No cache bookkeeping needed: the rewritten file has new content, so the next
|
|
342
|
+
// determineFilesToProcess() call computes a different hash than the stored meta hash and
|
|
343
|
+
// naturally treats this image as needing reprocessing. Stored meta will be overwritten
|
|
344
|
+
// with the new hash when determineFilesToProcess() runs.
|
|
345
|
+
|
|
346
|
+
rewritten++;
|
|
347
|
+
logger.log(`✂️ Rewrote ${path.relative(rootPathProject, file)}: ${metadata.width}x${metadata.height} → max ${MAX_SOURCE_DIMENSION}px, ${formatBytes(sizeBefore)} → ${formatBytes(sizeAfter)}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (rewritten === 0) {
|
|
351
|
+
logger.log(`✅ No oversized sources found (all within ${MAX_SOURCE_DIMENSION}px)`);
|
|
352
|
+
} else {
|
|
353
|
+
logger.log(`✂️ Rewrote ${rewritten} oversized source image(s)`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
276
357
|
// Build responsive configurations from PICTURE_SIZES
|
|
277
358
|
function getResponsiveConfigs() {
|
|
278
359
|
const configs = [];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// attachLogFile(name) — duplicate process.stdout + process.stderr writes to logs/<name>.log
|
|
2
|
+
// in the consumer project root (process.cwd()).
|
|
3
|
+
//
|
|
4
|
+
// Mirrors EM's attach-log-file pattern so devs (and Claude) can `tail -f logs/dev.log` to see
|
|
5
|
+
// every line of output a UJM session produces — gulp tasks, jekyll child, webpack, SCSS, the
|
|
6
|
+
// works. ANSI color codes are stripped from the file output so it's grep-friendly; the
|
|
7
|
+
// console continues to receive the original colored output unchanged.
|
|
8
|
+
//
|
|
9
|
+
// Skipped entirely when Manager.isServer() returns true — CI/cloud runs don't need a logs/
|
|
10
|
+
// directory left behind in the workspace.
|
|
11
|
+
//
|
|
12
|
+
// Truncates fresh on each call (flags: 'w'), so a new `npm start` doesn't accumulate stale
|
|
13
|
+
// lines from the previous run.
|
|
14
|
+
//
|
|
15
|
+
// Idempotent: calling twice with the same path just returns the existing stream.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
|
|
21
|
+
|
|
22
|
+
let activeStream = null;
|
|
23
|
+
let activePath = null;
|
|
24
|
+
let originalStdoutWrite = null;
|
|
25
|
+
let originalStderrWrite = null;
|
|
26
|
+
|
|
27
|
+
function attachLogFile(name) {
|
|
28
|
+
// Skip on CI/cloud — controlled by UJ_IS_SERVER env var (set by workflows).
|
|
29
|
+
const Manager = require('../build.js');
|
|
30
|
+
if (Manager.isServer()) return null;
|
|
31
|
+
|
|
32
|
+
if (!name) return null;
|
|
33
|
+
|
|
34
|
+
const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
|
|
35
|
+
|
|
36
|
+
if (activeStream && activePath === abs) return activeStream;
|
|
37
|
+
if (activeStream) detach();
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
40
|
+
const stream = fs.createWriteStream(abs, { flags: 'w' });
|
|
41
|
+
|
|
42
|
+
stream.write(`# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
|
|
43
|
+
|
|
44
|
+
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
45
|
+
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
46
|
+
|
|
47
|
+
process.stdout.write = function (chunk, ...rest) {
|
|
48
|
+
try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
49
|
+
return originalStdoutWrite(chunk, ...rest);
|
|
50
|
+
};
|
|
51
|
+
process.stderr.write = function (chunk, ...rest) {
|
|
52
|
+
try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
|
|
53
|
+
return originalStderrWrite(chunk, ...rest);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
activeStream = stream;
|
|
57
|
+
activePath = abs;
|
|
58
|
+
|
|
59
|
+
return stream;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detach() {
|
|
63
|
+
if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
|
|
64
|
+
if (originalStderrWrite) process.stderr.write = originalStderrWrite;
|
|
65
|
+
if (activeStream) activeStream.end();
|
|
66
|
+
activeStream = null;
|
|
67
|
+
activePath = null;
|
|
68
|
+
originalStdoutWrite = null;
|
|
69
|
+
originalStderrWrite = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stripAnsi(s) {
|
|
73
|
+
return String(s).replace(ANSI_PATTERN, '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = attachLogFile;
|
|
77
|
+
module.exports.detach = detach;
|
|
78
|
+
module.exports.stripAnsi = stripAnsi;
|
package/docs/images.md
CHANGED
|
@@ -40,3 +40,30 @@ When posts are created via BEM's `POST /admin/post` endpoint:
|
|
|
40
40
|
2. Images are uploaded to `src/assets/images/blog/post-{id}/` on GitHub
|
|
41
41
|
3. The body is rewritten to use `@post/{filename}` format
|
|
42
42
|
4. Failed downloads are skipped (original external URL preserved)
|
|
43
|
+
|
|
44
|
+
## Image Processing Pipeline (`imagemin` gulp task)
|
|
45
|
+
|
|
46
|
+
The `imagemin` task (`src/gulp/tasks/imagemin.js`) processes every file under `src/assets/images/**/*.{jpg,jpeg,png,gif,svg,webp}` into `dist/assets/images/`. For raster formats supported by `gulp-responsive-modern` (`jpg`, `jpeg`, `png`), each source produces **8 outputs**: original + `1024px`, `640px`, `320px` variants, each as the source format **and** WebP.
|
|
47
|
+
|
|
48
|
+
Outputs are content-addressed and cached on a dedicated branch (`cache-uj-imagemin`). Subsequent builds only reprocess images whose source hash changed.
|
|
49
|
+
|
|
50
|
+
### Source size matters
|
|
51
|
+
|
|
52
|
+
The responsive pipeline streams every input through `sharp` to generate variants. Sources with very large dimensions (tens of thousands of pixels) decode into hundreds of MB of raw pixel data per worker. In practice this can stall the underlying stream so quietly that gulp can't see the failure — the build "succeeds" but the affected images never land in `_site/`.
|
|
53
|
+
|
|
54
|
+
**Recommendation:** Keep source images at sensible dimensions before they land in `src/assets/images/`. A 4096px longest-side cap at the upload step is plenty for blog hero images (the largest responsive variant is 1024px). If you ingest images via an automated pipeline (e.g. BEM's `admin/post` endpoint downloading external URLs), cap them there.
|
|
55
|
+
|
|
56
|
+
### Cleanup for existing oversized sources: `UJ_IMAGEMIN_REWRITE_SOURCES`
|
|
57
|
+
|
|
58
|
+
If oversized images already exist in your repo and you can't easily re-upload them, run the imagemin task once with the rewrite flag set:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
UJ_IMAGEMIN_REWRITE_SOURCES=true npm run build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When set, the imagemin task — **before** it pipes images into `gulp-responsive-modern` — scans every file scheduled for processing and rewrites in place any whose longest dimension exceeds `4096px`. Rewrites use `sharp` with `fit: 'inside'` (preserves aspect ratio, never enlarges), JPEG quality 80 / `mozjpeg` / progressive, and PNG quality 80. Cache hashes for affected files are updated so the new content is the new cache key.
|
|
65
|
+
|
|
66
|
+
**Notes:**
|
|
67
|
+
- The flag is **opt-in by design** — running it commits real diffs to your source images. Intended for one-off cleanup runs, not for regular builds or CI.
|
|
68
|
+
- Only files that the cache layer has decided need processing get checked. Already-cached images are untouched. To force-rewrite cached oversized images, also clear the cache (`npx mgr clean`) or delete the `cache-uj-imagemin` branch entries for the relevant files.
|
|
69
|
+
- 4096px is a hardcoded cap (`MAX_SOURCE_DIMENSION` in `src/gulp/tasks/imagemin.js`). Not currently configurable per project — open a PR if you need this.
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
The local development server URL is stored in `.temp/_config_browsersync.yml` in the consuming project's root directory. Read this file to determine the correct URL for browsing and testing. By default, use `https://192.168.86.69:4000`.
|
|
4
4
|
|
|
5
|
+
## Log Files
|
|
6
|
+
|
|
7
|
+
UJM tees every line of stdout/stderr from the gulp pipeline into a log file in the consumer project root, so you can `tail -f` it or `grep` through it after a run:
|
|
8
|
+
|
|
9
|
+
- `npm start` → `logs/dev.log`
|
|
10
|
+
- `npm run build` (i.e. `UJ_BUILD_MODE=true`) → `logs/build.log`
|
|
11
|
+
|
|
12
|
+
Both files **truncate fresh on each run** — the most recent session only. ANSI color codes are stripped from the file (so it's grep-friendly); the terminal continues to receive colored output unchanged. Captures everything that flows through stdout/stderr: `Manager.logger(...)` output, raw `console.log` calls, gulp task names, jekyll's child output, webpack output, the works.
|
|
13
|
+
|
|
14
|
+
**Skipped on CI/cloud.** When `UJ_IS_SERVER=true` (set by GitHub Actions workflows and other server contexts), the tee is bypassed entirely — no `logs/` directory is written. Implementation: [src/utils/attach-log-file.js](../src/utils/attach-log-file.js), attached at the top of [src/gulp/main.js](../src/gulp/main.js).
|
|
15
|
+
|
|
5
16
|
## Connecting to Local Firebase Emulators
|
|
6
17
|
|
|
7
18
|
Set the `FIREBASE_EMULATOR_CONNECT` environment variable to `true` to connect the frontend to local Firebase services (Auth, Firestore, Functions, etc.):
|