ultimate-jekyll-manager 1.9.11 → 1.9.13

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.
@@ -32,6 +32,8 @@ form[data-form-state]:not([data-form-state="ready"]) [type="submit"] {
32
32
  form[data-form-state]:not([data-form-state="ready"]) [type="submit"] {
33
33
  opacity: var(--bs-btn-disabled-opacity);
34
34
  color: var(--bs-btn-disabled-color);
35
+ background-color: var(--bs-btn-disabled-bg);
36
+ border-color: var(--bs-btn-disabled-border-color);
35
37
  }
36
38
 
37
39
  // Prevent pointer events on all children inside buttons and links
@@ -45,6 +45,13 @@ $campaign-push-color: #4CAF50;
45
45
  min-width: 200px;
46
46
  }
47
47
 
48
+ .calendar-toolbar-utc-clock {
49
+ display: block;
50
+ font-size: 0.7rem;
51
+ font-weight: 400;
52
+ color: var(--bs-secondary-color);
53
+ }
54
+
48
55
  .calendar-toolbar-views .btn {
49
56
  padding: 0.25rem 0.75rem;
50
57
  font-size: 0.8rem;
@@ -31,6 +31,11 @@ export default function () {
31
31
  // Initialize the appropriate form based on the page (with autoReady: false)
32
32
  initializePageForm();
33
33
 
34
+ // Disable form fields while checking for OAuth redirect result.
35
+ // State stays 'initializing' so spinners remain visible during the check.
36
+ // formManager.ready() transitions to 'ready' and re-enables if no redirect is found.
37
+ formManager._setDisabled(true);
38
+
34
39
  // Check for redirect result from OAuth providers BEFORE enabling form
35
40
  // This prevents the form from appearing interactive while redirect is processing
36
41
  const hasRedirectResult = await handleRedirectResult();
@@ -279,13 +284,36 @@ export default function () {
279
284
  }
280
285
 
281
286
  async function handleRedirectResult() {
282
- try {
283
- // Import Firebase auth functions
287
+ // Resolve the redirect result — either from Firebase or a dev simulation
288
+ let result, additionalUserInfo;
289
+ const simulateRedirect = url.searchParams.get('_dev_simulateRedirect');
290
+
291
+ if (simulateRedirect) {
292
+ console.log('[Auth] Simulating OAuth redirect result:', simulateRedirect);
293
+ await new Promise(resolve => setTimeout(resolve, 2000));
294
+
295
+ if (simulateRedirect !== 'error') {
296
+ result = {
297
+ user: { uid: 'dev-sim-uid', email: 'dev@test.local', providerData: [{ providerId: 'google.com' }] },
298
+ providerId: 'google.com',
299
+ };
300
+ additionalUserInfo = { isNewUser: simulateRedirect === 'signup' };
301
+ }
302
+ } else {
284
303
  const { getAuth, getRedirectResult, getAdditionalUserInfo } = await import('@firebase/auth');
285
304
  const auth = getAuth();
305
+ result = await getRedirectResult(auth);
306
+ if (result?.user) {
307
+ additionalUserInfo = getAdditionalUserInfo(result);
308
+ }
309
+ }
286
310
 
287
- // Check for redirect result
288
- const result = await getRedirectResult(auth);
311
+ try {
312
+ if (simulateRedirect === 'error') {
313
+ const fakeError = new Error('Simulated: An account already exists with different credentials');
314
+ fakeError.code = 'auth/account-exists-with-different-credential';
315
+ throw fakeError;
316
+ }
289
317
 
290
318
  // Log results for debugging
291
319
  console.log('[Auth] Redirect result:', result);
@@ -300,13 +328,6 @@ export default function () {
300
328
  // Determine the provider from the result
301
329
  const providerId = result.providerId || result.user.providerData?.[0]?.providerId || 'unknown';
302
330
 
303
- // Track based on whether this is a new user. Firebase Auth v9+ modular SDK
304
- // does NOT expose additionalUserInfo as a direct property on UserCredential —
305
- // you must call getAdditionalUserInfo(result) to get it. The legacy compat SDK
306
- // exposed it as a direct property, hence the v9 migration footgun. Verified
307
- // against @firebase/auth's auth-public.d.ts: UserCredential only declares
308
- // { user, providerId, operationType }.
309
- const additionalUserInfo = getAdditionalUserInfo(result);
310
331
  const isNewUser = additionalUserInfo?.isNewUser;
311
332
  const pagePath = document.documentElement.getAttribute('data-page-path');
312
333
  const isSignupPage = pagePath === '/signup';
@@ -328,6 +349,18 @@ export default function () {
328
349
  formManager.showSuccess('Successfully signed in!');
329
350
  }
330
351
 
352
+ // In simulation mode, handle the redirect ourselves since the auth
353
+ // state listener won't fire (no real Firebase login happened).
354
+ if (simulateRedirect) {
355
+ const authReturnUrl = url.searchParams.get('authReturnUrl');
356
+ const redirectTo = authReturnUrl && webManager.isValidRedirectUrl(authReturnUrl)
357
+ ? authReturnUrl
358
+ : '/dashboard';
359
+ console.log('[Auth] Simulated redirect to:', redirectTo);
360
+ await new Promise(resolve => setTimeout(resolve, 1500));
361
+ window.location.href = redirectTo;
362
+ }
363
+
331
364
  // Return true to indicate redirect was successfully processed
332
365
  return true;
333
366
  } catch (error) {
@@ -57,6 +57,7 @@ export default class CalendarRenderer {
57
57
  </div>
58
58
  <div class="calendar-toolbar-period">
59
59
  ${core.formatPeriodLabel()}
60
+ <span class="calendar-toolbar-utc-clock">${this._formatUTCClock()}</span>
60
61
  </div>
61
62
  <div class="calendar-toolbar-views btn-group" role="group" aria-label="Calendar view">
62
63
  ${VIEW_MODES.map((mode) => `
@@ -528,6 +529,9 @@ export default class CalendarRenderer {
528
529
  // ============================================
529
530
  _startNowLine() {
530
531
  clearInterval(this._nowLineInterval);
532
+ clearInterval(this._clockInterval);
533
+ this._updateUTCClock();
534
+ this._clockInterval = setInterval(() => this._updateUTCClock(), 1000);
531
535
  if (this.core.viewMode !== 'day' && this.core.viewMode !== 'week' && this.core.viewMode !== 'month') {
532
536
  return;
533
537
  }
@@ -767,6 +771,23 @@ export default class CalendarRenderer {
767
771
  return hours;
768
772
  }
769
773
 
774
+ _formatUTCClock() {
775
+ const now = new Date();
776
+ const h = now.getUTCHours();
777
+ const m = now.getUTCMinutes();
778
+ const s = now.getUTCSeconds();
779
+ const period = h >= 12 ? 'p' : 'a';
780
+ const display = h === 0 ? 12 : h > 12 ? h - 12 : h;
781
+ return `${display}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}${period} UTC`;
782
+ }
783
+
784
+ _updateUTCClock() {
785
+ const $clock = this.$toolbar.querySelector('.calendar-toolbar-utc-clock');
786
+ if ($clock) {
787
+ $clock.textContent = this._formatUTCClock();
788
+ }
789
+ }
790
+
770
791
  _formatTime(timeStr) {
771
792
  if (!timeStr) { return ''; }
772
793
  const [h, m] = timeStr.split(':').map(Number);
package/dist/build.js CHANGED
@@ -120,7 +120,8 @@ Manager.getConfig = function (type) {
120
120
  : 'dist';
121
121
  const resolvedPath = path.join(basePath, '_config.yml');
122
122
 
123
- return yaml.load(jetpack.read(resolvedPath));
123
+ const content = jetpack.read(resolvedPath);
124
+ return content ? yaml.load(content) : undefined;
124
125
  }
125
126
  Manager.prototype.getConfig = Manager.getConfig;
126
127
 
@@ -232,6 +232,7 @@ newsletter:
232
232
  <h3 class="h5 my-2">{{ post.post.title }}</h3>
233
233
  <p class="small text-body-secondary mb-0">
234
234
  <b class="text-body">{%- uj_member post.post.author, "name" -%}</b>
235
+ · {{ post.date | date: "%b %d, %Y" }}
235
236
  · {% uj_readtime post.content %} min read
236
237
  </p>
237
238
  </a>
@@ -228,6 +228,7 @@ cta:
228
228
  <h3 class="h5 my-2">{{ post.post.title }}</h3>
229
229
  <p class="small text-body-secondary mb-0">
230
230
  <b class="text-body">{%- uj_member post.post.author, "name" -%}</b>
231
+ · {{ post.date | date: "%b %d, %Y" }}
231
232
  · {% uj_readtime post.content %} min read
232
233
  </p>
233
234
  </a>
@@ -470,6 +471,7 @@ cta:
470
471
  <h3 class="h5 my-2">{{ post.post.title }}</h3>
471
472
  <p class="small text-body-secondary mb-0">
472
473
  <b class="text-body">{%- uj_member post.post.author, "name" -%}</b>
474
+ · {{ post.date | date: "%b %d, %Y" }}
473
475
  · {% uj_readtime post.content %} min read
474
476
  </p>
475
477
  </a>
@@ -467,7 +467,7 @@ function parseFrontmatter(filePath) {
467
467
  const frontmatterContent = content.substring(3, frontmatterEnd).trim();
468
468
 
469
469
  // Parse YAML frontmatter
470
- const frontmatter = yaml.load(frontmatterContent);
470
+ const frontmatter = frontmatterContent ? yaml.load(frontmatterContent) : undefined;
471
471
  return frontmatter || {};
472
472
  } catch (error) {
473
473
  logger.log(`Error parsing frontmatter for ${filePath}:`, error.message);
@@ -810,7 +810,7 @@ function getIgnoredPages() {
810
810
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
811
811
 
812
812
  if (frontmatterMatch) {
813
- const frontmatter = yaml.load(frontmatterMatch[1]);
813
+ const frontmatter = frontmatterMatch[1] ? yaml.load(frontmatterMatch[1]) : undefined;
814
814
  if (frontmatter?.permalink) {
815
815
  redirectPermalinks.push(frontmatter.permalink.replace(/^\//, '')); // Remove leading slash
816
816
  }
@@ -2,6 +2,16 @@
2
2
  const jetpack = require('fs-jetpack');
3
3
  const yaml = require('js-yaml');
4
4
 
5
+ function safeLoadYaml(content) {
6
+ if (!content) return undefined;
7
+ try {
8
+ return yaml.load(content);
9
+ } catch (e) {
10
+ if (e.name === 'YAMLException' && /expected a document/.test(e.message)) return undefined;
11
+ throw e;
12
+ }
13
+ }
14
+
5
15
  /**
6
16
  * Merges Jekyll collections and defaults from UJM's config with the
7
17
  * consuming project's config. Jekyll's --config does a shallow merge
@@ -15,9 +25,9 @@ function mergeJekyllConfigs(ujmConfigPath, projectConfigPath, outputPath, logger
15
25
  return null;
16
26
  }
17
27
 
18
- const ujmConfig = yaml.load(ujmContent) || {};
28
+ const ujmConfig = safeLoadYaml(ujmContent) || {};
19
29
  const projectContent = jetpack.read(projectConfigPath);
20
- const projectConfig = projectContent ? (yaml.load(projectContent) || {}) : {};
30
+ const projectConfig = projectContent ? (safeLoadYaml(projectContent) || {}) : {};
21
31
 
22
32
  // Extract collections and defaults from both
23
33
  const ujmCollections = ujmConfig.collections || {};
@@ -16,6 +16,31 @@ FIREBASE_EMULATOR_CONNECT=true npm start
16
16
 
17
17
  This value is written to `.temp/_config_browsersync.yml` under `web_manager.env.FIREBASE_EMULATOR_CONNECT` and made available to the frontend at build time.
18
18
 
19
+ ## Dev Query Parameters (`_dev_*`)
20
+
21
+ Pages accept `_dev_` prefixed query parameters to simulate hard-to-reach states without real backend/OAuth interactions. These only affect frontend behavior — no data is written.
22
+
23
+ ### Auth pages (`/signin`, `/signup`, `/reset`)
24
+
25
+ | Parameter | Values | Behavior |
26
+ |---|---|---|
27
+ | `_dev_simulateRedirect` | `true`, `success` | Simulates a successful OAuth redirect: 2s loading state → login tracking + success message. Auth state listener handles navigation. |
28
+ | | `signup` | Simulates a new-user OAuth redirect: 2s loading → signup tracking + success. On `/signin`, triggers `reverseAccidentalSignup` (the Google auto-creation reversal flow). |
29
+ | | `error` | Simulates an OAuth error (`auth/account-exists-with-different-credential`): 2s loading → error message → form re-enables. |
30
+
31
+ All three modes exercise the real `handleRedirectResult()` code paths — the simulation injects fake data into the same tracking, error handling, and form state transitions that a real OAuth redirect uses.
32
+
33
+ ### Other pages
34
+
35
+ | Parameter | Page | Behavior |
36
+ |---|---|---|
37
+ | `_dev_prefill` | `/account/security`, `/account/referrals` | Pre-fills form with fake data |
38
+ | `_dev_trialEligible` | `/payment/checkout` | Overrides trial eligibility check |
39
+ | `_dev_preDelay` | `/payment/checkout` | Adds a delay before checkout init |
40
+ | `_dev_cardProcessor` | `/payment/checkout` | Forces a specific card processor |
41
+ | `_dev_recaptcha` | `/payment/checkout` | Overrides reCAPTCHA behavior |
42
+ | `_dev_subscription` | `/account` | Overrides subscription state |
43
+
19
44
  ## PurgeCSS
20
45
 
21
46
  PurgeCSS runs automatically in production builds and can be enabled locally with `UJ_PURGECSS=true`. Consuming projects can add custom safelist patterns via `config/ultimate-jekyll-manager.json` under `sass.purgecss.safelist`:
@@ -234,7 +234,7 @@ Same protocol as EM (`__EM_TEST__`) and BXM (`__BXM_TEST__`). One marker per fra
234
234
 
235
235
  - **Framework suites**: glob `<ujm>/dist/test/suites/**/*.js` (resolved from `__dirname/suites` in `runner.js`).
236
236
  - **Consumer suites**: glob `<cwd>/test/**/*.js`.
237
- - **Excluded**: any directory starting with `_` (handy for shared helpers).
237
+ - **Excluded** (`_` underscore convention): any file or directory starting with `_` is excluded from suite discovery. Put shared helpers, fixture data, and non-test support files in `_`-prefixed paths — e.g. `test/_fixtures/`, `test/_helpers/`. The runner still specifically loads `test/_init.js` as the lifecycle hook. Matches the same convention in BEM/EM/BXM.
238
238
  - **Framework boot suites** are excluded when the cwd's `package.json#name` is not `ultimate-jekyll-manager` — they target UJM's fixture site, not the consumer's. Consumers write their own boot tests in `<cwd>/test/boot/`.
239
239
 
240
240
  ## `test/_init.js` — pre-test lifecycle hook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.9.11",
3
+ "version": "1.9.13",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -76,23 +76,23 @@
76
76
  "lighthouse": "Removed from deps to avoid @sentry/core version conflicts with web-manager. Install globally: npm i -g lighthouse"
77
77
  },
78
78
  "dependencies": {
79
- "@babel/core": "^7.29.7",
80
- "@babel/preset-env": "^7.29.7",
79
+ "@babel/core": "^8.0.1",
80
+ "@babel/preset-env": "^8.0.2",
81
81
  "@fullhuman/postcss-purgecss": "^8.0.0",
82
82
  "@minify-html/node": "^0.18.1",
83
83
  "@octokit/rest": "^22.0.1",
84
84
  "@popperjs/core": "^2.11.8",
85
85
  "@prettier/plugin-xml": "^3.4.2",
86
- "adm-zip": "^0.5.17",
86
+ "adm-zip": "^0.5.18",
87
87
  "babel-loader": "^10.1.1",
88
88
  "browser-sync": "^3.0.4",
89
89
  "chalk": "^5.6.2",
90
90
  "chart.js": "^4.5.1",
91
91
  "cheerio": "^1.2.0",
92
92
  "chrome-launcher": "^1.2.1",
93
- "dompurify": "^3.4.6",
93
+ "dompurify": "^3.4.11",
94
94
  "dotenv": "^17.4.2",
95
- "fast-xml-parser": "^5.8.0",
95
+ "fast-xml-parser": "^5.9.3",
96
96
  "fs-jetpack": "^5.1.0",
97
97
  "glob": "^13.0.6",
98
98
  "gulp-clean-css": "^4.3.0",
@@ -102,9 +102,9 @@
102
102
  "gulp-responsive-modern": "^1.0.0",
103
103
  "gulp-sass": "^6.0.1",
104
104
  "html-minifier-terser": "^7.2.0",
105
- "html-validate": "^11.4.0",
105
+ "html-validate": "^11.5.3",
106
106
  "itwcw-package-analytics": "^1.0.8",
107
- "js-yaml": "^4.1.1",
107
+ "js-yaml": "^5.2.0",
108
108
  "json5": "^2.2.3",
109
109
  "libsodium-wrappers": "^0.8.4",
110
110
  "lodash": "^4.18.1",
@@ -112,12 +112,12 @@
112
112
  "minimatch": "^10.2.5",
113
113
  "node-powertools": "^3.0.0",
114
114
  "npm-api": "^1.0.1",
115
- "postcss": "^8.5.15",
116
- "prettier": "^3.8.3",
117
- "sass": "^1.100.0",
115
+ "postcss": "^8.5.16",
116
+ "prettier": "^3.9.4",
117
+ "sass": "^1.101.0",
118
118
  "spellchecker": "^3.7.1",
119
- "web-manager": "^4.3.1",
120
- "webpack": "^5.107.2",
119
+ "web-manager": "^4.3.2",
120
+ "webpack": "^5.108.3",
121
121
  "wonderful-fetch": "^2.0.5",
122
122
  "wonderful-version": "^1.3.2",
123
123
  "yargs": "^18.0.0"
@@ -127,6 +127,6 @@
127
127
  },
128
128
  "devDependencies": {
129
129
  "prepare-package": "^2.1.0",
130
- "puppeteer": "^25.1.0"
130
+ "puppeteer": "^25.2.1"
131
131
  }
132
132
  }