@akinon/projectzero 2.0.0-beta.20 → 2.0.0-beta.22
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 +14 -0
- package/app-template/CHANGELOG.md +170 -0
- package/app-template/next.config.mjs +0 -1
- package/app-template/package.json +31 -30
- package/app-template/src/app/[pz]/[...prettyurl]/page.tsx +2 -2
- package/app-template/src/app/[pz]/account/layout.tsx +2 -1
- package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/blog/[slug]/page.tsx +4 -2
- package/app-template/src/app/[pz]/category/[pk]/page.tsx +11 -1
- package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +2 -2
- package/app-template/src/app/[pz]/layout.tsx +3 -1
- package/app-template/src/app/[pz]/list/page.tsx +11 -1
- package/app-template/src/app/[pz]/page.tsx +13 -35
- package/app-template/src/app/[pz]/pages/[slug]/page.tsx +19 -0
- package/app-template/src/app/[pz]/product/[pk]/page.tsx +2 -2
- package/app-template/src/app/api/barcode-search/route.ts +1 -1
- package/app-template/src/app/api/cache/route.ts +1 -1
- package/app-template/src/app/api/image-proxy/route.ts +1 -1
- package/app-template/src/app/api/logout/route.ts +1 -1
- package/app-template/src/app/api/product-categories/route.ts +1 -1
- package/app-template/src/app/api/similar-product-list/route.ts +1 -1
- package/app-template/src/app/api/similar-products/route.ts +1 -1
- package/app-template/src/app/api/virtual-try-on/route.ts +1 -1
- package/app-template/src/app/api/web-vitals/route.ts +1 -1
- package/app-template/src/components/quantity-selector.tsx +16 -4
- package/app-template/src/components/widget-content.tsx +3 -3
- package/app-template/src/routes/index.ts +6 -6
- package/app-template/src/utils/__tests__/theme-page-context.test.ts +145 -0
- package/app-template/src/utils/theme-page-context.ts +309 -0
- package/app-template/src/views/basket/basket-item.tsx +107 -691
- package/app-template/src/views/basket/index.ts +0 -2
- package/app-template/src/views/basket/summary.tsx +75 -496
- package/app-template/src/views/breadcrumb.tsx +38 -13
- package/app-template/src/views/category/category-header.tsx +66 -289
- package/app-template/src/views/category/category-info.tsx +24 -173
- package/app-template/src/views/category/filters/index.tsx +48 -208
- package/app-template/src/views/category/layout.tsx +5 -7
- package/app-template/src/views/checkout/index.tsx +0 -5
- package/app-template/src/views/checkout/steps/payment/index.tsx +2 -5
- package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -72
- package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +40 -171
- package/app-template/src/views/checkout/steps/shipping/address-box.tsx +12 -74
- package/app-template/src/views/checkout/steps/shipping/addresses.tsx +45 -128
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +27 -232
- package/app-template/src/views/checkout/summary.tsx +29 -303
- package/app-template/src/views/footer.tsx +13 -415
- package/app-template/src/views/guest-login/index.tsx +1 -1
- package/app-template/src/views/header/action-menu.tsx +45 -277
- package/app-template/src/views/header/band.tsx +21 -6
- package/app-template/src/views/header/index.tsx +47 -109
- package/app-template/src/views/header/mini-basket.tsx +45 -820
- package/app-template/src/views/header/navbar.tsx +111 -178
- package/app-template/src/views/header/search/index.tsx +32 -71
- package/app-template/src/views/header/search/results.tsx +65 -127
- package/app-template/src/views/product/accordion-wrapper.tsx +43 -135
- package/app-template/src/views/product/index.ts +1 -1
- package/app-template/src/views/product/layout.tsx +7 -2
- package/app-template/src/views/product/misc-buttons.tsx +25 -339
- package/app-template/src/views/product/product-actions.tsx +8 -137
- package/app-template/src/views/product/product-info.tsx +31 -69
- package/app-template/src/views/product/product-share.tsx +8 -11
- package/app-template/src/views/product/slider.tsx +79 -117
- package/app-template/src/views/product-item/index.tsx +46 -119
- package/app-template/src/widgets/footer-social.tsx +16 -47
- package/app-template/src/widgets/footer-subscription/index.tsx +17 -183
- package/codemods/migrate-auth-v5/index.js +339 -0
- package/codemods/migrate-auth-v5/transform.js +86 -0
- package/dist/commands/plugins.js +23 -2
- package/package.json +1 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +0 -15
- package/app-template/src/views/basket/basket-summary-context.tsx +0 -560
- package/app-template/src/views/basket/designer-context.tsx +0 -617
- package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +0 -190
- package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +0 -286
- package/app-template/src/views/breadcrumb/constants.ts +0 -15
- package/app-template/src/views/breadcrumb/index.tsx +0 -127
- package/app-template/src/views/category/native-widget-context.tsx +0 -257
- package/app-template/src/views/category/product-list-registrar.tsx +0 -665
- package/app-template/src/views/checkout/checkout-address-registrar.tsx +0 -254
- package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +0 -183
- package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +0 -259
- package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +0 -253
- package/app-template/src/views/checkout/checkout-summary-registrar.tsx +0 -183
- package/app-template/src/views/checkout/constants.ts +0 -5
- package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +0 -15
- package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +0 -18
- package/app-template/src/views/footer/footer-app-banner-context.tsx +0 -326
- package/app-template/src/views/footer/footer-bottom-context.tsx +0 -215
- package/app-template/src/views/footer/footer-bottom-wrapper.tsx +0 -74
- package/app-template/src/views/footer/footer-layout-constants.ts +0 -35
- package/app-template/src/views/footer/footer-layout-registrar.tsx +0 -342
- package/app-template/src/views/footer/footer-layout-switcher.tsx +0 -110
- package/app-template/src/views/footer/footer-menu-context.tsx +0 -211
- package/app-template/src/views/footer/footer-native-widgets.tsx +0 -60
- package/app-template/src/views/footer/footer-social-context.tsx +0 -254
- package/app-template/src/views/footer/footer-subscription-context.tsx +0 -210
- package/app-template/src/views/footer/footer-utils.ts +0 -43
- package/app-template/src/views/footer/footer-value-props-context.tsx +0 -326
- package/app-template/src/views/footer/logo-settings.ts +0 -183
- package/app-template/src/views/footer/native-widget-config.ts +0 -262
- package/app-template/src/views/footer/subscription-settings.ts +0 -122
- package/app-template/src/views/footer/use-footer-logo.ts +0 -162
- package/app-template/src/views/header/designer-context.tsx +0 -261
- package/app-template/src/views/header/header-announcement-registrar.tsx +0 -267
- package/app-template/src/views/header/header-client-wrapper.tsx +0 -496
- package/app-template/src/views/header/header-content.tsx +0 -1026
- package/app-template/src/views/header/header-currency-registrar.tsx +0 -348
- package/app-template/src/views/header/header-icons-context.tsx +0 -262
- package/app-template/src/views/header/header-language-registrar.tsx +0 -348
- package/app-template/src/views/header/header-layout-context.tsx +0 -143
- package/app-template/src/views/header/header-layout-registrar.tsx +0 -658
- package/app-template/src/views/header/header-logo-context.tsx +0 -228
- package/app-template/src/views/header/header-logo.tsx +0 -118
- package/app-template/src/views/header/header-mini-basket-context.tsx +0 -524
- package/app-template/src/views/header/header-search-registrar.tsx +0 -511
- package/app-template/src/views/header/header-text-slider-registrar.tsx +0 -382
- package/app-template/src/views/header/inline-search.tsx +0 -262
- package/app-template/src/views/header/navbar-menu-context.tsx +0 -219
- package/app-template/src/views/header/search/search-input.tsx +0 -61
- package/app-template/src/views/header/server-settings-parser.ts +0 -1105
- package/app-template/src/views/header/use-header-icons.ts +0 -241
- package/app-template/src/views/header/use-header-logo.ts +0 -213
- package/app-template/src/views/header/use-navbar-menu.ts +0 -179
- package/app-template/src/views/product/accordion-section.tsx +0 -61
- package/app-template/src/views/product/custom-button-group.tsx +0 -69
- package/app-template/src/views/product/favorites-button-section.tsx +0 -69
- package/app-template/src/views/product/find-in-store-section.tsx +0 -60
- package/app-template/src/views/product/product-info-section.tsx +0 -140
- package/app-template/src/views/product/quantity-section.tsx +0 -73
- package/app-template/src/views/product/sale-tag.tsx +0 -10
- package/app-template/src/views/product/share-section.tsx +0 -357
- package/app-template/src/views/product/variants-section.tsx +0 -126
- package/app-template/src/views/product-detail/constants.ts +0 -272
- package/app-template/src/views/product-detail/index.ts +0 -10
- package/app-template/src/views/product-detail/product-detail-registrar.tsx +0 -616
- package/app-template/src/widgets/footer-app-banner.tsx +0 -444
- package/app-template/src/widgets/footer-bottom.tsx +0 -127
- package/app-template/src/widgets/footer-menu-compact.tsx +0 -238
- package/app-template/src/widgets/footer-menu-two.tsx +0 -298
- package/app-template/src/widgets/footer-social-client.tsx +0 -251
- package/app-template/src/widgets/footer-value-props.tsx +0 -201
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
|
|
3
|
+
import { Link, Icon } from '@theme/components';
|
|
3
4
|
import { getWidgetData } from '@akinon/next/data/server';
|
|
4
|
-
import FooterSocialClient from './footer-social-client';
|
|
5
|
-
|
|
6
|
-
type ThemeConfig = {
|
|
7
|
-
theme_settings: string | object;
|
|
8
|
-
};
|
|
9
5
|
|
|
10
6
|
type TargetBlank = {
|
|
11
7
|
value: string;
|
|
@@ -35,50 +31,23 @@ type FooterSocialType = {
|
|
|
35
31
|
};
|
|
36
32
|
|
|
37
33
|
export default async function FooterSocial() {
|
|
38
|
-
const themeConfig = await getWidgetData<ThemeConfig>({
|
|
39
|
-
slug: 'theme-config'
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let socialNetworks = {};
|
|
43
|
-
try {
|
|
44
|
-
if (themeConfig?.attributes?.theme_settings) {
|
|
45
|
-
const settingsRaw = themeConfig.attributes.theme_settings;
|
|
46
|
-
let settings;
|
|
47
|
-
|
|
48
|
-
if (typeof settingsRaw === 'string') {
|
|
49
|
-
settings = JSON.parse(settingsRaw);
|
|
50
|
-
} else if (
|
|
51
|
-
typeof settingsRaw === 'object' &&
|
|
52
|
-
settingsRaw !== null &&
|
|
53
|
-
'value' in settingsRaw
|
|
54
|
-
) {
|
|
55
|
-
const value = (settingsRaw as any).value;
|
|
56
|
-
settings = typeof value === 'string' ? JSON.parse(value) : value;
|
|
57
|
-
} else {
|
|
58
|
-
settings = settingsRaw;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
socialNetworks = settings.socialNetworks || {};
|
|
62
|
-
}
|
|
63
|
-
} catch (e) {
|
|
64
|
-
console.error('Error parsing theme settings:', e);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const activeNetworks = Object.entries(socialNetworks)
|
|
68
|
-
.filter(([_, value]: [string, any]) => value.icon)
|
|
69
|
-
.map(([key, value]: [string, any]) => ({
|
|
70
|
-
key,
|
|
71
|
-
url: value.url || '#',
|
|
72
|
-
icon: value.icon
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
34
|
const data = await getWidgetData<FooterSocialType>({ slug: 'footer-social' });
|
|
76
|
-
const socialItems = data?.attributes?.social_items || [];
|
|
77
35
|
|
|
78
36
|
return (
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
37
|
+
<div className="flex flex-wrap items-center justify-around py-4 border-t border-gray md:justify-center">
|
|
38
|
+
{data?.attributes?.social_items?.map((item, i) => (
|
|
39
|
+
<Link
|
|
40
|
+
key={i}
|
|
41
|
+
href={item?.value?.redirect_url}
|
|
42
|
+
className="p-2 border rounded-full border-gray md:mr-1 md:last:mr-0"
|
|
43
|
+
target={item?.value?.is_target_blank === 'true' ? '_blank' : '_self'}
|
|
44
|
+
>
|
|
45
|
+
<Icon
|
|
46
|
+
size={18}
|
|
47
|
+
name={item?.value?.icon_class?.replace('pz-icon-', '')}
|
|
48
|
+
/>
|
|
49
|
+
</Link>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
83
52
|
);
|
|
84
53
|
}
|
|
@@ -1,196 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
import 'server-only';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import clsx from 'clsx';
|
|
5
|
-
import { useDesignerFeatures } from '@akinon/next/components/theme-editor/hooks/use-designer-features';
|
|
3
|
+
import { getWidgetData } from '@akinon/next/data/server';
|
|
6
4
|
|
|
7
5
|
import FooterSubscriptionForm from './footer-subscription-form';
|
|
8
|
-
import {
|
|
9
|
-
FOOTER_PLACEHOLDER_ID,
|
|
10
|
-
FOOTER_SUBSCRIPTION_SECTION_ID
|
|
11
|
-
} from '../../views/footer/native-widget-config';
|
|
12
|
-
import {
|
|
13
|
-
FOOTER_SUBSCRIPTION_BLOCKS,
|
|
14
|
-
FOOTER_SUBSCRIPTION_DEFAULT_CONTENT
|
|
15
|
-
} from '../../views/footer/subscription-settings';
|
|
16
|
-
import { useFooterSubscriptionDesigner } from '../../views/footer/footer-subscription-context';
|
|
17
6
|
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const extractTextValue = (value: unknown): string | undefined => {
|
|
27
|
-
if (value == null) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
32
|
-
return String(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof value === 'object') {
|
|
36
|
-
const record = value as Record<string, unknown>;
|
|
37
|
-
const candidateKeys = [
|
|
38
|
-
'content',
|
|
39
|
-
'value',
|
|
40
|
-
'text',
|
|
41
|
-
'label',
|
|
42
|
-
'default',
|
|
43
|
-
'desktop',
|
|
44
|
-
'mobile',
|
|
45
|
-
'tablet'
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
for (const key of candidateKeys) {
|
|
49
|
-
const candidate = record[key];
|
|
50
|
-
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
51
|
-
return candidate;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const objValue of Object.values(record)) {
|
|
56
|
-
if (typeof objValue === 'string' && objValue.trim().length > 0) {
|
|
57
|
-
return objValue;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return undefined;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const kebabToCamel = (str: string): string =>
|
|
66
|
-
str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
67
|
-
|
|
68
|
-
const normalizeStyles = (
|
|
69
|
-
styles: Record<string, unknown> | undefined
|
|
70
|
-
): CSSProperties => {
|
|
71
|
-
if (!styles) return {};
|
|
72
|
-
|
|
73
|
-
const result: Record<string, string | number> = {};
|
|
74
|
-
|
|
75
|
-
for (const [key, value] of Object.entries(styles)) {
|
|
76
|
-
if (value == null) continue;
|
|
77
|
-
|
|
78
|
-
const camelKey = kebabToCamel(key);
|
|
79
|
-
let resolved: unknown = value;
|
|
80
|
-
|
|
81
|
-
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
82
|
-
const responsive = value as Record<string, unknown>;
|
|
83
|
-
resolved = responsive.desktop ?? responsive.mobile ?? responsive.tablet;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (resolved != null) {
|
|
87
|
-
if (typeof resolved === 'number') {
|
|
88
|
-
result[camelKey] = resolved;
|
|
89
|
-
} else if (typeof resolved === 'string') {
|
|
90
|
-
result[camelKey] = resolved;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return result as CSSProperties;
|
|
7
|
+
type FooterSubscriptionType = {
|
|
8
|
+
description: {
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
title: {
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
96
14
|
};
|
|
97
15
|
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
const blockState = blockVersion >= 0 ? getBlock(blockConfig.id) : undefined;
|
|
102
|
-
const styles = normalizeStyles(blockState?.styles as Record<string, unknown>);
|
|
103
|
-
const { handleClick } = useDesignerFeatures({
|
|
104
|
-
blockId: blockConfig.id,
|
|
105
|
-
placeholderId: FOOTER_PLACEHOLDER_ID,
|
|
106
|
-
sectionId: FOOTER_SUBSCRIPTION_SECTION_ID,
|
|
107
|
-
isDesigner,
|
|
108
|
-
blockInfo: {
|
|
109
|
-
id: blockConfig.id,
|
|
110
|
-
type: blockConfig.type,
|
|
111
|
-
label: blockConfig.label
|
|
112
|
-
}
|
|
16
|
+
export default async function FooterSubscription() {
|
|
17
|
+
const data = await getWidgetData<FooterSubscriptionType>({
|
|
18
|
+
slug: 'footer-subscription'
|
|
113
19
|
});
|
|
114
20
|
|
|
115
|
-
return {
|
|
116
|
-
styles,
|
|
117
|
-
isDesigner,
|
|
118
|
-
isSelected: selectedBlockId === blockConfig.id,
|
|
119
|
-
onClick: isDesigner ? handleClick : undefined,
|
|
120
|
-
content: extractTextValue(blockState?.value)
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export default function FooterSubscription({
|
|
125
|
-
title,
|
|
126
|
-
description
|
|
127
|
-
}: FooterSubscriptionProps) {
|
|
128
|
-
const titleBlock = useSubscriptionBlock(FOOTER_SUBSCRIPTION_BLOCKS.TITLE);
|
|
129
|
-
const descriptionBlock = useSubscriptionBlock(
|
|
130
|
-
FOOTER_SUBSCRIPTION_BLOCKS.DESCRIPTION
|
|
131
|
-
);
|
|
132
|
-
const formBlock = useSubscriptionBlock(FOOTER_SUBSCRIPTION_BLOCKS.FORM);
|
|
133
|
-
const displayTitle =
|
|
134
|
-
titleBlock.content || title || FOOTER_SUBSCRIPTION_DEFAULT_CONTENT.title;
|
|
135
|
-
const displayDescription =
|
|
136
|
-
descriptionBlock.content ||
|
|
137
|
-
description ||
|
|
138
|
-
FOOTER_SUBSCRIPTION_DEFAULT_CONTENT.description;
|
|
139
|
-
const shouldRenderDescription =
|
|
140
|
-
descriptionBlock.isDesigner || Boolean(displayDescription);
|
|
141
|
-
|
|
142
21
|
return (
|
|
143
|
-
<div className="
|
|
144
|
-
<h3
|
|
145
|
-
|
|
146
|
-
data-block-id={
|
|
147
|
-
titleBlock.isDesigner
|
|
148
|
-
? FOOTER_SUBSCRIPTION_BLOCKS.TITLE.id
|
|
149
|
-
: undefined
|
|
150
|
-
}
|
|
151
|
-
onClick={titleBlock.onClick}
|
|
152
|
-
className={clsx(
|
|
153
|
-
'mb-4 text-lg text-[#121212]',
|
|
154
|
-
titleBlock.isDesigner && 'cursor-pointer',
|
|
155
|
-
titleBlock.isSelected && 'ring-2 ring-blue-500 ring-offset-2 rounded'
|
|
156
|
-
)}
|
|
157
|
-
>
|
|
158
|
-
{displayTitle}
|
|
22
|
+
<div className="py-4 border-t md:border-t-0 lg:pl-7">
|
|
23
|
+
<h3 className="mb-1 text-xs font-medium">
|
|
24
|
+
{data?.attributes?.title?.value}
|
|
159
25
|
</h3>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<p
|
|
163
|
-
style={descriptionBlock.styles}
|
|
164
|
-
data-block-id={
|
|
165
|
-
descriptionBlock.isDesigner
|
|
166
|
-
? FOOTER_SUBSCRIPTION_BLOCKS.DESCRIPTION.id
|
|
167
|
-
: undefined
|
|
168
|
-
}
|
|
169
|
-
onClick={descriptionBlock.onClick}
|
|
170
|
-
className={clsx(
|
|
171
|
-
'text-sm text-[#121212] text-opacity-75 mb-6 leading-relaxed',
|
|
172
|
-
descriptionBlock.isDesigner && 'cursor-pointer',
|
|
173
|
-
descriptionBlock.isSelected &&
|
|
174
|
-
'ring-2 ring-blue-500 ring-offset-2 rounded'
|
|
175
|
-
)}
|
|
176
|
-
>
|
|
177
|
-
{displayDescription}
|
|
178
|
-
</p>
|
|
179
|
-
)}
|
|
180
|
-
|
|
181
|
-
<div
|
|
182
|
-
style={formBlock.styles}
|
|
183
|
-
data-block-id={
|
|
184
|
-
formBlock.isDesigner ? FOOTER_SUBSCRIPTION_BLOCKS.FORM.id : undefined
|
|
185
|
-
}
|
|
186
|
-
onClick={formBlock.onClick}
|
|
187
|
-
className={clsx(
|
|
188
|
-
formBlock.isDesigner && 'cursor-pointer inline-block',
|
|
189
|
-
formBlock.isSelected && 'ring-2 ring-blue-500 ring-offset-2 rounded'
|
|
190
|
-
)}
|
|
191
|
-
>
|
|
192
|
-
<FooterSubscriptionForm />
|
|
193
|
-
</div>
|
|
26
|
+
<p className="mb-2 text-xs">{data?.attributes?.description?.value}</p>
|
|
27
|
+
<FooterSubscriptionForm />
|
|
194
28
|
</div>
|
|
195
29
|
);
|
|
196
30
|
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next-auth v4 -> v5 migration codemod for Project Zero brands.
|
|
3
|
+
*
|
|
4
|
+
* Handles two brand patterns:
|
|
5
|
+
* Tip A (re-export only): brand's [...nextauth].ts is a 3-line
|
|
6
|
+
* `import Auth from '@akinon/next/api/auth'; export default Auth;`
|
|
7
|
+
* → fully automated; brand's auth.ts + route.ts are generated.
|
|
8
|
+
*
|
|
9
|
+
* Tip B (custom nextAuthOptions): brand has its own config using
|
|
10
|
+
* Pages API req/res. Skeleton is generated and original logic is
|
|
11
|
+
* transferred with TODO markers. Manual conversion of req/res to
|
|
12
|
+
* cookies()/headers() is required afterwards.
|
|
13
|
+
*
|
|
14
|
+
* In both cases, client usage `getServerSession` is rewritten to `auth`
|
|
15
|
+
* via the sibling `transform.js` jscodeshift script.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* cd <brand-root>
|
|
19
|
+
* node <pz-next>/packages/projectzero/codemods/migrate-auth-v5/index.js [--dry-run]
|
|
20
|
+
*
|
|
21
|
+
* Or via the projectzero CLI:
|
|
22
|
+
* npx @akinon/projectzero codemod --codemod=migrate-auth-v5
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const jscodeshift = require('jscodeshift/src/Runner');
|
|
28
|
+
|
|
29
|
+
const TIP_A_SIZE_LIMIT = 256;
|
|
30
|
+
|
|
31
|
+
const TIP_A_AUTH_CONTENT = `import { createAuth } from '@akinon/next/api/auth';
|
|
32
|
+
|
|
33
|
+
export const { handlers, auth, signIn, signOut } = createAuth();
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const ROUTE_CONTENT = `import { handlers } from 'auth';
|
|
37
|
+
|
|
38
|
+
export const { GET, POST } = handlers;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const TIP_B_HEADER = `/**
|
|
42
|
+
* NOTE: This file was generated by \`migrate-auth-v5\`.
|
|
43
|
+
* It replaces \`src/pages/api/auth/[...nextauth].ts\` which used
|
|
44
|
+
* the Pages API \`req\`/\`res\` objects that next-auth v5 (App Router)
|
|
45
|
+
* no longer provides.
|
|
46
|
+
*
|
|
47
|
+
* MANUAL MIGRATION STEPS (required before first run):
|
|
48
|
+
*
|
|
49
|
+
* 1. \`req.cookies['X']\`
|
|
50
|
+
* -> \`(await cookies()).get('X')?.value\`
|
|
51
|
+
*
|
|
52
|
+
* 2. \`req.headers['X']\` or \`req.headers.X\`
|
|
53
|
+
* -> \`(await headers()).get('X') ?? ''\`
|
|
54
|
+
*
|
|
55
|
+
* 3. \`res.setHeader('Set-Cookie', ['name=value; Path=/; HttpOnly; ...'])\`
|
|
56
|
+
* -> \`(await cookies()).set('name', value, { path: '/', httpOnly: true, secure: true, maxAge })\`
|
|
57
|
+
*
|
|
58
|
+
* 4. \`throw new Error(JSON.stringify(errors))\`
|
|
59
|
+
* -> \`class PzCredentialsError extends CredentialsSignin { constructor(errs) { super(); this.code = JSON.stringify(errs); } }\`
|
|
60
|
+
* -> \`throw new PzCredentialsError(errors)\`
|
|
61
|
+
* (Import \`CredentialsSignin\` from \`'next-auth'\`.)
|
|
62
|
+
*
|
|
63
|
+
* 5. Each \`authorize\` callback is now \`async\` and no longer receives
|
|
64
|
+
* the outer \`req\`/\`res\`. Wrap cookie/header reads in \`await\`.
|
|
65
|
+
*
|
|
66
|
+
* Reference implementation (copy the shape as needed):
|
|
67
|
+
* packages/akinon-next/api/auth.ts on the pz-next beta branch.
|
|
68
|
+
*
|
|
69
|
+
* Search for \`TODO:v5\` markers below for spots that definitely need work.
|
|
70
|
+
*/`;
|
|
71
|
+
|
|
72
|
+
function log(msg) {
|
|
73
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
74
|
+
console.log(`[${ts}] ${msg}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectAuthType(cwd) {
|
|
78
|
+
const oldPath = path.join(cwd, 'src', 'pages', 'api', 'auth', '[...nextauth].ts');
|
|
79
|
+
if (!fs.existsSync(oldPath)) {
|
|
80
|
+
return { type: null, oldPath };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stats = fs.statSync(oldPath);
|
|
84
|
+
const content = fs.readFileSync(oldPath, 'utf-8');
|
|
85
|
+
|
|
86
|
+
const isReExport =
|
|
87
|
+
stats.size < TIP_A_SIZE_LIMIT &&
|
|
88
|
+
/from\s+['"]@akinon\/next\/api\/auth['"]/.test(content) &&
|
|
89
|
+
/export\s+default\s+Auth/.test(content);
|
|
90
|
+
|
|
91
|
+
return { type: isReExport ? 'A' : 'B', oldPath, content };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureParentDir(filePath, { dryRun }) {
|
|
95
|
+
const dir = path.dirname(filePath);
|
|
96
|
+
if (!fs.existsSync(dir)) {
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
log(`[DRY] mkdir -p ${dir}`);
|
|
99
|
+
} else {
|
|
100
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeFileSafe(filePath, content, { dryRun }) {
|
|
106
|
+
ensureParentDir(filePath, { dryRun });
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
log(`[DRY] CREATE ${filePath} (${content.length} bytes)`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (fs.existsSync(filePath)) {
|
|
112
|
+
log(` WARN: ${filePath} already exists; overwriting`);
|
|
113
|
+
}
|
|
114
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
115
|
+
log(` CREATE ${filePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function deleteFileSafe(filePath, { dryRun }) {
|
|
119
|
+
if (!fs.existsSync(filePath)) return;
|
|
120
|
+
if (dryRun) {
|
|
121
|
+
log(`[DRY] DELETE ${filePath}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
fs.unlinkSync(filePath);
|
|
125
|
+
log(` DELETE ${filePath}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cleanupEmptyAuthPagesDir(cwd, { dryRun }) {
|
|
129
|
+
const authDir = path.join(cwd, 'src', 'pages', 'api', 'auth');
|
|
130
|
+
if (!fs.existsSync(authDir)) return;
|
|
131
|
+
const remaining = fs.readdirSync(authDir);
|
|
132
|
+
if (remaining.length > 0) return;
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
log(`[DRY] rmdir ${authDir}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
fs.rmdirSync(authDir);
|
|
138
|
+
log(` RMDIR ${authDir}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function migrateTipA(cwd, { dryRun, oldPath }) {
|
|
142
|
+
const authPath = path.join(cwd, 'src', 'auth.ts');
|
|
143
|
+
const routePath = path.join(
|
|
144
|
+
cwd,
|
|
145
|
+
'src',
|
|
146
|
+
'app',
|
|
147
|
+
'api',
|
|
148
|
+
'auth',
|
|
149
|
+
'[...nextauth]',
|
|
150
|
+
'route.ts'
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
log(`Tip A migration`);
|
|
154
|
+
deleteFileSafe(oldPath, { dryRun });
|
|
155
|
+
writeFileSafe(authPath, TIP_A_AUTH_CONTENT, { dryRun });
|
|
156
|
+
writeFileSafe(routePath, ROUTE_CONTENT, { dryRun });
|
|
157
|
+
cleanupEmptyAuthPagesDir(cwd, { dryRun });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function transformTipBSource(original) {
|
|
161
|
+
let src = original;
|
|
162
|
+
|
|
163
|
+
src = src.replace(
|
|
164
|
+
/import\s*\{\s*NextApiRequest\s*,\s*NextApiResponse\s*\}\s*from\s*['"]next['"];?\s*\n/,
|
|
165
|
+
''
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
src = src.replace(
|
|
169
|
+
/import\s+NextAuth(?:\s*,\s*\{([^}]*)\})?\s+from\s+['"]next-auth['"];?\s*\n/,
|
|
170
|
+
(_match, typeImports) => {
|
|
171
|
+
const lines = [
|
|
172
|
+
`import { createAuth } from '@akinon/next/api/auth';`,
|
|
173
|
+
`import { cookies, headers } from 'next/headers';`
|
|
174
|
+
];
|
|
175
|
+
if (typeImports && typeImports.trim()) {
|
|
176
|
+
const cleaned = typeImports
|
|
177
|
+
.split(',')
|
|
178
|
+
.map((s) => s.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(', ');
|
|
181
|
+
if (cleaned) {
|
|
182
|
+
lines.push(`import type { ${cleaned} } from 'next-auth';`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return lines.join('\n') + '\n';
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
src = src.replace(
|
|
190
|
+
/const\s+nextAuthOptions\s*=\s*\(\s*req\s*:\s*NextApiRequest\s*,\s*res\s*:\s*NextApiResponse\s*\)\s*=>\s*\{/,
|
|
191
|
+
`const getCustomAuthConfig = () => {\n // TODO:v5 See migration notes at top of file.`
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
src = src.replace(
|
|
195
|
+
/const\s+nextAuthOptions\s*=\s*\(\s*req\s*,\s*res\s*\)\s*=>\s*\{/,
|
|
196
|
+
`const getCustomAuthConfig = () => {\n // TODO:v5 See migration notes at top of file.`
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
src = src.replace(
|
|
200
|
+
/const\s+Auth\s*=\s*\(\s*req[^)]*\)\s*=>\s*\{\s*return\s+NextAuth\(\s*req\s*,\s*res\s*,\s*nextAuthOptions\(\s*req\s*,\s*res\s*\)\s*\);\s*\};?\s*\n/,
|
|
201
|
+
''
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
src = src.replace(
|
|
205
|
+
/const\s+Auth\s*=\s*\(\s*req[^)]*\)\s*=>\s*NextAuth\([^;]*\);\s*\n/,
|
|
206
|
+
''
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
src = src.replace(
|
|
210
|
+
/export\s+default\s+Auth\s*;?\s*$/m,
|
|
211
|
+
`export const { handlers, auth, signIn, signOut } = createAuth(getCustomAuthConfig);\n`
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
src = src.replace(
|
|
215
|
+
/\b(req\.cookies(?:\?\.|\.)[a-zA-Z_$][\w$]*|req\.cookies\[[^\]]+\])/g,
|
|
216
|
+
'/* TODO:v5 cookies() */ $1'
|
|
217
|
+
);
|
|
218
|
+
src = src.replace(
|
|
219
|
+
/\b(req\.headers(?:\?\.|\.)[a-zA-Z_$][\w$]*|req\.headers\[[^\]]+\])/g,
|
|
220
|
+
'/* TODO:v5 headers() */ $1'
|
|
221
|
+
);
|
|
222
|
+
src = src.replace(
|
|
223
|
+
/\b(req\.body(?:\?\.|\.)?[a-zA-Z_$]?[\w$]*)/g,
|
|
224
|
+
'/* TODO:v5 read body via credentials */ $1'
|
|
225
|
+
);
|
|
226
|
+
src = src.replace(
|
|
227
|
+
/\b(res\.[a-zA-Z_$][\w$]*\()/g,
|
|
228
|
+
'/* TODO:v5 cookies().set */ $1'
|
|
229
|
+
);
|
|
230
|
+
src = src.replace(
|
|
231
|
+
/\bthrow\s+new\s+Error\(JSON\.stringify\(errors\)\)/g,
|
|
232
|
+
'/* TODO:v5 CredentialsSignin */ throw new Error(JSON.stringify(errors))'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return `${TIP_B_HEADER}\n\n${src}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function migrateTipB(cwd, { dryRun, oldPath, content }) {
|
|
239
|
+
const authPath = path.join(cwd, 'src', 'auth.ts');
|
|
240
|
+
const routePath = path.join(
|
|
241
|
+
cwd,
|
|
242
|
+
'src',
|
|
243
|
+
'app',
|
|
244
|
+
'api',
|
|
245
|
+
'auth',
|
|
246
|
+
'[...nextauth]',
|
|
247
|
+
'route.ts'
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
log(`Tip B migration (${content.length} bytes to transfer)`);
|
|
251
|
+
|
|
252
|
+
const newAuthContent = transformTipBSource(content);
|
|
253
|
+
|
|
254
|
+
writeFileSafe(authPath, newAuthContent, { dryRun });
|
|
255
|
+
writeFileSafe(routePath, ROUTE_CONTENT, { dryRun });
|
|
256
|
+
deleteFileSafe(oldPath, { dryRun });
|
|
257
|
+
cleanupEmptyAuthPagesDir(cwd, { dryRun });
|
|
258
|
+
|
|
259
|
+
log(` Manual work left: req/res -> cookies()/headers() conversions inside ${authPath}`);
|
|
260
|
+
log(` Grep for 'TODO:v5' to find spots.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function runClientTransform(cwd, { dryRun }) {
|
|
264
|
+
const transformPath = path.resolve(__dirname, 'transform.js');
|
|
265
|
+
const targets = ['src'].map((d) => path.join(cwd, d)).filter(fs.existsSync);
|
|
266
|
+
if (targets.length === 0) {
|
|
267
|
+
log(`No src/ directory found, skipping client transform`);
|
|
268
|
+
return Promise.resolve();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
log(`Running client transform (getServerSession -> auth)`);
|
|
272
|
+
|
|
273
|
+
return jscodeshift
|
|
274
|
+
.run(transformPath, targets, {
|
|
275
|
+
verbose: 0,
|
|
276
|
+
dry: Boolean(dryRun),
|
|
277
|
+
print: Boolean(dryRun),
|
|
278
|
+
extensions: 'ts,tsx',
|
|
279
|
+
parser: 'tsx',
|
|
280
|
+
ignorePattern: '**/node_modules/**',
|
|
281
|
+
silent: true
|
|
282
|
+
})
|
|
283
|
+
.then(
|
|
284
|
+
(stats) => {
|
|
285
|
+
log(
|
|
286
|
+
` Client transform: ${stats.ok} changed, ${stats.nochange} unchanged, ${stats.error} errored`
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
(err) => {
|
|
290
|
+
log(` Client transform error: ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const transform = () => {
|
|
296
|
+
const workingDir = path.resolve(process.cwd());
|
|
297
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
298
|
+
|
|
299
|
+
log(
|
|
300
|
+
`migrate-auth-v5 starting in ${workingDir}${dryRun ? ' (DRY RUN)' : ''}`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const detection = detectAuthType(workingDir);
|
|
304
|
+
|
|
305
|
+
if (!detection.type) {
|
|
306
|
+
log(`No src/pages/api/auth/[...nextauth].ts found, nothing to migrate.`);
|
|
307
|
+
return Promise.resolve();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
log(`Detected pattern: Tip ${detection.type}`);
|
|
311
|
+
|
|
312
|
+
if (detection.type === 'A') {
|
|
313
|
+
migrateTipA(workingDir, { dryRun, oldPath: detection.oldPath });
|
|
314
|
+
} else {
|
|
315
|
+
migrateTipB(workingDir, {
|
|
316
|
+
dryRun,
|
|
317
|
+
oldPath: detection.oldPath,
|
|
318
|
+
content: detection.content
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return runClientTransform(workingDir, { dryRun }).then(() => {
|
|
323
|
+
log(`migrate-auth-v5 done`);
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
module.exports = {
|
|
328
|
+
transform
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
const result = transform();
|
|
333
|
+
if (result && typeof result.then === 'function') {
|
|
334
|
+
result.catch((err) => {
|
|
335
|
+
console.error(err);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|