@wp-typia/project-tools 0.16.6 → 0.16.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/runtime/built-in-block-code-artifacts.js +5 -5
- package/dist/runtime/cli-add-block.d.ts +36 -0
- package/dist/runtime/cli-add-block.js +518 -0
- package/dist/runtime/cli-add-shared.d.ts +93 -0
- package/dist/runtime/cli-add-shared.js +201 -0
- package/dist/runtime/cli-add-workspace.d.ts +81 -0
- package/dist/runtime/cli-add-workspace.js +582 -0
- package/dist/runtime/cli-add.d.ts +11 -131
- package/dist/runtime/cli-add.js +10 -1250
- package/dist/runtime/cli-prompt.d.ts +25 -0
- package/dist/runtime/cli-prompt.js +32 -20
- package/dist/runtime/cli-scaffold.js +1 -2
- package/dist/runtime/migration-types.d.ts +9 -53
- package/dist/runtime/scaffold.js +1 -2
- package/dist/runtime/template-source.js +1 -2
- package/package.json +6 -8
- package/templates/_shared/base/package.json.mustache +1 -0
- package/templates/_shared/compound/core/package.json.mustache +1 -1
- package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +5 -5
- package/templates/_shared/compound/persistence/package.json.mustache +1 -1
- package/templates/_shared/persistence/core/package.json.mustache +1 -0
- package/templates/interactivity/package.json.mustache +2 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { promises as fsp } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_IDS, } from "./hooked-blocks.js";
|
|
5
|
+
import { toKebabCase, toSnakeCase, } from "./string-case.js";
|
|
6
|
+
import { WORKSPACE_TEMPLATE_PACKAGE, } from "./workspace-project.js";
|
|
7
|
+
/**
|
|
8
|
+
* Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
|
|
9
|
+
*/
|
|
10
|
+
export const ADD_KIND_IDS = ["block", "variation", "pattern", "binding-source", "hooked-block"];
|
|
11
|
+
/**
|
|
12
|
+
* Supported built-in block families accepted by `wp-typia add block --template`.
|
|
13
|
+
*/
|
|
14
|
+
export const ADD_BLOCK_TEMPLATE_IDS = [
|
|
15
|
+
"basic",
|
|
16
|
+
"interactivity",
|
|
17
|
+
"persistence",
|
|
18
|
+
"compound",
|
|
19
|
+
];
|
|
20
|
+
const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
21
|
+
export function normalizeBlockSlug(input) {
|
|
22
|
+
return toKebabCase(input);
|
|
23
|
+
}
|
|
24
|
+
export function assertValidGeneratedSlug(label, slug, usage) {
|
|
25
|
+
if (!slug) {
|
|
26
|
+
throw new Error(`${label} is required. Use \`${usage}\`.`);
|
|
27
|
+
}
|
|
28
|
+
if (!WORKSPACE_GENERATED_SLUG_PATTERN.test(slug)) {
|
|
29
|
+
throw new Error(`${label} must start with a letter and contain only lowercase letters, numbers, and hyphens.`);
|
|
30
|
+
}
|
|
31
|
+
return slug;
|
|
32
|
+
}
|
|
33
|
+
export function assertValidHookedBlockPosition(position) {
|
|
34
|
+
if (HOOKED_BLOCK_POSITION_IDS.includes(position)) {
|
|
35
|
+
return position;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`Hook position must be one of: ${HOOKED_BLOCK_POSITION_IDS.join(", ")}.`);
|
|
38
|
+
}
|
|
39
|
+
export function getWorkspaceBootstrapPath(workspace) {
|
|
40
|
+
const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
|
|
41
|
+
return path.join(workspace.projectDir, `${workspaceBaseName}.php`);
|
|
42
|
+
}
|
|
43
|
+
export function buildWorkspacePhpPrefix(workspacePhpPrefix, slug) {
|
|
44
|
+
return toSnakeCase(`${workspacePhpPrefix}_${slug}`);
|
|
45
|
+
}
|
|
46
|
+
export function isAddBlockTemplateId(value) {
|
|
47
|
+
return ADD_BLOCK_TEMPLATE_IDS.includes(value);
|
|
48
|
+
}
|
|
49
|
+
export function quoteTsString(value) {
|
|
50
|
+
return JSON.stringify(value);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Apply a text transform to an existing file only when the contents change.
|
|
54
|
+
*/
|
|
55
|
+
export async function patchFile(filePath, transform) {
|
|
56
|
+
const currentSource = await fsp.readFile(filePath, "utf8");
|
|
57
|
+
const nextSource = transform(currentSource);
|
|
58
|
+
if (nextSource !== currentSource) {
|
|
59
|
+
await fsp.writeFile(filePath, nextSource, "utf8");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read a file when it exists and otherwise return `null`.
|
|
64
|
+
*/
|
|
65
|
+
export async function readOptionalFile(filePath) {
|
|
66
|
+
if (!fs.existsSync(filePath)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return fsp.readFile(filePath, "utf8");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Restore a file to its captured source, deleting it when the snapshot was `null`.
|
|
73
|
+
*/
|
|
74
|
+
export async function restoreOptionalFile(filePath, source) {
|
|
75
|
+
if (source === null) {
|
|
76
|
+
await fsp.rm(filePath, { force: true });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
80
|
+
await fsp.writeFile(filePath, source, "utf8");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Capture the current contents of a set of workspace files for rollback.
|
|
84
|
+
*/
|
|
85
|
+
export async function snapshotWorkspaceFiles(filePaths) {
|
|
86
|
+
const uniquePaths = Array.from(new Set(filePaths));
|
|
87
|
+
return Promise.all(uniquePaths.map(async (filePath) => ({
|
|
88
|
+
filePath,
|
|
89
|
+
source: await readOptionalFile(filePath),
|
|
90
|
+
})));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Undo a partially applied workspace mutation from a captured snapshot.
|
|
94
|
+
*/
|
|
95
|
+
export async function rollbackWorkspaceMutation(snapshot) {
|
|
96
|
+
for (const targetPath of snapshot.targetPaths) {
|
|
97
|
+
await fsp.rm(targetPath, { force: true, recursive: true });
|
|
98
|
+
}
|
|
99
|
+
for (const snapshotDir of snapshot.snapshotDirs) {
|
|
100
|
+
await fsp.rm(snapshotDir, { force: true, recursive: true });
|
|
101
|
+
}
|
|
102
|
+
for (const { filePath, source } of snapshot.fileSources) {
|
|
103
|
+
await restoreOptionalFile(filePath, source);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function resolveWorkspaceBlock(inventory, blockSlug) {
|
|
107
|
+
const block = inventory.blocks.find((entry) => entry.slug === blockSlug);
|
|
108
|
+
if (!block) {
|
|
109
|
+
throw new Error(`Unknown workspace block "${blockSlug}". Choose one of: ${inventory.blocks.map((entry) => entry.slug).join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
return block;
|
|
112
|
+
}
|
|
113
|
+
export function assertValidHookAnchor(anchorBlockName) {
|
|
114
|
+
const trimmed = anchorBlockName.trim();
|
|
115
|
+
if (!trimmed) {
|
|
116
|
+
throw new Error("`wp-typia add hooked-block` requires --anchor <anchor-block-name>.");
|
|
117
|
+
}
|
|
118
|
+
if (!HOOKED_BLOCK_ANCHOR_PATTERN.test(trimmed)) {
|
|
119
|
+
throw new Error("`wp-typia add hooked-block` requires --anchor <anchor-block-name> to use the full `namespace/slug` block name format.");
|
|
120
|
+
}
|
|
121
|
+
return trimmed;
|
|
122
|
+
}
|
|
123
|
+
export function readWorkspaceBlockJson(projectDir, blockSlug) {
|
|
124
|
+
const blockJsonPath = path.join(projectDir, "src", "blocks", blockSlug, "block.json");
|
|
125
|
+
if (!fs.existsSync(blockJsonPath)) {
|
|
126
|
+
throw new Error(`Missing ${path.relative(projectDir, blockJsonPath)} for workspace block "${blockSlug}".`);
|
|
127
|
+
}
|
|
128
|
+
let blockJson;
|
|
129
|
+
try {
|
|
130
|
+
blockJson = JSON.parse(fs.readFileSync(blockJsonPath, "utf8"));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw new Error(error instanceof Error
|
|
134
|
+
? `Failed to parse ${path.relative(projectDir, blockJsonPath)}: ${error.message}`
|
|
135
|
+
: `Failed to parse ${path.relative(projectDir, blockJsonPath)}.`);
|
|
136
|
+
}
|
|
137
|
+
if (!blockJson || typeof blockJson !== "object" || Array.isArray(blockJson)) {
|
|
138
|
+
throw new Error(`${path.relative(projectDir, blockJsonPath)} must contain a JSON object.`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
blockJson: blockJson,
|
|
142
|
+
blockJsonPath,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export function getMutableBlockHooks(blockJson, blockJsonRelativePath) {
|
|
146
|
+
const blockHooks = blockJson.blockHooks;
|
|
147
|
+
if (blockHooks === undefined) {
|
|
148
|
+
const nextHooks = {};
|
|
149
|
+
blockJson.blockHooks = nextHooks;
|
|
150
|
+
return nextHooks;
|
|
151
|
+
}
|
|
152
|
+
if (!blockHooks || typeof blockHooks !== "object" || Array.isArray(blockHooks)) {
|
|
153
|
+
throw new Error(`${blockJsonRelativePath} must define blockHooks as an object when present.`);
|
|
154
|
+
}
|
|
155
|
+
return blockHooks;
|
|
156
|
+
}
|
|
157
|
+
export function assertVariationDoesNotExist(projectDir, blockSlug, variationSlug, inventory) {
|
|
158
|
+
const variationPath = path.join(projectDir, "src", "blocks", blockSlug, "variations", `${variationSlug}.ts`);
|
|
159
|
+
if (fs.existsSync(variationPath)) {
|
|
160
|
+
throw new Error(`A variation already exists at ${path.relative(projectDir, variationPath)}. Choose a different name.`);
|
|
161
|
+
}
|
|
162
|
+
if (inventory.variations.some((entry) => entry.block === blockSlug && entry.slug === variationSlug)) {
|
|
163
|
+
throw new Error(`A variation inventory entry already exists for ${blockSlug}/${variationSlug}. Choose a different name.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function assertPatternDoesNotExist(projectDir, patternSlug, inventory) {
|
|
167
|
+
const patternPath = path.join(projectDir, "src", "patterns", `${patternSlug}.php`);
|
|
168
|
+
if (fs.existsSync(patternPath)) {
|
|
169
|
+
throw new Error(`A pattern already exists at ${path.relative(projectDir, patternPath)}. Choose a different name.`);
|
|
170
|
+
}
|
|
171
|
+
if (inventory.patterns.some((entry) => entry.slug === patternSlug)) {
|
|
172
|
+
throw new Error(`A pattern inventory entry already exists for ${patternSlug}. Choose a different name.`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function assertBindingSourceDoesNotExist(projectDir, bindingSourceSlug, inventory) {
|
|
176
|
+
const bindingSourceDir = path.join(projectDir, "src", "bindings", bindingSourceSlug);
|
|
177
|
+
if (fs.existsSync(bindingSourceDir)) {
|
|
178
|
+
throw new Error(`A binding source already exists at ${path.relative(projectDir, bindingSourceDir)}. Choose a different name.`);
|
|
179
|
+
}
|
|
180
|
+
if (inventory.bindingSources.some((entry) => entry.slug === bindingSourceSlug)) {
|
|
181
|
+
throw new Error(`A binding source inventory entry already exists for ${bindingSourceSlug}. Choose a different name.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns help text for the canonical `wp-typia add` subcommands.
|
|
186
|
+
*/
|
|
187
|
+
export function formatAddHelpText() {
|
|
188
|
+
return `Usage:
|
|
189
|
+
wp-typia add block <name> --template <${ADD_BLOCK_TEMPLATE_IDS.join("|")}> [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
|
|
190
|
+
wp-typia add variation <name> --block <block-slug>
|
|
191
|
+
wp-typia add pattern <name>
|
|
192
|
+
wp-typia add binding-source <name>
|
|
193
|
+
wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <${HOOKED_BLOCK_POSITION_IDS.join("|")}>
|
|
194
|
+
|
|
195
|
+
Notes:
|
|
196
|
+
\`wp-typia add\` runs only inside official ${WORKSPACE_TEMPLATE_PACKAGE} workspaces.
|
|
197
|
+
\`add variation\` targets an existing block slug from \`scripts/block-config.ts\`.
|
|
198
|
+
\`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
|
|
199
|
+
\`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
|
|
200
|
+
\`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.`;
|
|
201
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { HookedBlockPositionId } from "./hooked-blocks.js";
|
|
2
|
+
import { type RunAddBindingSourceCommandOptions, type RunAddHookedBlockCommandOptions, type RunAddPatternCommandOptions, type RunAddVariationCommandOptions } from "./cli-add-shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Add one variation entry to an existing workspace block.
|
|
5
|
+
*
|
|
6
|
+
* @param options Command options for the variation scaffold workflow.
|
|
7
|
+
* @param options.blockName Target workspace block slug that will own the variation.
|
|
8
|
+
* @param options.cwd Working directory used to resolve the nearest official workspace.
|
|
9
|
+
* Defaults to `process.cwd()`.
|
|
10
|
+
* @param options.variationName Human-entered variation name that will be normalized
|
|
11
|
+
* and validated before files are written.
|
|
12
|
+
* @returns A promise that resolves with the normalized `blockSlug`,
|
|
13
|
+
* `variationSlug`, and owning `projectDir` after the variation files and
|
|
14
|
+
* inventory entry have been written successfully.
|
|
15
|
+
* @throws {Error} When the command is run outside an official workspace, when
|
|
16
|
+
* the target block is unknown, when the variation slug is invalid, or when a
|
|
17
|
+
* conflicting file or inventory entry already exists.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runAddVariationCommand({ blockName, cwd, variationName, }: RunAddVariationCommandOptions): Promise<{
|
|
20
|
+
blockSlug: string;
|
|
21
|
+
projectDir: string;
|
|
22
|
+
variationSlug: string;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Add one PHP block pattern shell to an official workspace project.
|
|
26
|
+
*
|
|
27
|
+
* @param options Command options for the pattern scaffold workflow.
|
|
28
|
+
* @param options.cwd Working directory used to resolve the nearest official workspace.
|
|
29
|
+
* Defaults to `process.cwd()`.
|
|
30
|
+
* @param options.patternName Human-entered pattern name that will be normalized
|
|
31
|
+
* and validated before files are written.
|
|
32
|
+
* @returns A promise that resolves with the normalized `patternSlug` and
|
|
33
|
+
* owning `projectDir` after the pattern file and inventory entry have been
|
|
34
|
+
* written successfully.
|
|
35
|
+
* @throws {Error} When the command is run outside an official workspace, when
|
|
36
|
+
* the pattern slug is invalid, or when a conflicting file or inventory entry
|
|
37
|
+
* already exists.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runAddPatternCommand({ cwd, patternName, }: RunAddPatternCommandOptions): Promise<{
|
|
40
|
+
patternSlug: string;
|
|
41
|
+
projectDir: string;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Add one block binding source scaffold to an official workspace project.
|
|
45
|
+
*
|
|
46
|
+
* @param options Command options for the binding-source scaffold workflow.
|
|
47
|
+
* @param options.bindingSourceName Human-entered binding source name that will
|
|
48
|
+
* be normalized and validated before files are written.
|
|
49
|
+
* @param options.cwd Working directory used to resolve the nearest official
|
|
50
|
+
* workspace. Defaults to `process.cwd()`.
|
|
51
|
+
* @returns A promise that resolves with the normalized `bindingSourceSlug` and
|
|
52
|
+
* owning `projectDir` after the server/editor files and inventory entry have
|
|
53
|
+
* been written successfully.
|
|
54
|
+
* @throws {Error} When the command is run outside an official workspace, when
|
|
55
|
+
* the slug is invalid, or when a conflicting file or inventory entry exists.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runAddBindingSourceCommand({ bindingSourceName, cwd, }: RunAddBindingSourceCommandOptions): Promise<{
|
|
58
|
+
bindingSourceSlug: string;
|
|
59
|
+
projectDir: string;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Add one `blockHooks` entry to an existing official workspace block.
|
|
63
|
+
*
|
|
64
|
+
* @param options Command options for the hooked-block workflow.
|
|
65
|
+
* @param options.anchorBlockName Full block name that will anchor the insertion.
|
|
66
|
+
* @param options.blockName Existing workspace block slug to patch.
|
|
67
|
+
* @param options.cwd Working directory used to resolve the nearest official workspace.
|
|
68
|
+
* Defaults to `process.cwd()`.
|
|
69
|
+
* @param options.position Hook position to store in `block.json`.
|
|
70
|
+
* @returns A promise that resolves with the normalized target block slug, anchor
|
|
71
|
+
* block name, position, and owning project directory after `block.json` is written.
|
|
72
|
+
* @throws {Error} When the command is run outside an official workspace, when
|
|
73
|
+
* the target block is unknown, when required flags are missing, or when the
|
|
74
|
+
* block already defines a hook for the requested anchor.
|
|
75
|
+
*/
|
|
76
|
+
export declare function runAddHookedBlockCommand({ anchorBlockName, blockName, cwd, position, }: RunAddHookedBlockCommandOptions): Promise<{
|
|
77
|
+
anchorBlockName: string;
|
|
78
|
+
blockSlug: string;
|
|
79
|
+
position: HookedBlockPositionId;
|
|
80
|
+
projectDir: string;
|
|
81
|
+
}>;
|