@wp-typia/project-tools 0.13.4 → 0.14.0
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/runtime/scaffold-onboarding.js +6 -3
- package/package.json +1 -1
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +29 -39
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +31 -33
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +7 -1
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/transport.ts.mustache +254 -0
- package/templates/_shared/persistence/core/src/api.ts.mustache +29 -39
- package/templates/_shared/persistence/core/src/data.ts.mustache +31 -33
- package/templates/_shared/persistence/core/src/interactivity.ts.mustache +7 -1
- package/templates/_shared/persistence/core/src/transport.ts.mustache +254 -0
|
@@ -37,12 +37,12 @@ export function getTemplateSourceOfTruthNote(templateId, { compoundPersistenceEn
|
|
|
37
37
|
if (templateId === "compound") {
|
|
38
38
|
const compoundBase = "`src/blocks/*/types.ts` files remain the source of truth for each block's `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include starter `typia.manifest.json` files so editor imports resolve before the first sync.";
|
|
39
39
|
if (compoundPersistenceEnabled) {
|
|
40
|
-
return `${compoundBase} For persistence-enabled parents, \`src/blocks/*/api-types.ts\` files remain the source of truth for \`src/blocks/*/api-schemas/*\` when you run \`sync-rest
|
|
40
|
+
return `${compoundBase} For persistence-enabled parents, \`src/blocks/*/api-types.ts\` files remain the source of truth for \`src/blocks/*/api-schemas/*\` when you run \`sync-rest\`, while \`src/blocks/*/transport.ts\` is the first-class transport seam for editor and frontend requests.`;
|
|
41
41
|
}
|
|
42
42
|
return compoundBase;
|
|
43
43
|
}
|
|
44
44
|
if (templateId === "persistence") {
|
|
45
|
-
return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. `src/api-types.ts` remains the source of truth for `src/api-schemas/*` when you run `sync-rest
|
|
45
|
+
return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. `src/api-types.ts` remains the source of truth for `src/api-schemas/*` when you run `sync-rest`, while `src/transport.ts` is the first-class transport seam for editor and frontend requests. This scaffold is intentionally server-rendered: `src/render.php` is the canonical frontend entry, and `src/save.tsx` returns `null` so PHP can inject post context, storage-backed state, and write-policy bootstrap data before hydration.";
|
|
46
46
|
}
|
|
47
47
|
return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. The basic scaffold stays static by design: `src/render.php` is only an opt-in server placeholder, `src/save.tsx` remains the canonical frontend output, and the generated webpack config keeps the current `@wordpress/scripts` CommonJS baseline unless you intentionally add `render` to `block.json`.";
|
|
48
48
|
}
|
|
@@ -61,13 +61,14 @@ ${formatRunScript(packageManager, "add-child", '--slug faq-item --title "FAQ Ite
|
|
|
61
61
|
|
|
62
62
|
This scaffolds a new hidden child block type, updates \`scripts/block-config.ts\` and \`src/blocks/*/children.ts\`, and leaves the default seeded child template unchanged.`;
|
|
63
63
|
}
|
|
64
|
-
function formatPhpRestExtensionPointsSection({ apiTypesPath, extraNote, mainPhpPath, mainPhpScope, }) {
|
|
64
|
+
function formatPhpRestExtensionPointsSection({ apiTypesPath, extraNote, mainPhpPath, mainPhpScope, transportPath, }) {
|
|
65
65
|
const schemaJsonGlob = apiTypesPath.replace(/api-types\.ts$/u, "api-schemas/*.schema.json");
|
|
66
66
|
const perContractOpenApiGlob = apiTypesPath.replace(/api-types\.ts$/u, "api-schemas/*.openapi.json");
|
|
67
67
|
const aggregateOpenApiPath = apiTypesPath.replace(/api-types\.ts$/u, "api.openapi.json");
|
|
68
68
|
const lines = [
|
|
69
69
|
`- Edit \`${mainPhpPath}\` when you need to ${mainPhpScope}.`,
|
|
70
70
|
"- Edit `inc/rest-auth.php` or `inc/rest-public.php` when you need to customize write permissions or token/request-id/nonce checks for the selected policy.",
|
|
71
|
+
`- Edit \`${transportPath}\` when you need to switch between direct WordPress REST and a contract-compatible proxy or BFF without changing the endpoint contracts.`,
|
|
71
72
|
`- Keep \`${apiTypesPath}\` as the source of truth for request and response contracts, then regenerate \`${schemaJsonGlob}\`, per-contract \`${perContractOpenApiGlob}\`, and \`${aggregateOpenApiPath}\` with \`sync-rest\`.`,
|
|
72
73
|
"- Avoid hand-editing generated schema and OpenAPI artifacts unless you are debugging generated output; they are meant to be regenerated from TypeScript contracts.",
|
|
73
74
|
];
|
|
@@ -85,6 +86,7 @@ export function getPhpRestExtensionPointsSection(templateId, { compoundPersisten
|
|
|
85
86
|
apiTypesPath: "src/api-types.ts",
|
|
86
87
|
mainPhpPath: `${slug}.php`,
|
|
87
88
|
mainPhpScope: "change storage helpers, route handlers, response shaping, or route registration",
|
|
89
|
+
transportPath: "src/transport.ts",
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
if (templateId === "compound" && compoundPersistenceEnabled) {
|
|
@@ -93,6 +95,7 @@ export function getPhpRestExtensionPointsSection(templateId, { compoundPersisten
|
|
|
93
95
|
extraNote: "The hidden child block does not own REST routes or storage.",
|
|
94
96
|
mainPhpPath: `${slug}.php`,
|
|
95
97
|
mainPhpScope: "change parent-block storage helpers, route handlers, response shaping, or route registration",
|
|
98
|
+
transportPath: `src/blocks/${slug}/transport.ts`,
|
|
96
99
|
});
|
|
97
100
|
}
|
|
98
101
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
callEndpoint,
|
|
3
|
-
resolveRestRouteUrl,
|
|
4
3
|
} from '@wp-typia/rest';
|
|
5
4
|
|
|
6
5
|
import {
|
|
@@ -11,58 +10,49 @@ import {
|
|
|
11
10
|
get{{pascalCase}}StateEndpoint,
|
|
12
11
|
write{{pascalCase}}StateEndpoint,
|
|
13
12
|
} from './api-client';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if ( typeof window === 'undefined' ) {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const wpApiSettings = ( window as typeof window & {
|
|
25
|
-
wpApiSettings?: { nonce?: string };
|
|
26
|
-
} ).wpApiSettings;
|
|
27
|
-
|
|
28
|
-
return typeof wpApiSettings?.nonce === 'string' && wpApiSettings.nonce.length > 0
|
|
29
|
-
? wpApiSettings.nonce
|
|
30
|
-
: undefined;
|
|
31
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
resolveTransportCallOptions,
|
|
15
|
+
type PersistenceTransportOptions,
|
|
16
|
+
} from './transport';
|
|
32
17
|
|
|
33
18
|
export const stateEndpoint = {
|
|
34
19
|
...get{{pascalCase}}StateEndpoint,
|
|
35
|
-
buildRequestOptions: () => ( {
|
|
36
|
-
url: resolveRestRouteUrl( get{{pascalCase}}StateEndpoint.path ),
|
|
37
|
-
} ),
|
|
38
20
|
};
|
|
39
21
|
|
|
40
22
|
export const writeStateEndpoint = {
|
|
41
23
|
...write{{pascalCase}}StateEndpoint,
|
|
42
|
-
buildRequestOptions: () => ( {
|
|
43
|
-
url: resolveRestRouteUrl( write{{pascalCase}}StateEndpoint.path ),
|
|
44
|
-
} ),
|
|
45
24
|
};
|
|
46
25
|
|
|
47
26
|
export function fetchState(
|
|
48
|
-
request: {{pascalCase}}StateQuery
|
|
27
|
+
request: {{pascalCase}}StateQuery,
|
|
28
|
+
options: PersistenceTransportOptions = {}
|
|
49
29
|
) {
|
|
50
|
-
return callEndpoint(
|
|
30
|
+
return callEndpoint(
|
|
31
|
+
stateEndpoint,
|
|
32
|
+
request,
|
|
33
|
+
resolveTransportCallOptions(
|
|
34
|
+
options.transportTarget ?? 'frontend',
|
|
35
|
+
'read',
|
|
36
|
+
stateEndpoint,
|
|
37
|
+
request,
|
|
38
|
+
options
|
|
39
|
+
)
|
|
40
|
+
);
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
export function writeState(
|
|
54
44
|
request: {{pascalCase}}WriteStateRequest,
|
|
55
|
-
|
|
45
|
+
options: PersistenceTransportOptions = {}
|
|
56
46
|
) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
return callEndpoint(
|
|
48
|
+
writeStateEndpoint,
|
|
49
|
+
request,
|
|
50
|
+
resolveTransportCallOptions(
|
|
51
|
+
options.transportTarget ?? 'frontend',
|
|
52
|
+
'write',
|
|
53
|
+
writeStateEndpoint,
|
|
54
|
+
request,
|
|
55
|
+
options
|
|
56
|
+
)
|
|
57
|
+
);
|
|
68
58
|
}
|
package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache
CHANGED
|
@@ -11,26 +11,13 @@ import type {
|
|
|
11
11
|
{{pascalCase}}WriteStateRequest,
|
|
12
12
|
} from './api-types';
|
|
13
13
|
import {
|
|
14
|
-
resolveRestNonce,
|
|
15
14
|
stateEndpoint,
|
|
16
15
|
writeStateEndpoint,
|
|
17
16
|
} from './api';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return nonce
|
|
24
|
-
? {
|
|
25
|
-
requestOptions: {
|
|
26
|
-
headers: {
|
|
27
|
-
'X-WP-Nonce': nonce,
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
: undefined;
|
|
32
|
-
}
|
|
33
|
-
{{/isAuthenticatedPersistencePolicy}}
|
|
17
|
+
import {
|
|
18
|
+
resolveTransportCallOptions,
|
|
19
|
+
type PersistenceTransportTarget,
|
|
20
|
+
} from './transport';
|
|
34
21
|
|
|
35
22
|
interface WriteStateMutationContext< Context > {
|
|
36
23
|
previous:
|
|
@@ -49,9 +36,8 @@ export interface Use{{pascalCase}}StateQueryOptions<
|
|
|
49
36
|
>,
|
|
50
37
|
'resolveCallOptions'
|
|
51
38
|
> {
|
|
52
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
53
39
|
restNonce?: string;
|
|
54
|
-
|
|
40
|
+
transportTarget?: PersistenceTransportTarget;
|
|
55
41
|
}
|
|
56
42
|
|
|
57
43
|
export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
@@ -62,7 +48,7 @@ export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
|
62
48
|
{{pascalCase}}StateResponse,
|
|
63
49
|
WriteStateMutationContext< Context >
|
|
64
50
|
>,
|
|
65
|
-
'invalidate' | 'onError' | 'onMutate'
|
|
51
|
+
'invalidate' | 'onError' | 'onMutate' | 'resolveCallOptions'
|
|
66
52
|
> {
|
|
67
53
|
onError?: (
|
|
68
54
|
error: unknown,
|
|
@@ -74,9 +60,8 @@ export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
|
74
60
|
request: {{pascalCase}}WriteStateRequest,
|
|
75
61
|
client: import('@wp-typia/rest/react').EndpointDataClient
|
|
76
62
|
) => Context | Promise<Context>;
|
|
77
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
78
63
|
restNonce?: string;
|
|
79
|
-
|
|
64
|
+
transportTarget?: PersistenceTransportTarget;
|
|
80
65
|
}
|
|
81
66
|
|
|
82
67
|
export function use{{pascalCase}}StateQuery<
|
|
@@ -85,20 +70,26 @@ export function use{{pascalCase}}StateQuery<
|
|
|
85
70
|
request: {{pascalCase}}StateQuery,
|
|
86
71
|
options: Use{{pascalCase}}StateQueryOptions< Selected > = {}
|
|
87
72
|
) {
|
|
88
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
89
73
|
const {
|
|
90
74
|
restNonce,
|
|
75
|
+
transportTarget = 'editor',
|
|
91
76
|
...queryOptions
|
|
92
77
|
} = options;
|
|
93
78
|
|
|
94
79
|
return useEndpointQuery( stateEndpoint, request, {
|
|
95
80
|
...queryOptions,
|
|
96
|
-
resolveCallOptions: () =>
|
|
81
|
+
resolveCallOptions: () =>
|
|
82
|
+
resolveTransportCallOptions(
|
|
83
|
+
transportTarget,
|
|
84
|
+
'read',
|
|
85
|
+
stateEndpoint,
|
|
86
|
+
request,
|
|
87
|
+
{
|
|
88
|
+
restNonce,
|
|
89
|
+
transportTarget,
|
|
90
|
+
}
|
|
91
|
+
),
|
|
97
92
|
} );
|
|
98
|
-
{{/isAuthenticatedPersistencePolicy}}
|
|
99
|
-
{{^isAuthenticatedPersistencePolicy}}
|
|
100
|
-
return useEndpointQuery( stateEndpoint, request, options );
|
|
101
|
-
{{/isAuthenticatedPersistencePolicy}}
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
export function useWrite{{pascalCase}}StateMutation<
|
|
@@ -109,9 +100,8 @@ export function useWrite{{pascalCase}}StateMutation<
|
|
|
109
100
|
const {
|
|
110
101
|
onError,
|
|
111
102
|
onMutate,
|
|
112
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
113
103
|
restNonce,
|
|
114
|
-
|
|
104
|
+
transportTarget = 'editor',
|
|
115
105
|
...mutationOptions
|
|
116
106
|
} = options;
|
|
117
107
|
|
|
@@ -185,8 +175,16 @@ export function useWrite{{pascalCase}}StateMutation<
|
|
|
185
175
|
userContext,
|
|
186
176
|
};
|
|
187
177
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
178
|
+
resolveCallOptions: ( request ) =>
|
|
179
|
+
resolveTransportCallOptions(
|
|
180
|
+
transportTarget,
|
|
181
|
+
'write',
|
|
182
|
+
writeStateEndpoint,
|
|
183
|
+
request,
|
|
184
|
+
{
|
|
185
|
+
restNonce,
|
|
186
|
+
transportTarget,
|
|
187
|
+
}
|
|
188
|
+
),
|
|
191
189
|
} );
|
|
192
190
|
}
|
|
@@ -47,6 +47,9 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
47
47
|
const result = await fetchState( {
|
|
48
48
|
postId: context.postId,
|
|
49
49
|
resourceKey: context.resourceKey,
|
|
50
|
+
}, {
|
|
51
|
+
restNonce: context.restNonce,
|
|
52
|
+
transportTarget: 'frontend',
|
|
50
53
|
} );
|
|
51
54
|
if ( ! result.isValid || ! result.data ) {
|
|
52
55
|
state.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';
|
|
@@ -96,7 +99,10 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
96
99
|
? context.publicWriteToken
|
|
97
100
|
: undefined,
|
|
98
101
|
resourceKey: context.resourceKey,
|
|
99
|
-
},
|
|
102
|
+
}, {
|
|
103
|
+
restNonce: context.restNonce,
|
|
104
|
+
transportTarget: 'frontend',
|
|
105
|
+
} );
|
|
100
106
|
if ( ! result.isValid || ! result.data ) {
|
|
101
107
|
state.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';
|
|
102
108
|
return;
|
package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/transport.ts.mustache
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EndpointCallOptions,
|
|
3
|
+
resolveRestRouteUrl,
|
|
4
|
+
} from '@wp-typia/rest';
|
|
5
|
+
|
|
6
|
+
export type PersistenceTransportTarget = 'editor' | 'frontend';
|
|
7
|
+
export type PersistenceTransportOperation = 'read' | 'write';
|
|
8
|
+
|
|
9
|
+
export interface PersistenceTransportOptions {
|
|
10
|
+
restNonce?: string;
|
|
11
|
+
transportTarget?: PersistenceTransportTarget;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EndpointWithPath {
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PersistenceTransportResolver = (
|
|
19
|
+
endpoint: EndpointWithPath,
|
|
20
|
+
request?: unknown,
|
|
21
|
+
options?: PersistenceTransportOptions
|
|
22
|
+
) => EndpointCallOptions;
|
|
23
|
+
|
|
24
|
+
interface PersistenceTransportTargetResolvers {
|
|
25
|
+
read: PersistenceTransportResolver;
|
|
26
|
+
write: PersistenceTransportResolver;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Replace any of these base URLs to route a given target through a contract-compatible proxy or BFF.
|
|
30
|
+
const EDITOR_READ_BASE_URL: string | undefined = undefined;
|
|
31
|
+
const EDITOR_WRITE_BASE_URL: string | undefined = undefined;
|
|
32
|
+
const FRONTEND_READ_BASE_URL: string | undefined = undefined;
|
|
33
|
+
const FRONTEND_WRITE_BASE_URL: string | undefined = undefined;
|
|
34
|
+
|
|
35
|
+
function resolveEndpointUrl(
|
|
36
|
+
endpointPath: string,
|
|
37
|
+
baseUrl?: string
|
|
38
|
+
): string {
|
|
39
|
+
if ( typeof baseUrl === 'string' && baseUrl.trim().length > 0 ) {
|
|
40
|
+
const origin =
|
|
41
|
+
typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
|
|
42
|
+
const resolvedBaseUrl = new URL( baseUrl.trim(), origin );
|
|
43
|
+
const [ pathWithQuery, hash = '' ] = endpointPath.split( '#', 2 );
|
|
44
|
+
const [ rawPath, rawQuery = '' ] = pathWithQuery.split( '?', 2 );
|
|
45
|
+
const normalizedEndpointPath = rawPath.replace( /^\/+/, '' );
|
|
46
|
+
|
|
47
|
+
if ( resolvedBaseUrl.searchParams.has( 'rest_route' ) ) {
|
|
48
|
+
const restRouteBase = resolvedBaseUrl.searchParams.get( 'rest_route' ) ?? '/';
|
|
49
|
+
const normalizedRestRouteBase = restRouteBase
|
|
50
|
+
.replace( /^\/+/, '' )
|
|
51
|
+
.replace( /\/+$/, '' );
|
|
52
|
+
const nextRestRoute = normalizedRestRouteBase.length > 0
|
|
53
|
+
? `/${ normalizedRestRouteBase }/${ normalizedEndpointPath }`
|
|
54
|
+
: `/${ normalizedEndpointPath }`;
|
|
55
|
+
resolvedBaseUrl.searchParams.set( 'rest_route', nextRestRoute );
|
|
56
|
+
} else {
|
|
57
|
+
const basePath = resolvedBaseUrl.pathname.endsWith( '/' )
|
|
58
|
+
? resolvedBaseUrl.pathname
|
|
59
|
+
: `${ resolvedBaseUrl.pathname }/`;
|
|
60
|
+
resolvedBaseUrl.pathname = `${ basePath }${ normalizedEndpointPath }`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for ( const [ key, value ] of new URLSearchParams( rawQuery ) ) {
|
|
64
|
+
resolvedBaseUrl.searchParams.append( key, value );
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if ( hash ) {
|
|
68
|
+
resolvedBaseUrl.hash = hash;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return resolvedBaseUrl.toString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return resolveRestRouteUrl( endpointPath );
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPlainObject(
|
|
78
|
+
value: unknown
|
|
79
|
+
): value is Record< string, unknown > {
|
|
80
|
+
if ( Object.prototype.toString.call( value ) !== '[object Object]' ) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const prototype = Object.getPrototypeOf( value );
|
|
85
|
+
return prototype === null || prototype === Object.prototype;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function encodeRequestQuery( request: unknown ): string {
|
|
89
|
+
if ( request === undefined || request === null ) {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if ( request instanceof URLSearchParams ) {
|
|
94
|
+
return request.toString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ( ! isPlainObject( request ) ) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'Persistence transport read requests must be plain objects or URLSearchParams.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const params = new URLSearchParams();
|
|
104
|
+
|
|
105
|
+
for ( const [ key, value ] of Object.entries( request ) ) {
|
|
106
|
+
if ( value === undefined || value === null ) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ( Array.isArray( value ) ) {
|
|
111
|
+
for ( const item of value ) {
|
|
112
|
+
params.append( key, String( item ) );
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
params.set( key, String( value ) );
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return params.toString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function joinUrlWithQuery(
|
|
124
|
+
url: string,
|
|
125
|
+
query: string
|
|
126
|
+
): string {
|
|
127
|
+
if ( ! query ) {
|
|
128
|
+
return url;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nextUrl = new URL(
|
|
132
|
+
url,
|
|
133
|
+
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for ( const [ key, value ] of new URLSearchParams( query ) ) {
|
|
137
|
+
nextUrl.searchParams.append( key, value );
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return nextUrl.toString();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resolveRestNonce( fallback?: string ): string | undefined {
|
|
144
|
+
if ( typeof fallback === 'string' && fallback.length > 0 ) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ( typeof window === 'undefined' ) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const wpApiSettings = ( window as typeof window & {
|
|
153
|
+
wpApiSettings?: { nonce?: string };
|
|
154
|
+
} ).wpApiSettings;
|
|
155
|
+
|
|
156
|
+
return typeof wpApiSettings?.nonce === 'string' && wpApiSettings.nonce.length > 0
|
|
157
|
+
? wpApiSettings.nonce
|
|
158
|
+
: undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildCallOptions( {
|
|
162
|
+
baseUrl,
|
|
163
|
+
endpoint,
|
|
164
|
+
includeRestNonce,
|
|
165
|
+
operation,
|
|
166
|
+
options,
|
|
167
|
+
request,
|
|
168
|
+
}: {
|
|
169
|
+
baseUrl?: string;
|
|
170
|
+
endpoint: EndpointWithPath;
|
|
171
|
+
includeRestNonce: boolean;
|
|
172
|
+
operation: PersistenceTransportOperation;
|
|
173
|
+
options?: PersistenceTransportOptions;
|
|
174
|
+
request?: unknown;
|
|
175
|
+
} ): EndpointCallOptions {
|
|
176
|
+
const nonce = includeRestNonce ? resolveRestNonce( options?.restNonce ) : undefined;
|
|
177
|
+
const requestOptions: EndpointCallOptions['requestOptions'] = {};
|
|
178
|
+
|
|
179
|
+
if ( nonce ) {
|
|
180
|
+
requestOptions.headers = {
|
|
181
|
+
'X-WP-Nonce': nonce,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ( typeof baseUrl === 'string' && baseUrl.trim().length > 0 ) {
|
|
186
|
+
const endpointUrl = resolveEndpointUrl( endpoint.path, baseUrl );
|
|
187
|
+
requestOptions.url =
|
|
188
|
+
operation === 'read'
|
|
189
|
+
? joinUrlWithQuery( endpointUrl, encodeRequestQuery( request ) )
|
|
190
|
+
: endpointUrl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return Object.keys( requestOptions ).length > 0
|
|
194
|
+
? {
|
|
195
|
+
requestOptions,
|
|
196
|
+
}
|
|
197
|
+
: {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const persistenceTransportTargets: Record<
|
|
201
|
+
PersistenceTransportTarget,
|
|
202
|
+
PersistenceTransportTargetResolvers
|
|
203
|
+
> = {
|
|
204
|
+
editor: {
|
|
205
|
+
read: ( endpoint, request, options ) =>
|
|
206
|
+
buildCallOptions( {
|
|
207
|
+
baseUrl: EDITOR_READ_BASE_URL,
|
|
208
|
+
endpoint,
|
|
209
|
+
includeRestNonce: {{isAuthenticatedPersistencePolicy}},
|
|
210
|
+
operation: 'read',
|
|
211
|
+
options,
|
|
212
|
+
request,
|
|
213
|
+
} ),
|
|
214
|
+
write: ( endpoint, request, options ) =>
|
|
215
|
+
buildCallOptions( {
|
|
216
|
+
baseUrl: EDITOR_WRITE_BASE_URL,
|
|
217
|
+
endpoint,
|
|
218
|
+
includeRestNonce: {{isAuthenticatedPersistencePolicy}},
|
|
219
|
+
operation: 'write',
|
|
220
|
+
options,
|
|
221
|
+
request,
|
|
222
|
+
} ),
|
|
223
|
+
},
|
|
224
|
+
frontend: {
|
|
225
|
+
read: ( endpoint, request, options ) =>
|
|
226
|
+
buildCallOptions( {
|
|
227
|
+
baseUrl: FRONTEND_READ_BASE_URL,
|
|
228
|
+
endpoint,
|
|
229
|
+
includeRestNonce: false,
|
|
230
|
+
operation: 'read',
|
|
231
|
+
options,
|
|
232
|
+
request,
|
|
233
|
+
} ),
|
|
234
|
+
write: ( endpoint, request, options ) =>
|
|
235
|
+
buildCallOptions( {
|
|
236
|
+
baseUrl: FRONTEND_WRITE_BASE_URL,
|
|
237
|
+
endpoint,
|
|
238
|
+
includeRestNonce: {{isAuthenticatedPersistencePolicy}},
|
|
239
|
+
operation: 'write',
|
|
240
|
+
options,
|
|
241
|
+
request,
|
|
242
|
+
} ),
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export function resolveTransportCallOptions(
|
|
247
|
+
target: PersistenceTransportTarget,
|
|
248
|
+
operation: PersistenceTransportOperation,
|
|
249
|
+
endpoint: EndpointWithPath,
|
|
250
|
+
request?: unknown,
|
|
251
|
+
options?: PersistenceTransportOptions
|
|
252
|
+
): EndpointCallOptions {
|
|
253
|
+
return persistenceTransportTargets[ target ][ operation ]( endpoint, request, options );
|
|
254
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
callEndpoint,
|
|
3
|
-
resolveRestRouteUrl,
|
|
4
3
|
} from '@wp-typia/rest';
|
|
5
4
|
|
|
6
5
|
import {
|
|
@@ -11,58 +10,49 @@ import {
|
|
|
11
10
|
get{{pascalCase}}StateEndpoint,
|
|
12
11
|
write{{pascalCase}}StateEndpoint,
|
|
13
12
|
} from './api-client';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if ( typeof window === 'undefined' ) {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const wpApiSettings = ( window as typeof window & {
|
|
25
|
-
wpApiSettings?: { nonce?: string };
|
|
26
|
-
} ).wpApiSettings;
|
|
27
|
-
|
|
28
|
-
return typeof wpApiSettings?.nonce === 'string' && wpApiSettings.nonce.length > 0
|
|
29
|
-
? wpApiSettings.nonce
|
|
30
|
-
: undefined;
|
|
31
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
resolveTransportCallOptions,
|
|
15
|
+
type PersistenceTransportOptions,
|
|
16
|
+
} from './transport';
|
|
32
17
|
|
|
33
18
|
export const stateEndpoint = {
|
|
34
19
|
...get{{pascalCase}}StateEndpoint,
|
|
35
|
-
buildRequestOptions: () => ( {
|
|
36
|
-
url: resolveRestRouteUrl( get{{pascalCase}}StateEndpoint.path ),
|
|
37
|
-
} ),
|
|
38
20
|
};
|
|
39
21
|
|
|
40
22
|
export const writeStateEndpoint = {
|
|
41
23
|
...write{{pascalCase}}StateEndpoint,
|
|
42
|
-
buildRequestOptions: () => ( {
|
|
43
|
-
url: resolveRestRouteUrl( write{{pascalCase}}StateEndpoint.path ),
|
|
44
|
-
} ),
|
|
45
24
|
};
|
|
46
25
|
|
|
47
26
|
export function fetchState(
|
|
48
|
-
request: {{pascalCase}}StateQuery
|
|
27
|
+
request: {{pascalCase}}StateQuery,
|
|
28
|
+
options: PersistenceTransportOptions = {}
|
|
49
29
|
) {
|
|
50
|
-
return callEndpoint(
|
|
30
|
+
return callEndpoint(
|
|
31
|
+
stateEndpoint,
|
|
32
|
+
request,
|
|
33
|
+
resolveTransportCallOptions(
|
|
34
|
+
options.transportTarget ?? 'frontend',
|
|
35
|
+
'read',
|
|
36
|
+
stateEndpoint,
|
|
37
|
+
request,
|
|
38
|
+
options
|
|
39
|
+
)
|
|
40
|
+
);
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
export function writeState(
|
|
54
44
|
request: {{pascalCase}}WriteStateRequest,
|
|
55
|
-
|
|
45
|
+
options: PersistenceTransportOptions = {}
|
|
56
46
|
) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
47
|
+
return callEndpoint(
|
|
48
|
+
writeStateEndpoint,
|
|
49
|
+
request,
|
|
50
|
+
resolveTransportCallOptions(
|
|
51
|
+
options.transportTarget ?? 'frontend',
|
|
52
|
+
'write',
|
|
53
|
+
writeStateEndpoint,
|
|
54
|
+
request,
|
|
55
|
+
options
|
|
56
|
+
)
|
|
57
|
+
);
|
|
68
58
|
}
|
|
@@ -11,26 +11,13 @@ import type {
|
|
|
11
11
|
{{pascalCase}}WriteStateRequest,
|
|
12
12
|
} from './api-types';
|
|
13
13
|
import {
|
|
14
|
-
resolveRestNonce,
|
|
15
14
|
stateEndpoint,
|
|
16
15
|
writeStateEndpoint,
|
|
17
16
|
} from './api';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return nonce
|
|
24
|
-
? {
|
|
25
|
-
requestOptions: {
|
|
26
|
-
headers: {
|
|
27
|
-
'X-WP-Nonce': nonce,
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
: undefined;
|
|
32
|
-
}
|
|
33
|
-
{{/isAuthenticatedPersistencePolicy}}
|
|
17
|
+
import {
|
|
18
|
+
resolveTransportCallOptions,
|
|
19
|
+
type PersistenceTransportTarget,
|
|
20
|
+
} from './transport';
|
|
34
21
|
|
|
35
22
|
interface WriteStateMutationContext< Context > {
|
|
36
23
|
previous:
|
|
@@ -49,9 +36,8 @@ export interface Use{{pascalCase}}StateQueryOptions<
|
|
|
49
36
|
>,
|
|
50
37
|
'resolveCallOptions'
|
|
51
38
|
> {
|
|
52
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
53
39
|
restNonce?: string;
|
|
54
|
-
|
|
40
|
+
transportTarget?: PersistenceTransportTarget;
|
|
55
41
|
}
|
|
56
42
|
|
|
57
43
|
export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
@@ -62,7 +48,7 @@ export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
|
62
48
|
{{pascalCase}}StateResponse,
|
|
63
49
|
WriteStateMutationContext< Context >
|
|
64
50
|
>,
|
|
65
|
-
'invalidate' | 'onError' | 'onMutate'
|
|
51
|
+
'invalidate' | 'onError' | 'onMutate' | 'resolveCallOptions'
|
|
66
52
|
> {
|
|
67
53
|
onError?: (
|
|
68
54
|
error: unknown,
|
|
@@ -74,9 +60,8 @@ export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
|
74
60
|
request: {{pascalCase}}WriteStateRequest,
|
|
75
61
|
client: import('@wp-typia/rest/react').EndpointDataClient
|
|
76
62
|
) => Context | Promise<Context>;
|
|
77
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
78
63
|
restNonce?: string;
|
|
79
|
-
|
|
64
|
+
transportTarget?: PersistenceTransportTarget;
|
|
80
65
|
}
|
|
81
66
|
|
|
82
67
|
export function use{{pascalCase}}StateQuery<
|
|
@@ -85,20 +70,26 @@ export function use{{pascalCase}}StateQuery<
|
|
|
85
70
|
request: {{pascalCase}}StateQuery,
|
|
86
71
|
options: Use{{pascalCase}}StateQueryOptions< Selected > = {}
|
|
87
72
|
) {
|
|
88
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
89
73
|
const {
|
|
90
74
|
restNonce,
|
|
75
|
+
transportTarget = 'editor',
|
|
91
76
|
...queryOptions
|
|
92
77
|
} = options;
|
|
93
78
|
|
|
94
79
|
return useEndpointQuery( stateEndpoint, request, {
|
|
95
80
|
...queryOptions,
|
|
96
|
-
resolveCallOptions: () =>
|
|
81
|
+
resolveCallOptions: () =>
|
|
82
|
+
resolveTransportCallOptions(
|
|
83
|
+
transportTarget,
|
|
84
|
+
'read',
|
|
85
|
+
stateEndpoint,
|
|
86
|
+
request,
|
|
87
|
+
{
|
|
88
|
+
restNonce,
|
|
89
|
+
transportTarget,
|
|
90
|
+
}
|
|
91
|
+
),
|
|
97
92
|
} );
|
|
98
|
-
{{/isAuthenticatedPersistencePolicy}}
|
|
99
|
-
{{^isAuthenticatedPersistencePolicy}}
|
|
100
|
-
return useEndpointQuery( stateEndpoint, request, options );
|
|
101
|
-
{{/isAuthenticatedPersistencePolicy}}
|
|
102
93
|
}
|
|
103
94
|
|
|
104
95
|
export function useWrite{{pascalCase}}StateMutation<
|
|
@@ -109,9 +100,8 @@ export function useWrite{{pascalCase}}StateMutation<
|
|
|
109
100
|
const {
|
|
110
101
|
onError,
|
|
111
102
|
onMutate,
|
|
112
|
-
{{#isAuthenticatedPersistencePolicy}}
|
|
113
103
|
restNonce,
|
|
114
|
-
|
|
104
|
+
transportTarget = 'editor',
|
|
115
105
|
...mutationOptions
|
|
116
106
|
} = options;
|
|
117
107
|
|
|
@@ -185,8 +175,16 @@ export function useWrite{{pascalCase}}StateMutation<
|
|
|
185
175
|
userContext,
|
|
186
176
|
};
|
|
187
177
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
178
|
+
resolveCallOptions: ( request ) =>
|
|
179
|
+
resolveTransportCallOptions(
|
|
180
|
+
transportTarget,
|
|
181
|
+
'write',
|
|
182
|
+
writeStateEndpoint,
|
|
183
|
+
request,
|
|
184
|
+
{
|
|
185
|
+
restNonce,
|
|
186
|
+
transportTarget,
|
|
187
|
+
}
|
|
188
|
+
),
|
|
191
189
|
} );
|
|
192
190
|
}
|
|
@@ -48,6 +48,9 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
48
48
|
const result = await fetchState( {
|
|
49
49
|
postId: context.postId,
|
|
50
50
|
resourceKey: context.resourceKey,
|
|
51
|
+
}, {
|
|
52
|
+
restNonce: context.restNonce,
|
|
53
|
+
transportTarget: 'frontend',
|
|
51
54
|
} );
|
|
52
55
|
if ( ! result.isValid || ! result.data ) {
|
|
53
56
|
state.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';
|
|
@@ -97,7 +100,10 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
97
100
|
? context.publicWriteToken
|
|
98
101
|
: undefined,
|
|
99
102
|
resourceKey: context.resourceKey,
|
|
100
|
-
},
|
|
103
|
+
}, {
|
|
104
|
+
restNonce: context.restNonce,
|
|
105
|
+
transportTarget: 'frontend',
|
|
106
|
+
} );
|
|
101
107
|
if ( ! result.isValid || ! result.data ) {
|
|
102
108
|
state.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';
|
|
103
109
|
return;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type EndpointCallOptions,
|
|
3
|
+
resolveRestRouteUrl,
|
|
4
|
+
} from '@wp-typia/rest';
|
|
5
|
+
|
|
6
|
+
export type PersistenceTransportTarget = 'editor' | 'frontend';
|
|
7
|
+
export type PersistenceTransportOperation = 'read' | 'write';
|
|
8
|
+
|
|
9
|
+
export interface PersistenceTransportOptions {
|
|
10
|
+
restNonce?: string;
|
|
11
|
+
transportTarget?: PersistenceTransportTarget;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EndpointWithPath {
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PersistenceTransportResolver = (
|
|
19
|
+
endpoint: EndpointWithPath,
|
|
20
|
+
request?: unknown,
|
|
21
|
+
options?: PersistenceTransportOptions
|
|
22
|
+
) => EndpointCallOptions;
|
|
23
|
+
|
|
24
|
+
interface PersistenceTransportTargetResolvers {
|
|
25
|
+
read: PersistenceTransportResolver;
|
|
26
|
+
write: PersistenceTransportResolver;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Replace any of these base URLs to route a given target through a contract-compatible proxy or BFF.
|
|
30
|
+
const EDITOR_READ_BASE_URL: string | undefined = undefined;
|
|
31
|
+
const EDITOR_WRITE_BASE_URL: string | undefined = undefined;
|
|
32
|
+
const FRONTEND_READ_BASE_URL: string | undefined = undefined;
|
|
33
|
+
const FRONTEND_WRITE_BASE_URL: string | undefined = undefined;
|
|
34
|
+
|
|
35
|
+
function resolveEndpointUrl(
|
|
36
|
+
endpointPath: string,
|
|
37
|
+
baseUrl?: string
|
|
38
|
+
): string {
|
|
39
|
+
if ( typeof baseUrl === 'string' && baseUrl.trim().length > 0 ) {
|
|
40
|
+
const origin =
|
|
41
|
+
typeof window !== 'undefined' ? window.location.origin : 'http://localhost';
|
|
42
|
+
const resolvedBaseUrl = new URL( baseUrl.trim(), origin );
|
|
43
|
+
const [ pathWithQuery, hash = '' ] = endpointPath.split( '#', 2 );
|
|
44
|
+
const [ rawPath, rawQuery = '' ] = pathWithQuery.split( '?', 2 );
|
|
45
|
+
const normalizedEndpointPath = rawPath.replace( /^\/+/, '' );
|
|
46
|
+
|
|
47
|
+
if ( resolvedBaseUrl.searchParams.has( 'rest_route' ) ) {
|
|
48
|
+
const restRouteBase = resolvedBaseUrl.searchParams.get( 'rest_route' ) ?? '/';
|
|
49
|
+
const normalizedRestRouteBase = restRouteBase
|
|
50
|
+
.replace( /^\/+/, '' )
|
|
51
|
+
.replace( /\/+$/, '' );
|
|
52
|
+
const nextRestRoute = normalizedRestRouteBase.length > 0
|
|
53
|
+
? `/${ normalizedRestRouteBase }/${ normalizedEndpointPath }`
|
|
54
|
+
: `/${ normalizedEndpointPath }`;
|
|
55
|
+
resolvedBaseUrl.searchParams.set( 'rest_route', nextRestRoute );
|
|
56
|
+
} else {
|
|
57
|
+
const basePath = resolvedBaseUrl.pathname.endsWith( '/' )
|
|
58
|
+
? resolvedBaseUrl.pathname
|
|
59
|
+
: `${ resolvedBaseUrl.pathname }/`;
|
|
60
|
+
resolvedBaseUrl.pathname = `${ basePath }${ normalizedEndpointPath }`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for ( const [ key, value ] of new URLSearchParams( rawQuery ) ) {
|
|
64
|
+
resolvedBaseUrl.searchParams.append( key, value );
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if ( hash ) {
|
|
68
|
+
resolvedBaseUrl.hash = hash;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return resolvedBaseUrl.toString();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return resolveRestRouteUrl( endpointPath );
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPlainObject(
|
|
78
|
+
value: unknown
|
|
79
|
+
): value is Record< string, unknown > {
|
|
80
|
+
if ( Object.prototype.toString.call( value ) !== '[object Object]' ) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const prototype = Object.getPrototypeOf( value );
|
|
85
|
+
return prototype === null || prototype === Object.prototype;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function encodeRequestQuery( request: unknown ): string {
|
|
89
|
+
if ( request === undefined || request === null ) {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if ( request instanceof URLSearchParams ) {
|
|
94
|
+
return request.toString();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ( ! isPlainObject( request ) ) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'Persistence transport read requests must be plain objects or URLSearchParams.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const params = new URLSearchParams();
|
|
104
|
+
|
|
105
|
+
for ( const [ key, value ] of Object.entries( request ) ) {
|
|
106
|
+
if ( value === undefined || value === null ) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ( Array.isArray( value ) ) {
|
|
111
|
+
for ( const item of value ) {
|
|
112
|
+
params.append( key, String( item ) );
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
params.set( key, String( value ) );
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return params.toString();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function joinUrlWithQuery(
|
|
124
|
+
url: string,
|
|
125
|
+
query: string
|
|
126
|
+
): string {
|
|
127
|
+
if ( ! query ) {
|
|
128
|
+
return url;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const nextUrl = new URL(
|
|
132
|
+
url,
|
|
133
|
+
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
for ( const [ key, value ] of new URLSearchParams( query ) ) {
|
|
137
|
+
nextUrl.searchParams.append( key, value );
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return nextUrl.toString();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resolveRestNonce( fallback?: string ): string | undefined {
|
|
144
|
+
if ( typeof fallback === 'string' && fallback.length > 0 ) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ( typeof window === 'undefined' ) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const wpApiSettings = ( window as typeof window & {
|
|
153
|
+
wpApiSettings?: { nonce?: string };
|
|
154
|
+
} ).wpApiSettings;
|
|
155
|
+
|
|
156
|
+
return typeof wpApiSettings?.nonce === 'string' && wpApiSettings.nonce.length > 0
|
|
157
|
+
? wpApiSettings.nonce
|
|
158
|
+
: undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildCallOptions( {
|
|
162
|
+
baseUrl,
|
|
163
|
+
endpoint,
|
|
164
|
+
includeRestNonce,
|
|
165
|
+
operation,
|
|
166
|
+
options,
|
|
167
|
+
request,
|
|
168
|
+
}: {
|
|
169
|
+
baseUrl?: string;
|
|
170
|
+
endpoint: EndpointWithPath;
|
|
171
|
+
includeRestNonce: boolean;
|
|
172
|
+
operation: PersistenceTransportOperation;
|
|
173
|
+
options?: PersistenceTransportOptions;
|
|
174
|
+
request?: unknown;
|
|
175
|
+
} ): EndpointCallOptions {
|
|
176
|
+
const nonce = includeRestNonce ? resolveRestNonce( options?.restNonce ) : undefined;
|
|
177
|
+
const requestOptions: EndpointCallOptions['requestOptions'] = {};
|
|
178
|
+
|
|
179
|
+
if ( nonce ) {
|
|
180
|
+
requestOptions.headers = {
|
|
181
|
+
'X-WP-Nonce': nonce,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ( typeof baseUrl === 'string' && baseUrl.trim().length > 0 ) {
|
|
186
|
+
const endpointUrl = resolveEndpointUrl( endpoint.path, baseUrl );
|
|
187
|
+
requestOptions.url =
|
|
188
|
+
operation === 'read'
|
|
189
|
+
? joinUrlWithQuery( endpointUrl, encodeRequestQuery( request ) )
|
|
190
|
+
: endpointUrl;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return Object.keys( requestOptions ).length > 0
|
|
194
|
+
? {
|
|
195
|
+
requestOptions,
|
|
196
|
+
}
|
|
197
|
+
: {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const persistenceTransportTargets: Record<
|
|
201
|
+
PersistenceTransportTarget,
|
|
202
|
+
PersistenceTransportTargetResolvers
|
|
203
|
+
> = {
|
|
204
|
+
editor: {
|
|
205
|
+
read: ( endpoint, request, options ) =>
|
|
206
|
+
buildCallOptions( {
|
|
207
|
+
baseUrl: EDITOR_READ_BASE_URL,
|
|
208
|
+
endpoint,
|
|
209
|
+
includeRestNonce: {{isAuthenticatedPersistencePolicy}},
|
|
210
|
+
operation: 'read',
|
|
211
|
+
options,
|
|
212
|
+
request,
|
|
213
|
+
} ),
|
|
214
|
+
write: ( endpoint, request, options ) =>
|
|
215
|
+
buildCallOptions( {
|
|
216
|
+
baseUrl: EDITOR_WRITE_BASE_URL,
|
|
217
|
+
endpoint,
|
|
218
|
+
includeRestNonce: {{isAuthenticatedPersistencePolicy}},
|
|
219
|
+
operation: 'write',
|
|
220
|
+
options,
|
|
221
|
+
request,
|
|
222
|
+
} ),
|
|
223
|
+
},
|
|
224
|
+
frontend: {
|
|
225
|
+
read: ( endpoint, request, options ) =>
|
|
226
|
+
buildCallOptions( {
|
|
227
|
+
baseUrl: FRONTEND_READ_BASE_URL,
|
|
228
|
+
endpoint,
|
|
229
|
+
includeRestNonce: false,
|
|
230
|
+
operation: 'read',
|
|
231
|
+
options,
|
|
232
|
+
request,
|
|
233
|
+
} ),
|
|
234
|
+
write: ( endpoint, request, options ) =>
|
|
235
|
+
buildCallOptions( {
|
|
236
|
+
baseUrl: FRONTEND_WRITE_BASE_URL,
|
|
237
|
+
endpoint,
|
|
238
|
+
includeRestNonce: {{isAuthenticatedPersistencePolicy}},
|
|
239
|
+
operation: 'write',
|
|
240
|
+
options,
|
|
241
|
+
request,
|
|
242
|
+
} ),
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export function resolveTransportCallOptions(
|
|
247
|
+
target: PersistenceTransportTarget,
|
|
248
|
+
operation: PersistenceTransportOperation,
|
|
249
|
+
endpoint: EndpointWithPath,
|
|
250
|
+
request?: unknown,
|
|
251
|
+
options?: PersistenceTransportOptions
|
|
252
|
+
): EndpointCallOptions {
|
|
253
|
+
return persistenceTransportTargets[ target ][ operation ]( endpoint, request, options );
|
|
254
|
+
}
|