@wp-typia/create-workspace-template 0.17.0 → 0.18.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/README.md +110 -11
- package/README.md.mustache +84 -9
- package/inc/rest-schema.php.mustache +184 -0
- package/package.json +2 -1
- package/package.json.mustache +7 -0
- package/scripts/block-config.ts.mustache +33 -1
- package/scripts/sync-rest-contracts.ts.mustache +449 -0
- package/scripts/sync-types-to-block-json.ts.mustache +138 -2
- package/src/inner-blocks-templates.ts.mustache +13 -0
- package/{{slugKebabCase}}.php.mustache +14 -1
package/README.md
CHANGED
|
@@ -6,20 +6,119 @@ Use it through the canonical CLI:
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx wp-typia create my-plugin --template @wp-typia/create-workspace-template
|
|
9
|
+
npx wp-typia create my-plugin --template workspace --profile plugin-qa
|
|
9
10
|
```
|
|
10
11
|
|
|
11
12
|
The generated project starts with an empty `src/blocks/*` workspace shell and is designed to grow through:
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
|
-
wp-typia
|
|
15
|
-
wp-typia
|
|
16
|
-
wp-typia
|
|
17
|
-
wp-typia
|
|
18
|
-
wp-typia
|
|
19
|
-
wp-typia
|
|
20
|
-
wp-typia
|
|
21
|
-
wp-typia
|
|
22
|
-
wp-typia
|
|
23
|
-
wp-typia
|
|
24
|
-
wp-typia
|
|
15
|
+
npm run wp-typia:add -- block my-block --template basic
|
|
16
|
+
npm run wp-typia:add -- integration-env local-smoke --wp-env --release-zip --service docker-compose
|
|
17
|
+
npm run wp-typia:add -- style callout-emphasis --block my-block
|
|
18
|
+
npm run wp-typia:add -- transform quote-to-block --from core/quote --to my-block
|
|
19
|
+
npm run wp-typia:add -- pattern hero-layout
|
|
20
|
+
npm run wp-typia:add -- pattern hero-photo-section --scope section --section-role hero --tags hero,image
|
|
21
|
+
npm run wp-typia:add -- binding-source hero-data
|
|
22
|
+
npm run wp-typia:add -- binding-source hero-data --block my-block --attribute headline
|
|
23
|
+
npm run wp-typia:add -- contract external-retrieve-response --type ExternalRetrieveResponse
|
|
24
|
+
npm run wp-typia:add -- rest-resource external-record --manual --namespace legacy/v1 --route-pattern '/records/(?P<post_id>[\d]+)' --permission-callback legacy_can_read_records --controller-class Legacy\\Records\\Controller
|
|
25
|
+
npm run wp-typia:add -- post-meta integration-state --post-type post --type IntegrationStateMeta
|
|
26
|
+
npm run wp-typia:add -- binding-source integration-state-source --from-post-meta integration-state --meta-path status --block my-block --attribute headline
|
|
27
|
+
npm run wp-typia:add -- editor-plugin review-workflow --slot sidebar
|
|
28
|
+
npm run wp-typia:add -- editor-plugin seo-notes --slot document-setting-panel
|
|
29
|
+
npm run wp-typia:add -- hooked-block my-block --anchor core/post-content --position after
|
|
25
30
|
```
|
|
31
|
+
|
|
32
|
+
Typed block nesting rules live in `BLOCK_NESTING` inside
|
|
33
|
+
`scripts/block-config.ts`. Declare `parent`, `ancestor`, or `allowedBlocks`
|
|
34
|
+
relationships there and `wp-typia sync --check` will validate referenced block
|
|
35
|
+
names in the workspace namespace while allowing external targets like
|
|
36
|
+
`core/group`, then keep the matching `block.json` metadata current.
|
|
37
|
+
Declare default editor `InnerBlocks` templates in `BLOCK_TEMPLATES` and
|
|
38
|
+
`wp-typia sync` will generate `src/inner-blocks-templates.ts` with constants you
|
|
39
|
+
can import from edit components.
|
|
40
|
+
Pattern files listed in `PATTERNS` are parsed during the same sync flow so
|
|
41
|
+
`wp-typia sync --check` can first validate typed catalog metadata such as
|
|
42
|
+
duplicate slugs, `scope`, `sectionRole`, `tags`, `thumbnailUrl`, and
|
|
43
|
+
`contentFile`, then catch serialized block content that violates the declared
|
|
44
|
+
`allowedBlocks`, `parent`, or `ancestor` rules. Unknown or unparseable pattern
|
|
45
|
+
blocks are reported as warnings so the first implementation stays non-mutating.
|
|
46
|
+
The validator reads serialized `<!-- wp:* -->` block comment boundaries
|
|
47
|
+
conservatively and does not execute dynamic PHP content.
|
|
48
|
+
For a complete generic family with a container, section, title, body, and media
|
|
49
|
+
block, see the Nesting Contracts Guide in the hosted docs and the checked
|
|
50
|
+
fixture at `tests/fixtures/nested-block-family.ts`.
|
|
51
|
+
|
|
52
|
+
## CLI binary policy
|
|
53
|
+
|
|
54
|
+
Official workspace projects install `wp-typia` as a local devDependency and
|
|
55
|
+
expose package scripts for the common CLI entrypoints:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"scripts": {
|
|
60
|
+
"sync-rest:package": "tsx scripts/sync-rest-contracts.ts --package",
|
|
61
|
+
"sync-rest:package:check": "tsx scripts/sync-rest-contracts.ts --package --check",
|
|
62
|
+
"wp-typia:sync": "wp-typia sync",
|
|
63
|
+
"wp-typia:doctor": "wp-typia doctor",
|
|
64
|
+
"wp-typia:doctor:workspace": "wp-typia doctor --workspace-only",
|
|
65
|
+
"wp-typia:add": "wp-typia add"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"wp-typia": "<version>"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Minimal Workspace vs Plugin QA Profile
|
|
74
|
+
|
|
75
|
+
Use the default workspace template when you want the smallest editable shell and
|
|
76
|
+
plan to add QA infrastructure later. Use `--profile plugin-qa` when the plugin
|
|
77
|
+
should start with local WordPress smoke checks, `.wp-env.json`, `.env.example`,
|
|
78
|
+
`scripts/integration-smoke/local-smoke.mjs`, and release zip scripts.
|
|
79
|
+
|
|
80
|
+
Existing minimal workspaces can adopt the same QA surface later:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm run wp-typia:add -- integration-env local-smoke --wp-env --release-zip
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use the script form from generated workspaces so the executable CLI package is
|
|
87
|
+
clearly separated from support packages such as `@wp-typia/project-tools`.
|
|
88
|
+
|
|
89
|
+
Workspaces that ship REST resources can run `sync-rest:package` before release
|
|
90
|
+
builds to copy generated runtime schemas into `inc/rest-schemas`. Add
|
|
91
|
+
`sync-rest:package:check` to release CI so stale or missing packaged schemas are
|
|
92
|
+
caught before a zip is built without TypeScript source files.
|
|
93
|
+
|
|
94
|
+
The generated `inc/rest-schema.php` helper centralizes schema loading and
|
|
95
|
+
WordPress REST sanitization. Generated REST resources use it automatically, and
|
|
96
|
+
custom PHP resources can call
|
|
97
|
+
`<phpPrefix>_get_wordpress_rest_schema( 'settings-response', array( 'resource' => 'settings' ) )`
|
|
98
|
+
or `<phpPrefix>_validate_and_sanitize_rest_payload(...)` to reuse the same
|
|
99
|
+
packaged/source lookup and `WP_Error` handling.
|
|
100
|
+
|
|
101
|
+
Use generated REST resources when the workspace should own the PHP route glue.
|
|
102
|
+
Use `wp-typia add rest-resource <name> --manual` for provider routes owned by
|
|
103
|
+
another plugin, editor contract, or legacy controller. Manual routes can declare
|
|
104
|
+
custom namespaces, `--path` or `--route-pattern` patterns with named captures,
|
|
105
|
+
and route-owner metadata such as `--permission-callback`,
|
|
106
|
+
`--controller-class`, and `--controller-extends` while still generating typed
|
|
107
|
+
schemas, OpenAPI, clients, and drift checks.
|
|
108
|
+
|
|
109
|
+
Manual settings contracts can model write-only credentials with
|
|
110
|
+
`--secret-field <field>`. The generated request type marks the field as
|
|
111
|
+
`tags.Secret<"has<Field>">` plus `tags.PreserveOnEmpty<true>`, response types
|
|
112
|
+
expose only the safe has-value field, and generated admin settings screens omit
|
|
113
|
+
blank secrets so existing stored values are preserved by default.
|
|
114
|
+
|
|
115
|
+
| Package manager | Doctor | Sync check | Add a block |
|
|
116
|
+
| --------------- | -------------------------- | ---------------------------------- | --------------------------------------------------------- |
|
|
117
|
+
| npm | `npm run wp-typia:doctor` | `npm run wp-typia:sync -- --check` | `npm run wp-typia:add -- block my-block --template basic` |
|
|
118
|
+
| pnpm | `pnpm run wp-typia:doctor` | `pnpm run wp-typia:sync --check` | `pnpm run wp-typia:add block my-block --template basic` |
|
|
119
|
+
| bun | `bun run wp-typia:doctor` | `bun run wp-typia:sync --check` | `bun run wp-typia:add block my-block --template basic` |
|
|
120
|
+
| yarn | `yarn run wp-typia:doctor` | `yarn run wp-typia:sync --check` | `yarn run wp-typia:add block my-block --template basic` |
|
|
121
|
+
|
|
122
|
+
Existing workspaces can adopt the same scripts without regenerating. Pin
|
|
123
|
+
`wp-typia` to the project version you want CI to run, then use
|
|
124
|
+
`wp-typia:doctor:workspace` when optional local runtimes should be advisory.
|
package/README.md.mustache
CHANGED
|
@@ -7,31 +7,106 @@
|
|
|
7
7
|
This scaffold is the official empty `wp-typia` workspace shell. It starts with
|
|
8
8
|
an empty `src/blocks/*` inventory and grows through `wp-typia add`.
|
|
9
9
|
|
|
10
|
+
Use the minimal workspace when you want the smallest editable shell and plan to
|
|
11
|
+
add QA infrastructure later. Scaffold with `--profile plugin-qa` when the plugin
|
|
12
|
+
should start with local WordPress smoke checks, `.wp-env.json`, `.env.example`,
|
|
13
|
+
`scripts/integration-smoke/local-smoke.mjs`, and release zip scripts. Existing
|
|
14
|
+
minimal workspaces can adopt the same surface later:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun run wp-typia:add integration-env local-smoke --wp-env --release-zip
|
|
18
|
+
```
|
|
19
|
+
|
|
10
20
|
## First Block
|
|
11
21
|
|
|
12
22
|
```bash
|
|
13
|
-
wp-typia
|
|
23
|
+
bun run wp-typia:add block counter-card --template basic
|
|
14
24
|
```
|
|
15
25
|
|
|
16
26
|
Persistence-ready blocks and compound parent/child blocks can be added later:
|
|
17
27
|
|
|
18
28
|
```bash
|
|
19
|
-
wp-typia
|
|
20
|
-
wp-typia
|
|
29
|
+
bun run wp-typia:add block counter-card --template persistence --data-storage custom-table --persistence-policy authenticated
|
|
30
|
+
bun run wp-typia:add block faq-stack --template compound --data-storage custom-table --persistence-policy public
|
|
21
31
|
```
|
|
22
32
|
|
|
23
33
|
Variations, block styles, transforms, patterns, REST resources, and post meta
|
|
24
34
|
contracts can also be added after the first block exists:
|
|
25
35
|
|
|
26
36
|
```bash
|
|
27
|
-
wp-typia
|
|
28
|
-
wp-typia
|
|
29
|
-
wp-typia
|
|
30
|
-
wp-typia
|
|
31
|
-
wp-typia
|
|
32
|
-
wp-typia
|
|
37
|
+
bun run wp-typia:add variation hero-card --block counter-card
|
|
38
|
+
bun run wp-typia:add style callout-emphasis --block counter-card
|
|
39
|
+
bun run wp-typia:add transform quote-to-counter --from core/quote --to counter-card
|
|
40
|
+
bun run wp-typia:add pattern hero-layout
|
|
41
|
+
bun run wp-typia:add pattern hero-photo-section --scope section --section-role hero --tags hero,image
|
|
42
|
+
bun run wp-typia:add rest-resource snapshots --namespace {{namespace}}/v1 --methods list,read,create
|
|
43
|
+
bun run wp-typia:add rest-resource external-record --manual --namespace legacy/v1 --route-pattern '/records/(?P<post_id>[\d]+)' --permission-callback legacy_can_read_records --controller-class Legacy\\Records\\Controller
|
|
44
|
+
bun run wp-typia:add post-meta integration-state --post-type post
|
|
45
|
+
bun run wp-typia:add binding-source integration-state-source --from-post-meta integration-state --meta-path status --block counter-card --attribute headline
|
|
33
46
|
```
|
|
34
47
|
|
|
48
|
+
Typed block nesting rules live in `BLOCK_NESTING` inside
|
|
49
|
+
`scripts/block-config.ts`. Declare `parent`, `ancestor`, or `allowedBlocks`
|
|
50
|
+
relationships there and `wp-typia sync --check` will validate referenced block
|
|
51
|
+
names in the workspace namespace while allowing external targets like
|
|
52
|
+
`core/group`, then keep the matching `block.json` metadata current.
|
|
53
|
+
Declare default editor `InnerBlocks` templates in `BLOCK_TEMPLATES` and
|
|
54
|
+
`wp-typia sync` will generate `src/inner-blocks-templates.ts` with constants you
|
|
55
|
+
can import from edit components.
|
|
56
|
+
Pattern files listed in `PATTERNS` are parsed during the same sync flow so
|
|
57
|
+
`wp-typia sync --check` can first validate typed catalog metadata such as
|
|
58
|
+
duplicate slugs, `scope`, `sectionRole`, `tags`, `thumbnailUrl`, and
|
|
59
|
+
`contentFile`, then catch serialized block content that violates the declared
|
|
60
|
+
`allowedBlocks`, `parent`, or `ancestor` rules. Unknown or unparseable pattern
|
|
61
|
+
blocks are reported as warnings so the first implementation stays non-mutating.
|
|
62
|
+
The validator reads serialized `<!-- wp:* -->` block comment boundaries
|
|
63
|
+
conservatively and does not execute dynamic PHP content.
|
|
64
|
+
For a complete generic family with a container, section, title, body, and media
|
|
65
|
+
block, see the Nesting Contracts Guide in the hosted docs and the checked
|
|
66
|
+
fixture at `tests/fixtures/nested-block-family.ts`.
|
|
67
|
+
|
|
68
|
+
## CLI Commands
|
|
69
|
+
|
|
70
|
+
This workspace installs the executable `wp-typia` CLI as a local devDependency
|
|
71
|
+
and exposes stable package scripts so support packages are not confused with the
|
|
72
|
+
binary package:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bun run wp-typia:doctor
|
|
76
|
+
bun run wp-typia:doctor:workspace
|
|
77
|
+
bun run wp-typia:sync --check
|
|
78
|
+
bun run sync-rest:package:check
|
|
79
|
+
bun run wp-typia:add block counter-card --template basic
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use `wp-typia:doctor:workspace` in CI when optional local runtimes are advisory
|
|
83
|
+
but workspace checks must still fail the job.
|
|
84
|
+
|
|
85
|
+
Run `sync-rest:package` before building release zips when REST resources are
|
|
86
|
+
registered. It copies generated runtime schemas into `inc/rest-schemas` and
|
|
87
|
+
`sync-rest:package:check` fails CI if packaged schemas are missing or stale.
|
|
88
|
+
|
|
89
|
+
The generated `inc/rest-schema.php` helper centralizes schema loading and
|
|
90
|
+
WordPress REST sanitization. Generated REST resources use it automatically, and
|
|
91
|
+
custom PHP resources can call
|
|
92
|
+
`{{phpPrefix}}_get_wordpress_rest_schema( 'settings-response', array( 'resource' => 'settings' ) )`
|
|
93
|
+
or `{{phpPrefix}}_validate_and_sanitize_rest_payload(...)` to reuse the same
|
|
94
|
+
packaged/source lookup and `WP_Error` handling.
|
|
95
|
+
|
|
96
|
+
Use generated REST resources when the workspace should own the PHP route glue.
|
|
97
|
+
Use `wp-typia add rest-resource <name> --manual` for provider routes owned by
|
|
98
|
+
another plugin, editor contract, or legacy controller. Manual routes can declare
|
|
99
|
+
custom namespaces, `--path` or `--route-pattern` patterns with named captures,
|
|
100
|
+
and route-owner metadata such as `--permission-callback`,
|
|
101
|
+
`--controller-class`, and `--controller-extends` while still generating typed
|
|
102
|
+
schemas, OpenAPI, clients, and drift checks.
|
|
103
|
+
|
|
104
|
+
Manual settings contracts can model write-only credentials with
|
|
105
|
+
`--secret-field <field>`. The generated request type marks the field as
|
|
106
|
+
`tags.Secret<"has<Field>">` plus `tags.PreserveOnEmpty<true>`, response types
|
|
107
|
+
expose only the safe has-value field, and generated admin settings screens omit
|
|
108
|
+
blank secrets so existing stored values are preserved by default.
|
|
109
|
+
|
|
35
110
|
## Development
|
|
36
111
|
|
|
37
112
|
```bash
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
if ( ! defined( 'ABSPATH' ) ) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if ( ! function_exists( '{{phpPrefix}}_is_valid_rest_schema_key' ) ) {
|
|
7
|
+
function {{phpPrefix}}_is_valid_rest_schema_key( $value ) {
|
|
8
|
+
return is_string( $value ) && 1 === preg_match( '/\A[A-Za-z0-9_-]+\z/', $value );
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if ( ! function_exists( '{{phpPrefix}}_is_valid_rest_resource_slug' ) ) {
|
|
13
|
+
function {{phpPrefix}}_is_valid_rest_resource_slug( $value ) {
|
|
14
|
+
return is_string( $value ) && 1 === preg_match( '/\A[a-z0-9]+(?:-[a-z0-9]+)*\z/', $value );
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if ( ! function_exists( '{{phpPrefix}}_resolve_rest_schema_paths' ) ) {
|
|
19
|
+
function {{phpPrefix}}_resolve_rest_schema_paths( $schema_name, $options = array() ) {
|
|
20
|
+
if ( ! {{phpPrefix}}_is_valid_rest_schema_key( $schema_name ) ) {
|
|
21
|
+
return new WP_Error(
|
|
22
|
+
'invalid_rest_schema_name',
|
|
23
|
+
'Invalid REST schema name.',
|
|
24
|
+
array( 'status' => 500 )
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$options = is_array( $options ) ? $options : array();
|
|
29
|
+
$project_root = dirname( __DIR__ );
|
|
30
|
+
$paths = array();
|
|
31
|
+
|
|
32
|
+
if ( isset( $options['resource'] ) && '' !== $options['resource'] ) {
|
|
33
|
+
if ( ! {{phpPrefix}}_is_valid_rest_resource_slug( $options['resource'] ) ) {
|
|
34
|
+
return new WP_Error(
|
|
35
|
+
'invalid_rest_schema_resource',
|
|
36
|
+
'Invalid REST schema resource slug.',
|
|
37
|
+
array( 'status' => 500 )
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
$resource_slug = $options['resource'];
|
|
42
|
+
$paths[] = __DIR__ . '/rest-schemas/rest/' . $resource_slug . '/' . $schema_name . '.schema.json';
|
|
43
|
+
$paths[] = $project_root . '/src/rest/' . $resource_slug . '/api-schemas/' . $schema_name . '.schema.json';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ( isset( $options['paths'] ) && is_array( $options['paths'] ) ) {
|
|
47
|
+
foreach ( $options['paths'] as $schema_path ) {
|
|
48
|
+
if ( is_string( $schema_path ) && '' !== $schema_path && ! in_array( $schema_path, $paths, true ) ) {
|
|
49
|
+
$paths[] = $schema_path;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return $paths;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ( ! function_exists( '{{phpPrefix}}_load_rest_schema' ) ) {
|
|
59
|
+
function {{phpPrefix}}_load_rest_schema( $schema_name, $options = array() ) {
|
|
60
|
+
$schema_paths = {{phpPrefix}}_resolve_rest_schema_paths( $schema_name, $options );
|
|
61
|
+
if ( is_wp_error( $schema_paths ) ) {
|
|
62
|
+
return $schema_paths;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
foreach ( $schema_paths as $schema_path ) {
|
|
66
|
+
if ( ! is_file( $schema_path ) ) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ( ! is_readable( $schema_path ) ) {
|
|
71
|
+
return new WP_Error(
|
|
72
|
+
'unreadable_rest_schema',
|
|
73
|
+
'Generated REST schema is not readable.',
|
|
74
|
+
array(
|
|
75
|
+
'path' => $schema_path,
|
|
76
|
+
'status' => 500,
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$schema_json = file_get_contents( $schema_path );
|
|
82
|
+
if ( false === $schema_json ) {
|
|
83
|
+
return new WP_Error(
|
|
84
|
+
'rest_schema_read_failed',
|
|
85
|
+
'Generated REST schema could not be read.',
|
|
86
|
+
array(
|
|
87
|
+
'path' => $schema_path,
|
|
88
|
+
'status' => 500,
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
$decoded = json_decode( $schema_json, true );
|
|
94
|
+
if ( ! is_array( $decoded ) ) {
|
|
95
|
+
return new WP_Error(
|
|
96
|
+
'malformed_rest_schema',
|
|
97
|
+
'Generated REST schema contains malformed JSON.',
|
|
98
|
+
array(
|
|
99
|
+
'json_error' => json_last_error_msg(),
|
|
100
|
+
'path' => $schema_path,
|
|
101
|
+
'status' => 500,
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return $decoded;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new WP_Error(
|
|
110
|
+
'missing_rest_schema',
|
|
111
|
+
'Generated REST schema could not be found.',
|
|
112
|
+
array(
|
|
113
|
+
'paths' => $schema_paths,
|
|
114
|
+
'schema' => $schema_name,
|
|
115
|
+
'status' => 500,
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if ( ! function_exists( '{{phpPrefix}}_prepare_rest_schema_for_wordpress' ) ) {
|
|
122
|
+
function {{phpPrefix}}_prepare_rest_schema_for_wordpress( $schema ) {
|
|
123
|
+
if ( ! is_array( $schema ) ) {
|
|
124
|
+
return $schema;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
unset( $schema['$schema'], $schema['title'] );
|
|
128
|
+
|
|
129
|
+
foreach ( array( 'properties', 'patternProperties', 'definitions', '$defs' ) as $schema_map_key ) {
|
|
130
|
+
if ( ! isset( $schema[ $schema_map_key ] ) || ! is_array( $schema[ $schema_map_key ] ) ) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
foreach ( $schema[ $schema_map_key ] as $key => $property_schema ) {
|
|
135
|
+
$schema[ $schema_map_key ][ $key ] = {{phpPrefix}}_prepare_rest_schema_for_wordpress( $property_schema );
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
foreach ( array( 'items', 'additionalProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else' ) as $nested_schema_key ) {
|
|
140
|
+
if ( isset( $schema[ $nested_schema_key ] ) && is_array( $schema[ $nested_schema_key ] ) ) {
|
|
141
|
+
$schema[ $nested_schema_key ] = {{phpPrefix}}_prepare_rest_schema_for_wordpress( $schema[ $nested_schema_key ] );
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
foreach ( array( 'allOf', 'anyOf', 'oneOf' ) as $schema_list_key ) {
|
|
146
|
+
if ( ! isset( $schema[ $schema_list_key ] ) || ! is_array( $schema[ $schema_list_key ] ) ) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
foreach ( $schema[ $schema_list_key ] as $index => $variant_schema ) {
|
|
151
|
+
$schema[ $schema_list_key ][ $index ] = {{phpPrefix}}_prepare_rest_schema_for_wordpress( $variant_schema );
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return $schema;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if ( ! function_exists( '{{phpPrefix}}_get_wordpress_rest_schema' ) ) {
|
|
160
|
+
function {{phpPrefix}}_get_wordpress_rest_schema( $schema_name, $options = array() ) {
|
|
161
|
+
$schema = {{phpPrefix}}_load_rest_schema( $schema_name, $options );
|
|
162
|
+
if ( is_wp_error( $schema ) ) {
|
|
163
|
+
return $schema;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {{phpPrefix}}_prepare_rest_schema_for_wordpress( $schema );
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if ( ! function_exists( '{{phpPrefix}}_validate_and_sanitize_rest_payload' ) ) {
|
|
171
|
+
function {{phpPrefix}}_validate_and_sanitize_rest_payload( $value, $schema_name, $param_name, $options = array() ) {
|
|
172
|
+
$rest_schema = {{phpPrefix}}_get_wordpress_rest_schema( $schema_name, $options );
|
|
173
|
+
if ( is_wp_error( $rest_schema ) ) {
|
|
174
|
+
return $rest_schema;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
|
|
178
|
+
if ( is_wp_error( $validation ) ) {
|
|
179
|
+
return $validation;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
|
|
183
|
+
}
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wp-typia/create-workspace-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Official empty wp-typia workspace template package",
|
|
5
5
|
"packageManager": "bun@1.3.11",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"package.json.mustache",
|
|
9
9
|
"tsconfig.json.mustache",
|
|
10
10
|
"webpack.config.js.mustache",
|
|
11
|
+
"inc/",
|
|
11
12
|
"scripts/",
|
|
12
13
|
"src/",
|
|
13
14
|
"languages/",
|
package/package.json.mustache
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"sync": "tsx scripts/sync-project.ts",
|
|
10
10
|
"sync-types": "tsx scripts/sync-types-to-block-json.ts",
|
|
11
11
|
"sync-rest": "tsx scripts/sync-rest-contracts.ts",
|
|
12
|
+
"sync-rest:package": "tsx scripts/sync-rest-contracts.ts --package",
|
|
13
|
+
"sync-rest:package:check": "tsx scripts/sync-rest-contracts.ts --package --check",
|
|
14
|
+
"wp-typia:sync": "wp-typia sync",
|
|
15
|
+
"wp-typia:doctor": "wp-typia doctor",
|
|
16
|
+
"wp-typia:doctor:workspace": "wp-typia doctor --workspace-only",
|
|
17
|
+
"wp-typia:add": "wp-typia add",
|
|
12
18
|
"build": "bun run sync --check && node scripts/build-workspace.mjs build",
|
|
13
19
|
"start": "bun run sync && node scripts/build-workspace.mjs start",
|
|
14
20
|
"dev": "bun run start",
|
|
@@ -31,6 +37,7 @@
|
|
|
31
37
|
"@wp-typia/block-types": "{{blockTypesPackageVersion}}",
|
|
32
38
|
"@wp-typia/project-tools": "{{projectToolsPackageVersion}}",
|
|
33
39
|
"@wp-typia/rest": "{{restPackageVersion}}",
|
|
40
|
+
"wp-typia": "{{wpTypiaPackageVersion}}",
|
|
34
41
|
"@typia/unplugin": "^12.0.1",
|
|
35
42
|
"@types/node": "^24.5.2",
|
|
36
43
|
"@types/wordpress__block-editor": "^11.5.17",
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineBlockNesting,
|
|
3
|
+
defineInnerBlocksTemplates,
|
|
4
|
+
} from '@wp-typia/block-runtime/metadata-core';
|
|
5
|
+
|
|
1
6
|
export interface WorkspaceBlockConfig {
|
|
2
7
|
slug: string;
|
|
3
8
|
attributeTypeName: string;
|
|
@@ -9,6 +14,14 @@ export interface WorkspaceBlockConfig {
|
|
|
9
14
|
>;
|
|
10
15
|
}
|
|
11
16
|
|
|
17
|
+
export const BLOCK_NESTING = defineBlockNesting( {
|
|
18
|
+
// Add parent, ancestor, and allowedBlocks relationships here.
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
export const BLOCK_TEMPLATES = defineInnerBlocksTemplates( {
|
|
22
|
+
// Add default InnerBlocks templates here.
|
|
23
|
+
} );
|
|
24
|
+
|
|
12
25
|
export interface WorkspaceVariationConfig {
|
|
13
26
|
block: string;
|
|
14
27
|
file: string;
|
|
@@ -30,14 +43,22 @@ export interface WorkspaceBlockTransformConfig {
|
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
export interface WorkspacePatternConfig {
|
|
33
|
-
|
|
46
|
+
contentFile?: string;
|
|
47
|
+
file?: string;
|
|
48
|
+
scope?: 'full' | 'section';
|
|
49
|
+
sectionRole?: string;
|
|
34
50
|
slug: string;
|
|
51
|
+
tags?: string[];
|
|
52
|
+
thumbnailUrl?: string;
|
|
53
|
+
title?: string;
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
export interface WorkspaceBindingSourceConfig {
|
|
38
57
|
attribute?: string;
|
|
39
58
|
block?: string;
|
|
40
59
|
editorFile: string;
|
|
60
|
+
metaPath?: string;
|
|
61
|
+
postMeta?: string;
|
|
41
62
|
serverFile: string;
|
|
42
63
|
slug: string;
|
|
43
64
|
}
|
|
@@ -63,6 +84,7 @@ export interface WorkspaceRestResourceBaseConfig {
|
|
|
63
84
|
typeof import( '@wp-typia/block-runtime/metadata-core' ).defineEndpointManifest
|
|
64
85
|
>;
|
|
65
86
|
secretFieldName?: string;
|
|
87
|
+
secretPreserveOnEmpty?: boolean;
|
|
66
88
|
secretStateFieldName?: string;
|
|
67
89
|
slug: string;
|
|
68
90
|
typesFile: string;
|
|
@@ -114,6 +136,11 @@ export interface WorkspaceAdminViewConfig {
|
|
|
114
136
|
source?: string;
|
|
115
137
|
}
|
|
116
138
|
|
|
139
|
+
export interface WorkspaceRestSchemaPackageConfig {
|
|
140
|
+
manifestFile?: string;
|
|
141
|
+
outputDir: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
export const BLOCKS: WorkspaceBlockConfig[] = [
|
|
118
145
|
// wp-typia add block entries
|
|
119
146
|
];
|
|
@@ -157,3 +184,8 @@ export const EDITOR_PLUGINS: WorkspaceEditorPluginConfig[] = [
|
|
|
157
184
|
export const ADMIN_VIEWS: WorkspaceAdminViewConfig[] = [
|
|
158
185
|
// wp-typia add admin-view entries
|
|
159
186
|
];
|
|
187
|
+
|
|
188
|
+
export const REST_SCHEMA_PACKAGE: WorkspaceRestSchemaPackageConfig = {
|
|
189
|
+
manifestFile: 'inc/rest-schemas/manifest.json',
|
|
190
|
+
outputDir: 'inc/rest-schemas',
|
|
191
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
+
import { promises as fsp } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
syncTypeSchemas,
|
|
9
10
|
} from '@wp-typia/block-runtime/metadata-core';
|
|
10
11
|
|
|
12
|
+
import * as workspaceConfig from './block-config';
|
|
11
13
|
import {
|
|
12
14
|
BLOCKS,
|
|
13
15
|
CONTRACTS,
|
|
@@ -19,9 +21,25 @@ import {
|
|
|
19
21
|
type WorkspaceRestResourceConfig,
|
|
20
22
|
} from './block-config';
|
|
21
23
|
|
|
24
|
+
type WorkspaceRestSchemaPackageConfig = {
|
|
25
|
+
manifestFile?: string;
|
|
26
|
+
outputDir: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_REST_SCHEMA_PACKAGE: WorkspaceRestSchemaPackageConfig = {
|
|
30
|
+
manifestFile: 'inc/rest-schemas/manifest.json',
|
|
31
|
+
outputDir: 'inc/rest-schemas',
|
|
32
|
+
};
|
|
33
|
+
const workspaceConfigWithPackage = workspaceConfig as {
|
|
34
|
+
REST_SCHEMA_PACKAGE?: WorkspaceRestSchemaPackageConfig;
|
|
35
|
+
};
|
|
36
|
+
const REST_SCHEMA_PACKAGE =
|
|
37
|
+
workspaceConfigWithPackage.REST_SCHEMA_PACKAGE ?? DEFAULT_REST_SCHEMA_PACKAGE;
|
|
38
|
+
|
|
22
39
|
function parseCliOptions( argv: string[] ) {
|
|
23
40
|
const options = {
|
|
24
41
|
check: false,
|
|
42
|
+
packageSchemas: false,
|
|
25
43
|
};
|
|
26
44
|
|
|
27
45
|
for ( const argument of argv ) {
|
|
@@ -30,12 +48,110 @@ function parseCliOptions( argv: string[] ) {
|
|
|
30
48
|
continue;
|
|
31
49
|
}
|
|
32
50
|
|
|
51
|
+
if ( argument === '--package' ) {
|
|
52
|
+
options.packageSchemas = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
throw new Error( `Unknown sync-rest flag: ${ argument }` );
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
return options;
|
|
37
60
|
}
|
|
38
61
|
|
|
62
|
+
type RuntimeRestSchemaPackageEntry = {
|
|
63
|
+
id: string;
|
|
64
|
+
kind: 'block' | 'rest-resource';
|
|
65
|
+
owner: string;
|
|
66
|
+
path: string;
|
|
67
|
+
schema: string;
|
|
68
|
+
source: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type RuntimeRestSchemaPackageManifest = {
|
|
72
|
+
generatedBy: 'wp-typia sync-rest --package';
|
|
73
|
+
outputDir: string;
|
|
74
|
+
schemaVersion: 1;
|
|
75
|
+
schemas: RuntimeRestSchemaPackageEntry[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function getNodeErrorCode( error: unknown ) {
|
|
79
|
+
return typeof error === 'object' &&
|
|
80
|
+
error !== null &&
|
|
81
|
+
'code' in error &&
|
|
82
|
+
typeof ( error as { code?: unknown } ).code === 'string'
|
|
83
|
+
? ( error as { code: string } ).code
|
|
84
|
+
: undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeProjectPath( filePath: string, label: string ) {
|
|
88
|
+
if ( path.isAbsolute( filePath ) ) {
|
|
89
|
+
throw new Error( `${ label } must be project-relative: ${ filePath }` );
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const normalized = path.normalize( filePath );
|
|
93
|
+
if ( normalized === '..' || normalized.startsWith( `..${ path.sep }` ) ) {
|
|
94
|
+
throw new Error( `${ label } cannot point outside the project: ${ filePath }` );
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return normalized.split( path.sep ).join( '/' );
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveRestSchemaPackageConfig() {
|
|
101
|
+
const outputDir = normalizeProjectPath(
|
|
102
|
+
REST_SCHEMA_PACKAGE.outputDir,
|
|
103
|
+
'REST_SCHEMA_PACKAGE.outputDir'
|
|
104
|
+
);
|
|
105
|
+
const manifestFile = normalizeProjectPath(
|
|
106
|
+
REST_SCHEMA_PACKAGE.manifestFile ?? path.join( outputDir, 'manifest.json' ),
|
|
107
|
+
'REST_SCHEMA_PACKAGE.manifestFile'
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if ( outputDir === '.' ) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'REST_SCHEMA_PACKAGE.outputDir must not be the project root; use a dedicated subdirectory such as "inc/rest-schemas".'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if ( manifestFile === outputDir || ! manifestFile.startsWith( `${ outputDir }/` ) ) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`REST_SCHEMA_PACKAGE.manifestFile must stay inside REST_SCHEMA_PACKAGE.outputDir (${ outputDir }): ${ manifestFile }`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
manifestFile,
|
|
124
|
+
outputDir,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assertSafeSchemaBaseName( baseName: string, owner: string ) {
|
|
129
|
+
if (
|
|
130
|
+
! /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test( baseName ) ||
|
|
131
|
+
baseName === '.' ||
|
|
132
|
+
baseName === '..'
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`${ owner }: REST schema contract key must be a file basename containing only letters, numbers, dots, underscores, or hyphens: ${ baseName }`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getPackagePath( outputDir: string, owner: string, ...segments: string[] ) {
|
|
141
|
+
const packagePath = normalizeProjectPath(
|
|
142
|
+
path.join( outputDir, ...segments ),
|
|
143
|
+
'REST schema package path'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if ( ! packagePath.startsWith( `${ outputDir }/` ) ) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`${ owner }: REST schema package path must stay inside ${ outputDir }: ${ packagePath }`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return packagePath;
|
|
153
|
+
}
|
|
154
|
+
|
|
39
155
|
function isRestEnabledBlock(
|
|
40
156
|
block: WorkspaceBlockConfig
|
|
41
157
|
): block is WorkspaceBlockConfig & {
|
|
@@ -98,6 +214,307 @@ function isWorkspacePostMetaContract(
|
|
|
98
214
|
);
|
|
99
215
|
}
|
|
100
216
|
|
|
217
|
+
function getBlockRestSchemaEntries(
|
|
218
|
+
block: WorkspaceBlockConfig & {
|
|
219
|
+
restManifest: NonNullable< WorkspaceBlockConfig[ 'restManifest' ] >;
|
|
220
|
+
},
|
|
221
|
+
outputDir: string
|
|
222
|
+
): RuntimeRestSchemaPackageEntry[] {
|
|
223
|
+
return Object.keys( block.restManifest.contracts ).map( ( baseName ) => {
|
|
224
|
+
const owner = `Block ${ block.slug }`;
|
|
225
|
+
assertSafeSchemaBaseName( baseName, owner );
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
id: `block:${ block.slug }:${ baseName }`,
|
|
229
|
+
kind: 'block',
|
|
230
|
+
owner: block.slug,
|
|
231
|
+
path: getPackagePath(
|
|
232
|
+
outputDir,
|
|
233
|
+
owner,
|
|
234
|
+
'blocks',
|
|
235
|
+
block.slug,
|
|
236
|
+
`${ baseName }.schema.json`
|
|
237
|
+
),
|
|
238
|
+
schema: baseName,
|
|
239
|
+
source: normalizeProjectPath(
|
|
240
|
+
path.join(
|
|
241
|
+
'src',
|
|
242
|
+
'blocks',
|
|
243
|
+
block.slug,
|
|
244
|
+
'api-schemas',
|
|
245
|
+
`${ baseName }.schema.json`
|
|
246
|
+
),
|
|
247
|
+
'REST schema source path'
|
|
248
|
+
),
|
|
249
|
+
};
|
|
250
|
+
} );
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getRestResourceSchemaEntries(
|
|
254
|
+
resource: WorkspaceRestResourceConfig & {
|
|
255
|
+
restManifest: NonNullable< WorkspaceRestResourceConfig[ 'restManifest' ] >;
|
|
256
|
+
typesFile: string;
|
|
257
|
+
},
|
|
258
|
+
outputDir: string
|
|
259
|
+
): RuntimeRestSchemaPackageEntry[] {
|
|
260
|
+
return Object.keys( resource.restManifest.contracts ).map( ( baseName ) => {
|
|
261
|
+
const owner = `REST resource ${ resource.slug }`;
|
|
262
|
+
assertSafeSchemaBaseName( baseName, owner );
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
id: `rest:${ resource.slug }:${ baseName }`,
|
|
266
|
+
kind: 'rest-resource',
|
|
267
|
+
owner: resource.slug,
|
|
268
|
+
path: getPackagePath(
|
|
269
|
+
outputDir,
|
|
270
|
+
owner,
|
|
271
|
+
'rest',
|
|
272
|
+
resource.slug,
|
|
273
|
+
`${ baseName }.schema.json`
|
|
274
|
+
),
|
|
275
|
+
schema: baseName,
|
|
276
|
+
source: normalizeProjectPath(
|
|
277
|
+
path.join(
|
|
278
|
+
path.dirname( resource.typesFile ),
|
|
279
|
+
'api-schemas',
|
|
280
|
+
`${ baseName }.schema.json`
|
|
281
|
+
),
|
|
282
|
+
'REST schema source path'
|
|
283
|
+
),
|
|
284
|
+
};
|
|
285
|
+
} );
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function buildRuntimeRestSchemaPackageManifest(
|
|
289
|
+
restBlocks: Array<
|
|
290
|
+
WorkspaceBlockConfig & {
|
|
291
|
+
restManifest: NonNullable< WorkspaceBlockConfig[ 'restManifest' ] >;
|
|
292
|
+
}
|
|
293
|
+
>,
|
|
294
|
+
restResources: Array<
|
|
295
|
+
WorkspaceRestResourceConfig & {
|
|
296
|
+
restManifest: NonNullable< WorkspaceRestResourceConfig[ 'restManifest' ] >;
|
|
297
|
+
typesFile: string;
|
|
298
|
+
}
|
|
299
|
+
>
|
|
300
|
+
): RuntimeRestSchemaPackageManifest {
|
|
301
|
+
const { outputDir } = resolveRestSchemaPackageConfig();
|
|
302
|
+
const schemas = [
|
|
303
|
+
...restBlocks.flatMap( ( block ) =>
|
|
304
|
+
getBlockRestSchemaEntries( block, outputDir )
|
|
305
|
+
),
|
|
306
|
+
...restResources.flatMap( ( resource ) =>
|
|
307
|
+
getRestResourceSchemaEntries( resource, outputDir )
|
|
308
|
+
),
|
|
309
|
+
].sort(
|
|
310
|
+
( left, right ) =>
|
|
311
|
+
left.path.localeCompare( right.path ) || left.id.localeCompare( right.id )
|
|
312
|
+
);
|
|
313
|
+
const seenPaths = new Set< string >();
|
|
314
|
+
const seenIds = new Set< string >();
|
|
315
|
+
|
|
316
|
+
for ( const schema of schemas ) {
|
|
317
|
+
if ( seenIds.has( schema.id ) ) {
|
|
318
|
+
throw new Error( `Duplicate runtime REST schema package id: ${ schema.id }` );
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if ( seenPaths.has( schema.path ) ) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Duplicate runtime REST schema package path: ${ schema.path }`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
seenIds.add( schema.id );
|
|
328
|
+
seenPaths.add( schema.path );
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
generatedBy: 'wp-typia sync-rest --package',
|
|
333
|
+
outputDir,
|
|
334
|
+
schemaVersion: 1,
|
|
335
|
+
schemas,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function readUtf8IfExists( filePath: string ) {
|
|
340
|
+
try {
|
|
341
|
+
return await fsp.readFile( filePath, 'utf8' );
|
|
342
|
+
} catch ( error ) {
|
|
343
|
+
if ( getNodeErrorCode( error ) === 'ENOENT' ) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function writeUtf8IfChanged( filePath: string, content: string ) {
|
|
352
|
+
const existing = await readUtf8IfExists( filePath );
|
|
353
|
+
if ( existing === content ) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
await fsp.mkdir( path.dirname( filePath ), { recursive: true } );
|
|
358
|
+
await fsp.writeFile( filePath, content, 'utf8' );
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function unlinkIfExists( filePath: string ) {
|
|
362
|
+
try {
|
|
363
|
+
await fsp.unlink( filePath );
|
|
364
|
+
} catch ( error ) {
|
|
365
|
+
if ( getNodeErrorCode( error ) === 'ENOENT' ) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function collectPackagedSchemaFiles( outputDir: string ) {
|
|
374
|
+
const schemaFiles: string[] = [];
|
|
375
|
+
|
|
376
|
+
async function walk( relativeDir: string ) {
|
|
377
|
+
const currentDir =
|
|
378
|
+
relativeDir.length > 0 ? path.join( outputDir, relativeDir ) : outputDir;
|
|
379
|
+
let directoryEntries: import( 'node:fs' ).Dirent[];
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
directoryEntries = await fsp.readdir( currentDir, {
|
|
383
|
+
withFileTypes: true,
|
|
384
|
+
} );
|
|
385
|
+
} catch ( error ) {
|
|
386
|
+
if ( getNodeErrorCode( error ) === 'ENOENT' ) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
for ( const directoryEntry of directoryEntries ) {
|
|
394
|
+
const relativePath = path.join( relativeDir, directoryEntry.name );
|
|
395
|
+
|
|
396
|
+
if ( directoryEntry.isDirectory() ) {
|
|
397
|
+
await walk( relativePath );
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
directoryEntry.isFile() &&
|
|
403
|
+
directoryEntry.name.endsWith( '.schema.json' )
|
|
404
|
+
) {
|
|
405
|
+
schemaFiles.push(
|
|
406
|
+
normalizeProjectPath(
|
|
407
|
+
path.join( outputDir, relativePath ),
|
|
408
|
+
'REST schema package file'
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
await walk( '' );
|
|
416
|
+
return schemaFiles.sort();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function syncRuntimeRestSchemaPackage(
|
|
420
|
+
restBlocks: Array<
|
|
421
|
+
WorkspaceBlockConfig & {
|
|
422
|
+
restManifest: NonNullable< WorkspaceBlockConfig[ 'restManifest' ] >;
|
|
423
|
+
}
|
|
424
|
+
>,
|
|
425
|
+
restResources: Array<
|
|
426
|
+
WorkspaceRestResourceConfig & {
|
|
427
|
+
restManifest: NonNullable< WorkspaceRestResourceConfig[ 'restManifest' ] >;
|
|
428
|
+
typesFile: string;
|
|
429
|
+
}
|
|
430
|
+
>,
|
|
431
|
+
options: { check: boolean }
|
|
432
|
+
) {
|
|
433
|
+
const { manifestFile, outputDir } = resolveRestSchemaPackageConfig();
|
|
434
|
+
const manifest = buildRuntimeRestSchemaPackageManifest(
|
|
435
|
+
restBlocks,
|
|
436
|
+
restResources
|
|
437
|
+
);
|
|
438
|
+
const manifestContent = `${ JSON.stringify( manifest, null, 2 ) }\n`;
|
|
439
|
+
const expectedPackagePaths = new Set(
|
|
440
|
+
manifest.schemas.map( ( schema ) => schema.path )
|
|
441
|
+
);
|
|
442
|
+
const packageFailures: string[] = [];
|
|
443
|
+
|
|
444
|
+
for ( const schema of manifest.schemas ) {
|
|
445
|
+
const sourceContent = await readUtf8IfExists( schema.source );
|
|
446
|
+
|
|
447
|
+
if ( sourceContent === undefined ) {
|
|
448
|
+
if ( ! options.check ) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Runtime REST schema source is missing: ${ schema.source }\nRun \`sync-rest\` before \`sync-rest --package\` to generate source schemas.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
packageFailures.push( `${ schema.source } (source missing)` );
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if ( options.check ) {
|
|
459
|
+
const packagedContent = await readUtf8IfExists( schema.path );
|
|
460
|
+
|
|
461
|
+
if ( packagedContent === undefined ) {
|
|
462
|
+
packageFailures.push( `${ schema.path } (missing)` );
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if ( packagedContent !== sourceContent ) {
|
|
467
|
+
packageFailures.push( `${ schema.path } (stale)` );
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await writeUtf8IfChanged( schema.path, sourceContent );
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const existingPackagedSchemas = await collectPackagedSchemaFiles( outputDir );
|
|
477
|
+
for ( const schemaPath of existingPackagedSchemas ) {
|
|
478
|
+
if ( expectedPackagePaths.has( schemaPath ) ) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if ( options.check ) {
|
|
483
|
+
packageFailures.push( `${ schemaPath } (unexpected)` );
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await unlinkIfExists( schemaPath );
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if ( options.check ) {
|
|
491
|
+
const existingManifestContent = await readUtf8IfExists( manifestFile );
|
|
492
|
+
|
|
493
|
+
if ( existingManifestContent === undefined ) {
|
|
494
|
+
if ( manifest.schemas.length > 0 ) {
|
|
495
|
+
packageFailures.push( `${ manifestFile } (missing)` );
|
|
496
|
+
}
|
|
497
|
+
} else if ( existingManifestContent !== manifestContent ) {
|
|
498
|
+
packageFailures.push( `${ manifestFile } (stale)` );
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if ( packageFailures.length > 0 ) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`Runtime REST schema package is missing or stale:\n${ packageFailures
|
|
504
|
+
.map( ( failure ) => `- ${ failure }` )
|
|
505
|
+
.join(
|
|
506
|
+
'\n'
|
|
507
|
+
) }\nRun \`sync-rest --package\` to refresh packaged REST schemas before building a release zip.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return manifest.schemas.length;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
await writeUtf8IfChanged( manifestFile, manifestContent );
|
|
515
|
+
return manifest.schemas.length;
|
|
516
|
+
}
|
|
517
|
+
|
|
101
518
|
async function assertTypeArtifactsCurrent( block: WorkspaceBlockConfig ) {
|
|
102
519
|
const report = await runSyncBlockMetadata(
|
|
103
520
|
{
|
|
@@ -139,6 +556,22 @@ async function main() {
|
|
|
139
556
|
postMetaContracts.length === 0 &&
|
|
140
557
|
restResources.length === 0
|
|
141
558
|
) {
|
|
559
|
+
if ( options.packageSchemas ) {
|
|
560
|
+
const packagedSchemaCount = await syncRuntimeRestSchemaPackage(
|
|
561
|
+
restBlocks,
|
|
562
|
+
restResources,
|
|
563
|
+
{
|
|
564
|
+
check: options.check,
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
console.log(
|
|
569
|
+
options.check
|
|
570
|
+
? `✅ Runtime REST schema package is already up to date (${ packagedSchemaCount } schemas).`
|
|
571
|
+
: `✅ Runtime REST schema package generated (${ packagedSchemaCount } schemas).`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
142
575
|
console.log(
|
|
143
576
|
options.check
|
|
144
577
|
? 'ℹ️ No REST-enabled workspace blocks, standalone contracts, post meta contracts, or plugin-level REST resources are registered yet. `sync-rest --check` is already clean.'
|
|
@@ -281,6 +714,22 @@ async function main() {
|
|
|
281
714
|
);
|
|
282
715
|
}
|
|
283
716
|
|
|
717
|
+
if ( options.packageSchemas ) {
|
|
718
|
+
const packagedSchemaCount = await syncRuntimeRestSchemaPackage(
|
|
719
|
+
restBlocks,
|
|
720
|
+
restResources,
|
|
721
|
+
{
|
|
722
|
+
check: options.check,
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
console.log(
|
|
727
|
+
options.check
|
|
728
|
+
? `✅ Runtime REST schema package is already up to date (${ packagedSchemaCount } schemas).`
|
|
729
|
+
: `✅ Runtime REST schema package generated (${ packagedSchemaCount } schemas).`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
284
733
|
console.log(
|
|
285
734
|
options.check
|
|
286
735
|
? '✅ REST contract schemas, standalone schemas, post meta schemas, portable API clients, and endpoint-aware OpenAPI documents are already up to date for workspace blocks, standalone contracts, post meta contracts, and plugin-level resources!'
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
formatBlockPatternContentNestingDiagnostics,
|
|
7
|
+
validateBlockPatternContentNesting,
|
|
8
|
+
syncInnerBlocksTemplateModule,
|
|
9
|
+
syncBlockMetadata,
|
|
10
|
+
validateBlockNestingContract,
|
|
11
|
+
validateInnerBlocksTemplates,
|
|
12
|
+
} from '@wp-typia/block-runtime/metadata-core';
|
|
13
|
+
import {
|
|
14
|
+
formatPatternCatalogDiagnostics,
|
|
15
|
+
resolvePatternCatalogContentFile,
|
|
16
|
+
validatePatternCatalog,
|
|
17
|
+
} from '@wp-typia/project-tools/pattern-catalog';
|
|
5
18
|
|
|
6
|
-
import { BLOCKS } from './block-config';
|
|
19
|
+
import { BLOCK_NESTING, BLOCK_TEMPLATES, BLOCKS, PATTERNS } from './block-config';
|
|
7
20
|
|
|
8
21
|
function parseCliOptions( argv: string[] ) {
|
|
9
22
|
const options = {
|
|
@@ -22,9 +35,129 @@ function parseCliOptions( argv: string[] ) {
|
|
|
22
35
|
return options;
|
|
23
36
|
}
|
|
24
37
|
|
|
38
|
+
function readWorkspaceBlockName( block: ( typeof BLOCKS )[ number ] ): string {
|
|
39
|
+
const blockJsonFile = path.join( 'src', 'blocks', block.slug, 'block.json' );
|
|
40
|
+
let blockJson: { name?: unknown };
|
|
41
|
+
try {
|
|
42
|
+
blockJson = JSON.parse( fs.readFileSync( blockJsonFile, 'utf8' ) ) as {
|
|
43
|
+
name?: unknown;
|
|
44
|
+
};
|
|
45
|
+
} catch ( error ) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Failed to read block.json for block "${ block.slug }" at ${ blockJsonFile }: ${
|
|
48
|
+
error instanceof Error ? error.message : String( error )
|
|
49
|
+
}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if ( typeof blockJson.name !== 'string' || blockJson.name.trim() === '' ) {
|
|
53
|
+
throw new Error( `${ blockJsonFile } must define a string "name".` );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return blockJson.name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readPatternFileContent( pattern: ( typeof PATTERNS )[ number ] ): string {
|
|
60
|
+
const patternFile = resolvePatternCatalogContentFile( pattern );
|
|
61
|
+
if ( ! patternFile ) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Pattern "${ pattern.slug }" must declare contentFile or file in scripts/block-config.ts.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return fs.readFileSync( patternFile, 'utf8' );
|
|
68
|
+
} catch ( error ) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Failed to read pattern "${ pattern.slug }" at ${ patternFile }: ${
|
|
71
|
+
error instanceof Error ? error.message : String( error )
|
|
72
|
+
}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validatePatternCatalogMetadata() {
|
|
78
|
+
const result = validatePatternCatalog( PATTERNS, {
|
|
79
|
+
projectDir: process.cwd(),
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
if ( result.warnings.length > 0 ) {
|
|
83
|
+
console.warn(
|
|
84
|
+
`⚠️ Pattern catalog validation warnings:\n${ formatPatternCatalogDiagnostics(
|
|
85
|
+
result.warnings
|
|
86
|
+
) }`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ( result.errors.length > 0 ) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Pattern catalog metadata is invalid:\n${ formatPatternCatalogDiagnostics(
|
|
93
|
+
result.errors
|
|
94
|
+
) }`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validatePatternContentNesting( knownBlockNames: readonly string[] ) {
|
|
100
|
+
const diagnostics = PATTERNS.flatMap( ( pattern ) => {
|
|
101
|
+
const content = readPatternFileContent( pattern );
|
|
102
|
+
return validateBlockPatternContentNesting( content, {
|
|
103
|
+
allowExternalBlockNames: true,
|
|
104
|
+
knownBlockNames,
|
|
105
|
+
nesting: BLOCK_NESTING,
|
|
106
|
+
patternFile: resolvePatternCatalogContentFile( pattern ),
|
|
107
|
+
} ).diagnostics;
|
|
108
|
+
} );
|
|
109
|
+
const warnings = diagnostics.filter(
|
|
110
|
+
( diagnostic ) => diagnostic.severity === 'warning'
|
|
111
|
+
);
|
|
112
|
+
const errors = diagnostics.filter(
|
|
113
|
+
( diagnostic ) => diagnostic.severity === 'error'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if ( warnings.length > 0 ) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`⚠️ Pattern nesting validation warnings:\n${ formatBlockPatternContentNestingDiagnostics(
|
|
119
|
+
warnings
|
|
120
|
+
) }`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ( errors.length > 0 ) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Pattern content violates block nesting contract:\n${ formatBlockPatternContentNestingDiagnostics(
|
|
127
|
+
errors
|
|
128
|
+
) }`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
25
133
|
async function main() {
|
|
26
134
|
const options = parseCliOptions( process.argv.slice( 2 ) );
|
|
27
135
|
|
|
136
|
+
const knownBlockNames = BLOCKS.map( readWorkspaceBlockName );
|
|
137
|
+
validateBlockNestingContract( BLOCK_NESTING, {
|
|
138
|
+
allowExternalBlockNames: true,
|
|
139
|
+
knownBlockNames,
|
|
140
|
+
} );
|
|
141
|
+
validateInnerBlocksTemplates( BLOCK_TEMPLATES, {
|
|
142
|
+
allowExternalBlockNames: true,
|
|
143
|
+
knownBlockNames,
|
|
144
|
+
nesting: BLOCK_NESTING,
|
|
145
|
+
} );
|
|
146
|
+
validatePatternCatalogMetadata();
|
|
147
|
+
validatePatternContentNesting( knownBlockNames );
|
|
148
|
+
await syncInnerBlocksTemplateModule(
|
|
149
|
+
{
|
|
150
|
+
allowExternalBlockNames: true,
|
|
151
|
+
knownBlockNames,
|
|
152
|
+
nesting: BLOCK_NESTING,
|
|
153
|
+
outputFile: path.join( 'src', 'inner-blocks-templates.ts' ),
|
|
154
|
+
templates: BLOCK_TEMPLATES,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
check: options.check,
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
28
161
|
if ( BLOCKS.length === 0 ) {
|
|
29
162
|
console.log(
|
|
30
163
|
options.check
|
|
@@ -38,9 +171,12 @@ async function main() {
|
|
|
38
171
|
const baseDir = path.join( 'src', 'blocks', block.slug );
|
|
39
172
|
const result = await syncBlockMetadata(
|
|
40
173
|
{
|
|
174
|
+
allowExternalBlockNames: true,
|
|
41
175
|
blockJsonFile: path.join( baseDir, 'block.json' ),
|
|
42
176
|
jsonSchemaFile: path.join( baseDir, 'typia.schema.json' ),
|
|
177
|
+
knownBlockNames,
|
|
43
178
|
manifestFile: path.join( baseDir, 'typia.manifest.json' ),
|
|
179
|
+
nesting: BLOCK_NESTING,
|
|
44
180
|
openApiFile: path.join( baseDir, 'typia.openapi.json' ),
|
|
45
181
|
sourceTypeName: block.attributeTypeName,
|
|
46
182
|
typesFile: block.typesFile,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* This file is generated by wp-typia. Do not edit manually. */
|
|
2
|
+
|
|
3
|
+
export type WpTypiaInnerBlocksTemplateAttributes = Record<string, unknown>;
|
|
4
|
+
export type WpTypiaInnerBlocksTemplateItem = [
|
|
5
|
+
blockName: string,
|
|
6
|
+
attributes?: WpTypiaInnerBlocksTemplateAttributes,
|
|
7
|
+
innerBlocks?: WpTypiaInnerBlocksTemplate,
|
|
8
|
+
];
|
|
9
|
+
export type WpTypiaInnerBlocksTemplate = WpTypiaInnerBlocksTemplateItem[];
|
|
10
|
+
|
|
11
|
+
export const INNER_BLOCKS_TEMPLATES = {} satisfies Record<string, WpTypiaInnerBlocksTemplate>;
|
|
12
|
+
|
|
13
|
+
export type WpTypiaInnerBlocksTemplateName = keyof typeof INNER_BLOCKS_TEMPLATES;
|
|
@@ -17,6 +17,15 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|
|
17
17
|
exit;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function {{phpPrefix}}_load_rest_schema_helpers() {
|
|
21
|
+
$helper_path = __DIR__ . '/inc/rest-schema.php';
|
|
22
|
+
if ( is_readable( $helper_path ) ) {
|
|
23
|
+
require_once $helper_path;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
{{phpPrefix}}_load_rest_schema_helpers();
|
|
28
|
+
|
|
20
29
|
foreach ( glob( __DIR__ . '/src/blocks/*/server.php' ) ?: array() as $server_module ) {
|
|
21
30
|
require_once $server_module;
|
|
22
31
|
}
|
|
@@ -168,7 +177,11 @@ function {{phpPrefix}}_register_pattern_category() {
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
function {{phpPrefix}}_register_patterns() {
|
|
171
|
-
|
|
180
|
+
$pattern_modules = array_merge(
|
|
181
|
+
glob( __DIR__ . '/src/patterns/*.php' ) ?: array(),
|
|
182
|
+
glob( __DIR__ . '/src/patterns/*/*.php' ) ?: array()
|
|
183
|
+
);
|
|
184
|
+
foreach ( $pattern_modules as $pattern_module ) {
|
|
172
185
|
require $pattern_module;
|
|
173
186
|
}
|
|
174
187
|
}
|