@wp-typia/project-tools 0.24.0 → 0.24.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.
- package/dist/runtime/cli-add-help.js +3 -2
- package/dist/runtime/cli-add-types.d.ts +2 -2
- package/dist/runtime/cli-add-types.js +2 -2
- package/dist/runtime/cli-add-workspace-ai-source-emitters.js +2 -26
- package/dist/runtime/cli-add-workspace-core-variation.d.ts +3 -1
- package/dist/runtime/cli-add-workspace-core-variation.js +108 -1
- package/dist/runtime/cli-add-workspace-pattern-options.js +21 -9
- package/dist/runtime/cli-add.d.ts +1 -1
- package/dist/runtime/cli-add.js +1 -1
- package/dist/runtime/cli-help.js +3 -2
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/json-utils.d.ts +2 -5
- package/dist/runtime/json-utils.js +2 -5
- package/dist/runtime/package-versions.js +5 -6
- package/dist/runtime/pattern-catalog-section-roles.d.ts +101 -0
- package/dist/runtime/pattern-catalog-section-roles.js +276 -0
- package/dist/runtime/pattern-catalog.d.ts +4 -58
- package/dist/runtime/pattern-catalog.js +5 -251
- package/dist/runtime/template-source-cache-markers.js +7 -2
- package/package.json +2 -2
|
@@ -14,7 +14,7 @@ export function formatAddHelpText() {
|
|
|
14
14
|
wp-typia add variation <name> --block <block-slug> [--dry-run]
|
|
15
15
|
wp-typia add style <name> --block <block-slug> [--dry-run]
|
|
16
16
|
wp-typia add transform <name> --from <namespace/block> --to <block-slug|namespace/block-slug> [--dry-run]
|
|
17
|
-
wp-typia add pattern <name> [--scope <full|section>] [--section-role <role>] [--tags <tag,...>] [--thumbnail-url <url>] [--dry-run]
|
|
17
|
+
wp-typia add pattern <name> [--scope <full|section>] [--section-role <role>] [--catalog-title <title>] [--tags <tag,...>] [--tag <tag>...] [--thumbnail-url <url>] [--dry-run]
|
|
18
18
|
wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>] [--from-post-meta|--post-meta <post-meta> [--meta-path <field>]] [--dry-run]
|
|
19
19
|
wp-typia add contract <name> [--type <ExportedTypeName>] [--dry-run]
|
|
20
20
|
wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <${REST_RESOURCE_METHOD_IDS.join(",")}>] [--route-pattern <route-pattern>] [--permission-callback <callback>] [--controller-class <ClassName>] [--controller-extends <BaseClass>] [--dry-run]
|
|
@@ -30,6 +30,7 @@ Notes:
|
|
|
30
30
|
Pass \`--dry-run\` to preview the workspace files that would change without writing them.
|
|
31
31
|
Interactive add flows let you choose a template when \`--template\` is omitted; non-interactive runs default to \`basic\`.
|
|
32
32
|
\`add core-variation\` registers editor-side variations for existing core or external blocks without generating block.json or Typia manifests.
|
|
33
|
+
Unknown \`core/*\` core-variation targets warn instead of failing so newer WordPress core blocks remain scaffoldable; third-party namespaces are allowed without local discovery.
|
|
33
34
|
\`add admin-view\` scaffolds an opt-in DataViews-powered WordPress admin screen under \`src/admin-views/\`.
|
|
34
35
|
Pass \`--source rest-resource:<slug>\` to reuse a list-capable REST resource.
|
|
35
36
|
Pass \`--source core-data:postType/post\` or \`--source core-data:taxonomy/category\` to bind a WordPress-owned entity collection.
|
|
@@ -41,7 +42,7 @@ Notes:
|
|
|
41
42
|
\`add variation\` targets an existing block slug from \`scripts/block-config.ts\`.
|
|
42
43
|
\`add style\` registers a Block Styles option for an existing generated block.
|
|
43
44
|
\`add transform\` adds a block-to-block transform into an existing generated block.
|
|
44
|
-
\`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/full/\` or \`src/patterns/sections/\` and records typed catalog metadata in \`PATTERNS
|
|
45
|
+
\`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/full/\` or \`src/patterns/sections/\` and records typed catalog metadata in \`PATTERNS\`; pass \`--catalog-title "Hero Photo"\` to override the generated catalog title, pass \`--tags hero,landing\` for a comma-separated tag list, and repeat \`--tag hero --tag landing\` for single tag values.
|
|
45
46
|
\`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`; pass \`--block\` and \`--attribute\` together to declare an end-to-end bindable attribute on an existing generated block. Pass \`--from-post-meta\` or \`--post-meta\` to generate a source backed by a typed post-meta contract; \`--meta-path\` selects one top-level field as the default binding arg.
|
|
46
47
|
\`add contract\` registers a standalone TypeScript wire contract under \`src/contracts/\` and generates a stable JSON Schema artifact without creating PHP route glue.
|
|
47
48
|
\`add rest-resource\` scaffolds plugin-level TypeScript REST contracts under \`src/rest/\` and PHP route glue under \`inc/rest/\`. Use \`--route-pattern\`, \`--permission-callback\`, \`--controller-class\`, and \`--controller-extends\` when an existing WordPress controller or permission model needs to own part of the generated route surface.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { PATTERN_CATALOG_SCOPE_IDS, type PatternCatalogScope } from "./pattern-catalog.js";
|
|
1
|
+
import { PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, type PatternCatalogScope } from "./pattern-catalog.js";
|
|
2
2
|
export { ADD_KIND_IDS } from "./cli-add-kind-ids.js";
|
|
3
3
|
export type { AddKindId } from "./cli-add-kind-ids.js";
|
|
4
|
-
export { PATTERN_CATALOG_SCOPE_IDS };
|
|
4
|
+
export { PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, };
|
|
5
5
|
export type { PatternCatalogScope };
|
|
6
6
|
/**
|
|
7
7
|
* Supported plugin-level REST resource methods accepted by
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { suggestCloseId } from "./id-suggestions.js";
|
|
2
|
-
import { PATTERN_CATALOG_SCOPE_IDS, } from "./pattern-catalog.js";
|
|
2
|
+
import { PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, } from "./pattern-catalog.js";
|
|
3
3
|
export { ADD_KIND_IDS } from "./cli-add-kind-ids.js";
|
|
4
|
-
export { PATTERN_CATALOG_SCOPE_IDS };
|
|
4
|
+
export { PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, };
|
|
5
5
|
/**
|
|
6
6
|
* Supported plugin-level REST resource methods accepted by
|
|
7
7
|
* `wp-typia add rest-resource --methods`.
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { quoteTsString } from "./cli-add-shared.js";
|
|
2
2
|
import { buildAiFeatureEndpointManifest } from "./ai-feature-artifacts.js";
|
|
3
3
|
import { OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY, createScaffoldCompatibilityConfig, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
|
|
4
|
+
import { formatResolveRestNonceSource, indentMultiline, } from "./cli-add-workspace-rest-source-utils.js";
|
|
4
5
|
import { toPascalCase, toTitleCase } from "./string-case.js";
|
|
5
6
|
export { buildAiFeatureSyncScriptSource, } from "./cli-add-workspace-ai-sync-script-source.js";
|
|
6
|
-
function indentMultiline(source, prefix) {
|
|
7
|
-
return source
|
|
8
|
-
.split("\n")
|
|
9
|
-
.map((line) => `${prefix}${line}`)
|
|
10
|
-
.join("\n");
|
|
11
|
-
}
|
|
12
7
|
/**
|
|
13
8
|
* Build the workspace inventory entry written into `scripts/block-config.ts` for one AI feature.
|
|
14
9
|
*/
|
|
@@ -175,26 +170,7 @@ import {
|
|
|
175
170
|
\trun${pascalCase}AiFeatureEndpoint,
|
|
176
171
|
} from './api-client';
|
|
177
172
|
|
|
178
|
-
|
|
179
|
-
\tif ( typeof fallback === 'string' && fallback.length > 0 ) {
|
|
180
|
-
\t\treturn fallback;
|
|
181
|
-
\t}
|
|
182
|
-
|
|
183
|
-
\tif ( typeof window === 'undefined' ) {
|
|
184
|
-
\t\treturn undefined;
|
|
185
|
-
\t}
|
|
186
|
-
|
|
187
|
-
\tconst wpApiSettings = (
|
|
188
|
-
\t\twindow as typeof window & {
|
|
189
|
-
\t\t\twpApiSettings?: { nonce?: string };
|
|
190
|
-
\t\t}
|
|
191
|
-
\t).wpApiSettings;
|
|
192
|
-
|
|
193
|
-
\treturn typeof wpApiSettings?.nonce === 'string' &&
|
|
194
|
-
\t\twpApiSettings.nonce.length > 0
|
|
195
|
-
\t\t? wpApiSettings.nonce
|
|
196
|
-
\t\t: undefined;
|
|
197
|
-
}
|
|
173
|
+
${formatResolveRestNonceSource("spaced")}
|
|
198
174
|
|
|
199
175
|
function isPlainObject( value: unknown ): value is Record< string, unknown > {
|
|
200
176
|
\treturn (
|
|
@@ -7,7 +7,8 @@ import { type RunAddCoreVariationCommandOptions } from "./cli-add-shared.js";
|
|
|
7
7
|
* Defaults to `process.cwd()`.
|
|
8
8
|
* @param options.targetBlockName Full `namespace/block` name that receives the variation.
|
|
9
9
|
* @param options.variationName Human-entered variation name normalized into the generated slug.
|
|
10
|
-
* @returns The normalized variation metadata
|
|
10
|
+
* @returns The normalized variation metadata, owning workspace directory, and
|
|
11
|
+
* optional warnings for suspicious but forward-compatible targets.
|
|
11
12
|
* @throws {Error} When the command is run outside an official workspace, the
|
|
12
13
|
* target block name is not full `namespace/block` form, or the generated file
|
|
13
14
|
* already exists.
|
|
@@ -17,4 +18,5 @@ export declare function runAddCoreVariationCommand({ cwd, targetBlockName, varia
|
|
|
17
18
|
targetBlockName: string;
|
|
18
19
|
variationFile: string;
|
|
19
20
|
variationSlug: string;
|
|
21
|
+
warnings?: string[];
|
|
20
22
|
}>;
|
|
@@ -8,6 +8,101 @@ import { toKebabCase, toPascalCase, toTitleCase } from "./string-case.js";
|
|
|
8
8
|
import { resolveWorkspaceProject } from "./workspace-project.js";
|
|
9
9
|
const CORE_VARIATIONS_EDITOR_PLUGIN_SLUG = "core-variations";
|
|
10
10
|
const CORE_VARIATION_USAGE = "wp-typia add core-variation <block-name> <name> or wp-typia add core-variation <name> --block <namespace/block>";
|
|
11
|
+
const KNOWN_CORE_VARIATION_TARGETS = new Set([
|
|
12
|
+
"core/archives",
|
|
13
|
+
"core/audio",
|
|
14
|
+
"core/avatar",
|
|
15
|
+
"core/block",
|
|
16
|
+
"core/button",
|
|
17
|
+
"core/buttons",
|
|
18
|
+
"core/calendar",
|
|
19
|
+
"core/categories",
|
|
20
|
+
"core/code",
|
|
21
|
+
"core/column",
|
|
22
|
+
"core/columns",
|
|
23
|
+
"core/comment-author-name",
|
|
24
|
+
"core/comment-content",
|
|
25
|
+
"core/comment-date",
|
|
26
|
+
"core/comment-edit-link",
|
|
27
|
+
"core/comment-reply-link",
|
|
28
|
+
"core/comment-template",
|
|
29
|
+
"core/comments",
|
|
30
|
+
"core/comments-pagination",
|
|
31
|
+
"core/comments-pagination-next",
|
|
32
|
+
"core/comments-pagination-numbers",
|
|
33
|
+
"core/comments-pagination-previous",
|
|
34
|
+
"core/comments-title",
|
|
35
|
+
"core/cover",
|
|
36
|
+
"core/details",
|
|
37
|
+
"core/embed",
|
|
38
|
+
"core/file",
|
|
39
|
+
"core/footnotes",
|
|
40
|
+
"core/freeform",
|
|
41
|
+
"core/gallery",
|
|
42
|
+
"core/group",
|
|
43
|
+
"core/heading",
|
|
44
|
+
"core/home-link",
|
|
45
|
+
"core/html",
|
|
46
|
+
"core/image",
|
|
47
|
+
"core/latest-comments",
|
|
48
|
+
"core/latest-posts",
|
|
49
|
+
"core/legacy-widget",
|
|
50
|
+
"core/list",
|
|
51
|
+
"core/list-item",
|
|
52
|
+
"core/loginout",
|
|
53
|
+
"core/media-text",
|
|
54
|
+
"core/missing",
|
|
55
|
+
"core/more",
|
|
56
|
+
"core/navigation",
|
|
57
|
+
"core/navigation-link",
|
|
58
|
+
"core/navigation-submenu",
|
|
59
|
+
"core/nextpage",
|
|
60
|
+
"core/page-list",
|
|
61
|
+
"core/paragraph",
|
|
62
|
+
"core/pattern",
|
|
63
|
+
"core/post-author",
|
|
64
|
+
"core/post-author-biography",
|
|
65
|
+
"core/post-author-name",
|
|
66
|
+
"core/post-comments",
|
|
67
|
+
"core/post-comments-form",
|
|
68
|
+
"core/post-content",
|
|
69
|
+
"core/post-date",
|
|
70
|
+
"core/post-excerpt",
|
|
71
|
+
"core/post-featured-image",
|
|
72
|
+
"core/post-navigation-link",
|
|
73
|
+
"core/post-terms",
|
|
74
|
+
"core/post-template",
|
|
75
|
+
"core/post-title",
|
|
76
|
+
"core/preformatted",
|
|
77
|
+
"core/pullquote",
|
|
78
|
+
"core/query",
|
|
79
|
+
"core/query-no-results",
|
|
80
|
+
"core/query-pagination",
|
|
81
|
+
"core/query-pagination-next",
|
|
82
|
+
"core/query-pagination-numbers",
|
|
83
|
+
"core/query-pagination-previous",
|
|
84
|
+
"core/query-title",
|
|
85
|
+
"core/quote",
|
|
86
|
+
"core/read-more",
|
|
87
|
+
"core/rss",
|
|
88
|
+
"core/search",
|
|
89
|
+
"core/separator",
|
|
90
|
+
"core/shortcode",
|
|
91
|
+
"core/site-logo",
|
|
92
|
+
"core/site-tagline",
|
|
93
|
+
"core/site-title",
|
|
94
|
+
"core/social-link",
|
|
95
|
+
"core/social-links",
|
|
96
|
+
"core/spacer",
|
|
97
|
+
"core/table",
|
|
98
|
+
"core/table-of-contents",
|
|
99
|
+
"core/tag-cloud",
|
|
100
|
+
"core/template-part",
|
|
101
|
+
"core/term-description",
|
|
102
|
+
"core/text-columns",
|
|
103
|
+
"core/verse",
|
|
104
|
+
"core/video",
|
|
105
|
+
]);
|
|
11
106
|
const CORE_VARIATION_SIMPLE_CONTAINER_BLOCKS = new Set([
|
|
12
107
|
"core/column",
|
|
13
108
|
"core/cover",
|
|
@@ -54,6 +149,13 @@ function buildCoreVariationImportPath(ref) {
|
|
|
54
149
|
function formatCoreVariationTitle(variationSlug) {
|
|
55
150
|
return toTitleCase(variationSlug);
|
|
56
151
|
}
|
|
152
|
+
function getUnknownCoreVariationTargetWarning(targetBlockName) {
|
|
153
|
+
if (!targetBlockName.startsWith("core/") ||
|
|
154
|
+
KNOWN_CORE_VARIATION_TARGETS.has(targetBlockName)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return `Target block "${targetBlockName}" uses the WordPress core namespace but is not in wp-typia's known core block list. The variation was generated for forward compatibility; verify the block name or update wp-typia if this is a newer core block.`;
|
|
158
|
+
}
|
|
57
159
|
function assertCoreVariationDoesNotExist(projectDir, targetBlockName, variationSlug) {
|
|
58
160
|
const variationFilePath = getCoreVariationFilePath(projectDir, targetBlockName, variationSlug);
|
|
59
161
|
if (fs.existsSync(variationFilePath)) {
|
|
@@ -261,7 +363,8 @@ async function writeCoreVariationRegistry(projectDir, targetBlockName, textDomai
|
|
|
261
363
|
* Defaults to `process.cwd()`.
|
|
262
364
|
* @param options.targetBlockName Full `namespace/block` name that receives the variation.
|
|
263
365
|
* @param options.variationName Human-entered variation name normalized into the generated slug.
|
|
264
|
-
* @returns The normalized variation metadata
|
|
366
|
+
* @returns The normalized variation metadata, owning workspace directory, and
|
|
367
|
+
* optional warnings for suspicious but forward-compatible targets.
|
|
265
368
|
* @throws {Error} When the command is run outside an official workspace, the
|
|
266
369
|
* target block name is not full `namespace/block` form, or the generated file
|
|
267
370
|
* already exists.
|
|
@@ -270,6 +373,7 @@ export async function runAddCoreVariationCommand({ cwd = process.cwd(), targetBl
|
|
|
270
373
|
const workspace = resolveWorkspaceProject(cwd);
|
|
271
374
|
const resolvedTargetBlockName = assertFullBlockName(targetBlockName, "core-variation target");
|
|
272
375
|
const variationSlug = assertValidGeneratedSlug("Core variation name", normalizeBlockSlug(variationName), CORE_VARIATION_USAGE);
|
|
376
|
+
const unknownCoreTargetWarning = getUnknownCoreVariationTargetWarning(resolvedTargetBlockName);
|
|
273
377
|
assertCoreVariationSlugIsNotRegistryIndex(variationSlug);
|
|
274
378
|
assertCoreVariationDoesNotExist(workspace.projectDir, resolvedTargetBlockName, variationSlug);
|
|
275
379
|
const bootstrapPath = path.join(workspace.projectDir, `${workspace.packageName.split("/").pop() ?? workspace.packageName}.php`);
|
|
@@ -313,6 +417,9 @@ export async function runAddCoreVariationCommand({ cwd = process.cwd(), targetBl
|
|
|
313
417
|
targetBlockName: resolvedTargetBlockName,
|
|
314
418
|
variationFile: path.relative(workspace.projectDir, variationFilePath),
|
|
315
419
|
variationSlug,
|
|
420
|
+
...(unknownCoreTargetWarning
|
|
421
|
+
? { warnings: [unknownCoreTargetWarning] }
|
|
422
|
+
: {}),
|
|
316
423
|
};
|
|
317
424
|
}
|
|
318
425
|
catch (error) {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { isValidPatternThumbnailUrl, PATTERN_CATALOG_SCOPE_IDS, } from "./pattern-catalog.js";
|
|
2
|
+
import { normalizeBlockSlug, } from "./cli-add-shared.js";
|
|
3
|
+
import { isValidPatternThumbnailUrl, PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, } from "./pattern-catalog.js";
|
|
4
4
|
import { toTitleCase } from "./string-case.js";
|
|
5
5
|
const PATTERN_CONTENT_FILE_ROOT = "src/patterns/";
|
|
6
|
-
const PATTERN_TAG_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
7
6
|
function assertValidPatternRelativePath(label, value, usage) {
|
|
8
7
|
const normalizedPath = value.trim().replace(/\\/gu, "/");
|
|
9
8
|
if (normalizedPath.length === 0 ||
|
|
@@ -41,17 +40,30 @@ function resolvePatternScope(scope) {
|
|
|
41
40
|
}
|
|
42
41
|
throw new Error(`Pattern scope must be one of: ${PATTERN_CATALOG_SCOPE_IDS.join(", ")}.`);
|
|
43
42
|
}
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Normalize and validate a raw pattern section role before catalog generation.
|
|
45
|
+
* Non-empty values are normalized with normalizeBlockSlug; valid roles start
|
|
46
|
+
* with a lowercase letter and then use lowercase letters, numbers, or hyphens.
|
|
47
|
+
*
|
|
48
|
+
* @param value Raw section-role input from CLI or programmatic callers.
|
|
49
|
+
* @returns Normalized role slug, or undefined when the input is empty.
|
|
50
|
+
* @throws When the normalized role does not match the shared section-role pattern.
|
|
51
|
+
*/
|
|
52
|
+
function normalizePatternSectionRole(value) {
|
|
45
53
|
if (value === undefined || value.trim() === "") {
|
|
46
54
|
return undefined;
|
|
47
55
|
}
|
|
48
|
-
|
|
56
|
+
const sectionRole = normalizeBlockSlug(value);
|
|
57
|
+
if (!PATTERN_SECTION_ROLE_PATTERN.test(sectionRole)) {
|
|
58
|
+
throw new Error("Pattern section role must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens. Section roles apply only with `--scope section`.");
|
|
59
|
+
}
|
|
60
|
+
return sectionRole;
|
|
49
61
|
}
|
|
50
62
|
function normalizePatternTags(tags) {
|
|
51
63
|
const rawTags = typeof tags === "string"
|
|
52
64
|
? tags.split(",")
|
|
53
65
|
: Array.isArray(tags)
|
|
54
|
-
?
|
|
66
|
+
? tags.flatMap((tag) => tag.split(","))
|
|
55
67
|
: [];
|
|
56
68
|
const normalizedTags = rawTags
|
|
57
69
|
.map((tag) => normalizeBlockSlug(tag))
|
|
@@ -91,12 +103,12 @@ function resolvePatternContentFile(patternSlug, patternScope, contentFile) {
|
|
|
91
103
|
*/
|
|
92
104
|
export function resolvePatternCatalogOptions(patternSlug, options) {
|
|
93
105
|
const patternScope = resolvePatternScope(options.patternScope);
|
|
94
|
-
const sectionRole =
|
|
106
|
+
const sectionRole = normalizePatternSectionRole(options.sectionRole);
|
|
95
107
|
if (patternScope === "section" && !sectionRole) {
|
|
96
|
-
throw new Error("`wp-typia add pattern --scope section` requires --section-role <role
|
|
108
|
+
throw new Error("`wp-typia add pattern --scope section` requires --section-role <role> because section-scoped patterns need a typed catalog section role.");
|
|
97
109
|
}
|
|
98
110
|
if (patternScope !== "section" && sectionRole) {
|
|
99
|
-
throw new Error("`--section-role`
|
|
111
|
+
throw new Error("`--section-role` only applies with `--scope section`. Use `--scope section --section-role <role>` or omit `--section-role` for full patterns.");
|
|
100
112
|
}
|
|
101
113
|
const title = options.catalogTitle && options.catalogTitle.trim() !== ""
|
|
102
114
|
? options.catalogTitle.trim()
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `cli-add-block` for built-in block scaffolding
|
|
8
8
|
* - `cli-add-workspace` for workspace mutation commands
|
|
9
9
|
*/
|
|
10
|
-
export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, PATTERN_CATALOG_SCOPE_IDS, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
|
|
10
|
+
export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, normalizeBlockSlug, PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
|
|
11
11
|
export type { AddBlockTemplateId, AddKindId, EditorPluginSlotId, PatternCatalogScope, } from "./cli-add-shared.js";
|
|
12
12
|
export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
|
|
13
13
|
export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddContractCommand, runAddCoreVariationCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddIntegrationEnvCommand, runAddPatternCommand, runAddPostMetaCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
|
package/dist/runtime/cli-add.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `cli-add-block` for built-in block scaffolding
|
|
8
8
|
* - `cli-add-workspace` for workspace mutation commands
|
|
9
9
|
*/
|
|
10
|
-
export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, PATTERN_CATALOG_SCOPE_IDS, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
|
|
10
|
+
export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, normalizeBlockSlug, PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, suggestAddBlockTemplateId, } from "./cli-add-shared.js";
|
|
11
11
|
export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
|
|
12
12
|
export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddContractCommand, runAddCoreVariationCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddIntegrationEnvCommand, runAddPatternCommand, runAddPostMetaCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
|
|
13
13
|
export { getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, } from "./workspace-inventory.js";
|
package/dist/runtime/cli-help.js
CHANGED
|
@@ -27,7 +27,7 @@ export function formatHelpText() {
|
|
|
27
27
|
wp-typia add variation <name> --block <block-slug>
|
|
28
28
|
wp-typia add style <name> --block <block-slug>
|
|
29
29
|
wp-typia add transform <name> --from <namespace/block> --to <block-slug|namespace/block-slug>
|
|
30
|
-
wp-typia add pattern <name> [--scope <full|section>] [--section-role <role>] [--tags <tag,...>] [--thumbnail-url <url>]
|
|
30
|
+
wp-typia add pattern <name> [--scope <full|section>] [--section-role <role>] [--catalog-title <title>] [--tags <tag,...>] [--tag <tag>...] [--thumbnail-url <url>]
|
|
31
31
|
wp-typia add binding-source <name> [--block <block-slug|namespace/block-slug> --attribute <attribute>] [--from-post-meta|--post-meta <post-meta> [--meta-path <field>]]
|
|
32
32
|
wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <method[,method...]>]
|
|
33
33
|
wp-typia add rest-resource <name> --manual [--namespace <vendor/v1>] [--method <GET|POST|PUT|PATCH|DELETE>] [--auth <public|authenticated|public-write-protected>] [--path <route-pattern>|--route-pattern <route-pattern>] [--permission-callback <callback>] [--controller-class <ClassName>] [--controller-extends <BaseClass>] [--query-type <Type>] [--body-type <Type>] [--response-type <Type>] [--secret-field <field>] [--secret-state-field|--secret-has-value-field <field>] [--secret-preserve-on-empty <true|false>]
|
|
@@ -56,6 +56,7 @@ Notes:
|
|
|
56
56
|
Use \`--template workspace\` as shorthand for \`@wp-typia/create-workspace-template\`, the official empty workspace scaffold behind \`wp-typia add ...\`.
|
|
57
57
|
Use \`--profile plugin-qa\` with \`--template workspace\` when a plugin needs local wp-env smoke checks, \`.env.example\`, and release zip scripts from day one; omit it for the minimal workspace shell.
|
|
58
58
|
Interactive add flows let you choose a template when \`--template\` is omitted; non-interactive runs default to \`basic\`.
|
|
59
|
+
Unknown \`core/*\` targets for \`add core-variation\` warn instead of failing so newer WordPress core blocks remain scaffoldable; third-party namespaces are allowed without local discovery.
|
|
59
60
|
\`add admin-view\` scaffolds an opt-in DataViews-powered WordPress admin screen under \`src/admin-views/\`.
|
|
60
61
|
Pass \`--source rest-resource:<slug>\` to reuse a list-capable REST resource.
|
|
61
62
|
Pass \`--source core-data:postType/post\` or \`--source core-data:taxonomy/category\` to bind a WordPress-owned entity collection.
|
|
@@ -67,7 +68,7 @@ Notes:
|
|
|
67
68
|
\`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
|
|
68
69
|
\`add style\` registers a Block Styles option for an existing generated block.
|
|
69
70
|
\`add transform\` adds a block-to-block transform into an existing generated block.
|
|
70
|
-
\`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/full/\` or \`src/patterns/sections
|
|
71
|
+
\`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/full/\` or \`src/patterns/sections/\`; pass \`--catalog-title "Hero Photo"\` to override the generated catalog title, pass \`--tags hero,landing\` for a comma-separated tag list, and repeat \`--tag hero --tag landing\` for single tag values.
|
|
71
72
|
\`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`; pass \`--block\` and \`--attribute\` together to declare a bindable generated-block attribute. Pass \`--from-post-meta\` or \`--post-meta\` to back the source from a typed post-meta contract and \`--meta-path\` to choose its default top-level field.
|
|
72
73
|
\`add rest-resource\` scaffolds plugin-level TypeScript REST contracts under \`src/rest/\` and PHP route glue under \`inc/rest/\`.
|
|
73
74
|
\`add rest-resource --manual\` tracks an external/provider REST route with typed schemas, OpenAPI, clients, and drift checks without generating PHP route/controller files while still allowing route-owner metadata such as permission callbacks and controller classes. Settings contracts can add \`--secret-field\` plus \`--secret-preserve-on-empty\` to model write-only credentials and preserve blank submissions.
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -40,6 +40,6 @@ export { EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, pruneExternalTemplateCache, } fro
|
|
|
40
40
|
export type { ExternalTemplateCachePruneOptions, ExternalTemplateCachePruneResult, } from "./template-source-cache.js";
|
|
41
41
|
export { STALE_TEMP_ROOT_MAX_AGE_MS, WP_TYPIA_TEMP_ROOT_PREFIX, cleanupManagedTempRoot, cleanupStaleTempRoots, createManagedTempRoot, getTrackedTempRoots, } from "./temp-roots.js";
|
|
42
42
|
export { createReadlinePrompt, createDoctorRunSummary, createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatAddHelpText, formatDoctorCheckLine, formatDoctorSummaryLine, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getDoctorExitFailureChecks, getDoctorExitFailureDetailLines, getDoctorFailureDetailLines, getFailingDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, HOOKED_BLOCK_POSITION_IDS, EDITOR_PLUGIN_SLOT_IDS, isCliDiagnosticError, runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddCoreVariationCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddPostMetaCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
|
|
43
|
-
export { extractPatternSectionRoleMatches, extractPatternSectionRolesFromAttributes, formatPatternCatalogDiagnostics, PATTERN_CATALOG_SCOPE_IDS, resolvePatternCatalogContentFile, validatePatternCatalog, } from "./pattern-catalog.js";
|
|
43
|
+
export { extractPatternSectionRoleMatches, extractPatternSectionRolesFromAttributes, formatPatternCatalogDiagnostics, PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, resolvePatternCatalogContentFile, validatePatternCatalog, } from "./pattern-catalog.js";
|
|
44
44
|
export type { CliDiagnosticCode, CliDiagnosticCodeError, CliDiagnosticMessage, DoctorCheck, DoctorCheckScope, DoctorExitPolicy, DoctorFailureSummary, DoctorRunSummary, EditorPluginSlotId, HookedBlockPositionId, ReadlinePrompt, WorkspaceBlockSelectOption, } from "./cli-core.js";
|
|
45
45
|
export type { PatternCatalogDiagnostic, PatternCatalogDiagnosticCode, PatternCatalogDiagnosticSeverity, PatternCatalogEntry, PatternCatalogScope, PatternCatalogSectionRoleConvention, PatternCatalogSectionRoleMatch, PatternCatalogValidationOptions, PatternCatalogValidationResult, } from "./pattern-catalog.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -31,4 +31,4 @@ export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOpti
|
|
|
31
31
|
export { EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, pruneExternalTemplateCache, } from "./template-source-cache.js";
|
|
32
32
|
export { STALE_TEMP_ROOT_MAX_AGE_MS, WP_TYPIA_TEMP_ROOT_PREFIX, cleanupManagedTempRoot, cleanupStaleTempRoots, createManagedTempRoot, getTrackedTempRoots, } from "./temp-roots.js";
|
|
33
33
|
export { createReadlinePrompt, createDoctorRunSummary, createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatAddHelpText, formatDoctorCheckLine, formatDoctorSummaryLine, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getDoctorExitFailureChecks, getDoctorExitFailureDetailLines, getDoctorFailureDetailLines, getFailingDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, HOOKED_BLOCK_POSITION_IDS, EDITOR_PLUGIN_SLOT_IDS, isCliDiagnosticError, runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddCoreVariationCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddPostMetaCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
|
|
34
|
-
export { extractPatternSectionRoleMatches, extractPatternSectionRolesFromAttributes, formatPatternCatalogDiagnostics, PATTERN_CATALOG_SCOPE_IDS, resolvePatternCatalogContentFile, validatePatternCatalog, } from "./pattern-catalog.js";
|
|
34
|
+
export { extractPatternSectionRoleMatches, extractPatternSectionRolesFromAttributes, formatPatternCatalogDiagnostics, PATTERN_CATALOG_SCOPE_IDS, PATTERN_SECTION_ROLE_PATTERN, PATTERN_TAG_PATTERN, resolvePatternCatalogContentFile, validatePatternCatalog, } from "./pattern-catalog.js";
|
|
@@ -4,11 +4,8 @@
|
|
|
4
4
|
* File-backed runtime JSON readers should use `safeJsonParse`,
|
|
5
5
|
* `readJsonFileSync`, or `readJsonFile` so malformed JSON reports the file path
|
|
6
6
|
* and operation context. Raw `JSON.parse` remains intentional for trusted
|
|
7
|
-
* in-memory clones, subprocess output, test fixtures, generated workspace
|
|
8
|
-
* script templates that embed their own path-aware parse handling
|
|
9
|
-
* package-version manifest cache probes with colocated path-aware wrappers,
|
|
10
|
-
* and cache/discovery probes that immediately catch malformed documents to
|
|
11
|
-
* continue with fallback behavior.
|
|
7
|
+
* in-memory clones, subprocess output, test fixtures, and generated workspace
|
|
8
|
+
* script templates that embed their own path-aware parse handling.
|
|
12
9
|
*
|
|
13
10
|
* This module keeps `cloneJsonValue` local instead of re-exporting the
|
|
14
11
|
* block-runtime helper so Bunli CLI bundles that only need project-tools JSON
|
|
@@ -6,11 +6,8 @@ import { promises as fsp } from "node:fs";
|
|
|
6
6
|
* File-backed runtime JSON readers should use `safeJsonParse`,
|
|
7
7
|
* `readJsonFileSync`, or `readJsonFile` so malformed JSON reports the file path
|
|
8
8
|
* and operation context. Raw `JSON.parse` remains intentional for trusted
|
|
9
|
-
* in-memory clones, subprocess output, test fixtures, generated workspace
|
|
10
|
-
* script templates that embed their own path-aware parse handling
|
|
11
|
-
* package-version manifest cache probes with colocated path-aware wrappers,
|
|
12
|
-
* and cache/discovery probes that immediately catch malformed documents to
|
|
13
|
-
* continue with fallback behavior.
|
|
9
|
+
* in-memory clones, subprocess output, test fixtures, and generated workspace
|
|
10
|
+
* script templates that embed their own path-aware parse handling.
|
|
14
11
|
*
|
|
15
12
|
* This module keeps `cloneJsonValue` local instead of re-exporting the
|
|
16
13
|
* block-runtime helper so Bunli CLI bundles that only need project-tools JSON
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getOptionalNodeErrorCode } from './fs-async.js';
|
|
5
|
+
import { safeJsonParse } from './json-utils.js';
|
|
5
6
|
import { PROJECT_TOOLS_PACKAGE_ROOT } from './template-registry.js';
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const DEFAULT_VERSION_RANGE = '^0.0.0';
|
|
@@ -93,12 +94,10 @@ function readPackageManifest(location) {
|
|
|
93
94
|
if (!location.packageJsonPath || location.source === null) {
|
|
94
95
|
return null;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new Error(`Failed to parse package version manifest at ${location.packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
-
}
|
|
97
|
+
return safeJsonParse(location.source, {
|
|
98
|
+
context: 'package version manifest',
|
|
99
|
+
filePath: location.packageJsonPath,
|
|
100
|
+
});
|
|
102
101
|
}
|
|
103
102
|
function tryReadPackageManifest(location) {
|
|
104
103
|
if (!location) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type ParsedBlockPatternBlock } from "@wp-typia/block-runtime/metadata-core";
|
|
2
|
+
import type { PatternCatalogDiagnostic, PatternCatalogEntry } from "./pattern-catalog.js";
|
|
3
|
+
/**
|
|
4
|
+
* Validates section role slugs used by pattern manifests and content markers.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PATTERN_SECTION_ROLE_PATTERN: RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* Convention used to discover section role markers in serialized pattern
|
|
9
|
+
* content. Defaults target `core/group` wrappers with a `section` base class,
|
|
10
|
+
* `section--{role}` role class tokens, and `metadata.sectionRole` attributes.
|
|
11
|
+
*/
|
|
12
|
+
export type PatternCatalogSectionRoleConvention = {
|
|
13
|
+
/**
|
|
14
|
+
* Serialized block name used as the section wrapper. Defaults to
|
|
15
|
+
* `core/group`.
|
|
16
|
+
*/
|
|
17
|
+
wrapperBlockName?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Optional class that marks a wrapper block as section-like even when the
|
|
20
|
+
* role marker is missing. Defaults to `section`.
|
|
21
|
+
*/
|
|
22
|
+
baseClassName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Class token pattern where exactly one `{role}` placeholder is replaced by
|
|
25
|
+
* the section role slug. Defaults to `section--{role}`.
|
|
26
|
+
*/
|
|
27
|
+
roleClassNamePattern?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Dot-separated block attribute paths that can carry role slugs. Defaults to
|
|
30
|
+
* `metadata.sectionRole`.
|
|
31
|
+
*/
|
|
32
|
+
roleAttributePaths?: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Warn when a full pattern repeats the same section role marker. Defaults to
|
|
35
|
+
* `false`.
|
|
36
|
+
*/
|
|
37
|
+
requireUniqueFullPatternRoles?: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Section wrapper match extracted from a parsed WordPress block tree.
|
|
41
|
+
*/
|
|
42
|
+
export type PatternCatalogSectionRoleMatch = {
|
|
43
|
+
blockName: string;
|
|
44
|
+
blockPath: string;
|
|
45
|
+
roles: readonly string[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Fully resolved section-role marker convention with its compiled class matcher.
|
|
49
|
+
*/
|
|
50
|
+
export type NormalizedPatternCatalogSectionRoleConvention = Required<PatternCatalogSectionRoleConvention> & {
|
|
51
|
+
/**
|
|
52
|
+
* Compiled matcher for role class tokens produced from `roleClassNamePattern`.
|
|
53
|
+
*/
|
|
54
|
+
roleClassNamePatternRegExp: RegExp;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a section-role convention and compile its class-token matcher.
|
|
58
|
+
*
|
|
59
|
+
* @param convention Optional marker convention override.
|
|
60
|
+
* @returns A fully populated convention ready for repeated block traversal.
|
|
61
|
+
* @throws When `roleClassNamePattern` does not contain exactly one `{role}` placeholder.
|
|
62
|
+
*/
|
|
63
|
+
export declare function normalizePatternCatalogSectionRoleConvention(convention?: PatternCatalogSectionRoleConvention): NormalizedPatternCatalogSectionRoleConvention;
|
|
64
|
+
/**
|
|
65
|
+
* Extract section role slugs from serialized block attributes using the
|
|
66
|
+
* configured class and metadata marker convention.
|
|
67
|
+
*
|
|
68
|
+
* @param attributes Parsed block attributes from serialized pattern content.
|
|
69
|
+
* @param convention Optional marker convention override.
|
|
70
|
+
* @returns Unique role marker values in discovery order.
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractPatternSectionRolesFromAttributes(attributes: Record<string, unknown>, convention?: PatternCatalogSectionRoleConvention): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Find section wrapper blocks and their role markers in parsed pattern content.
|
|
75
|
+
*
|
|
76
|
+
* @param blocks Parsed block tree returned by `validateBlockPatternContentNesting`.
|
|
77
|
+
* @param convention Optional marker convention override.
|
|
78
|
+
* @returns Section wrapper matches with serialized block paths.
|
|
79
|
+
*/
|
|
80
|
+
export declare function extractPatternSectionRoleMatches(blocks: readonly ParsedBlockPatternBlock[], convention?: PatternCatalogSectionRoleConvention): PatternCatalogSectionRoleMatch[];
|
|
81
|
+
/**
|
|
82
|
+
* Build the known valid section-role set from pattern catalog entries.
|
|
83
|
+
*
|
|
84
|
+
* @param patterns Catalog entries that may declare a `sectionRole`.
|
|
85
|
+
* @returns Valid section role slugs declared by the catalog.
|
|
86
|
+
*/
|
|
87
|
+
export declare function collectKnownPatternSectionRoles(patterns: readonly Pick<PatternCatalogEntry, "sectionRole">[]): ReadonlySet<string>;
|
|
88
|
+
/**
|
|
89
|
+
* Validate serialized pattern content against the catalog section-role rules.
|
|
90
|
+
*
|
|
91
|
+
* @param options Pattern content, marker convention, known roles, and diagnostic context.
|
|
92
|
+
* @returns Diagnostics for invalid, missing, mismatched, unknown, or duplicate section-role markers.
|
|
93
|
+
*/
|
|
94
|
+
export declare function validatePatternContentSectionRoles({ content, contentFile, convention, knownSectionRoles, label, pattern, }: {
|
|
95
|
+
content: string;
|
|
96
|
+
contentFile: string;
|
|
97
|
+
convention: NormalizedPatternCatalogSectionRoleConvention;
|
|
98
|
+
knownSectionRoles: ReadonlySet<string>;
|
|
99
|
+
label: string;
|
|
100
|
+
pattern: PatternCatalogEntry;
|
|
101
|
+
}): PatternCatalogDiagnostic[];
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { validateBlockPatternContentNesting, } from "@wp-typia/block-runtime/metadata-core";
|
|
2
|
+
/**
|
|
3
|
+
* Validates section role slugs used by pattern manifests and content markers.
|
|
4
|
+
*/
|
|
5
|
+
export const PATTERN_SECTION_ROLE_PATTERN = /^[a-z][a-z0-9-]*$/u;
|
|
6
|
+
const DEFAULT_SECTION_ROLE_CONVENTION = {
|
|
7
|
+
baseClassName: "section",
|
|
8
|
+
requireUniqueFullPatternRoles: false,
|
|
9
|
+
roleAttributePaths: ["metadata.sectionRole"],
|
|
10
|
+
roleClassNamePattern: "section--{role}",
|
|
11
|
+
wrapperBlockName: "core/group",
|
|
12
|
+
};
|
|
13
|
+
function createPatternCatalogDiagnostic(diagnostic) {
|
|
14
|
+
return diagnostic;
|
|
15
|
+
}
|
|
16
|
+
function normalizeSectionRoleConventionInput(convention = {}) {
|
|
17
|
+
return {
|
|
18
|
+
baseClassName: convention.baseClassName ??
|
|
19
|
+
DEFAULT_SECTION_ROLE_CONVENTION.baseClassName,
|
|
20
|
+
requireUniqueFullPatternRoles: convention.requireUniqueFullPatternRoles ??
|
|
21
|
+
DEFAULT_SECTION_ROLE_CONVENTION.requireUniqueFullPatternRoles,
|
|
22
|
+
roleAttributePaths: convention.roleAttributePaths ??
|
|
23
|
+
DEFAULT_SECTION_ROLE_CONVENTION.roleAttributePaths,
|
|
24
|
+
roleClassNamePattern: convention.roleClassNamePattern ??
|
|
25
|
+
DEFAULT_SECTION_ROLE_CONVENTION.roleClassNamePattern,
|
|
26
|
+
wrapperBlockName: convention.wrapperBlockName ??
|
|
27
|
+
DEFAULT_SECTION_ROLE_CONVENTION.wrapperBlockName,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function escapeRegExp(value) {
|
|
31
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
32
|
+
}
|
|
33
|
+
function createRoleClassNamePattern(pattern) {
|
|
34
|
+
const parts = pattern.split("{role}");
|
|
35
|
+
if (parts.length !== 2) {
|
|
36
|
+
throw new Error(`roleClassNamePattern must contain exactly one "{role}" placeholder.`);
|
|
37
|
+
}
|
|
38
|
+
return new RegExp(`^${escapeRegExp(parts[0] ?? "")}(?<role>\\S*)${escapeRegExp(parts[1] ?? "")}$`, "u");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a section-role convention and compile its class-token matcher.
|
|
42
|
+
*
|
|
43
|
+
* @param convention Optional marker convention override.
|
|
44
|
+
* @returns A fully populated convention ready for repeated block traversal.
|
|
45
|
+
* @throws When `roleClassNamePattern` does not contain exactly one `{role}` placeholder.
|
|
46
|
+
*/
|
|
47
|
+
export function normalizePatternCatalogSectionRoleConvention(convention = {}) {
|
|
48
|
+
const normalized = normalizeSectionRoleConventionInput(convention);
|
|
49
|
+
return {
|
|
50
|
+
...normalized,
|
|
51
|
+
roleClassNamePatternRegExp: createRoleClassNamePattern(normalized.roleClassNamePattern),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function getClassNameTokens(attributes) {
|
|
55
|
+
const className = attributes.className;
|
|
56
|
+
if (typeof className !== "string") {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return className.split(/\s+/u).filter((token) => token.length > 0);
|
|
60
|
+
}
|
|
61
|
+
function getAttributePathValue(attributes, pathName) {
|
|
62
|
+
return pathName.split(".").reduce((current, segment) => {
|
|
63
|
+
if (current !== null &&
|
|
64
|
+
typeof current === "object" &&
|
|
65
|
+
!Array.isArray(current) &&
|
|
66
|
+
Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
67
|
+
return current[segment];
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}, attributes);
|
|
71
|
+
}
|
|
72
|
+
function collectStringValues(value) {
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
return [value];
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.filter((item) => typeof item === "string");
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
function uniqueValues(values) {
|
|
82
|
+
return [...new Set(values)];
|
|
83
|
+
}
|
|
84
|
+
function formatRoleList(roles) {
|
|
85
|
+
if (roles.length === 0) {
|
|
86
|
+
return "none";
|
|
87
|
+
}
|
|
88
|
+
return roles.map((role) => `"${role}"`).join(", ");
|
|
89
|
+
}
|
|
90
|
+
function formatBlockPaths(paths) {
|
|
91
|
+
return paths.map((blockPath) => `at ${blockPath}`).join(", ");
|
|
92
|
+
}
|
|
93
|
+
function describeRoleMarkerConvention(convention) {
|
|
94
|
+
return `${convention.wrapperBlockName} wrappers with class "${convention.roleClassNamePattern}" or attributes ${convention.roleAttributePaths.join(", ")}`;
|
|
95
|
+
}
|
|
96
|
+
function isSectionWrapperCandidate(block, roles, convention) {
|
|
97
|
+
if (block.blockName !== convention.wrapperBlockName) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (roles.length > 0) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return getClassNameTokens(block.attributes).includes(convention.baseClassName);
|
|
104
|
+
}
|
|
105
|
+
function collectSectionRoleMatches(blocks, convention, pathSegments = []) {
|
|
106
|
+
return blocks.flatMap((block, index) => {
|
|
107
|
+
const blockPathSegments = [
|
|
108
|
+
...pathSegments,
|
|
109
|
+
`${block.blockName}[${index}]`,
|
|
110
|
+
];
|
|
111
|
+
const blockPath = blockPathSegments.join(" > ");
|
|
112
|
+
const roles = extractPatternSectionRolesFromNormalizedAttributes(block.attributes, convention);
|
|
113
|
+
const matches = isSectionWrapperCandidate(block, roles, convention)
|
|
114
|
+
? [
|
|
115
|
+
{
|
|
116
|
+
blockName: block.blockName,
|
|
117
|
+
blockPath,
|
|
118
|
+
roles,
|
|
119
|
+
},
|
|
120
|
+
]
|
|
121
|
+
: [];
|
|
122
|
+
return [
|
|
123
|
+
...matches,
|
|
124
|
+
...collectSectionRoleMatches(block.innerBlocks, convention, blockPathSegments),
|
|
125
|
+
];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function unescapeSerializedBlockCommentJsonQuotes(content) {
|
|
129
|
+
return content.replace(/<!--([\s\S]*?)-->/gu, (comment, body) => {
|
|
130
|
+
const source = body.trim();
|
|
131
|
+
if (!source.startsWith("wp:") && !source.startsWith("/wp:")) {
|
|
132
|
+
return comment;
|
|
133
|
+
}
|
|
134
|
+
return `<!--${body.replace(/\\"/gu, '"')}-->`;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Extract section role slugs from serialized block attributes using the
|
|
139
|
+
* configured class and metadata marker convention.
|
|
140
|
+
*
|
|
141
|
+
* @param attributes Parsed block attributes from serialized pattern content.
|
|
142
|
+
* @param convention Optional marker convention override.
|
|
143
|
+
* @returns Unique role marker values in discovery order.
|
|
144
|
+
*/
|
|
145
|
+
export function extractPatternSectionRolesFromAttributes(attributes, convention = {}) {
|
|
146
|
+
const normalized = normalizePatternCatalogSectionRoleConvention(convention);
|
|
147
|
+
return extractPatternSectionRolesFromNormalizedAttributes(attributes, normalized);
|
|
148
|
+
}
|
|
149
|
+
function extractPatternSectionRolesFromNormalizedAttributes(attributes, normalized) {
|
|
150
|
+
const classRoles = getClassNameTokens(attributes)
|
|
151
|
+
.map((token) => normalized.roleClassNamePatternRegExp.exec(token)?.groups?.role)
|
|
152
|
+
.filter((role) => typeof role === "string");
|
|
153
|
+
const attributeRoles = normalized.roleAttributePaths.flatMap((pathName) => collectStringValues(getAttributePathValue(attributes, pathName)));
|
|
154
|
+
return uniqueValues([...classRoles, ...attributeRoles]);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Find section wrapper blocks and their role markers in parsed pattern content.
|
|
158
|
+
*
|
|
159
|
+
* @param blocks Parsed block tree returned by `validateBlockPatternContentNesting`.
|
|
160
|
+
* @param convention Optional marker convention override.
|
|
161
|
+
* @returns Section wrapper matches with serialized block paths.
|
|
162
|
+
*/
|
|
163
|
+
export function extractPatternSectionRoleMatches(blocks, convention = {}) {
|
|
164
|
+
return collectSectionRoleMatches(blocks, normalizePatternCatalogSectionRoleConvention(convention));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Build the known valid section-role set from pattern catalog entries.
|
|
168
|
+
*
|
|
169
|
+
* @param patterns Catalog entries that may declare a `sectionRole`.
|
|
170
|
+
* @returns Valid section role slugs declared by the catalog.
|
|
171
|
+
*/
|
|
172
|
+
export function collectKnownPatternSectionRoles(patterns) {
|
|
173
|
+
return new Set(patterns
|
|
174
|
+
.map((pattern) => pattern.sectionRole)
|
|
175
|
+
.filter((sectionRole) => typeof sectionRole === "string" &&
|
|
176
|
+
PATTERN_SECTION_ROLE_PATTERN.test(sectionRole)));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Validate serialized pattern content against the catalog section-role rules.
|
|
180
|
+
*
|
|
181
|
+
* @param options Pattern content, marker convention, known roles, and diagnostic context.
|
|
182
|
+
* @returns Diagnostics for invalid, missing, mismatched, unknown, or duplicate section-role markers.
|
|
183
|
+
*/
|
|
184
|
+
export function validatePatternContentSectionRoles({ content, contentFile, convention, knownSectionRoles, label, pattern, }) {
|
|
185
|
+
const diagnostics = [];
|
|
186
|
+
const parsed = validateBlockPatternContentNesting(content, {
|
|
187
|
+
allowExternalBlockNames: true,
|
|
188
|
+
nesting: {},
|
|
189
|
+
patternFile: contentFile,
|
|
190
|
+
});
|
|
191
|
+
let matches = collectSectionRoleMatches(parsed.blocks, convention);
|
|
192
|
+
const unescapedContent = unescapeSerializedBlockCommentJsonQuotes(content);
|
|
193
|
+
if (unescapedContent !== content) {
|
|
194
|
+
const unescapedParsed = validateBlockPatternContentNesting(unescapedContent, {
|
|
195
|
+
allowExternalBlockNames: true,
|
|
196
|
+
nesting: {},
|
|
197
|
+
patternFile: contentFile,
|
|
198
|
+
});
|
|
199
|
+
const unescapedMatches = collectSectionRoleMatches(unescapedParsed.blocks, convention);
|
|
200
|
+
if (unescapedMatches.some((match) => match.roles.length > 0) ||
|
|
201
|
+
matches.length === 0) {
|
|
202
|
+
matches = unescapedMatches;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const locatedRoles = matches.flatMap((match) => match.roles.map((role) => ({
|
|
206
|
+
blockPath: match.blockPath,
|
|
207
|
+
role,
|
|
208
|
+
})));
|
|
209
|
+
const invalidRoles = locatedRoles.filter(({ role }) => !PATTERN_SECTION_ROLE_PATTERN.test(role));
|
|
210
|
+
for (const invalidRole of uniqueValues(invalidRoles.map(({ role }) => role))) {
|
|
211
|
+
const paths = invalidRoles
|
|
212
|
+
.filter(({ role }) => role === invalidRole)
|
|
213
|
+
.map(({ blockPath }) => blockPath);
|
|
214
|
+
diagnostics.push(createPatternCatalogDiagnostic({
|
|
215
|
+
code: "invalid-pattern-section-role-marker",
|
|
216
|
+
message: `${label}: section role marker "${invalidRole}" in ${contentFile} must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens (${formatBlockPaths(paths)}).`,
|
|
217
|
+
patternSlug: pattern.slug,
|
|
218
|
+
severity: "error",
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
const validLocatedRoles = locatedRoles.filter(({ role }) => PATTERN_SECTION_ROLE_PATTERN.test(role));
|
|
222
|
+
const validRoles = validLocatedRoles.map(({ role }) => role);
|
|
223
|
+
const uniqueValidRoles = uniqueValues(validRoles);
|
|
224
|
+
const unknownRoles = uniqueValidRoles.filter((role) => !knownSectionRoles.has(role));
|
|
225
|
+
for (const unknownRole of unknownRoles) {
|
|
226
|
+
const paths = validLocatedRoles
|
|
227
|
+
.filter(({ role }) => role === unknownRole)
|
|
228
|
+
.map(({ blockPath }) => blockPath);
|
|
229
|
+
diagnostics.push(createPatternCatalogDiagnostic({
|
|
230
|
+
code: "unknown-pattern-section-role-marker",
|
|
231
|
+
message: `${label}: section role marker "${unknownRole}" in ${contentFile} is not declared by any PATTERNS sectionRole (${formatBlockPaths(paths)}).`,
|
|
232
|
+
patternSlug: pattern.slug,
|
|
233
|
+
severity: "warning",
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
const scope = pattern.scope ?? "full";
|
|
237
|
+
const expectedSectionRole = typeof pattern.sectionRole === "string" &&
|
|
238
|
+
PATTERN_SECTION_ROLE_PATTERN.test(pattern.sectionRole)
|
|
239
|
+
? pattern.sectionRole
|
|
240
|
+
: undefined;
|
|
241
|
+
if (scope === "section" && expectedSectionRole) {
|
|
242
|
+
if (validRoles.length === 0) {
|
|
243
|
+
diagnostics.push(createPatternCatalogDiagnostic({
|
|
244
|
+
code: "missing-pattern-section-role-marker",
|
|
245
|
+
message: `${label}: section-scoped pattern content in ${contentFile} must include section role marker "${expectedSectionRole}" using ${describeRoleMarkerConvention(convention)}.`,
|
|
246
|
+
patternSlug: pattern.slug,
|
|
247
|
+
severity: "error",
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
else if (!validRoles.includes(expectedSectionRole)) {
|
|
251
|
+
diagnostics.push(createPatternCatalogDiagnostic({
|
|
252
|
+
code: "mismatched-pattern-section-role",
|
|
253
|
+
message: `${label}: manifest sectionRole "${expectedSectionRole}" was not found in ${contentFile}; found ${formatRoleList(uniqueValidRoles)}.`,
|
|
254
|
+
patternSlug: pattern.slug,
|
|
255
|
+
severity: "error",
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (scope === "full" && convention.requireUniqueFullPatternRoles) {
|
|
260
|
+
for (const role of uniqueValidRoles) {
|
|
261
|
+
const paths = validLocatedRoles
|
|
262
|
+
.filter((locatedRole) => locatedRole.role === role)
|
|
263
|
+
.map(({ blockPath }) => blockPath);
|
|
264
|
+
if (paths.length <= 1) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
diagnostics.push(createPatternCatalogDiagnostic({
|
|
268
|
+
code: "duplicate-pattern-section-role-marker",
|
|
269
|
+
message: `${label}: full pattern content in ${contentFile} repeats section role marker "${role}" (${formatBlockPaths(paths)}).`,
|
|
270
|
+
patternSlug: pattern.slug,
|
|
271
|
+
severity: "warning",
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return diagnostics;
|
|
276
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PatternCatalogSectionRoleConvention } from "./pattern-catalog-section-roles.js";
|
|
2
|
+
export { extractPatternSectionRoleMatches, extractPatternSectionRolesFromAttributes, PATTERN_SECTION_ROLE_PATTERN, } from "./pattern-catalog-section-roles.js";
|
|
3
|
+
export type { PatternCatalogSectionRoleConvention, PatternCatalogSectionRoleMatch, } from "./pattern-catalog-section-roles.js";
|
|
2
4
|
export declare const PATTERN_CATALOG_SCOPE_IDS: readonly ["full", "section"];
|
|
3
5
|
export type PatternCatalogScope = (typeof PATTERN_CATALOG_SCOPE_IDS)[number];
|
|
4
6
|
export type PatternCatalogEntry = {
|
|
@@ -19,46 +21,6 @@ export type PatternCatalogDiagnostic = {
|
|
|
19
21
|
patternSlug?: string;
|
|
20
22
|
severity: PatternCatalogDiagnosticSeverity;
|
|
21
23
|
};
|
|
22
|
-
/**
|
|
23
|
-
* Convention used to discover section role markers in serialized pattern
|
|
24
|
-
* content. Defaults target `core/group` wrappers with a `section` base class,
|
|
25
|
-
* `section--{role}` role class tokens, and `metadata.sectionRole` attributes.
|
|
26
|
-
*/
|
|
27
|
-
export type PatternCatalogSectionRoleConvention = {
|
|
28
|
-
/**
|
|
29
|
-
* Serialized block name used as the section wrapper. Defaults to
|
|
30
|
-
* `core/group`.
|
|
31
|
-
*/
|
|
32
|
-
wrapperBlockName?: string;
|
|
33
|
-
/**
|
|
34
|
-
* Optional class that marks a wrapper block as section-like even when the
|
|
35
|
-
* role marker is missing. Defaults to `section`.
|
|
36
|
-
*/
|
|
37
|
-
baseClassName?: string;
|
|
38
|
-
/**
|
|
39
|
-
* Class token pattern where exactly one `{role}` placeholder is replaced by
|
|
40
|
-
* the section role slug. Defaults to `section--{role}`.
|
|
41
|
-
*/
|
|
42
|
-
roleClassNamePattern?: string;
|
|
43
|
-
/**
|
|
44
|
-
* Dot-separated block attribute paths that can carry role slugs. Defaults to
|
|
45
|
-
* `metadata.sectionRole`.
|
|
46
|
-
*/
|
|
47
|
-
roleAttributePaths?: readonly string[];
|
|
48
|
-
/**
|
|
49
|
-
* Warn when a full pattern repeats the same section role marker. Defaults to
|
|
50
|
-
* `false`.
|
|
51
|
-
*/
|
|
52
|
-
requireUniqueFullPatternRoles?: boolean;
|
|
53
|
-
};
|
|
54
|
-
/**
|
|
55
|
-
* Section wrapper match extracted from a parsed WordPress block tree.
|
|
56
|
-
*/
|
|
57
|
-
export type PatternCatalogSectionRoleMatch = {
|
|
58
|
-
blockName: string;
|
|
59
|
-
blockPath: string;
|
|
60
|
-
roles: readonly string[];
|
|
61
|
-
};
|
|
62
24
|
/**
|
|
63
25
|
* Options for validating typed pattern catalog entries and, when `projectDir`
|
|
64
26
|
* is provided, their serialized pattern content. Set `sectionRoleConvention` to
|
|
@@ -74,23 +36,7 @@ export type PatternCatalogValidationResult = {
|
|
|
74
36
|
errors: PatternCatalogDiagnostic[];
|
|
75
37
|
warnings: PatternCatalogDiagnostic[];
|
|
76
38
|
};
|
|
77
|
-
|
|
78
|
-
* Extract section role slugs from serialized block attributes using the
|
|
79
|
-
* configured class and metadata marker convention.
|
|
80
|
-
*
|
|
81
|
-
* @param attributes Parsed block attributes from serialized pattern content.
|
|
82
|
-
* @param convention Optional marker convention override.
|
|
83
|
-
* @returns Unique role marker values in discovery order.
|
|
84
|
-
*/
|
|
85
|
-
export declare function extractPatternSectionRolesFromAttributes(attributes: Record<string, unknown>, convention?: PatternCatalogSectionRoleConvention): string[];
|
|
86
|
-
/**
|
|
87
|
-
* Find section wrapper blocks and their role markers in parsed pattern content.
|
|
88
|
-
*
|
|
89
|
-
* @param blocks Parsed block tree returned by `validateBlockPatternContentNesting`.
|
|
90
|
-
* @param convention Optional marker convention override.
|
|
91
|
-
* @returns Section wrapper matches with serialized block paths.
|
|
92
|
-
*/
|
|
93
|
-
export declare function extractPatternSectionRoleMatches(blocks: readonly ParsedBlockPatternBlock[], convention?: PatternCatalogSectionRoleConvention): PatternCatalogSectionRoleMatch[];
|
|
39
|
+
export declare const PATTERN_TAG_PATTERN: RegExp;
|
|
94
40
|
/**
|
|
95
41
|
* Validate pattern thumbnail references with the same URL/path rules used by
|
|
96
42
|
* catalog diagnostics and `wp-typia add pattern`.
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { PATTERN_SECTION_ROLE_PATTERN, collectKnownPatternSectionRoles, normalizePatternCatalogSectionRoleConvention, validatePatternContentSectionRoles, } from "./pattern-catalog-section-roles.js";
|
|
4
|
+
export { extractPatternSectionRoleMatches, extractPatternSectionRolesFromAttributes, PATTERN_SECTION_ROLE_PATTERN, } from "./pattern-catalog-section-roles.js";
|
|
4
5
|
export const PATTERN_CATALOG_SCOPE_IDS = ["full", "section"];
|
|
5
6
|
const PATTERN_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/u;
|
|
6
|
-
const
|
|
7
|
-
const PATTERN_TAG_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
7
|
+
export const PATTERN_TAG_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
|
|
8
8
|
const PATTERN_CONTENT_FILE_ROOT = "src/patterns/";
|
|
9
|
-
const DEFAULT_SECTION_ROLE_CONVENTION = {
|
|
10
|
-
baseClassName: "section",
|
|
11
|
-
requireUniqueFullPatternRoles: false,
|
|
12
|
-
roleAttributePaths: ["metadata.sectionRole"],
|
|
13
|
-
roleClassNamePattern: "section--{role}",
|
|
14
|
-
wrapperBlockName: "core/group",
|
|
15
|
-
};
|
|
16
9
|
function createPatternCatalogDiagnostic(diagnostic) {
|
|
17
10
|
return diagnostic;
|
|
18
11
|
}
|
|
@@ -37,146 +30,6 @@ function isPatternContentFilePath(value) {
|
|
|
37
30
|
return ((segments.length === 1 || segments.length === 2) &&
|
|
38
31
|
segments.every((segment) => segment.length > 0));
|
|
39
32
|
}
|
|
40
|
-
function normalizeSectionRoleConventionInput(convention = {}) {
|
|
41
|
-
return {
|
|
42
|
-
baseClassName: convention.baseClassName ??
|
|
43
|
-
DEFAULT_SECTION_ROLE_CONVENTION.baseClassName,
|
|
44
|
-
requireUniqueFullPatternRoles: convention.requireUniqueFullPatternRoles ??
|
|
45
|
-
DEFAULT_SECTION_ROLE_CONVENTION.requireUniqueFullPatternRoles,
|
|
46
|
-
roleAttributePaths: convention.roleAttributePaths ??
|
|
47
|
-
DEFAULT_SECTION_ROLE_CONVENTION.roleAttributePaths,
|
|
48
|
-
roleClassNamePattern: convention.roleClassNamePattern ??
|
|
49
|
-
DEFAULT_SECTION_ROLE_CONVENTION.roleClassNamePattern,
|
|
50
|
-
wrapperBlockName: convention.wrapperBlockName ??
|
|
51
|
-
DEFAULT_SECTION_ROLE_CONVENTION.wrapperBlockName,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
function escapeRegExp(value) {
|
|
55
|
-
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
56
|
-
}
|
|
57
|
-
function createRoleClassNamePattern(pattern) {
|
|
58
|
-
const parts = pattern.split("{role}");
|
|
59
|
-
if (parts.length !== 2) {
|
|
60
|
-
throw new Error(`roleClassNamePattern must contain exactly one "{role}" placeholder.`);
|
|
61
|
-
}
|
|
62
|
-
return new RegExp(`^${escapeRegExp(parts[0] ?? "")}(?<role>\\S*)${escapeRegExp(parts[1] ?? "")}$`, "u");
|
|
63
|
-
}
|
|
64
|
-
function normalizeSectionRoleConvention(convention = {}) {
|
|
65
|
-
const normalized = normalizeSectionRoleConventionInput(convention);
|
|
66
|
-
return {
|
|
67
|
-
...normalized,
|
|
68
|
-
roleClassNamePatternRegExp: createRoleClassNamePattern(normalized.roleClassNamePattern),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function getClassNameTokens(attributes) {
|
|
72
|
-
const className = attributes.className;
|
|
73
|
-
if (typeof className !== "string") {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
return className.split(/\s+/u).filter((token) => token.length > 0);
|
|
77
|
-
}
|
|
78
|
-
function getAttributePathValue(attributes, pathName) {
|
|
79
|
-
return pathName.split(".").reduce((current, segment) => {
|
|
80
|
-
if (current !== null &&
|
|
81
|
-
typeof current === "object" &&
|
|
82
|
-
!Array.isArray(current) &&
|
|
83
|
-
Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
84
|
-
return current[segment];
|
|
85
|
-
}
|
|
86
|
-
return undefined;
|
|
87
|
-
}, attributes);
|
|
88
|
-
}
|
|
89
|
-
function collectStringValues(value) {
|
|
90
|
-
if (typeof value === "string") {
|
|
91
|
-
return [value];
|
|
92
|
-
}
|
|
93
|
-
if (Array.isArray(value)) {
|
|
94
|
-
return value.filter((item) => typeof item === "string");
|
|
95
|
-
}
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
function uniqueValues(values) {
|
|
99
|
-
return [...new Set(values)];
|
|
100
|
-
}
|
|
101
|
-
function formatRoleList(roles) {
|
|
102
|
-
if (roles.length === 0) {
|
|
103
|
-
return "none";
|
|
104
|
-
}
|
|
105
|
-
return roles.map((role) => `"${role}"`).join(", ");
|
|
106
|
-
}
|
|
107
|
-
function formatBlockPaths(paths) {
|
|
108
|
-
return paths.map((blockPath) => `at ${blockPath}`).join(", ");
|
|
109
|
-
}
|
|
110
|
-
function describeRoleMarkerConvention(convention) {
|
|
111
|
-
return `${convention.wrapperBlockName} wrappers with class "${convention.roleClassNamePattern}" or attributes ${convention.roleAttributePaths.join(", ")}`;
|
|
112
|
-
}
|
|
113
|
-
function isSectionWrapperCandidate(block, roles, convention) {
|
|
114
|
-
if (block.blockName !== convention.wrapperBlockName) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
if (roles.length > 0) {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
return getClassNameTokens(block.attributes).includes(convention.baseClassName);
|
|
121
|
-
}
|
|
122
|
-
function collectSectionRoleMatches(blocks, convention, pathSegments = []) {
|
|
123
|
-
return blocks.flatMap((block, index) => {
|
|
124
|
-
const blockPathSegments = [
|
|
125
|
-
...pathSegments,
|
|
126
|
-
`${block.blockName}[${index}]`,
|
|
127
|
-
];
|
|
128
|
-
const blockPath = blockPathSegments.join(" > ");
|
|
129
|
-
const roles = extractPatternSectionRolesFromAttributes(block.attributes, convention);
|
|
130
|
-
const matches = isSectionWrapperCandidate(block, roles, convention)
|
|
131
|
-
? [
|
|
132
|
-
{
|
|
133
|
-
blockName: block.blockName,
|
|
134
|
-
blockPath,
|
|
135
|
-
roles,
|
|
136
|
-
},
|
|
137
|
-
]
|
|
138
|
-
: [];
|
|
139
|
-
return [
|
|
140
|
-
...matches,
|
|
141
|
-
...collectSectionRoleMatches(block.innerBlocks, convention, blockPathSegments),
|
|
142
|
-
];
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
function unescapeSerializedBlockCommentJsonQuotes(content) {
|
|
146
|
-
return content.replace(/<!--([\s\S]*?)-->/gu, (comment, body) => {
|
|
147
|
-
const source = body.trim();
|
|
148
|
-
if (!source.startsWith("wp:") && !source.startsWith("/wp:")) {
|
|
149
|
-
return comment;
|
|
150
|
-
}
|
|
151
|
-
return `<!--${body.replace(/\\"/gu, '"')}-->`;
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Extract section role slugs from serialized block attributes using the
|
|
156
|
-
* configured class and metadata marker convention.
|
|
157
|
-
*
|
|
158
|
-
* @param attributes Parsed block attributes from serialized pattern content.
|
|
159
|
-
* @param convention Optional marker convention override.
|
|
160
|
-
* @returns Unique role marker values in discovery order.
|
|
161
|
-
*/
|
|
162
|
-
export function extractPatternSectionRolesFromAttributes(attributes, convention = {}) {
|
|
163
|
-
const normalized = normalizeSectionRoleConvention(convention);
|
|
164
|
-
const classRoles = getClassNameTokens(attributes)
|
|
165
|
-
.map((token) => normalized.roleClassNamePatternRegExp.exec(token)?.groups?.role)
|
|
166
|
-
.filter((role) => typeof role === "string");
|
|
167
|
-
const attributeRoles = normalized.roleAttributePaths.flatMap((pathName) => collectStringValues(getAttributePathValue(attributes, pathName)));
|
|
168
|
-
return uniqueValues([...classRoles, ...attributeRoles]);
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Find section wrapper blocks and their role markers in parsed pattern content.
|
|
172
|
-
*
|
|
173
|
-
* @param blocks Parsed block tree returned by `validateBlockPatternContentNesting`.
|
|
174
|
-
* @param convention Optional marker convention override.
|
|
175
|
-
* @returns Section wrapper matches with serialized block paths.
|
|
176
|
-
*/
|
|
177
|
-
export function extractPatternSectionRoleMatches(blocks, convention = {}) {
|
|
178
|
-
return collectSectionRoleMatches(blocks, normalizeSectionRoleConvention(convention));
|
|
179
|
-
}
|
|
180
33
|
/**
|
|
181
34
|
* Validate pattern thumbnail references with the same URL/path rules used by
|
|
182
35
|
* catalog diagnostics and `wp-typia add pattern`.
|
|
@@ -199,105 +52,6 @@ export function isValidPatternThumbnailUrl(value) {
|
|
|
199
52
|
export function resolvePatternCatalogContentFile(pattern) {
|
|
200
53
|
return pattern.contentFile ?? pattern.file;
|
|
201
54
|
}
|
|
202
|
-
function createKnownSectionRoleSet(patterns) {
|
|
203
|
-
return new Set(patterns
|
|
204
|
-
.map((pattern) => pattern.sectionRole)
|
|
205
|
-
.filter((sectionRole) => typeof sectionRole === "string" &&
|
|
206
|
-
PATTERN_SECTION_ROLE_PATTERN.test(sectionRole)));
|
|
207
|
-
}
|
|
208
|
-
function validatePatternContentSectionRoles({ content, contentFile, convention, knownSectionRoles, label, pattern, }) {
|
|
209
|
-
const diagnostics = [];
|
|
210
|
-
const parsed = validateBlockPatternContentNesting(content, {
|
|
211
|
-
allowExternalBlockNames: true,
|
|
212
|
-
nesting: {},
|
|
213
|
-
patternFile: contentFile,
|
|
214
|
-
});
|
|
215
|
-
let matches = collectSectionRoleMatches(parsed.blocks, convention);
|
|
216
|
-
const unescapedContent = unescapeSerializedBlockCommentJsonQuotes(content);
|
|
217
|
-
if (unescapedContent !== content) {
|
|
218
|
-
const unescapedParsed = validateBlockPatternContentNesting(unescapedContent, {
|
|
219
|
-
allowExternalBlockNames: true,
|
|
220
|
-
nesting: {},
|
|
221
|
-
patternFile: contentFile,
|
|
222
|
-
});
|
|
223
|
-
const unescapedMatches = collectSectionRoleMatches(unescapedParsed.blocks, convention);
|
|
224
|
-
if (unescapedMatches.some((match) => match.roles.length > 0) ||
|
|
225
|
-
matches.length === 0) {
|
|
226
|
-
matches = unescapedMatches;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
const locatedRoles = matches.flatMap((match) => match.roles.map((role) => ({
|
|
230
|
-
blockPath: match.blockPath,
|
|
231
|
-
role,
|
|
232
|
-
})));
|
|
233
|
-
const invalidRoles = locatedRoles.filter(({ role }) => !PATTERN_SECTION_ROLE_PATTERN.test(role));
|
|
234
|
-
for (const invalidRole of uniqueValues(invalidRoles.map(({ role }) => role))) {
|
|
235
|
-
const paths = invalidRoles
|
|
236
|
-
.filter(({ role }) => role === invalidRole)
|
|
237
|
-
.map(({ blockPath }) => blockPath);
|
|
238
|
-
diagnostics.push(createPatternCatalogDiagnostic({
|
|
239
|
-
code: "invalid-pattern-section-role-marker",
|
|
240
|
-
message: `${label}: section role marker "${invalidRole}" in ${contentFile} must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens (${formatBlockPaths(paths)}).`,
|
|
241
|
-
patternSlug: pattern.slug,
|
|
242
|
-
severity: "error",
|
|
243
|
-
}));
|
|
244
|
-
}
|
|
245
|
-
const validLocatedRoles = locatedRoles.filter(({ role }) => PATTERN_SECTION_ROLE_PATTERN.test(role));
|
|
246
|
-
const validRoles = validLocatedRoles.map(({ role }) => role);
|
|
247
|
-
const uniqueValidRoles = uniqueValues(validRoles);
|
|
248
|
-
const unknownRoles = uniqueValidRoles.filter((role) => !knownSectionRoles.has(role));
|
|
249
|
-
for (const unknownRole of unknownRoles) {
|
|
250
|
-
const paths = validLocatedRoles
|
|
251
|
-
.filter(({ role }) => role === unknownRole)
|
|
252
|
-
.map(({ blockPath }) => blockPath);
|
|
253
|
-
diagnostics.push(createPatternCatalogDiagnostic({
|
|
254
|
-
code: "unknown-pattern-section-role-marker",
|
|
255
|
-
message: `${label}: section role marker "${unknownRole}" in ${contentFile} is not declared by any PATTERNS sectionRole (${formatBlockPaths(paths)}).`,
|
|
256
|
-
patternSlug: pattern.slug,
|
|
257
|
-
severity: "warning",
|
|
258
|
-
}));
|
|
259
|
-
}
|
|
260
|
-
const scope = pattern.scope ?? "full";
|
|
261
|
-
const expectedSectionRole = typeof pattern.sectionRole === "string" &&
|
|
262
|
-
PATTERN_SECTION_ROLE_PATTERN.test(pattern.sectionRole)
|
|
263
|
-
? pattern.sectionRole
|
|
264
|
-
: undefined;
|
|
265
|
-
if (scope === "section" && expectedSectionRole) {
|
|
266
|
-
if (validRoles.length === 0) {
|
|
267
|
-
diagnostics.push(createPatternCatalogDiagnostic({
|
|
268
|
-
code: "missing-pattern-section-role-marker",
|
|
269
|
-
message: `${label}: section-scoped pattern content in ${contentFile} must include section role marker "${expectedSectionRole}" using ${describeRoleMarkerConvention(convention)}.`,
|
|
270
|
-
patternSlug: pattern.slug,
|
|
271
|
-
severity: "error",
|
|
272
|
-
}));
|
|
273
|
-
}
|
|
274
|
-
else if (!validRoles.includes(expectedSectionRole)) {
|
|
275
|
-
diagnostics.push(createPatternCatalogDiagnostic({
|
|
276
|
-
code: "mismatched-pattern-section-role",
|
|
277
|
-
message: `${label}: manifest sectionRole "${expectedSectionRole}" was not found in ${contentFile}; found ${formatRoleList(uniqueValidRoles)}.`,
|
|
278
|
-
patternSlug: pattern.slug,
|
|
279
|
-
severity: "error",
|
|
280
|
-
}));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
if (scope === "full" && convention.requireUniqueFullPatternRoles) {
|
|
284
|
-
for (const role of uniqueValidRoles) {
|
|
285
|
-
const paths = validLocatedRoles
|
|
286
|
-
.filter((locatedRole) => locatedRole.role === role)
|
|
287
|
-
.map(({ blockPath }) => blockPath);
|
|
288
|
-
if (paths.length <= 1) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
diagnostics.push(createPatternCatalogDiagnostic({
|
|
292
|
-
code: "duplicate-pattern-section-role-marker",
|
|
293
|
-
message: `${label}: full pattern content in ${contentFile} repeats section role marker "${role}" (${formatBlockPaths(paths)}).`,
|
|
294
|
-
patternSlug: pattern.slug,
|
|
295
|
-
severity: "warning",
|
|
296
|
-
}));
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return diagnostics;
|
|
300
|
-
}
|
|
301
55
|
/**
|
|
302
56
|
* Validate typed pattern catalog metadata declared in `scripts/block-config.ts`.
|
|
303
57
|
*
|
|
@@ -316,7 +70,7 @@ export function validatePatternCatalog(patterns, options = {}) {
|
|
|
316
70
|
let sectionRoleConvention = null;
|
|
317
71
|
if (options.sectionRoleConvention !== false) {
|
|
318
72
|
try {
|
|
319
|
-
sectionRoleConvention =
|
|
73
|
+
sectionRoleConvention = normalizePatternCatalogSectionRoleConvention(options.sectionRoleConvention);
|
|
320
74
|
}
|
|
321
75
|
catch (error) {
|
|
322
76
|
diagnostics.push(createPatternCatalogDiagnostic({
|
|
@@ -326,7 +80,7 @@ export function validatePatternCatalog(patterns, options = {}) {
|
|
|
326
80
|
}));
|
|
327
81
|
}
|
|
328
82
|
}
|
|
329
|
-
const knownSectionRoles =
|
|
83
|
+
const knownSectionRoles = collectKnownPatternSectionRoles(patterns);
|
|
330
84
|
for (const [index, pattern] of patterns.entries()) {
|
|
331
85
|
const label = pattern.slug || `PATTERNS[${index}]`;
|
|
332
86
|
if (!PATTERN_SLUG_PATTERN.test(pattern.slug)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeJsonParse } from './json-utils.js';
|
|
1
2
|
/**
|
|
2
3
|
* Marker file written after a cache entry is fully populated.
|
|
3
4
|
*/
|
|
@@ -41,7 +42,9 @@ export function sanitizeExternalTemplateCacheMetadata(metadata) {
|
|
|
41
42
|
export function parseExternalTemplateCacheEntryMarker(markerText) {
|
|
42
43
|
let marker;
|
|
43
44
|
try {
|
|
44
|
-
marker =
|
|
45
|
+
marker = safeJsonParse(markerText, {
|
|
46
|
+
context: 'external template cache entry marker',
|
|
47
|
+
});
|
|
45
48
|
}
|
|
46
49
|
catch {
|
|
47
50
|
return null;
|
|
@@ -78,7 +81,9 @@ export function isExternalTemplateCacheEntryFreshForTtl(createdAtMs, nowMs, ttlM
|
|
|
78
81
|
export function parseExternalTemplateCachePruneMarker(markerText) {
|
|
79
82
|
let marker;
|
|
80
83
|
try {
|
|
81
|
-
marker =
|
|
84
|
+
marker = safeJsonParse(markerText, {
|
|
85
|
+
context: 'external template cache prune marker',
|
|
86
|
+
});
|
|
82
87
|
}
|
|
83
88
|
catch {
|
|
84
89
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wp-typia/project-tools",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "Project orchestration and programmatic tooling for wp-typia",
|
|
5
5
|
"packageManager": "bun@1.3.11",
|
|
6
6
|
"type": "module",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"@wp-typia/api-client": "^0.4.5",
|
|
169
169
|
"@wp-typia/block-runtime": "^0.7.0",
|
|
170
170
|
"@wp-typia/rest": "^0.3.13",
|
|
171
|
-
"@wp-typia/block-types": "^0.3.
|
|
171
|
+
"@wp-typia/block-types": "^0.3.2",
|
|
172
172
|
"mustache": "^4.2.0",
|
|
173
173
|
"npm-package-arg": "^13.0.0",
|
|
174
174
|
"semver": "^7.7.3",
|