ultimate-jekyll-manager 1.9.25 → 1.9.27
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/dist/assets/js/pages/token/index.js +27 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/token.html +7 -1
- package/dist/gulp/tasks/defaults.js +8 -0
- package/dist/test/suites/build/defaults-file-options.test.js +59 -0
- package/dist/test/suites/page/token-error-state.test.js +124 -0
- package/docs/build-system.md +13 -0
- package/docs/test-framework.md +2 -0
- package/package.json +1 -1
|
@@ -8,6 +8,14 @@ export default function () {
|
|
|
8
8
|
const $status = document.getElementById('token-status');
|
|
9
9
|
const $error = document.getElementById('token-error');
|
|
10
10
|
const $errorMessage = document.getElementById('token-error-message');
|
|
11
|
+
const $spinner = document.getElementById('token-spinner');
|
|
12
|
+
const $actions = document.getElementById('token-actions');
|
|
13
|
+
const $retry = document.getElementById('token-retry');
|
|
14
|
+
|
|
15
|
+
// Retry re-runs the whole flow — reloading re-attempts auth + token generation
|
|
16
|
+
if ($retry) {
|
|
17
|
+
$retry.addEventListener('click', () => window.location.reload());
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
// Get URL params
|
|
13
21
|
const url = new URL(window.location.href);
|
|
@@ -83,7 +91,7 @@ export default function () {
|
|
|
83
91
|
const returnUrl = new URL(authReturnUrl);
|
|
84
92
|
returnUrl.searchParams.set('authToken', token);
|
|
85
93
|
|
|
86
|
-
// LEGACY:
|
|
94
|
+
// LEGACY: Add the legacy payload shape for old desktop app deep links
|
|
87
95
|
// TODO: Remove this block when legacy desktop app support is no longer needed
|
|
88
96
|
_legacyTranslateTokenRedirect(returnUrl, token);
|
|
89
97
|
|
|
@@ -101,6 +109,7 @@ export default function () {
|
|
|
101
109
|
// Extension background will detect this and close the tab
|
|
102
110
|
url.searchParams.set('authToken', token);
|
|
103
111
|
window.history.replaceState({}, '', url.toString());
|
|
112
|
+
stopSpinner();
|
|
104
113
|
updateStatus('You can close this tab now.');
|
|
105
114
|
}
|
|
106
115
|
} catch (error) {
|
|
@@ -150,8 +159,10 @@ export default function () {
|
|
|
150
159
|
$status.appendChild($p);
|
|
151
160
|
}
|
|
152
161
|
|
|
153
|
-
// Show error message
|
|
162
|
+
// Show error message + swap the spinner for the retry / go-home actions
|
|
154
163
|
function showError(message) {
|
|
164
|
+
stopSpinner();
|
|
165
|
+
|
|
155
166
|
if ($error && $errorMessage) {
|
|
156
167
|
$errorMessage.textContent = message;
|
|
157
168
|
$error.classList.remove('d-none');
|
|
@@ -159,14 +170,25 @@ export default function () {
|
|
|
159
170
|
if ($status) {
|
|
160
171
|
$status.classList.add('d-none');
|
|
161
172
|
}
|
|
173
|
+
if ($actions) {
|
|
174
|
+
$actions.classList.remove('d-none');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Stop the loading spinner once the flow reaches a terminal state
|
|
179
|
+
function stopSpinner() {
|
|
180
|
+
if ($spinner) {
|
|
181
|
+
$spinner.classList.add('d-none');
|
|
182
|
+
}
|
|
162
183
|
}
|
|
163
184
|
|
|
164
|
-
// LEGACY:
|
|
165
|
-
// Legacy desktop apps
|
|
185
|
+
// LEGACY: Add the legacy token shape for desktop app deep links
|
|
186
|
+
// Legacy desktop apps read ?payload={"token":"X"} on custom protocol URLs — ADDED
|
|
187
|
+
// alongside ?authToken=X (never replacing it): old apps read payload and ignore
|
|
188
|
+
// authToken, modern apps (Electron Manager) read authToken and ignore payload.
|
|
166
189
|
// TODO: Remove this function AND its call above when legacy desktop app support is no longer needed
|
|
167
190
|
function _legacyTranslateTokenRedirect(returnUrl, token) {
|
|
168
191
|
if (returnUrl.protocol !== 'http:' && returnUrl.protocol !== 'https:') {
|
|
169
|
-
returnUrl.searchParams.delete('authToken');
|
|
170
192
|
returnUrl.searchParams.set('payload', JSON.stringify({ token: token }));
|
|
171
193
|
}
|
|
172
194
|
}
|
|
@@ -20,7 +20,7 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<!-- Spinner -->
|
|
23
|
-
<div class="text-center mb-4">
|
|
23
|
+
<div id="token-spinner" class="text-center mb-4">
|
|
24
24
|
<div class="spinner-border text-primary" role="status">
|
|
25
25
|
<span class="visually-hidden">Loading...</span>
|
|
26
26
|
</div>
|
|
@@ -35,6 +35,12 @@ layout: themes/[ site.theme.id ]/frontend/core/cover
|
|
|
35
35
|
<div id="token-error" class="alert alert-danger d-none" role="alert">
|
|
36
36
|
<strong>Error:</strong> <span id="token-error-message"></span>
|
|
37
37
|
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Actions (hidden until the flow resolves with an error) -->
|
|
40
|
+
<div id="token-actions" class="text-center d-none">
|
|
41
|
+
<button type="button" id="token-retry" class="btn btn-primary w-100">Try again</button>
|
|
42
|
+
<a href="/" class="btn btn-link btn-sm text-muted d-block mt-2">Go home</a>
|
|
43
|
+
</div>
|
|
38
44
|
</div>
|
|
39
45
|
</div>
|
|
40
46
|
</section>
|
|
@@ -55,6 +55,11 @@ const FILE_MAP = {
|
|
|
55
55
|
'src/**/*': {
|
|
56
56
|
overwrite: false,
|
|
57
57
|
},
|
|
58
|
+
// Consumer-owned after seeding (e.g. test/_init.js fixture hooks) — copy when
|
|
59
|
+
// missing, never clobber the consumer's version on setup reruns
|
|
60
|
+
'test/**/*': {
|
|
61
|
+
overwrite: false,
|
|
62
|
+
},
|
|
58
63
|
'src/**/*.{html,md,json}': {
|
|
59
64
|
skip: (file) => {
|
|
60
65
|
// Get the name
|
|
@@ -604,6 +609,9 @@ function defaultsWatcher(complete) {
|
|
|
604
609
|
// Default Task
|
|
605
610
|
module.exports = series(defaults, defaultsWatcher);
|
|
606
611
|
|
|
612
|
+
// Exposed for the build-layer FILE_MAP test (defaults-file-options.test.js)
|
|
613
|
+
module.exports.getFileOptions = getFileOptions;
|
|
614
|
+
|
|
607
615
|
function getFileOptions(filePath) {
|
|
608
616
|
const defaults = {
|
|
609
617
|
overwrite: true,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// getFileOptions from src/gulp/tasks/defaults.js — the FILE_MAP rules that decide
|
|
2
|
+
// which shipped defaults may overwrite consumer files on `npx mgr setup` reruns.
|
|
3
|
+
// Regression here silently clobbers consumer-owned files (test/_init.js was reset
|
|
4
|
+
// to the stub on every setup because no rule matched it and the fall-through
|
|
5
|
+
// default is overwrite: true).
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
layer: 'build',
|
|
9
|
+
description: 'defaults FILE_MAP (gulp/tasks/defaults.js getFileOptions)',
|
|
10
|
+
type: 'group',
|
|
11
|
+
tests: [
|
|
12
|
+
{
|
|
13
|
+
name: 'consumer-owned seeds are copy-once (test/, hooks/, src/)',
|
|
14
|
+
run: async (ctx) => {
|
|
15
|
+
const { getFileOptions } = require('../../../gulp/tasks/defaults.js');
|
|
16
|
+
|
|
17
|
+
// The regression case: consumer fixture hook must survive setup reruns
|
|
18
|
+
ctx.expect(getFileOptions('test/_init.js').overwrite).toBe(false);
|
|
19
|
+
ctx.expect(getFileOptions('test/README.md').overwrite).toBe(false);
|
|
20
|
+
|
|
21
|
+
ctx.expect(getFileOptions('hooks/build/pre.js').overwrite).toBe(false);
|
|
22
|
+
ctx.expect(getFileOptions('src/assets/js/main.js').overwrite).toBe(false);
|
|
23
|
+
ctx.expect(getFileOptions('src/service-worker.js').overwrite).toBe(false);
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'framework-owned files always overwrite',
|
|
28
|
+
run: async (ctx) => {
|
|
29
|
+
const { getFileOptions } = require('../../../gulp/tasks/defaults.js');
|
|
30
|
+
|
|
31
|
+
const workflow = getFileOptions('.github/workflows/build.yml');
|
|
32
|
+
ctx.expect(workflow.overwrite).toBe(true);
|
|
33
|
+
ctx.expect(!!workflow.template).toBe(true);
|
|
34
|
+
|
|
35
|
+
// Last-match-wins: team images override the earlier src/**/* (false) rule
|
|
36
|
+
ctx.expect(getFileOptions('src/assets/images/team/rare-ivy/profile.jpg').overwrite).toBe(true);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'merge and skip rules resolve as declared',
|
|
41
|
+
run: async (ctx) => {
|
|
42
|
+
const { getFileOptions } = require('../../../gulp/tasks/defaults.js');
|
|
43
|
+
|
|
44
|
+
ctx.expect(getFileOptions('CLAUDE.md').mergeLines).toBe(true);
|
|
45
|
+
|
|
46
|
+
const config = getFileOptions('config/ultimate-jekyll-manager.json');
|
|
47
|
+
ctx.expect(config.overwrite).toBe(true);
|
|
48
|
+
ctx.expect(config.merge).toBe(true);
|
|
49
|
+
|
|
50
|
+
ctx.expect(getFileOptions('test/.DS_Store').skip).toBe(true);
|
|
51
|
+
|
|
52
|
+
// Unmatched files fall through to overwrite: true — any new consumer-owned
|
|
53
|
+
// default MUST get an explicit rule or setup will clobber it
|
|
54
|
+
ctx.expect(getFileOptions('dist/robots.txt').overwrite).toBe(true);
|
|
55
|
+
ctx.expect(getFileOptions('dist/robots.txt').rule).toBeNull();
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// /token page: when the flow resolves with an error (e.g. token fetch fails) the
|
|
2
|
+
// loading spinner must STOP and be replaced by a "Try again" (reload) button plus
|
|
3
|
+
// a smaller "Go home" link — previously the spinner spun forever behind the error.
|
|
4
|
+
//
|
|
5
|
+
// Each run() is serialized to the browser (Puppeteer), so every test builds the
|
|
6
|
+
// token card DOM (mirrors classy's auth/token.html) and replicates the module's
|
|
7
|
+
// stopSpinner/showError logic inline — the page module can't be imported (ESM +
|
|
8
|
+
// web-manager deps) and module-scope helpers aren't in the browser's scope.
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
layer: 'page',
|
|
12
|
+
description: '/token error state swaps the spinner for retry/home actions',
|
|
13
|
+
type: 'group',
|
|
14
|
+
tests: [
|
|
15
|
+
{
|
|
16
|
+
name: 'showError stops the spinner and reveals the retry/home actions',
|
|
17
|
+
run: async (ctx) => {
|
|
18
|
+
const $root = document.createElement('div');
|
|
19
|
+
$root.innerHTML = ''
|
|
20
|
+
+ '<div id="token-spinner" class="text-center mb-4"><div class="spinner-border" role="status"></div></div>'
|
|
21
|
+
+ '<div id="token-status" class="text-center"><p class="text-muted small">Generating secure token...</p></div>'
|
|
22
|
+
+ '<div id="token-error" class="alert alert-danger d-none" role="alert"><strong>Error:</strong> <span id="token-error-message"></span></div>'
|
|
23
|
+
+ '<div id="token-actions" class="text-center d-none">'
|
|
24
|
+
+ '<button type="button" id="token-retry" class="btn btn-primary w-100">Try again</button>'
|
|
25
|
+
+ '<a href="/" class="btn btn-link btn-sm text-muted d-block mt-2">Go home</a>'
|
|
26
|
+
+ '</div>';
|
|
27
|
+
document.body.appendChild($root);
|
|
28
|
+
|
|
29
|
+
const $status = $root.querySelector('#token-status');
|
|
30
|
+
const $error = $root.querySelector('#token-error');
|
|
31
|
+
const $errorMessage = $root.querySelector('#token-error-message');
|
|
32
|
+
const $spinner = $root.querySelector('#token-spinner');
|
|
33
|
+
const $actions = $root.querySelector('#token-actions');
|
|
34
|
+
|
|
35
|
+
function stopSpinner() {
|
|
36
|
+
if ($spinner) { $spinner.classList.add('d-none'); }
|
|
37
|
+
}
|
|
38
|
+
function showError(message) {
|
|
39
|
+
stopSpinner();
|
|
40
|
+
if ($error && $errorMessage) {
|
|
41
|
+
$errorMessage.textContent = message;
|
|
42
|
+
$error.classList.remove('d-none');
|
|
43
|
+
}
|
|
44
|
+
if ($status) { $status.classList.add('d-none'); }
|
|
45
|
+
if ($actions) { $actions.classList.remove('d-none'); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Baseline: spinner visible, actions + error hidden
|
|
49
|
+
ctx.expect($spinner.classList.contains('d-none')).toBe(false);
|
|
50
|
+
ctx.expect($actions.classList.contains('d-none')).toBe(true);
|
|
51
|
+
ctx.expect($error.classList.contains('d-none')).toBe(true);
|
|
52
|
+
|
|
53
|
+
showError('Failed to generate token. Please try again.');
|
|
54
|
+
|
|
55
|
+
// Spinner + status gone, error + actions shown, message set
|
|
56
|
+
ctx.expect($spinner.classList.contains('d-none')).toBe(true);
|
|
57
|
+
ctx.expect($status.classList.contains('d-none')).toBe(true);
|
|
58
|
+
ctx.expect($error.classList.contains('d-none')).toBe(false);
|
|
59
|
+
ctx.expect($actions.classList.contains('d-none')).toBe(false);
|
|
60
|
+
ctx.expect($errorMessage.textContent).toBe('Failed to generate token. Please try again.');
|
|
61
|
+
|
|
62
|
+
$root.remove();
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'retry button click re-runs the flow (reload)',
|
|
67
|
+
run: async (ctx) => {
|
|
68
|
+
const $root = document.createElement('div');
|
|
69
|
+
$root.innerHTML = '<button type="button" id="token-retry" class="btn btn-primary w-100">Try again</button>';
|
|
70
|
+
document.body.appendChild($root);
|
|
71
|
+
|
|
72
|
+
const $retry = $root.querySelector('#token-retry');
|
|
73
|
+
|
|
74
|
+
// Mirror the module's wiring with reload stubbed (can't reload the harness)
|
|
75
|
+
let reloadCount = 0;
|
|
76
|
+
$retry.addEventListener('click', () => { reloadCount++; });
|
|
77
|
+
|
|
78
|
+
$retry.click();
|
|
79
|
+
$retry.click();
|
|
80
|
+
|
|
81
|
+
ctx.expect(reloadCount).toBe(2);
|
|
82
|
+
|
|
83
|
+
$root.remove();
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'go-home link points at the site root',
|
|
88
|
+
run: async (ctx) => {
|
|
89
|
+
const $root = document.createElement('div');
|
|
90
|
+
$root.innerHTML = '<div id="token-actions"><a href="/" class="btn btn-link btn-sm text-muted d-block mt-2">Go home</a></div>';
|
|
91
|
+
document.body.appendChild($root);
|
|
92
|
+
|
|
93
|
+
const $home = $root.querySelector('#token-actions a');
|
|
94
|
+
|
|
95
|
+
ctx.expect($home).toBeTruthy();
|
|
96
|
+
ctx.expect($home.getAttribute('href')).toBe('/');
|
|
97
|
+
|
|
98
|
+
$root.remove();
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'terminal success (close-tab flow) also stops the spinner',
|
|
103
|
+
run: async (ctx) => {
|
|
104
|
+
const $root = document.createElement('div');
|
|
105
|
+
$root.innerHTML = ''
|
|
106
|
+
+ '<div id="token-spinner" class="text-center mb-4"><div class="spinner-border" role="status"></div></div>'
|
|
107
|
+
+ '<div id="token-actions" class="text-center d-none"></div>';
|
|
108
|
+
document.body.appendChild($root);
|
|
109
|
+
|
|
110
|
+
const $spinner = $root.querySelector('#token-spinner');
|
|
111
|
+
const $actions = $root.querySelector('#token-actions');
|
|
112
|
+
|
|
113
|
+
// The extension "You can close this tab now." branch calls stopSpinner()
|
|
114
|
+
if ($spinner) { $spinner.classList.add('d-none'); }
|
|
115
|
+
|
|
116
|
+
ctx.expect($spinner.classList.contains('d-none')).toBe(true);
|
|
117
|
+
// No error, so the retry/home actions stay hidden
|
|
118
|
+
ctx.expect($actions.classList.contains('d-none')).toBe(true);
|
|
119
|
+
|
|
120
|
+
$root.remove();
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
package/docs/build-system.md
CHANGED
|
@@ -22,6 +22,19 @@ serve → build → developmentRebuild
|
|
|
22
22
|
|
|
23
23
|
Pure helpers are exposed under [src/gulp/tasks/utils/](../src/gulp/tasks/utils/) (`merge-jekyll-configs`, `_validate-yaml`, `template-transform`, `collectTextNodes`, `dictionary`, `github-cache`, `formatDocument`) — these are the highest-value test targets (zero I/O, callable directly in `build`-layer tests).
|
|
24
24
|
|
|
25
|
+
## Defaults distribution (`defaults` task)
|
|
26
|
+
|
|
27
|
+
The `defaults` task copies `dist/defaults/**` from the framework into the consumer project on every build/setup. Per-file behavior is decided by `FILE_MAP` in [defaults.js](../src/gulp/tasks/defaults.js) (`getFileOptions` — **last matching glob wins**), in four classes:
|
|
28
|
+
|
|
29
|
+
| Class | Behavior | Rules |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Copy-once (consumer-owned) | Seeded when missing, NEVER overwritten | `**/*.md`, `hooks/**/*`, `src/**/*`, `test/**/*` (e.g. `test/_init.js`) |
|
|
32
|
+
| Always-overwrite (framework-owned) | Replaced every run | `.github/workflows/build.yml`, `src/assets/images/team/**/*`, and **any file with no matching rule** (fall-through default) |
|
|
33
|
+
| Merge | Combined with the consumer's version | `config/ultimate-jekyll-manager.json` (deep config merge), `CLAUDE.md` / `_.env` / `_.gitignore` (marker-based line merge) |
|
|
34
|
+
| Skip | Never copied | `**/.DS_Store`, `**/__temp/**/*` |
|
|
35
|
+
|
|
36
|
+
⚠️ The fall-through default is `overwrite: true` — when adding a new consumer-owned file to `src/defaults/`, it MUST get an explicit copy-once rule or setup reruns will clobber the consumer's edits. The rule classes are locked by the build-layer test `defaults-file-options.test.js`.
|
|
37
|
+
|
|
25
38
|
## Config flow
|
|
26
39
|
|
|
27
40
|
Three config files in the consumer project feed the build:
|
package/docs/test-framework.md
CHANGED
|
@@ -241,6 +241,8 @@ Same protocol as EM (`__EM_TEST__`) and BXM (`__BXM_TEST__`). One marker per fra
|
|
|
241
241
|
|
|
242
242
|
The runner loads an optional `test/_init.js` from **both** test roots — the framework (`<UJM>/test/_init.js`) and the consumer project (`<cwd>/test/_init.js`) — and runs it **once, before any suite** (it is NOT itself run as a test; the `_`-prefix keeps it out of discovery). Mirrors the same hook in BEM/EM/BXM so all four frameworks share one shape.
|
|
243
243
|
|
|
244
|
+
The stub shipped in `src/defaults/test/_init.js` is **seeded once and never overwritten** — the file is consumer-owned, so setup reruns preserve whatever fixture logic the project puts in it (`test/**/*` is copy-once in the defaults task's `FILE_MAP`; see [build-system.md](build-system.md#defaults-distribution-defaults-task)).
|
|
245
|
+
|
|
244
246
|
The module **must export a function** — `module.exports = (ctx) => ({ ... })` — called with `{ projectRoot }` and returning the hook object. It may declare:
|
|
245
247
|
|
|
246
248
|
- `async setup({ projectRoot })` — runs once before the suites, e.g. to scaffold a fixture file the boot layer needs.
|