@akinon/pz-theme 2.0.50-beta.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.50-beta.0
3
+ ## 2.0.50
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [cbbbfd75]
8
- - @akinon/next@2.0.50-beta.0
9
- - @akinon/theme-contract@2.0.50-beta.0
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
10
10
 
11
11
  ## 2.0.49
12
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-beta.0",
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-beta.0",
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-beta.0",
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-beta.0",
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
+ });
@@ -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
- [/^\/(users\/)?auth\/register/, PLACEHOLDER_SLUGS.register],
61
- [/^\/(users\/)?register/, PLACEHOLDER_SLUGS.register],
62
- [/^\/(users\/)?auth\/forgot-password/, PLACEHOLDER_SLUGS.forgotPassword],
63
- [/^\/users\/password\/reset/, PLACEHOLDER_SLUGS.forgotPassword],
64
- [/^\/(users\/)?auth\/guest/, PLACEHOLDER_SLUGS.guestLogin],
65
- [/^\/(users\/)?auth/, PLACEHOLDER_SLUGS.auth],
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