@akinon/pz-theme 2.0.50-rc.0 → 2.0.50
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 +7 -31
- package/package.json +4 -4
- package/src/__tests__/auth-slot-coverage.test.ts +145 -0
- package/src/chrome/route-slug.ts +26 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,36 +1,12 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.50
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Updated dependencies
|
|
11
|
-
- Updated dependencies [2fea414]
|
|
12
|
-
- Updated dependencies [b55acb768]
|
|
13
|
-
- Updated dependencies [760258c1c]
|
|
14
|
-
- Updated dependencies [143be2b9d]
|
|
15
|
-
- Updated dependencies [7889b08f]
|
|
16
|
-
- Updated dependencies [9f8cd3bc5]
|
|
17
|
-
- Updated dependencies [bfafa3f49]
|
|
18
|
-
- Updated dependencies [57d7eb30]
|
|
19
|
-
- Updated dependencies [d99a6a7d5]
|
|
20
|
-
- Updated dependencies [9db81a71]
|
|
21
|
-
- Updated dependencies [591e345e1]
|
|
22
|
-
- Updated dependencies [4de5303c5]
|
|
23
|
-
- Updated dependencies [95b139dc1]
|
|
24
|
-
- Updated dependencies [1d00f2d0]
|
|
25
|
-
- Updated dependencies [4ac7b2a1]
|
|
26
|
-
- Updated dependencies [e9598c7]
|
|
27
|
-
- Updated dependencies [4998a9631]
|
|
28
|
-
- Updated dependencies [804d2bd6]
|
|
29
|
-
- Updated dependencies [3909d3224]
|
|
30
|
-
- Updated dependencies [6a3d8a63]
|
|
31
|
-
- Updated dependencies [e18836b20]
|
|
32
|
-
- @akinon/next@2.0.50-rc.0
|
|
33
|
-
- @akinon/theme-contract@2.0.50-rc.0
|
|
3
|
+
## 2.0.50
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 79d611c: ZERO-4745: Mount the login body slot and anchor the auth route rules to segment boundaries
|
|
8
|
+
- @akinon/next@2.0.50
|
|
9
|
+
- @akinon/theme-contract@2.0.50
|
|
34
10
|
|
|
35
11
|
## 2.0.49
|
|
36
12
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.50
|
|
4
|
+
"version": "2.0.50",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.50
|
|
11
|
+
"@akinon/next": "2.0.50",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.50
|
|
16
|
+
"@akinon/theme-contract": "2.0.50",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.50
|
|
23
|
+
"@akinon/next": "2.0.50",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import { PLACEHOLDER_SLUGS } from '../chrome/placeholder-slugs';
|
|
5
|
+
import { isPageMountedSlug, resolveBodySlug } from '../chrome/route-slug';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The auth half of the promise the account suite pins: a slug the editor offers
|
|
9
|
+
* must be mounted by a real page or layout, or the designer saves a composition
|
|
10
|
+
* the storefront never reads. Auth was exactly that gap — four slugs, route
|
|
11
|
+
* rules in both repos, starter templates, and no mount anywhere.
|
|
12
|
+
*
|
|
13
|
+
* Auth mounts live in two route trees (`auth` and `users/password`), so paths
|
|
14
|
+
* here are relative to the `[pz]` segment rather than to one page dir.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const APP_DIR = path.resolve(
|
|
18
|
+
__dirname,
|
|
19
|
+
'../../../..',
|
|
20
|
+
'apps/projectzeronext/src/app/[pz]'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** Placeholder key -> the file (relative to `[pz]`) mounting it. */
|
|
24
|
+
const EXPECTED_MOUNTS: Record<string, string> = {
|
|
25
|
+
auth: 'auth/(login)/layout.tsx',
|
|
26
|
+
forgotPassword: 'users/password/reset/layout.tsx'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Login and register share ONE page in the reference app, and that is a
|
|
31
|
+
* decision rather than a gap: the shape (tabbed or not) is composed on the
|
|
32
|
+
* login body, so no `/auth/register` or `/auth/guest` route ships. The dir
|
|
33
|
+
* assertions below fail the moment someone adds one of those routes — the cue
|
|
34
|
+
* to mount its slot, not to delete the assertion.
|
|
35
|
+
*/
|
|
36
|
+
const UNMOUNTED_AUTH_KEYS: Record<string, string> = {
|
|
37
|
+
register: 'auth/register',
|
|
38
|
+
guestLogin: 'auth/guest'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Every auth path the editor's page picker offers, in the reference app's
|
|
43
|
+
* spelling and in the brand spellings the rule table accepts. Resolving these
|
|
44
|
+
* is what makes the completeness check route-truthful: a fifth auth slug with a
|
|
45
|
+
* rule but no mount lands here rather than in a hand-kept list.
|
|
46
|
+
*/
|
|
47
|
+
const AUTH_PICKER_PATHS = [
|
|
48
|
+
'/auth',
|
|
49
|
+
'/users/auth',
|
|
50
|
+
'/auth/register',
|
|
51
|
+
'/users/auth/register',
|
|
52
|
+
'/register',
|
|
53
|
+
'/auth/forgot-password',
|
|
54
|
+
'/users/password/reset',
|
|
55
|
+
'/auth/guest',
|
|
56
|
+
'/users/auth/guest'
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Paths that LOOK like the login screen to a prefix rule but are not it. The
|
|
61
|
+
* callback is the dangerous one: `oauth-login` renders no markup at all (its
|
|
62
|
+
* whole job is a `signIn()` effect), so a login body composed over it would
|
|
63
|
+
* break social sign-in with nothing on screen to notice.
|
|
64
|
+
*/
|
|
65
|
+
const NEVER_THE_LOGIN_BODY = [
|
|
66
|
+
'/auth/oauth-login',
|
|
67
|
+
'/users/auth/oauth-login',
|
|
68
|
+
'/authors',
|
|
69
|
+
'/authors/latest',
|
|
70
|
+
'/auth-help',
|
|
71
|
+
'/registered-office'
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const keyForSlug = (slug: string | null) =>
|
|
75
|
+
Object.keys(PLACEHOLDER_SLUGS).find(
|
|
76
|
+
(key) => PLACEHOLDER_SLUGS[key as keyof typeof PLACEHOLDER_SLUGS] === slug
|
|
77
|
+
) ?? null;
|
|
78
|
+
|
|
79
|
+
const getAuthKeysFromRouteTable = () =>
|
|
80
|
+
Array.from(
|
|
81
|
+
new Set(AUTH_PICKER_PATHS.map((p) => keyForSlug(resolveBodySlug(p))))
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const allAuthKeys = () => [
|
|
85
|
+
...Object.keys(EXPECTED_MOUNTS),
|
|
86
|
+
...Object.keys(UNMOUNTED_AUTH_KEYS)
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const maybeDescribe = fs.existsSync(APP_DIR) ? describe : describe.skip;
|
|
90
|
+
|
|
91
|
+
maybeDescribe('auth slot coverage in the reference app', () => {
|
|
92
|
+
it('mounts every auth slug that has a route in its own layout', () => {
|
|
93
|
+
Object.entries(EXPECTED_MOUNTS).forEach(([key, relativePath]) => {
|
|
94
|
+
const file = path.join(APP_DIR, relativePath);
|
|
95
|
+
expect(fs.existsSync(file)).toBe(true);
|
|
96
|
+
expect(fs.readFileSync(file, 'utf8')).toMatch(
|
|
97
|
+
new RegExp(`PLACEHOLDER_SLUGS\\.${key}\\b`)
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('keeps the login mount inside the route group, off the provider callback', () => {
|
|
103
|
+
// The mount must sit beside the login page in `(login)`, not one level up in
|
|
104
|
+
// `auth/layout.tsx` — that layout would also wrap `auth/oauth-login`.
|
|
105
|
+
expect(fs.existsSync(path.join(APP_DIR, 'auth/oauth-login'))).toBe(true);
|
|
106
|
+
expect(fs.existsSync(path.join(APP_DIR, 'auth/layout.tsx'))).toBe(false);
|
|
107
|
+
expect(fs.existsSync(path.join(APP_DIR, 'auth/(login)/page.tsx'))).toBe(
|
|
108
|
+
true
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('resolves no body slug for paths that merely start like /auth', () => {
|
|
113
|
+
NEVER_THE_LOGIN_BODY.forEach((p) => {
|
|
114
|
+
expect(resolveBodySlug(p)).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('still resolves the login screen itself, in both spellings', () => {
|
|
119
|
+
expect(resolveBodySlug('/auth')).toBe(PLACEHOLDER_SLUGS.auth);
|
|
120
|
+
expect(resolveBodySlug('/auth/')).toBe(PLACEHOLDER_SLUGS.auth);
|
|
121
|
+
expect(resolveBodySlug('/users/auth')).toBe(PLACEHOLDER_SLUGS.auth);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('accounts for every auth key the route table can resolve, exactly once', () => {
|
|
125
|
+
expect(allAuthKeys().sort()).toEqual(getAuthKeysFromRouteTable().sort());
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('declares every auth slug page-mounted so a root catch-all skips it', () => {
|
|
129
|
+
// Without this a future route-aware catch-all would compose the login body
|
|
130
|
+
// a second time in the root layout.
|
|
131
|
+
allAuthKeys().forEach((key) => {
|
|
132
|
+
expect(
|
|
133
|
+
isPageMountedSlug(
|
|
134
|
+
PLACEHOLDER_SLUGS[key as keyof typeof PLACEHOLDER_SLUGS]
|
|
135
|
+
)
|
|
136
|
+
).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('keeps the single-auth-page decision honest: no split routes exist', () => {
|
|
141
|
+
Object.values(UNMOUNTED_AUTH_KEYS).forEach((dirName) => {
|
|
142
|
+
expect(fs.existsSync(path.join(APP_DIR, dirName))).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
package/src/chrome/route-slug.ts
CHANGED
|
@@ -56,13 +56,22 @@ const DEFAULT_RULES: RouteSlugRule[] = [
|
|
|
56
56
|
// account layout mounts for it.
|
|
57
57
|
[/^\/account(\/|$)/, PLACEHOLDER_SLUGS.accountShell],
|
|
58
58
|
|
|
59
|
-
// Auth
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
// Auth — every rule ends at a segment boundary, and the login rule matches
|
|
60
|
+
// the login screen ONLY. A prefix rule here reaches further than it reads:
|
|
61
|
+
// `/auth/oauth-login` is the provider callback, a component whose whole job
|
|
62
|
+
// is a `signIn()` effect and which renders no markup, while `/authors` and
|
|
63
|
+
// `/auth-help` are ordinary content pages. Composing the login body onto any
|
|
64
|
+
// of them would replace a page nobody asked to compose — and in the
|
|
65
|
+
// callback's case invisibly, since it has no markup to compare against.
|
|
66
|
+
[/^\/(users\/)?auth\/register(\/|$)/, PLACEHOLDER_SLUGS.register],
|
|
67
|
+
[/^\/(users\/)?register(\/|$)/, PLACEHOLDER_SLUGS.register],
|
|
68
|
+
[
|
|
69
|
+
/^\/(users\/)?auth\/forgot-password(\/|$)/,
|
|
70
|
+
PLACEHOLDER_SLUGS.forgotPassword
|
|
71
|
+
],
|
|
72
|
+
[/^\/users\/password\/reset(\/|$)/, PLACEHOLDER_SLUGS.forgotPassword],
|
|
73
|
+
[/^\/(users\/)?auth\/guest(\/|$)/, PLACEHOLDER_SLUGS.guestLogin],
|
|
74
|
+
[/^\/(users\/)?auth\/?$/, PLACEHOLDER_SLUGS.auth],
|
|
66
75
|
|
|
67
76
|
// Order flow
|
|
68
77
|
[/^\/orders\/completed/, PLACEHOLDER_SLUGS.orderSuccess],
|
|
@@ -117,6 +126,16 @@ const PAGE_MOUNTED_SLUGS = new Set<string>([
|
|
|
117
126
|
PLACEHOLDER_SLUGS.accountNotifications,
|
|
118
127
|
PLACEHOLDER_SLUGS.accountFaq,
|
|
119
128
|
PLACEHOLDER_SLUGS.accountContact,
|
|
129
|
+
// Auth. The login screen mounts its slot inside an `auth/(login)` route
|
|
130
|
+
// group, so a root catch-all would compose it a second time — and would also
|
|
131
|
+
// reach `auth/oauth-login`, a provider callback that must never be
|
|
132
|
+
// replaceable. Register and guest login have no route of their own in the
|
|
133
|
+
// reference app (the /auth tabs carry both); brands that do split them out
|
|
134
|
+
// mount their own layout, the same way.
|
|
135
|
+
PLACEHOLDER_SLUGS.auth,
|
|
136
|
+
PLACEHOLDER_SLUGS.register,
|
|
137
|
+
PLACEHOLDER_SLUGS.forgotPassword,
|
|
138
|
+
PLACEHOLDER_SLUGS.guestLogin,
|
|
120
139
|
PLACEHOLDER_SLUGS.flatPage
|
|
121
140
|
]);
|
|
122
141
|
|