@wp-typia/project-tools 0.15.0 → 0.15.2

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.
Files changed (34) hide show
  1. package/dist/runtime/cli-add.js +26 -70
  2. package/dist/runtime/cli-doctor.js +25 -9
  3. package/dist/runtime/cli-help.js +1 -0
  4. package/dist/runtime/cli-templates.js +10 -0
  5. package/dist/runtime/json-utils.d.ts +5 -8
  6. package/dist/runtime/json-utils.js +5 -10
  7. package/dist/runtime/metadata-analysis.d.ts +7 -11
  8. package/dist/runtime/metadata-analysis.js +7 -285
  9. package/dist/runtime/metadata-model.d.ts +7 -84
  10. package/dist/runtime/metadata-model.js +7 -59
  11. package/dist/runtime/metadata-parser.d.ts +5 -51
  12. package/dist/runtime/metadata-parser.js +5 -792
  13. package/dist/runtime/metadata-php-render.d.ts +5 -27
  14. package/dist/runtime/metadata-php-render.js +5 -547
  15. package/dist/runtime/metadata-projection.d.ts +7 -7
  16. package/dist/runtime/metadata-projection.js +7 -233
  17. package/dist/runtime/object-utils.d.ts +1 -1
  18. package/dist/runtime/object-utils.js +3 -6
  19. package/dist/runtime/persistence-rest-artifacts.d.ts +76 -0
  20. package/dist/runtime/persistence-rest-artifacts.js +99 -0
  21. package/dist/runtime/scaffold.d.ts +10 -2
  22. package/dist/runtime/scaffold.js +95 -1
  23. package/dist/runtime/template-builtins.js +1 -1
  24. package/dist/runtime/template-registry.d.ts +2 -1
  25. package/dist/runtime/template-registry.js +13 -2
  26. package/package.json +9 -8
  27. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +103 -7
  28. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +14 -0
  29. package/templates/_shared/persistence/core/src/api.ts.mustache +28 -9
  30. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +17 -11
  31. package/templates/interactivity/src/block.json.mustache +1 -0
  32. package/templates/interactivity/src/editor.scss.mustache +8 -0
  33. package/templates/interactivity/src/index.tsx.mustache +1 -0
  34. package/templates/persistence/src/edit.tsx.mustache +6 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wp-typia/project-tools",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -26,9 +26,9 @@
26
26
  "package.json"
27
27
  ],
28
28
  "scripts": {
29
- "build": "bun run --filter @wp-typia/block-runtime build && rm -rf dist && tsc -p tsconfig.runtime.json",
30
- "test": "bun run --filter @wp-typia/block-runtime build && bun run build && bun test tests/*.test.ts",
31
- "test:coverage": "bun run --filter @wp-typia/block-runtime build && bun run build && bun test tests/*.test.ts --coverage --coverage-reporter=lcov --coverage-dir=coverage",
29
+ "build": "bun run --filter @wp-typia/api-client build && bun run --filter @wp-typia/block-runtime build && rm -rf dist && tsc -p tsconfig.runtime.json",
30
+ "test": "bun run build && bun test tests/*.test.ts",
31
+ "test:coverage": "bun run build && bun test tests/*.test.ts --coverage --coverage-reporter=lcov --coverage-dir=coverage",
32
32
  "clean": "rm -rf dist",
33
33
  "prepack": "bun run build"
34
34
  },
@@ -61,14 +61,15 @@
61
61
  "bun": ">=1.3.11"
62
62
  },
63
63
  "dependencies": {
64
- "@wp-typia/api-client": "^0.4.0",
65
- "@wp-typia/block-runtime": "^0.4.0",
66
- "@wp-typia/rest": "^0.3.4",
67
- "@wp-typia/block-types": "^0.2.0",
64
+ "@wp-typia/api-client": "^0.4.2",
65
+ "@wp-typia/block-runtime": "^0.4.2",
66
+ "@wp-typia/rest": "^0.3.5",
67
+ "@wp-typia/block-types": "^0.2.1",
68
68
  "mustache": "^4.2.0",
69
69
  "npm-package-arg": "^13.0.0",
70
70
  "semver": "^7.7.3",
71
71
  "tar": "^7.4.3",
72
+ "typia": "^12.0.1",
72
73
  "typescript": "^5.9.2"
73
74
  },
74
75
  "devDependencies": {
@@ -2,14 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
- const PARENT_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}';
6
- const PARENT_BLOCK_NAMESPACE = '{{namespace}}';
7
- const PARENT_BLOCK_SLUG = '{{slugKebabCase}}';
8
- const PARENT_BLOCK_TITLE = {{titleJson}};
9
- const PARENT_TYPE_NAME = '{{pascalCase}}';
10
- const PARENT_STYLE_IMPORT = '../{{slugKebabCase}}/style.scss';
11
5
  const PROJECT_ROOT = process.cwd();
12
- const TEXT_DOMAIN = '{{textDomain}}';
13
6
 
14
7
  const ALLOWED_CHILD_MARKER = '// add-child: insert new allowed child block names here';
15
8
  const BLOCK_CONFIG_MARKER = '// add-child: insert new block config entries here';
@@ -22,6 +15,16 @@ type StarterManifestDocument = {
22
15
  sourceType: string;
23
16
  };
24
17
 
18
+ type CompoundParentConfig = {
19
+ blockName: string;
20
+ namespace: string;
21
+ slug: string;
22
+ styleImport: string;
23
+ textDomain: string;
24
+ title: string;
25
+ typeName: string;
26
+ };
27
+
25
28
  function parseArgs() {
26
29
  const args = process.argv.slice( 2 );
27
30
  const parsed: {
@@ -81,6 +84,26 @@ function toTitleCase( input: string ): string {
81
84
  .join( ' ' );
82
85
  }
83
86
 
87
+ function readJsonFile( filePath: string ): Record< string, unknown > {
88
+ let parsed: unknown;
89
+
90
+ try {
91
+ parsed = JSON.parse( fs.readFileSync( filePath, 'utf8' ) );
92
+ } catch ( error ) {
93
+ const errorMessage = error instanceof Error ? error.message : String( error );
94
+ throw new Error(
95
+ `Unable to parse JSON from ${ filePath }: ${ errorMessage }`,
96
+ { cause: error instanceof Error ? error : undefined }
97
+ );
98
+ }
99
+
100
+ if ( ! parsed || typeof parsed !== 'object' || Array.isArray( parsed ) ) {
101
+ throw new Error( `${ filePath } must contain a JSON object.` );
102
+ }
103
+
104
+ return parsed as Record< string, unknown >;
105
+ }
106
+
84
107
  function resolveValidatedNamespace( value: string ): string {
85
108
  const normalizedNamespace = toKebabCase( value );
86
109
 
@@ -101,6 +124,79 @@ function resolveValidatedBlockSlug( value: string ): string {
101
124
  return normalizedSlug;
102
125
  }
103
126
 
127
+ function resolveCompoundParentConfig(): CompoundParentConfig {
128
+ const blocksRoot = path.join( PROJECT_ROOT, 'src', 'blocks' );
129
+
130
+ if ( ! fs.existsSync( blocksRoot ) ) {
131
+ throw new Error(
132
+ 'This command expects a compound scaffold with src/blocks/<parent>/children.ts and scripts/block-config.ts.'
133
+ );
134
+ }
135
+
136
+ const parentCandidates = fs
137
+ .readdirSync( blocksRoot, { withFileTypes: true } )
138
+ .filter( ( entry ) => entry.isDirectory() )
139
+ .map( ( entry ) => path.join( blocksRoot, entry.name ) )
140
+ .filter( ( candidateDir ) =>
141
+ fs.existsSync( path.join( candidateDir, 'children.ts' ) )
142
+ );
143
+
144
+ if ( parentCandidates.length !== 1 ) {
145
+ throw new Error(
146
+ `Unable to resolve the compound parent block. Expected exactly one src/blocks/<parent>/children.ts entry, found ${ parentCandidates.length }.`
147
+ );
148
+ }
149
+
150
+ const parentDir = parentCandidates[ 0 ];
151
+ const parentSlug = resolveValidatedBlockSlug( path.basename( parentDir ) );
152
+ const blockJsonPath = path.join( parentDir, 'block.json' );
153
+
154
+ if ( ! fs.existsSync( blockJsonPath ) ) {
155
+ throw new Error( `Unable to resolve ${ blockJsonPath } for the compound parent block.` );
156
+ }
157
+
158
+ const blockJson = readJsonFile( blockJsonPath );
159
+ const blockName = typeof blockJson.name === 'string' ? blockJson.name.trim() : '';
160
+ const separatorIndex = blockName.indexOf( '/' );
161
+
162
+ if ( separatorIndex <= 0 || separatorIndex === blockName.length - 1 ) {
163
+ throw new Error(
164
+ `The parent block metadata at ${ blockJsonPath } must declare a valid "name" like "namespace/slug".`
165
+ );
166
+ }
167
+
168
+ const namespace = resolveValidatedNamespace( blockName.slice( 0, separatorIndex ) );
169
+ const title =
170
+ typeof blockJson.title === 'string' && blockJson.title.trim().length > 0
171
+ ? blockJson.title.trim()
172
+ : toTitleCase( parentSlug );
173
+ const textDomain =
174
+ typeof blockJson.textdomain === 'string' &&
175
+ blockJson.textdomain.trim().length > 0
176
+ ? blockJson.textdomain.trim()
177
+ : parentSlug;
178
+
179
+ return {
180
+ blockName,
181
+ namespace,
182
+ slug: parentSlug,
183
+ styleImport: `../${ parentSlug }/style.scss`,
184
+ textDomain,
185
+ title,
186
+ typeName: toPascalCase( parentSlug ),
187
+ };
188
+ }
189
+
190
+ const {
191
+ blockName: PARENT_BLOCK_NAME,
192
+ namespace: PARENT_BLOCK_NAMESPACE,
193
+ slug: PARENT_BLOCK_SLUG,
194
+ styleImport: PARENT_STYLE_IMPORT,
195
+ textDomain: TEXT_DOMAIN,
196
+ title: PARENT_BLOCK_TITLE,
197
+ typeName: PARENT_TYPE_NAME,
198
+ } = resolveCompoundParentConfig();
199
+
104
200
  function buildBlockCssClassName( namespace: string, slug: string ): string {
105
201
  const normalizedSlug = resolveValidatedBlockSlug( slug );
106
202
  const normalizedNamespace =
@@ -5,11 +5,17 @@ import {
5
5
  type ValidationResult,
6
6
  } from '@wp-typia/api-client';
7
7
  import type {
8
+ {{pascalCase}}BootstrapQuery,
9
+ {{pascalCase}}BootstrapResponse,
8
10
  {{pascalCase}}StateQuery,
9
11
  {{pascalCase}}StateResponse,
10
12
  {{pascalCase}}WriteStateRequest,
11
13
  } from './api-types';
12
14
 
15
+ const validateBootstrapQuery =
16
+ typia.createValidate< {{pascalCase}}BootstrapQuery >();
17
+ const validateBootstrapResponse =
18
+ typia.createValidate< {{pascalCase}}BootstrapResponse >();
13
19
  const validateStateQuery = typia.createValidate< {{pascalCase}}StateQuery >();
14
20
  const validateWriteStateRequest =
15
21
  typia.createValidate< {{pascalCase}}WriteStateRequest >();
@@ -17,6 +23,14 @@ const validateStateResponse =
17
23
  typia.createValidate< {{pascalCase}}StateResponse >();
18
24
 
19
25
  export const apiValidators = {
26
+ bootstrapQuery: (
27
+ input: unknown
28
+ ): ValidationResult< {{pascalCase}}BootstrapQuery > =>
29
+ toValidationResult( validateBootstrapQuery( input ) ),
30
+ bootstrapResponse: (
31
+ input: unknown
32
+ ): ValidationResult< {{pascalCase}}BootstrapResponse > =>
33
+ toValidationResult( validateBootstrapResponse( input ) ),
20
34
  stateQuery: (
21
35
  input: unknown
22
36
  ): ValidationResult< {{pascalCase}}StateQuery > =>
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  callEndpoint,
3
+ type ApiEndpoint as RestApiEndpoint,
3
4
  } from '@wp-typia/rest';
4
5
 
5
6
  import {
@@ -17,17 +18,35 @@ import {
17
18
  type PersistenceTransportOptions,
18
19
  } from './transport';
19
20
 
20
- export const bootstrapEndpoint = {
21
- ...get{{pascalCase}}BootstrapEndpoint,
22
- };
21
+ function createRestEndpoint< Req, Res >(
22
+ endpoint: {
23
+ method: RestApiEndpoint< Req, Res >[ 'method' ];
24
+ path: string;
25
+ validateRequest: RestApiEndpoint< Req, Res >[ 'validateRequest' ];
26
+ validateResponse: RestApiEndpoint< Req, Res >[ 'validateResponse' ];
27
+ }
28
+ ): RestApiEndpoint< Req, Res > {
29
+ // Strip generator-only helper fields so the runtime client only sees the
30
+ // canonical RestApiEndpoint surface it expects.
31
+ return {
32
+ method: endpoint.method,
33
+ path: endpoint.path,
34
+ validateRequest: endpoint.validateRequest,
35
+ validateResponse: endpoint.validateResponse,
36
+ };
37
+ }
38
+
39
+ export const bootstrapEndpoint = createRestEndpoint(
40
+ get{{pascalCase}}BootstrapEndpoint
41
+ );
23
42
 
24
- export const stateEndpoint = {
25
- ...get{{pascalCase}}StateEndpoint,
26
- };
43
+ export const stateEndpoint = createRestEndpoint(
44
+ get{{pascalCase}}StateEndpoint
45
+ );
27
46
 
28
- export const writeStateEndpoint = {
29
- ...write{{pascalCase}}StateEndpoint,
30
- };
47
+ export const writeStateEndpoint = createRestEndpoint(
48
+ write{{pascalCase}}StateEndpoint
49
+ );
31
50
 
32
51
  export function fetchState(
33
52
  request: {{pascalCase}}StateQuery,
@@ -7,6 +7,9 @@ import type {
7
7
  {{pascalCase}}Context,
8
8
  {{pascalCase}}State,
9
9
  } from './types';
10
+ import type {
11
+ {{pascalCase}}WriteStateRequest,
12
+ } from './api-types';
10
13
 
11
14
  function hasExpiredPublicWriteToken(
12
15
  expiresAt?: number
@@ -123,6 +126,7 @@ const { actions, state } = store( '{{slugKebabCase}}', {
123
126
  let bootstrapSucceeded = false;
124
127
  let lastBootstrapError =
125
128
  'Unable to initialize write access';
129
+ const includeRestNonce = {{isAuthenticatedPersistencePolicy}};
126
130
 
127
131
  for ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {
128
132
  try {
@@ -156,6 +160,8 @@ const { actions, state } = store( '{{slugKebabCase}}', {
156
160
  ? result.data.publicWriteToken
157
161
  : '';
158
162
  clientState.writeNonce =
163
+ includeRestNonce &&
164
+ 'restNonce' in result.data &&
159
165
  typeof result.data.restNonce === 'string' &&
160
166
  result.data.restNonce.length > 0
161
167
  ? result.data.restNonce
@@ -231,20 +237,20 @@ const { actions, state } = store( '{{slugKebabCase}}', {
231
237
  context.error = '';
232
238
 
233
239
  try {
234
- const result = await writeState( {
240
+ const request = {
235
241
  delta: 1,
236
242
  postId: context.postId,
237
- publicWriteRequestId:
238
- context.persistencePolicy === 'public'
239
- ? generatePublicWriteRequestId()
240
- : undefined,
241
- publicWriteToken:
242
- context.persistencePolicy === 'public' &&
243
- clientState.writeToken.length > 0
244
- ? clientState.writeToken
245
- : undefined,
246
243
  resourceKey: context.resourceKey,
247
- }, {
244
+ } as {{pascalCase}}WriteStateRequest;
245
+ if ( {{isPublicPersistencePolicy}} ) {
246
+ request.publicWriteRequestId =
247
+ generatePublicWriteRequestId() as {{pascalCase}}WriteStateRequest[ 'publicWriteRequestId' ];
248
+ if ( clientState.writeToken.length > 0 ) {
249
+ request.publicWriteToken =
250
+ clientState.writeToken as {{pascalCase}}WriteStateRequest[ 'publicWriteToken' ];
251
+ }
252
+ }
253
+ const result = await writeState( request, {
248
254
  restNonce:
249
255
  clientState.writeNonce.length > 0
250
256
  ? clientState.writeNonce
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "textdomain": "{{textDomain}}",
70
70
  "editorScript": "file:./index.js",
71
+ "editorStyle": "file:./index.css",
71
72
  "style": "file:./style-index.css",
72
73
  "viewScriptModule": "file:./interactivity.js"
73
74
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * {{title}} Block Editor Styles
3
+ */
4
+
5
+ .{{cssClassName}} {
6
+ outline: 1px dashed #ddd;
7
+ outline-offset: -1px;
8
+ }
@@ -9,6 +9,7 @@ import {
9
9
  import Edit from './edit';
10
10
  import Save from './save';
11
11
  import metadata from './block.json';
12
+ import './editor.scss';
12
13
  import './style.scss';
13
14
 
14
15
  import type { {{pascalCase}}Attributes } from './types';
@@ -66,15 +66,15 @@ export default function Edit( {
66
66
  validateEditorUpdate
67
67
  );
68
68
  const alignmentValue = editorFields.getStringValue(
69
- attributes,
69
+ attributes as unknown as Record< string, unknown >,
70
70
  'alignment',
71
71
  'left'
72
72
  );
73
73
  const persistencePolicy = '{{persistencePolicy}}';
74
- const persistencePolicyDescription =
75
- persistencePolicy === 'authenticated'
76
- ? __( 'Writes require a logged-in user and a valid REST nonce.', '{{textDomain}}' )
77
- : __( 'Anonymous writes use signed short-lived public tokens, per-request ids, and coarse rate limiting.', '{{textDomain}}' );
74
+ const persistencePolicyDescription = __(
75
+ {{persistencePolicyDescriptionJson}},
76
+ '{{textDomain}}'
77
+ );
78
78
 
79
79
  return (
80
80
  <>
@@ -91,7 +91,7 @@ export default function Edit( {
91
91
  </BlockControls>
92
92
  <InspectorControls>
93
93
  <InspectorFromManifest
94
- attributes={ attributes }
94
+ attributes={ attributes as unknown as Record< string, unknown > }
95
95
  fieldLookup={ editorFields }
96
96
  onChange={ updateField }
97
97
  paths={ [ 'alignment', 'isVisible', 'showCount', 'buttonLabel' ] }