ultimate-jekyll-manager 1.1.8 → 1.1.10
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 +31 -0
- package/dist/assets/js/core/auth.js +9 -1
- package/dist/assets/js/libs/auth.js +48 -0
- package/dist/assets/js/pages/account/sections/billing.js +1 -1
- package/dist/assets/js/pages/admin/users/index.js +109 -1
- package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +62 -0
- package/dist/gulp/tasks/defaults.js +5 -2
- package/dist/gulp/tasks/distribute.js +29 -26
- package/dist/gulp/tasks/jsonToHtml.js +86 -80
- package/dist/gulp/tasks/minifyHtml.js +55 -51
- package/dist/gulp/tasks/sass.js +7 -6
- package/dist/gulp/tasks/utils/template-transform.js +35 -35
- package/package.json +12 -13
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
---
|
|
18
|
+
## [1.1.10] - 2026-05-10
|
|
19
|
+
### Removed
|
|
20
|
+
- `through2` dependency. Replaced with native `node:stream` `Transform` across 6 gulp task files (`defaults.js`, `distribute.js`, `jsonToHtml.js`, `minifyHtml.js`, `sass.js`, `utils/template-transform.js`). through2@5 became ESM-only with no `require` condition in its exports, breaking CJS require; the built-in `Transform` is a drop-in replacement
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Bumped `@babel/preset-env` from ^7.29.2 to ^7.29.5
|
|
24
|
+
- Bumped `dompurify` from ^3.3.3 to ^3.4.2
|
|
25
|
+
- Bumped `dotenv` from ^17.4.1 to ^17.4.2
|
|
26
|
+
- Bumped `fast-xml-parser` from ^5.5.11 to ^5.7.3
|
|
27
|
+
- Bumped `gulp-filter` from ^9.0.1 to ^10.0.0 (Node 22 ESM-CJS interop keeps `require('gulp-filter').default` working)
|
|
28
|
+
- Bumped `html-validate` from ^10.11.3 to ^10.16.0
|
|
29
|
+
- Bumped `libsodium-wrappers` from ^0.8.3 to ^0.8.4
|
|
30
|
+
- Bumped `postcss` from ^8.5.9 to ^8.5.14
|
|
31
|
+
- Bumped `prettier` from ^3.8.2 to ^3.8.3
|
|
32
|
+
- Bumped `web-manager` from ^4.1.40 to ^4.1.41
|
|
33
|
+
- Bumped `webpack` from ^5.106.1 to ^5.106.2
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
## [1.1.9] - 2026-04-23
|
|
37
|
+
### Added
|
|
38
|
+
- Admin users page: "Sign in as user" dropdown option that calls BEM `POST /backend-manager/user/token` to generate a custom auth token, then shows a modal with the sign-in URL (copy button + open-in-new-tab button)
|
|
39
|
+
- Modal opens immediately in a loading state while the token is generated, then swaps to ready/error state
|
|
40
|
+
- Auth signin page: handle `authCustomToken` URL param via Firebase `signInWithCustomToken`, redirecting to `authReturnUrl` (validated) or `/dashboard`
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- Billing section: cancel subscription button now appears for suspended paid subscriptions (previously hidden). Logic updated to `isPaid && rawStatus !== 'cancelled' && !resolved.cancelling` so it correctly shows for active, trialing, and suspended paid subs, while hiding for free users, already-cancelled subs, and subs with pending cancellation
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- Admin users table: dropdown trigger button restyled using `btn-outline-adaptive rounded-circle` for a cleaner look
|
|
47
|
+
|
|
17
48
|
---
|
|
18
49
|
## [1.1.8] - 2026-04-22
|
|
19
50
|
### Changed
|
|
@@ -152,7 +152,15 @@ function updateAuthLinks() {
|
|
|
152
152
|
|
|
153
153
|
$link.addEventListener('click', (e) => {
|
|
154
154
|
const url = new URL($link.href, window.location.origin);
|
|
155
|
-
|
|
155
|
+
const currentUrl = new URL(window.location.href);
|
|
156
|
+
const existingReturnUrl = currentUrl.searchParams.get('authReturnUrl');
|
|
157
|
+
|
|
158
|
+
if (existingReturnUrl) {
|
|
159
|
+
url.searchParams.set('authReturnUrl', existingReturnUrl);
|
|
160
|
+
} else if (!authPaths.includes(currentUrl.pathname)) {
|
|
161
|
+
url.searchParams.set('authReturnUrl', window.location.href);
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
$link.href = url.toString();
|
|
157
165
|
});
|
|
158
166
|
} catch (e) {}
|
|
@@ -22,6 +22,12 @@ export default function () {
|
|
|
22
22
|
// Check for authSignout parameter first
|
|
23
23
|
await handleAuthSignout();
|
|
24
24
|
|
|
25
|
+
// Check for authCustomToken parameter (admin impersonation / custom token sign-in)
|
|
26
|
+
const customTokenHandled = await handleCustomTokenSignin();
|
|
27
|
+
if (customTokenHandled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
// Initialize the appropriate form based on the page (with autoReady: false)
|
|
26
32
|
initializePageForm();
|
|
27
33
|
|
|
@@ -209,6 +215,48 @@ export default function () {
|
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
async function handleCustomTokenSignin() {
|
|
219
|
+
const url = new URL(window.location.href);
|
|
220
|
+
const customToken = url.searchParams.get('authCustomToken');
|
|
221
|
+
|
|
222
|
+
if (!customToken) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
console.log('[Auth] Signing in with custom token');
|
|
228
|
+
|
|
229
|
+
const { getAuth, signInWithCustomToken } = await import('@firebase/auth');
|
|
230
|
+
const auth = getAuth();
|
|
231
|
+
|
|
232
|
+
const userCredential = await signInWithCustomToken(auth, customToken);
|
|
233
|
+
console.log('[Auth] Custom token sign-in successful:', userCredential.user.email || userCredential.user.uid);
|
|
234
|
+
|
|
235
|
+
trackLogin('custom-token', userCredential.user);
|
|
236
|
+
|
|
237
|
+
const authReturnUrl = url.searchParams.get('authReturnUrl');
|
|
238
|
+
const redirectTo = authReturnUrl && webManager.isValidRedirectUrl(authReturnUrl)
|
|
239
|
+
? authReturnUrl
|
|
240
|
+
: '/dashboard';
|
|
241
|
+
|
|
242
|
+
window.location.href = redirectTo;
|
|
243
|
+
return true;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
webManager.sentry().captureException(new Error('Custom token sign-in error', { cause: error }));
|
|
246
|
+
console.error('[Auth] Custom token sign-in failed:', error);
|
|
247
|
+
|
|
248
|
+
const url = new URL(window.location.href);
|
|
249
|
+
url.searchParams.delete('authCustomToken');
|
|
250
|
+
window.history.replaceState({}, document.title, url.toString());
|
|
251
|
+
|
|
252
|
+
webManager.utilities().showNotification(
|
|
253
|
+
`Custom token sign-in failed: ${error.message || 'Invalid or expired token'}`,
|
|
254
|
+
{ type: 'danger', timeout: 8000 }
|
|
255
|
+
);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
212
260
|
async function handleAuthSignout() {
|
|
213
261
|
const url = new URL(window.location.href);
|
|
214
262
|
const authSignout = url.searchParams.get('authSignout');
|
|
@@ -140,7 +140,7 @@ function buildBillingState(account) {
|
|
|
140
140
|
upgrade: !isPaid || rawStatus === 'cancelled',
|
|
141
141
|
change: resolved.active,
|
|
142
142
|
manage: isPaid && rawStatus !== 'cancelled',
|
|
143
|
-
cancel: resolved.
|
|
143
|
+
cancel: isPaid && rawStatus !== 'cancelled' && !resolved.cancelling,
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
146
|
};
|
|
@@ -172,7 +172,7 @@ function renderUsers() {
|
|
|
172
172
|
<td class="text-muted small">${updatedText}</td>
|
|
173
173
|
<td>
|
|
174
174
|
<div class="dropdown">
|
|
175
|
-
<button class="btn btn-sm btn-
|
|
175
|
+
<button class="btn btn-sm btn-adaptive rounded-circle" type="button" data-bs-toggle="dropdown">
|
|
176
176
|
${getPrerenderedIcon('ellipsis-vertical', 'fa-sm')}
|
|
177
177
|
</button>
|
|
178
178
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
@@ -192,6 +192,10 @@ function renderUsers() {
|
|
|
192
192
|
${getPrerenderedIcon('fire', 'fa-sm me-2')}
|
|
193
193
|
View in Explorer
|
|
194
194
|
</a></li>
|
|
195
|
+
<li><a class="dropdown-item small btn-signin-as" href="#">
|
|
196
|
+
${getPrerenderedIcon('right-to-bracket', 'fa-sm me-2')}
|
|
197
|
+
Sign in as user
|
|
198
|
+
</a></li>
|
|
195
199
|
<li><hr class="dropdown-divider"></li>
|
|
196
200
|
<li><a class="dropdown-item small text-danger btn-delete-user" href="#">
|
|
197
201
|
${getPrerenderedIcon('trash', 'fa-sm me-2')}
|
|
@@ -223,6 +227,11 @@ function renderUsers() {
|
|
|
223
227
|
window.location.href = `/admin/firebase?collection=users&doc=${uid}`;
|
|
224
228
|
});
|
|
225
229
|
|
|
230
|
+
$row.querySelector('.btn-signin-as').addEventListener('click', (e) => {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
signInAsUser(uid, email);
|
|
233
|
+
});
|
|
234
|
+
|
|
226
235
|
$row.querySelector('.btn-delete-user').addEventListener('click', (e) => {
|
|
227
236
|
e.preventDefault();
|
|
228
237
|
deleteUser(uid, email);
|
|
@@ -269,6 +278,105 @@ function viewUser(uid, userData) {
|
|
|
269
278
|
modal.show();
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
async function signInAsUser(uid, email) {
|
|
282
|
+
openSignInAsModalLoading(email);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const response = await authorizedFetch(`${webManager.getApiUrl()}/backend-manager/user/token`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
timeout: 30000,
|
|
288
|
+
response: 'json',
|
|
289
|
+
tries: 1,
|
|
290
|
+
log: true,
|
|
291
|
+
body: { uid: uid },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const token = response?.token;
|
|
295
|
+
if (!token) {
|
|
296
|
+
throw new Error('No token returned from server');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const signinUrl = new URL('/signin', window.location.origin);
|
|
300
|
+
signinUrl.searchParams.set('authSignout', 'true');
|
|
301
|
+
signinUrl.searchParams.set('authCustomToken', token);
|
|
302
|
+
signinUrl.searchParams.set('authReturnUrl', '/dashboard');
|
|
303
|
+
|
|
304
|
+
showSignInAsModalReady(email, signinUrl.toString());
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('Failed to create sign-in link:', error);
|
|
307
|
+
showSignInAsModalError(error.message || 'Unknown error');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function openSignInAsModalLoading(email) {
|
|
312
|
+
const $loading = document.getElementById('signin-as-loading');
|
|
313
|
+
const $ready = document.getElementById('signin-as-ready');
|
|
314
|
+
const $error = document.getElementById('signin-as-error');
|
|
315
|
+
const $loadingEmail = document.getElementById('signin-as-loading-email');
|
|
316
|
+
const $navigateBtn = document.getElementById('btn-signin-as-navigate');
|
|
317
|
+
|
|
318
|
+
if ($loading) $loading.classList.remove('d-none');
|
|
319
|
+
if ($ready) $ready.classList.add('d-none');
|
|
320
|
+
if ($error) $error.classList.add('d-none');
|
|
321
|
+
if ($navigateBtn) $navigateBtn.classList.add('d-none');
|
|
322
|
+
if ($loadingEmail) $loadingEmail.textContent = email;
|
|
323
|
+
|
|
324
|
+
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('signin-as-modal'));
|
|
325
|
+
modal.show();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function showSignInAsModalReady(email, urlString) {
|
|
329
|
+
const $loading = document.getElementById('signin-as-loading');
|
|
330
|
+
const $ready = document.getElementById('signin-as-ready');
|
|
331
|
+
const $error = document.getElementById('signin-as-error');
|
|
332
|
+
const $email = document.getElementById('signin-as-email');
|
|
333
|
+
const $url = document.getElementById('signin-as-url');
|
|
334
|
+
const $copyBtn = document.getElementById('btn-signin-as-copy');
|
|
335
|
+
const $navigateBtn = document.getElementById('btn-signin-as-navigate');
|
|
336
|
+
|
|
337
|
+
if ($loading) $loading.classList.add('d-none');
|
|
338
|
+
if ($error) $error.classList.add('d-none');
|
|
339
|
+
if ($ready) $ready.classList.remove('d-none');
|
|
340
|
+
if ($navigateBtn) $navigateBtn.classList.remove('d-none');
|
|
341
|
+
if ($email) $email.textContent = email;
|
|
342
|
+
if ($url) $url.value = urlString;
|
|
343
|
+
|
|
344
|
+
if ($copyBtn) {
|
|
345
|
+
$copyBtn.onclick = async () => {
|
|
346
|
+
await navigator.clipboard.writeText(urlString).catch(() => {});
|
|
347
|
+
const originalHTML = $copyBtn.innerHTML;
|
|
348
|
+
$copyBtn.innerHTML = `${getPrerenderedIcon('circle-check', 'fa-sm')}`;
|
|
349
|
+
$copyBtn.classList.add('btn-success');
|
|
350
|
+
$copyBtn.classList.remove('btn-outline-adaptive');
|
|
351
|
+
setTimeout(() => {
|
|
352
|
+
$copyBtn.innerHTML = originalHTML;
|
|
353
|
+
$copyBtn.classList.remove('btn-success');
|
|
354
|
+
$copyBtn.classList.add('btn-outline-adaptive');
|
|
355
|
+
}, 1500);
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if ($navigateBtn) {
|
|
360
|
+
$navigateBtn.onclick = () => {
|
|
361
|
+
window.open(urlString, '_blank', 'noopener');
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function showSignInAsModalError(message) {
|
|
367
|
+
const $loading = document.getElementById('signin-as-loading');
|
|
368
|
+
const $ready = document.getElementById('signin-as-ready');
|
|
369
|
+
const $error = document.getElementById('signin-as-error');
|
|
370
|
+
const $errorMessage = document.getElementById('signin-as-error-message');
|
|
371
|
+
const $navigateBtn = document.getElementById('btn-signin-as-navigate');
|
|
372
|
+
|
|
373
|
+
if ($loading) $loading.classList.add('d-none');
|
|
374
|
+
if ($ready) $ready.classList.add('d-none');
|
|
375
|
+
if ($error) $error.classList.remove('d-none');
|
|
376
|
+
if ($navigateBtn) $navigateBtn.classList.add('d-none');
|
|
377
|
+
if ($errorMessage) $errorMessage.textContent = message;
|
|
378
|
+
}
|
|
379
|
+
|
|
272
380
|
async function deleteUser(uid, email) {
|
|
273
381
|
if (!confirm(`Delete user ${email} (${uid})?\n\nThis will permanently delete their account and cannot be undone.`)) {
|
|
274
382
|
return;
|
|
@@ -28,6 +28,10 @@ prerender_icons:
|
|
|
28
28
|
- name: "trash"
|
|
29
29
|
- name: "ellipsis-vertical"
|
|
30
30
|
- name: "copy"
|
|
31
|
+
- name: "right-to-bracket"
|
|
32
|
+
- name: "circle-check"
|
|
33
|
+
- name: "arrow-up-right-from-square"
|
|
34
|
+
- name: "circle-xmark"
|
|
31
35
|
---
|
|
32
36
|
|
|
33
37
|
<!-- Page Header Actions -->
|
|
@@ -181,6 +185,64 @@ prerender_icons:
|
|
|
181
185
|
</div>
|
|
182
186
|
</div>
|
|
183
187
|
|
|
188
|
+
<!-- Sign In As User Modal -->
|
|
189
|
+
<div class="modal fade" id="signin-as-modal" tabindex="-1" aria-labelledby="signin-as-modal-label" aria-hidden="true">
|
|
190
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
191
|
+
<div class="modal-content">
|
|
192
|
+
<div class="modal-header">
|
|
193
|
+
<h6 class="modal-title" id="signin-as-modal-label">Sign in as user</h6>
|
|
194
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<!-- Loading State -->
|
|
198
|
+
<div class="modal-body text-center py-5" id="signin-as-loading">
|
|
199
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
|
200
|
+
<span class="visually-hidden">Loading...</span>
|
|
201
|
+
</div>
|
|
202
|
+
<h5 class="mb-2">Generating sign-in link</h5>
|
|
203
|
+
<p class="text-muted mb-0">
|
|
204
|
+
Creating a one-time token for <strong id="signin-as-loading-email" class="text-body"></strong>...
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Ready State -->
|
|
209
|
+
<div class="modal-body text-center d-none" id="signin-as-ready">
|
|
210
|
+
<div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-success bg-opacity-10 mx-auto mb-3">
|
|
211
|
+
{% uj_icon "circle-check", "fa-2xl text-success" %}
|
|
212
|
+
</div>
|
|
213
|
+
<h5 class="mb-2">Sign-in link ready</h5>
|
|
214
|
+
<p class="text-muted mb-3">
|
|
215
|
+
A one-time sign-in link for <strong id="signin-as-email" class="text-body"></strong> has been generated. Open it in an incognito window to avoid signing out of your admin session.
|
|
216
|
+
</p>
|
|
217
|
+
<div class="input-group mb-2">
|
|
218
|
+
<input type="text" class="form-control form-control-sm font-monospace" id="signin-as-url" readonly>
|
|
219
|
+
<button type="button" class="btn btn-sm btn-outline-adaptive" id="btn-signin-as-copy">
|
|
220
|
+
{% uj_icon "copy", "fa-sm" %}
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
<small class="text-muted d-block">The link contains a short-lived custom token</small>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Error State -->
|
|
227
|
+
<div class="modal-body text-center py-5 d-none" id="signin-as-error">
|
|
228
|
+
<div class="rounded-circle d-inline-flex align-items-center justify-content-center p-3 bg-danger bg-opacity-10 mx-auto mb-3">
|
|
229
|
+
{% uj_icon "circle-xmark", "fa-2xl text-danger" %}
|
|
230
|
+
</div>
|
|
231
|
+
<h5 class="mb-2">Failed to generate link</h5>
|
|
232
|
+
<p class="text-muted mb-0" id="signin-as-error-message"></p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div class="modal-footer">
|
|
236
|
+
<button type="button" class="btn btn-sm btn-outline-adaptive" data-bs-dismiss="modal">Cancel</button>
|
|
237
|
+
<button type="button" class="btn btn-sm btn-adaptive d-none" id="btn-signin-as-navigate">
|
|
238
|
+
{% uj_icon "arrow-up-right-from-square", "fa-sm me-1" %}
|
|
239
|
+
Open in new tab
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
184
246
|
<!-- User Edit Modal -->
|
|
185
247
|
<div class="modal fade" id="user-edit-modal" tabindex="-1" aria-labelledby="user-edit-modal-label" aria-hidden="true">
|
|
186
248
|
<div class="modal-dialog">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const Manager = new (require('../../build.js'));
|
|
3
3
|
const logger = Manager.logger('defaults');
|
|
4
4
|
const { src, dest, watch, series } = require('gulp');
|
|
5
|
-
const
|
|
5
|
+
const { Transform } = require('node:stream');
|
|
6
6
|
const jetpack = require('fs-jetpack');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const { minimatch } = require('minimatch');
|
|
@@ -406,7 +406,9 @@ function defaults(complete, changedFile) {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
function customTransform() {
|
|
409
|
-
return
|
|
409
|
+
return new Transform({
|
|
410
|
+
objectMode: true,
|
|
411
|
+
transform(file, _, callback) {
|
|
410
412
|
// Skip if it's a directory
|
|
411
413
|
if (file.isDirectory()) {
|
|
412
414
|
return callback(null, file);
|
|
@@ -539,6 +541,7 @@ function customTransform() {
|
|
|
539
541
|
|
|
540
542
|
// Complete
|
|
541
543
|
return callback();
|
|
544
|
+
},
|
|
542
545
|
});
|
|
543
546
|
}
|
|
544
547
|
function defaultsWatcher(complete) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const Manager = new (require('../../build.js'));
|
|
3
3
|
const logger = Manager.logger('distribute');
|
|
4
4
|
const { src, dest, watch, series } = require('gulp');
|
|
5
|
-
const
|
|
5
|
+
const { Transform } = require('node:stream');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const jetpack = require('fs-jetpack');
|
|
8
8
|
const { template } = require('node-powertools');
|
|
@@ -180,38 +180,41 @@ function getFilesRecursive(dir) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function customTransform() {
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
return new Transform({
|
|
184
|
+
objectMode: true,
|
|
185
|
+
transform(file, _, callback) {
|
|
186
|
+
// Skip if it's a directory
|
|
187
|
+
if (file.isDirectory()) {
|
|
188
|
+
return callback(null, file);
|
|
189
|
+
}
|
|
188
190
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
// Get relative path from src base
|
|
192
|
+
const relativePath = path.relative(file.base, file.path).replace(/\\/g, '/');
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// Log
|
|
195
|
+
if (LOUD) {
|
|
196
|
+
logger.log(`Processing file: ${relativePath}`);
|
|
197
|
+
}
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
// Change path if it starts with 'pages/'
|
|
200
|
+
// if (relativePath.startsWith('pages/')) {
|
|
201
|
+
// // Remove 'pages/' prefix
|
|
202
|
+
// const newRelativePath = relativePath.replace(/^pages\//, '');
|
|
201
203
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
// // Update file path to remove pages directory
|
|
205
|
+
// // This will make src/pages/index.html -> dist/index.html
|
|
206
|
+
// file.path = path.join(file.base, newRelativePath);
|
|
205
207
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
208
|
+
// // Log
|
|
209
|
+
// logger.log(` -> Moving from pages/ to root: ${newRelativePath}`);
|
|
210
|
+
// }
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
// Push the file
|
|
213
|
+
this.push(file);
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
// Continue
|
|
216
|
+
callback();
|
|
217
|
+
},
|
|
215
218
|
});
|
|
216
219
|
}
|
|
217
220
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const Manager = new (require('../../build.js'));
|
|
3
3
|
const logger = Manager.logger('json-to-html');
|
|
4
4
|
const { src, dest, watch, series } = require('gulp');
|
|
5
|
-
const
|
|
5
|
+
const { Transform } = require('node:stream');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const jetpack = require('fs-jetpack');
|
|
8
8
|
const { template } = require('node-powertools');
|
|
@@ -42,90 +42,96 @@ function jsonToHtml(complete) {
|
|
|
42
42
|
|
|
43
43
|
// First, copy JSON files to _data directory
|
|
44
44
|
const jsonCopy = src(input)
|
|
45
|
-
.pipe(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
45
|
+
.pipe(new Transform({
|
|
46
|
+
objectMode: true,
|
|
47
|
+
transform(file, _, callback) {
|
|
48
|
+
if (file.isDirectory()) {
|
|
49
|
+
return callback(null, file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Parse JSON5 content
|
|
54
|
+
const json5Content = file.contents.toString();
|
|
55
|
+
const parsedData = JSON5.parse(json5Content);
|
|
56
|
+
|
|
57
|
+
// Convert to regular JSON (pretty printed for readability)
|
|
58
|
+
const regularJson = JSON.stringify(parsedData, null, 2);
|
|
59
|
+
|
|
60
|
+
// Also write to _data directory for Jekyll to pick up
|
|
61
|
+
const relativePath = path.relative(file.base, file.path);
|
|
62
|
+
const dataPath = path.join(dataOutput, relativePath);
|
|
63
|
+
const dataDir = path.dirname(dataPath);
|
|
64
|
+
|
|
65
|
+
// Ensure directory exists
|
|
66
|
+
jetpack.dir(dataDir);
|
|
67
|
+
|
|
68
|
+
// Write regular JSON file to _data
|
|
69
|
+
jetpack.write(dataPath, regularJson);
|
|
70
|
+
|
|
71
|
+
// Track compiled files
|
|
72
|
+
compiled[dataPath] = true;
|
|
73
|
+
|
|
74
|
+
// Update file contents with regular JSON
|
|
75
|
+
file.contents = Buffer.from(regularJson);
|
|
76
|
+
|
|
77
|
+
callback(null, file);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.error(`Error parsing JSON5 file ${file.path}: ${err.message}`);
|
|
80
|
+
callback(err);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
80
83
|
}))
|
|
81
84
|
.pipe(dest(dataOutput));
|
|
82
85
|
|
|
83
86
|
// Then create HTML wrapper files
|
|
84
87
|
const htmlGeneration = src(input)
|
|
85
|
-
.pipe(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
88
|
+
.pipe(new Transform({
|
|
89
|
+
objectMode: true,
|
|
90
|
+
transform(file, _, callback) {
|
|
91
|
+
// Skip if it's a directory
|
|
92
|
+
if (file.isDirectory()) {
|
|
93
|
+
return callback(null, file);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Get relative path from src/_includes
|
|
97
|
+
const relativePath = path.relative(file.base, file.path);
|
|
98
|
+
const relativeDir = path.dirname(relativePath);
|
|
99
|
+
const basename = path.basename(file.path, '.json');
|
|
100
|
+
|
|
101
|
+
// Convert path for Jekyll data reference
|
|
102
|
+
// _includes/frontend/sections/nav.json -> _includes.frontend.sections.nav
|
|
103
|
+
const dataPath = '_includes.' + relativePath
|
|
104
|
+
.replace(/\\/g, '/')
|
|
105
|
+
.replace('.json', '')
|
|
106
|
+
.split('/')
|
|
107
|
+
.join('.');
|
|
108
|
+
|
|
109
|
+
// Determine the template path
|
|
110
|
+
// Look for corresponding template in themes/{theme}/{target}/...
|
|
111
|
+
const templatePath = `themes/${config.theme.id}/${relativeDir}/${basename}.html`;
|
|
112
|
+
|
|
113
|
+
// Create the HTML content
|
|
114
|
+
const htmlContent = template(INCLUDE_TEMPLATE, {
|
|
115
|
+
dataPath: dataPath,
|
|
116
|
+
templatePath: templatePath
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Update file contents and extension
|
|
120
|
+
file.contents = Buffer.from(htmlContent);
|
|
121
|
+
file.path = file.path.replace('.json', '.html');
|
|
122
|
+
|
|
123
|
+
// Log transformation
|
|
124
|
+
logger.log(`Converting: ${relativePath} -> ${path.basename(file.path)}`);
|
|
125
|
+
logger.log(` Data path: site.data.${dataPath}`);
|
|
126
|
+
logger.log(` Template: ${templatePath}`);
|
|
127
|
+
|
|
128
|
+
// Track the full output path
|
|
129
|
+
const fullPath = path.resolve(output, path.relative(file.base, file.path));
|
|
130
|
+
compiled[fullPath] = true;
|
|
131
|
+
|
|
132
|
+
// Continue
|
|
133
|
+
callback(null, file);
|
|
134
|
+
},
|
|
129
135
|
}))
|
|
130
136
|
.pipe(dest(output));
|
|
131
137
|
|
|
@@ -4,7 +4,7 @@ const logger = Manager.logger('minifyHtml');
|
|
|
4
4
|
const { src, dest, series } = require('gulp');
|
|
5
5
|
const { minify: minifyRust } = require('@minify-html/node');
|
|
6
6
|
const { minify: minifyJs } = require('terser');
|
|
7
|
-
const
|
|
7
|
+
const { Transform } = require('node:stream');
|
|
8
8
|
|
|
9
9
|
// Load package
|
|
10
10
|
const package = Manager.getPackage('main');
|
|
@@ -85,61 +85,65 @@ function minifyHtmlTask(complete) {
|
|
|
85
85
|
|
|
86
86
|
// Process HTML files
|
|
87
87
|
return src(input)
|
|
88
|
-
.pipe(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
88
|
+
.pipe(new Transform({
|
|
89
|
+
objectMode: true,
|
|
90
|
+
transform(file, _enc, callback) {
|
|
91
|
+
if (file.isBuffer()) {
|
|
92
|
+
fileQueue.push({ file });
|
|
93
|
+
callback();
|
|
94
|
+
} else {
|
|
95
|
+
callback(null, file);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
async flush(callback) {
|
|
99
|
+
// This function is called when all files have been queued
|
|
100
|
+
if (fileQueue.length === 0) {
|
|
101
|
+
logger.log('No HTML files to minify');
|
|
102
|
+
return callback();
|
|
103
|
+
}
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
const totalFiles = fileQueue.length;
|
|
106
|
+
logger.log(`Minifying ${totalFiles} HTML files...`);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Process files in batches
|
|
110
|
+
for (let i = 0; i < fileQueue.length; i += CONCURRENCY_LIMIT) {
|
|
111
|
+
const batch = fileQueue.slice(i, i + CONCURRENCY_LIMIT);
|
|
112
|
+
|
|
113
|
+
// Process batch in parallel
|
|
114
|
+
const processedFiles = await Promise.all(
|
|
115
|
+
batch.map(async ({ file }) => {
|
|
116
|
+
try {
|
|
117
|
+
const htmlContent = file.contents.toString();
|
|
118
|
+
const finalHtml = await minifyFileContent(htmlContent, options, file.path);
|
|
119
|
+
file.contents = Buffer.from(finalHtml);
|
|
120
|
+
processed.count++;
|
|
121
|
+
|
|
122
|
+
// Log progress every 50 files or on last file
|
|
123
|
+
if (processed.count % 50 === 0 || processed.count === totalFiles) {
|
|
124
|
+
const percentage = ((processed.count / totalFiles) * 100).toFixed(1);
|
|
125
|
+
logger.log(`Progress: ${processed.count}/${totalFiles} files (${percentage}%)`);
|
|
126
|
+
Manager.logMemory(logger, `After ${processed.count} files`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return file;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logger.error(`Error minifying ${file.path}: ${err.message}`);
|
|
132
|
+
return file;
|
|
124
133
|
}
|
|
134
|
+
})
|
|
135
|
+
);
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return file;
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
);
|
|
137
|
+
// Push processed files to the stream
|
|
138
|
+
processedFiles.forEach(file => this.push(file));
|
|
139
|
+
}
|
|
133
140
|
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
callback();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
logger.error(`Batch processing error: ${err.message}`);
|
|
144
|
+
callback(err);
|
|
136
145
|
}
|
|
137
|
-
|
|
138
|
-
callback();
|
|
139
|
-
} catch (err) {
|
|
140
|
-
logger.error(`Batch processing error: ${err.message}`);
|
|
141
|
-
callback(err);
|
|
142
|
-
}
|
|
146
|
+
},
|
|
143
147
|
}))
|
|
144
148
|
.pipe(dest(output))
|
|
145
149
|
.on('finish', () => {
|
package/dist/gulp/tasks/sass.js
CHANGED
|
@@ -13,7 +13,7 @@ const { template } = require('node-powertools');
|
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const postcss = require('gulp-postcss');
|
|
15
15
|
const purgeCss = require('@fullhuman/postcss-purgecss');
|
|
16
|
-
const
|
|
16
|
+
const { Transform } = require('node:stream');
|
|
17
17
|
|
|
18
18
|
// Load package
|
|
19
19
|
const package = Manager.getPackage('main');
|
|
@@ -324,19 +324,20 @@ function sass(complete) {
|
|
|
324
324
|
let purgeCssLogged = false;
|
|
325
325
|
stream = stream
|
|
326
326
|
.pipe(postcss([purgeCssPlugin]))
|
|
327
|
-
.pipe(
|
|
328
|
-
|
|
327
|
+
.pipe(new Transform({
|
|
328
|
+
objectMode: true,
|
|
329
|
+
transform(file, enc, cb) {
|
|
329
330
|
cb(null, file);
|
|
330
331
|
},
|
|
331
|
-
|
|
332
|
+
flush(cb) {
|
|
332
333
|
if (!purgeCssLogged) {
|
|
333
334
|
purgeCssLogged = true;
|
|
334
335
|
const purgeCssTime = ((performance.now() - purgeCssStartTime) / 1000).toFixed(2);
|
|
335
336
|
logger.log(`PurgeCSS completed in ${purgeCssTime}s`);
|
|
336
337
|
}
|
|
337
338
|
cb();
|
|
338
|
-
}
|
|
339
|
-
));
|
|
339
|
+
},
|
|
340
|
+
}));
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
// Process
|
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
// Libraries
|
|
2
|
-
const
|
|
2
|
+
const { Transform } = require('node:stream');
|
|
3
3
|
const { template } = require('node-powertools');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Creates a
|
|
7
|
+
* Creates a transform stream that processes template variables in files
|
|
8
8
|
**/
|
|
9
9
|
function createTemplateTransform(data) {
|
|
10
10
|
const extensions = ['html', 'md', 'liquid', 'json']
|
|
11
11
|
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Check if file extension matches
|
|
19
|
-
const ext = path.extname(file.path).toLowerCase().slice(1);
|
|
20
|
-
if (!extensions.includes(ext)) {
|
|
21
|
-
return callback(null, file);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Log
|
|
25
|
-
// console.log(`Processing file: ${file.path}`);
|
|
26
|
-
|
|
27
|
-
// Process the file contents
|
|
28
|
-
try {
|
|
29
|
-
const contents = file.contents.toString();
|
|
30
|
-
|
|
31
|
-
// Process templates
|
|
32
|
-
const templated = template(contents, data, {
|
|
33
|
-
brackets: ['[', ']'],
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Update file contents if changed
|
|
37
|
-
if (contents !== templated) {
|
|
38
|
-
file.contents = Buffer.from(templated);
|
|
39
|
-
const relativePath = file.relative || file.path;
|
|
12
|
+
return new Transform({
|
|
13
|
+
objectMode: true,
|
|
14
|
+
transform(file, encoding, callback) {
|
|
15
|
+
// Skip directories
|
|
16
|
+
if (file.isDirectory()) {
|
|
17
|
+
return callback(null, file);
|
|
40
18
|
}
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error(`Error processing templates in ${file.path}:`, error);
|
|
43
|
-
}
|
|
44
19
|
|
|
45
|
-
|
|
46
|
-
|
|
20
|
+
// Check if file extension matches
|
|
21
|
+
const ext = path.extname(file.path).toLowerCase().slice(1);
|
|
22
|
+
if (!extensions.includes(ext)) {
|
|
23
|
+
return callback(null, file);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Process the file contents
|
|
27
|
+
try {
|
|
28
|
+
const contents = file.contents.toString();
|
|
29
|
+
|
|
30
|
+
// Process templates
|
|
31
|
+
const templated = template(contents, data, {
|
|
32
|
+
brackets: ['[', ']'],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Update file contents if changed
|
|
36
|
+
if (contents !== templated) {
|
|
37
|
+
file.contents = Buffer.from(templated);
|
|
38
|
+
const relativePath = file.relative || file.path;
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Error processing templates in ${file.path}:`, error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Pass the file through
|
|
45
|
+
callback(null, file);
|
|
46
|
+
},
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@babel/core": "^7.29.0",
|
|
69
|
-
"@babel/preset-env": "^7.29.
|
|
69
|
+
"@babel/preset-env": "^7.29.5",
|
|
70
70
|
"@fullhuman/postcss-purgecss": "^8.0.0",
|
|
71
71
|
"@minify-html/node": "^0.18.1",
|
|
72
72
|
"@octokit/rest": "^22.0.1",
|
|
@@ -79,35 +79,34 @@
|
|
|
79
79
|
"chart.js": "^4.5.1",
|
|
80
80
|
"cheerio": "^1.2.0",
|
|
81
81
|
"chrome-launcher": "^1.2.1",
|
|
82
|
-
"dompurify": "^3.
|
|
83
|
-
"dotenv": "^17.4.
|
|
84
|
-
"fast-xml-parser": "^5.
|
|
82
|
+
"dompurify": "^3.4.2",
|
|
83
|
+
"dotenv": "^17.4.2",
|
|
84
|
+
"fast-xml-parser": "^5.7.3",
|
|
85
85
|
"fs-jetpack": "^5.1.0",
|
|
86
86
|
"glob": "^13.0.6",
|
|
87
87
|
"gulp-clean-css": "^4.3.0",
|
|
88
|
-
"gulp-filter": "^
|
|
88
|
+
"gulp-filter": "^10.0.0",
|
|
89
89
|
"gulp-postcss": "^10.0.0",
|
|
90
90
|
"gulp-rename": "^2.1.0",
|
|
91
91
|
"gulp-responsive-modern": "^1.0.0",
|
|
92
92
|
"gulp-sass": "^6.0.1",
|
|
93
93
|
"html-minifier-terser": "^7.2.0",
|
|
94
|
-
"html-validate": "^10.
|
|
94
|
+
"html-validate": "^10.16.0",
|
|
95
95
|
"itwcw-package-analytics": "^1.0.8",
|
|
96
96
|
"js-yaml": "^4.1.1",
|
|
97
97
|
"json5": "^2.2.3",
|
|
98
|
-
"libsodium-wrappers": "^0.8.
|
|
98
|
+
"libsodium-wrappers": "^0.8.4",
|
|
99
99
|
"lodash": "^4.18.1",
|
|
100
100
|
"markdown-it": "^14.1.1",
|
|
101
101
|
"minimatch": "^10.2.5",
|
|
102
102
|
"node-powertools": "^3.0.0",
|
|
103
103
|
"npm-api": "^1.0.1",
|
|
104
|
-
"postcss": "^8.5.
|
|
105
|
-
"prettier": "^3.8.
|
|
104
|
+
"postcss": "^8.5.14",
|
|
105
|
+
"prettier": "^3.8.3",
|
|
106
106
|
"sass": "^1.99.0",
|
|
107
107
|
"spellchecker": "^3.7.1",
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"webpack": "^5.106.1",
|
|
108
|
+
"web-manager": "^4.1.41",
|
|
109
|
+
"webpack": "^5.106.2",
|
|
111
110
|
"wonderful-fetch": "^2.0.5",
|
|
112
111
|
"wonderful-version": "^1.3.2",
|
|
113
112
|
"yargs": "^18.0.0"
|