@wp-typia/project-tools 0.11.1

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 (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,336 @@
1
+ <?php
2
+ /**
3
+ * Plugin Name: {{title}}
4
+ * Description: {{description}}
5
+ * Version: 0.1.0
6
+ * Requires at least: 6.7
7
+ * Tested up to: 6.9
8
+ * Requires PHP: 8.0
9
+ * Author: {{author}}
10
+ * License: GPL-2.0-or-later
11
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12
+ * Text Domain: {{textDomain}}
13
+ * Domain Path: /languages
14
+ */
15
+
16
+ if ( ! defined( 'ABSPATH' ) ) {
17
+ exit;
18
+ }
19
+
20
+ function {{phpPrefix}}_load_textdomain() {
21
+ load_plugin_textdomain(
22
+ '{{textDomain}}',
23
+ false,
24
+ dirname( plugin_basename( __FILE__ ) ) . '/languages'
25
+ );
26
+ }
27
+
28
+ define( '{{phpPrefixUpper}}_DATA_STORAGE_MODE', '{{dataStorageMode}}' );
29
+
30
+ function {{phpPrefix}}_get_build_dir() {
31
+ $candidates = array(
32
+ __DIR__ . '/build',
33
+ __DIR__ . '/build/{{slugKebabCase}}',
34
+ );
35
+
36
+ foreach ( $candidates as $candidate ) {
37
+ if ( file_exists( $candidate . '/block.json' ) ) {
38
+ return $candidate;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function {{phpPrefix}}_get_counter_table_name() {
46
+ global $wpdb;
47
+ return $wpdb->prefix . '{{phpPrefix}}_counters';
48
+ }
49
+
50
+ function {{phpPrefix}}_get_counter_lock_name( $post_id, $resource_key ) {
51
+ return 'wpt_pcl_' . md5(
52
+ '{{phpPrefix}}|' . (int) $post_id . '|' . (string) $resource_key
53
+ );
54
+ }
55
+
56
+ function {{phpPrefix}}_with_counter_lock( $post_id, $resource_key, $callback ) {
57
+ global $wpdb;
58
+
59
+ $lock_name = {{phpPrefix}}_get_counter_lock_name( $post_id, $resource_key );
60
+ $acquired = (int) $wpdb->get_var(
61
+ $wpdb->prepare(
62
+ 'SELECT GET_LOCK(%s, 5)',
63
+ $lock_name
64
+ )
65
+ );
66
+
67
+ if ( 1 !== $acquired ) {
68
+ return new WP_Error( 'counter_lock_timeout', 'Could not acquire the counter lock.', array( 'status' => 503 ) );
69
+ }
70
+
71
+ try {
72
+ return $callback();
73
+ } finally {
74
+ $wpdb->get_var(
75
+ $wpdb->prepare(
76
+ 'SELECT RELEASE_LOCK(%s)',
77
+ $lock_name
78
+ )
79
+ );
80
+ }
81
+ }
82
+
83
+ function {{phpPrefix}}_maybe_install_storage() {
84
+ if ( 'custom-table' !== '{{dataStorageMode}}' ) {
85
+ return;
86
+ }
87
+
88
+ global $wpdb;
89
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
90
+
91
+ $table_name = {{phpPrefix}}_get_counter_table_name();
92
+ $charset_collate = $wpdb->get_charset_collate();
93
+ $sql = "CREATE TABLE {$table_name} (
94
+ post_id bigint(20) unsigned NOT NULL,
95
+ resource_key varchar(100) NOT NULL,
96
+ count bigint(20) unsigned NOT NULL DEFAULT 0,
97
+ updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
98
+ PRIMARY KEY (post_id, resource_key)
99
+ ) {$charset_collate};";
100
+
101
+ dbDelta( $sql );
102
+ $table_exists = $wpdb->get_var(
103
+ $wpdb->prepare(
104
+ 'SHOW TABLES LIKE %s',
105
+ $table_name
106
+ )
107
+ );
108
+
109
+ if ( $table_name === $table_exists ) {
110
+ update_option( '{{phpPrefix}}_storage_version', '1.0.0' );
111
+ }
112
+ }
113
+
114
+ function {{phpPrefix}}_ensure_storage_installed() {
115
+ if ( 'custom-table' === '{{dataStorageMode}}' && '1.0.0' !== get_option( '{{phpPrefix}}_storage_version', '' ) ) {
116
+ {{phpPrefix}}_maybe_install_storage();
117
+ }
118
+ }
119
+
120
+ function {{phpPrefix}}_load_schema( $schema_name ) {
121
+ $build_dir = {{phpPrefix}}_get_build_dir();
122
+ if ( ! $build_dir ) {
123
+ return null;
124
+ }
125
+
126
+ $path = $build_dir . '/api-schemas/' . $schema_name . '.schema.json';
127
+ if ( ! file_exists( $path ) ) {
128
+ return null;
129
+ }
130
+
131
+ $decoded = json_decode( file_get_contents( $path ), true );
132
+ return is_array( $decoded ) ? $decoded : null;
133
+ }
134
+
135
+ function {{phpPrefix}}_sanitize_rest_schema( $schema ) {
136
+ if ( ! is_array( $schema ) ) {
137
+ return $schema;
138
+ }
139
+
140
+ unset( $schema['$schema'], $schema['title'] );
141
+
142
+ if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
143
+ foreach ( $schema['properties'] as $key => $property_schema ) {
144
+ $schema['properties'][ $key ] = {{phpPrefix}}_sanitize_rest_schema( $property_schema );
145
+ }
146
+ }
147
+
148
+ if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
149
+ $schema['items'] = {{phpPrefix}}_sanitize_rest_schema( $schema['items'] );
150
+ }
151
+
152
+ return $schema;
153
+ }
154
+
155
+ function {{phpPrefix}}_validate_and_sanitize_request( $value, $schema_name, $param_name ) {
156
+ $schema = {{phpPrefix}}_load_schema( $schema_name );
157
+ if ( ! is_array( $schema ) ) {
158
+ return new WP_Error( 'missing_schema', 'Missing REST schema.', array( 'status' => 500 ) );
159
+ }
160
+
161
+ $rest_schema = {{phpPrefix}}_sanitize_rest_schema( $schema );
162
+ $validation = rest_validate_value_from_schema( $value, $rest_schema, $param_name );
163
+ if ( is_wp_error( $validation ) ) {
164
+ return $validation;
165
+ }
166
+
167
+ return rest_sanitize_value_from_schema( $value, $rest_schema, $param_name );
168
+ }
169
+
170
+ function {{phpPrefix}}_get_counter( $post_id, $resource_key ) {
171
+ global $wpdb;
172
+
173
+ if ( 'custom-table' === '{{dataStorageMode}}' ) {
174
+ $table_name = {{phpPrefix}}_get_counter_table_name();
175
+ $count = $wpdb->get_var(
176
+ $wpdb->prepare(
177
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from an internal helper.
178
+ "SELECT count FROM {$table_name} WHERE post_id = %d AND resource_key = %s",
179
+ $post_id,
180
+ $resource_key
181
+ )
182
+ );
183
+
184
+ return null === $count ? 0 : (int) $count;
185
+ }
186
+
187
+ $meta_key = '_' . '{{phpPrefix}}' . '_counter_' . sanitize_key( $resource_key );
188
+ return (int) get_post_meta( $post_id, $meta_key, true );
189
+ }
190
+
191
+ function {{phpPrefix}}_increment_counter( $post_id, $resource_key, $delta ) {
192
+ global $wpdb;
193
+
194
+ if ( 'custom-table' === '{{dataStorageMode}}' ) {
195
+ $table_name = {{phpPrefix}}_get_counter_table_name();
196
+ $delta_value = (int) $delta;
197
+ $initial_count = max( 0, $delta_value );
198
+ $result = $wpdb->query(
199
+ $wpdb->prepare(
200
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from an internal helper.
201
+ "INSERT INTO {$table_name} (post_id, resource_key, count, updated_at)
202
+ VALUES (%d, %s, %d, %s)
203
+ ON DUPLICATE KEY UPDATE
204
+ count = GREATEST(0, count + %d),
205
+ updated_at = VALUES(updated_at)",
206
+ $post_id,
207
+ $resource_key,
208
+ $initial_count,
209
+ current_time( 'mysql', true ),
210
+ $delta_value
211
+ )
212
+ );
213
+
214
+ if ( false === $result ) {
215
+ return new WP_Error( 'counter_update_failed', 'Failed to update the counter.', array( 'status' => 500 ) );
216
+ }
217
+
218
+ return {{phpPrefix}}_get_counter( $post_id, $resource_key );
219
+ }
220
+
221
+ return {{phpPrefix}}_with_counter_lock(
222
+ $post_id,
223
+ $resource_key,
224
+ function () use ( $delta, $post_id, $resource_key ) {
225
+ $meta_key = '_' . '{{phpPrefix}}' . '_counter_' . sanitize_key( $resource_key );
226
+ $next_count = max( 0, {{phpPrefix}}_get_counter( $post_id, $resource_key ) + (int) $delta );
227
+ update_post_meta( $post_id, $meta_key, $next_count );
228
+ return $next_count;
229
+ }
230
+ );
231
+ }
232
+
233
+ function {{phpPrefix}}_build_state_response( $post_id, $resource_key, $count ) {
234
+ return array(
235
+ 'postId' => (int) $post_id,
236
+ 'resourceKey' => (string) $resource_key,
237
+ 'count' => (int) $count,
238
+ 'storage' => '{{dataStorageMode}}',
239
+ );
240
+ }
241
+
242
+ function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
243
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
244
+ array(
245
+ 'postId' => $request->get_param( 'postId' ),
246
+ 'resourceKey' => $request->get_param( 'resourceKey' ),
247
+ ),
248
+ 'state-query',
249
+ 'query'
250
+ );
251
+
252
+ if ( is_wp_error( $payload ) ) {
253
+ return $payload;
254
+ }
255
+
256
+ $count = {{phpPrefix}}_get_counter( (int) $payload['postId'], (string) $payload['resourceKey'] );
257
+ return rest_ensure_response(
258
+ {{phpPrefix}}_build_state_response(
259
+ (int) $payload['postId'],
260
+ (string) $payload['resourceKey'],
261
+ $count
262
+ )
263
+ );
264
+ }
265
+
266
+ function {{phpPrefix}}_handle_write_state( WP_REST_Request $request ) {
267
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
268
+ $request->get_json_params(),
269
+ 'write-state-request',
270
+ 'body'
271
+ );
272
+
273
+ if ( is_wp_error( $payload ) ) {
274
+ return $payload;
275
+ }
276
+
277
+ $count = {{phpPrefix}}_increment_counter(
278
+ (int) $payload['postId'],
279
+ (string) $payload['resourceKey'],
280
+ isset( $payload['delta'] ) ? (int) $payload['delta'] : 1
281
+ );
282
+
283
+ if ( is_wp_error( $count ) ) {
284
+ return $count;
285
+ }
286
+
287
+ return rest_ensure_response(
288
+ {{phpPrefix}}_build_state_response(
289
+ (int) $payload['postId'],
290
+ (string) $payload['resourceKey'],
291
+ $count
292
+ )
293
+ );
294
+ }
295
+
296
+ function {{phpPrefix}}_deny_write_without_policy() {
297
+ return new WP_Error(
298
+ 'rest_forbidden',
299
+ 'This scaffold requires an explicit write policy helper before enabling writes.',
300
+ array( 'status' => 403 )
301
+ );
302
+ }
303
+
304
+ function {{phpPrefix}}_register_routes() {
305
+ register_rest_route(
306
+ '{{namespace}}/v1',
307
+ '/{{slugKebabCase}}/state',
308
+ array(
309
+ array(
310
+ 'methods' => WP_REST_Server::READABLE,
311
+ 'callback' => '{{phpPrefix}}_handle_get_state',
312
+ 'permission_callback' => '__return_true',
313
+ ),
314
+ array(
315
+ 'methods' => WP_REST_Server::CREATABLE,
316
+ 'callback' => '{{phpPrefix}}_handle_write_state',
317
+ 'permission_callback' => '{{phpPrefix}}_deny_write_without_policy',
318
+ ),
319
+ )
320
+ );
321
+ }
322
+
323
+ function {{phpPrefix}}_register_block() {
324
+ $build_dir = {{phpPrefix}}_get_build_dir();
325
+ if ( ! $build_dir ) {
326
+ return;
327
+ }
328
+
329
+ register_block_type( $build_dir );
330
+ }
331
+
332
+ register_activation_hook( __FILE__, '{{phpPrefix}}_maybe_install_storage' );
333
+ add_action( 'init', '{{phpPrefix}}_load_textdomain' );
334
+ add_action( 'init', '{{phpPrefix}}_ensure_storage_installed' );
335
+ add_action( 'init', '{{phpPrefix}}_register_block' );
336
+ add_action( 'rest_api_init', '{{phpPrefix}}_register_routes' );
@@ -0,0 +1,308 @@
1
+ <?php
2
+ /**
3
+ * Plugin Name: {{title}}
4
+ * Description: {{description}}
5
+ * Version: 0.1.0
6
+ * Requires at least: 6.7
7
+ * Tested up to: 6.9
8
+ * Requires PHP: 8.0
9
+ * Author: {{author}}
10
+ * License: GPL-2.0-or-later
11
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12
+ * Text Domain: {{textDomain}}
13
+ * Domain Path: /languages
14
+ */
15
+
16
+ if ( ! defined( 'ABSPATH' ) ) {
17
+ exit;
18
+ }
19
+
20
+ function {{phpPrefix}}_load_textdomain() {
21
+ load_plugin_textdomain(
22
+ '{{textDomain}}',
23
+ false,
24
+ dirname( plugin_basename( __FILE__ ) ) . '/languages'
25
+ );
26
+ }
27
+
28
+ define( '{{phpPrefixUpper}}_DATA_STORAGE_MODE', '{{dataStorageMode}}' );
29
+ define( '{{phpPrefixUpper}}_PUBLIC_WRITE_TTL', HOUR_IN_SECONDS );
30
+ define( '{{phpPrefixUpper}}_PUBLIC_WRITE_RATE_LIMIT_WINDOW', MINUTE_IN_SECONDS );
31
+ define( '{{phpPrefixUpper}}_PUBLIC_WRITE_RATE_LIMIT_MAX', 10 );
32
+ // These helper files are scaffold-owned plumbing. Customize them only when you are changing auth or request validation behavior.
33
+ require_once __DIR__ . '/inc/rest-shared.php';
34
+ require_once __DIR__ . '/inc/rest-public.php';
35
+
36
+ function {{phpPrefix}}_get_build_dir() {
37
+ $candidates = array(
38
+ __DIR__ . '/build',
39
+ __DIR__ . '/build/{{slugKebabCase}}',
40
+ );
41
+
42
+ foreach ( $candidates as $candidate ) {
43
+ if ( file_exists( $candidate . '/block.json' ) ) {
44
+ return $candidate;
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ function {{phpPrefix}}_get_counter_table_name() {
52
+ global $wpdb;
53
+ return $wpdb->prefix . '{{phpPrefix}}_counters';
54
+ }
55
+
56
+ function {{phpPrefix}}_get_counter_lock_name( $post_id, $resource_key ) {
57
+ return 'wpt_pcl_' . md5(
58
+ '{{phpPrefix}}|' . (int) $post_id . '|' . (string) $resource_key
59
+ );
60
+ }
61
+
62
+ function {{phpPrefix}}_with_counter_lock( $post_id, $resource_key, $callback ) {
63
+ global $wpdb;
64
+
65
+ $lock_name = {{phpPrefix}}_get_counter_lock_name( $post_id, $resource_key );
66
+ $acquired = (int) $wpdb->get_var(
67
+ $wpdb->prepare(
68
+ 'SELECT GET_LOCK(%s, 5)',
69
+ $lock_name
70
+ )
71
+ );
72
+
73
+ if ( 1 !== $acquired ) {
74
+ return new WP_Error( 'counter_lock_timeout', 'Could not acquire the counter lock.', array( 'status' => 503 ) );
75
+ }
76
+
77
+ try {
78
+ return $callback();
79
+ } finally {
80
+ $wpdb->get_var(
81
+ $wpdb->prepare(
82
+ 'SELECT RELEASE_LOCK(%s)',
83
+ $lock_name
84
+ )
85
+ );
86
+ }
87
+ }
88
+
89
+ function {{phpPrefix}}_maybe_install_storage() {
90
+ if ( 'custom-table' !== '{{dataStorageMode}}' ) {
91
+ return;
92
+ }
93
+
94
+ global $wpdb;
95
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
96
+
97
+ $table_name = {{phpPrefix}}_get_counter_table_name();
98
+ $charset_collate = $wpdb->get_charset_collate();
99
+ $sql = "CREATE TABLE {$table_name} (
100
+ post_id bigint(20) unsigned NOT NULL,
101
+ resource_key varchar(100) NOT NULL,
102
+ count bigint(20) unsigned NOT NULL DEFAULT 0,
103
+ updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
104
+ PRIMARY KEY (post_id, resource_key)
105
+ ) {$charset_collate};";
106
+
107
+ dbDelta( $sql );
108
+ $table_exists = $wpdb->get_var(
109
+ $wpdb->prepare(
110
+ 'SHOW TABLES LIKE %s',
111
+ $table_name
112
+ )
113
+ );
114
+
115
+ if ( $table_name === $table_exists ) {
116
+ update_option( '{{phpPrefix}}_storage_version', '1.0.0' );
117
+ }
118
+ }
119
+
120
+ function {{phpPrefix}}_ensure_storage_installed() {
121
+ if ( 'custom-table' === '{{dataStorageMode}}' && '1.0.0' !== get_option( '{{phpPrefix}}_storage_version', '' ) ) {
122
+ {{phpPrefix}}_maybe_install_storage();
123
+ }
124
+ }
125
+
126
+ function {{phpPrefix}}_get_rest_build_dir() {
127
+ return {{phpPrefix}}_get_build_dir();
128
+ }
129
+
130
+ function {{phpPrefix}}_get_counter( $post_id, $resource_key ) {
131
+ global $wpdb;
132
+
133
+ if ( 'custom-table' === '{{dataStorageMode}}' ) {
134
+ $table_name = {{phpPrefix}}_get_counter_table_name();
135
+ $count = $wpdb->get_var(
136
+ $wpdb->prepare(
137
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from an internal helper.
138
+ "SELECT count FROM {$table_name} WHERE post_id = %d AND resource_key = %s",
139
+ $post_id,
140
+ $resource_key
141
+ )
142
+ );
143
+
144
+ return null === $count ? 0 : (int) $count;
145
+ }
146
+
147
+ $meta_key = '_' . '{{phpPrefix}}' . '_counter_' . sanitize_key( $resource_key );
148
+ return (int) get_post_meta( $post_id, $meta_key, true );
149
+ }
150
+
151
+ // Customize storage helpers here when you need a different backend or aggregate behavior.
152
+ function {{phpPrefix}}_increment_counter( $post_id, $resource_key, $delta ) {
153
+ global $wpdb;
154
+
155
+ if ( 'custom-table' === '{{dataStorageMode}}' ) {
156
+ $table_name = {{phpPrefix}}_get_counter_table_name();
157
+ $delta_value = (int) $delta;
158
+ $initial_count = max( 0, $delta_value );
159
+ $result = $wpdb->query(
160
+ $wpdb->prepare(
161
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name comes from an internal helper.
162
+ "INSERT INTO {$table_name} (post_id, resource_key, count, updated_at)
163
+ VALUES (%d, %s, %d, %s)
164
+ ON DUPLICATE KEY UPDATE
165
+ count = GREATEST(0, count + %d),
166
+ updated_at = VALUES(updated_at)",
167
+ $post_id,
168
+ $resource_key,
169
+ $initial_count,
170
+ current_time( 'mysql', true ),
171
+ $delta_value
172
+ )
173
+ );
174
+
175
+ if ( false === $result ) {
176
+ return new WP_Error( 'counter_update_failed', 'Failed to update the counter.', array( 'status' => 500 ) );
177
+ }
178
+
179
+ return {{phpPrefix}}_get_counter( $post_id, $resource_key );
180
+ }
181
+
182
+ return {{phpPrefix}}_with_counter_lock(
183
+ $post_id,
184
+ $resource_key,
185
+ function () use ( $delta, $post_id, $resource_key ) {
186
+ $meta_key = '_' . '{{phpPrefix}}' . '_counter_' . sanitize_key( $resource_key );
187
+ $next_count = max( 0, {{phpPrefix}}_get_counter( $post_id, $resource_key ) + (int) $delta );
188
+ update_post_meta( $post_id, $meta_key, $next_count );
189
+ return $next_count;
190
+ }
191
+ );
192
+ }
193
+
194
+ function {{phpPrefix}}_build_state_response( $post_id, $resource_key, $count ) {
195
+ return array(
196
+ 'postId' => (int) $post_id,
197
+ 'resourceKey' => (string) $resource_key,
198
+ 'count' => (int) $count,
199
+ 'storage' => '{{dataStorageMode}}',
200
+ );
201
+ }
202
+
203
+ // Route handlers are the main product-level extension point for request/response shaping.
204
+ function {{phpPrefix}}_handle_get_state( WP_REST_Request $request ) {
205
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
206
+ array(
207
+ 'postId' => $request->get_param( 'postId' ),
208
+ 'resourceKey' => $request->get_param( 'resourceKey' ),
209
+ ),
210
+ {{phpPrefix}}_get_rest_build_dir(),
211
+ 'state-query',
212
+ 'query'
213
+ );
214
+
215
+ if ( is_wp_error( $payload ) ) {
216
+ return $payload;
217
+ }
218
+
219
+ $count = {{phpPrefix}}_get_counter( (int) $payload['postId'], (string) $payload['resourceKey'] );
220
+ return rest_ensure_response(
221
+ {{phpPrefix}}_build_state_response(
222
+ (int) $payload['postId'],
223
+ (string) $payload['resourceKey'],
224
+ $count
225
+ )
226
+ );
227
+ }
228
+
229
+ function {{phpPrefix}}_handle_write_state( WP_REST_Request $request ) {
230
+ $payload = {{phpPrefix}}_validate_and_sanitize_request(
231
+ $request->get_json_params(),
232
+ {{phpPrefix}}_get_rest_build_dir(),
233
+ 'write-state-request',
234
+ 'body'
235
+ );
236
+
237
+ if ( is_wp_error( $payload ) ) {
238
+ return $payload;
239
+ }
240
+
241
+ $request_consumed = {{phpPrefix}}_consume_public_write_request_id(
242
+ (int) $payload['postId'],
243
+ (string) $payload['resourceKey'],
244
+ isset( $payload['publicWriteRequestId'] ) ? (string) $payload['publicWriteRequestId'] : ''
245
+ );
246
+
247
+ if ( is_wp_error( $request_consumed ) ) {
248
+ return $request_consumed;
249
+ }
250
+
251
+ $count = {{phpPrefix}}_increment_counter(
252
+ (int) $payload['postId'],
253
+ (string) $payload['resourceKey'],
254
+ isset( $payload['delta'] ) ? (int) $payload['delta'] : 1
255
+ );
256
+
257
+ if ( is_wp_error( $count ) ) {
258
+ {{phpPrefix}}_release_public_write_request_id(
259
+ (int) $payload['postId'],
260
+ (string) $payload['resourceKey'],
261
+ isset( $payload['publicWriteRequestId'] ) ? (string) $payload['publicWriteRequestId'] : ''
262
+ );
263
+ return $count;
264
+ }
265
+
266
+ return rest_ensure_response(
267
+ {{phpPrefix}}_build_state_response(
268
+ (int) $payload['postId'],
269
+ (string) $payload['resourceKey'],
270
+ $count
271
+ )
272
+ );
273
+ }
274
+
275
+ // Add or reorganize endpoints here as the REST surface grows.
276
+ function {{phpPrefix}}_register_routes() {
277
+ register_rest_route(
278
+ '{{namespace}}/v1',
279
+ '/{{slugKebabCase}}/state',
280
+ array(
281
+ array(
282
+ 'methods' => WP_REST_Server::READABLE,
283
+ 'callback' => '{{phpPrefix}}_handle_get_state',
284
+ 'permission_callback' => '__return_true',
285
+ ),
286
+ array(
287
+ 'methods' => WP_REST_Server::CREATABLE,
288
+ 'callback' => '{{phpPrefix}}_handle_write_state',
289
+ 'permission_callback' => '{{phpPrefix}}_can_write_publicly',
290
+ ),
291
+ )
292
+ );
293
+ }
294
+
295
+ function {{phpPrefix}}_register_block() {
296
+ $build_dir = {{phpPrefix}}_get_build_dir();
297
+ if ( ! $build_dir ) {
298
+ return;
299
+ }
300
+
301
+ register_block_type( $build_dir );
302
+ }
303
+
304
+ register_activation_hook( __FILE__, '{{phpPrefix}}_maybe_install_storage' );
305
+ add_action( 'init', '{{phpPrefix}}_load_textdomain' );
306
+ add_action( 'init', '{{phpPrefix}}_ensure_storage_installed' );
307
+ add_action( 'init', '{{phpPrefix}}_register_block' );
308
+ add_action( 'rest_api_init', '{{phpPrefix}}_register_routes' );
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://schemas.wp.org/trunk/wp-env.json",
3
+ "core": null,
4
+ "port": 8889,
5
+ "testsEnvironment": false,
6
+ "plugins": [
7
+ "."
8
+ ],
9
+ "config": {
10
+ "WP_DEBUG": true,
11
+ "WP_DEBUG_LOG": true,
12
+ "WP_DEBUG_DISPLAY": false,
13
+ "SCRIPT_DEBUG": true,
14
+ "WP_ENVIRONMENT_TYPE": "local"
15
+ }
16
+ }
@@ -0,0 +1,22 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./tests/e2e",
5
+ reporter: [["list"], ["html", { outputFolder: "playwright-report" }]],
6
+ timeout: 30_000,
7
+ expect: {
8
+ timeout: 10_000,
9
+ },
10
+ use: {
11
+ baseURL: "http://localhost:8889",
12
+ screenshot: "only-on-failure",
13
+ trace: "on-first-retry",
14
+ video: "retain-on-failure",
15
+ },
16
+ projects: [
17
+ {
18
+ name: "chromium",
19
+ use: { ...devices["Desktop Chrome"] },
20
+ },
21
+ ],
22
+ });