@wp-typia/project-tools 0.16.5 → 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,518 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { promises as fsp } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { syncBlockMetadata, } from "@wp-typia/block-runtime/metadata-core";
|
|
6
|
+
import { ensureMigrationDirectories, parseMigrationConfig, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
|
|
7
|
+
import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
|
|
8
|
+
import { snapshotProjectVersion } from "./migrations.js";
|
|
9
|
+
import { getDefaultAnswers, scaffoldProject } from "./scaffold.js";
|
|
10
|
+
import { copyInterpolatedDirectory, } from "./template-render.js";
|
|
11
|
+
import { SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
|
|
12
|
+
import { appendWorkspaceInventoryEntries, } from "./workspace-inventory.js";
|
|
13
|
+
import { resolveWorkspaceProject, } from "./workspace-project.js";
|
|
14
|
+
import { ADD_BLOCK_TEMPLATE_IDS, buildWorkspacePhpPrefix, isAddBlockTemplateId, normalizeBlockSlug, patchFile, quoteTsString, readOptionalFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
|
|
15
|
+
const COLLECTION_IMPORT_LINE = "import '../../collection';";
|
|
16
|
+
const REST_MANIFEST_IMPORT_PATTERN = /import\s*\{[^}]*\bdefineEndpointManifest\b[^}]*\}\s*from\s*["']@wp-typia\/block-runtime\/metadata-core["'];?/m;
|
|
17
|
+
function buildServerTemplateRoot(persistencePolicy) {
|
|
18
|
+
return path.join(SHARED_WORKSPACE_TEMPLATE_ROOT, persistencePolicy === "public" ? "persistence-public" : "persistence-auth");
|
|
19
|
+
}
|
|
20
|
+
function buildSingleBlockConfigEntry(variables) {
|
|
21
|
+
return [
|
|
22
|
+
"\t{",
|
|
23
|
+
`\t\tslug: ${quoteTsString(variables.slugKebabCase)},`,
|
|
24
|
+
`\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}Attributes`)},`,
|
|
25
|
+
`\t\ttypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/types.ts`)},`,
|
|
26
|
+
"\t},",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
function buildPersistenceBlockConfigEntry(variables) {
|
|
30
|
+
return [
|
|
31
|
+
"\t{",
|
|
32
|
+
`\t\tapiTypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/api-types.ts`)},`,
|
|
33
|
+
`\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}Attributes`)},`,
|
|
34
|
+
"\t\trestManifest: defineEndpointManifest( {",
|
|
35
|
+
"\t\t\tcontracts: {",
|
|
36
|
+
"\t\t\t\t'bootstrap-query': {",
|
|
37
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}BootstrapQuery`)},`,
|
|
38
|
+
"\t\t\t\t},",
|
|
39
|
+
"\t\t\t\t'bootstrap-response': {",
|
|
40
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}BootstrapResponse`)},`,
|
|
41
|
+
"\t\t\t\t},",
|
|
42
|
+
"\t\t\t\t'state-query': {",
|
|
43
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}StateQuery`)},`,
|
|
44
|
+
"\t\t\t\t},",
|
|
45
|
+
"\t\t\t\t'state-response': {",
|
|
46
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}StateResponse`)},`,
|
|
47
|
+
"\t\t\t\t},",
|
|
48
|
+
"\t\t\t\t'write-state-request': {",
|
|
49
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}WriteStateRequest`)},`,
|
|
50
|
+
"\t\t\t\t},",
|
|
51
|
+
"\t\t\t},",
|
|
52
|
+
"\t\t\tendpoints: [",
|
|
53
|
+
"\t\t\t\t{",
|
|
54
|
+
"\t\t\t\t\tauth: 'public',",
|
|
55
|
+
"\t\t\t\t\tmethod: 'GET',",
|
|
56
|
+
`\t\t\t\t\toperationId: ${quoteTsString(`get${variables.pascalCase}State`)},`,
|
|
57
|
+
`\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/state`)},`,
|
|
58
|
+
"\t\t\t\t\tqueryContract: 'state-query',",
|
|
59
|
+
"\t\t\t\t\tresponseContract: 'state-response',",
|
|
60
|
+
`\t\t\t\t\tsummary: 'Read the current persisted state.',`,
|
|
61
|
+
`\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
|
|
62
|
+
"\t\t\t\t},",
|
|
63
|
+
"\t\t\t\t{",
|
|
64
|
+
`\t\t\t\t\tauth: ${quoteTsString(variables.restWriteAuthIntent)},`,
|
|
65
|
+
"\t\t\t\t\tbodyContract: 'write-state-request',",
|
|
66
|
+
"\t\t\t\t\tmethod: 'POST',",
|
|
67
|
+
`\t\t\t\t\toperationId: ${quoteTsString(`write${variables.pascalCase}State`)},`,
|
|
68
|
+
`\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/state`)},`,
|
|
69
|
+
"\t\t\t\t\tresponseContract: 'state-response',",
|
|
70
|
+
`\t\t\t\t\tsummary: 'Write the current persisted state.',`,
|
|
71
|
+
`\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
|
|
72
|
+
"\t\t\t\t\twordpressAuth: {",
|
|
73
|
+
`\t\t\t\t\t\tmechanism: ${quoteTsString(variables.restWriteAuthMechanism)},`,
|
|
74
|
+
"\t\t\t\t\t},",
|
|
75
|
+
"\t\t\t\t},",
|
|
76
|
+
"\t\t\t\t{",
|
|
77
|
+
"\t\t\t\t\tauth: 'public',",
|
|
78
|
+
"\t\t\t\t\tmethod: 'GET',",
|
|
79
|
+
`\t\t\t\t\toperationId: ${quoteTsString(`get${variables.pascalCase}Bootstrap`)},`,
|
|
80
|
+
`\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/bootstrap`)},`,
|
|
81
|
+
"\t\t\t\t\tqueryContract: 'bootstrap-query',",
|
|
82
|
+
"\t\t\t\t\tresponseContract: 'bootstrap-response',",
|
|
83
|
+
`\t\t\t\t\tsummary: 'Read fresh session bootstrap state for the current viewer.',`,
|
|
84
|
+
`\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
|
|
85
|
+
"\t\t\t\t},",
|
|
86
|
+
"\t\t\t],",
|
|
87
|
+
"\t\t\tinfo: {",
|
|
88
|
+
`\t\t\t\ttitle: ${quoteTsString(`${variables.title} REST API`)},`,
|
|
89
|
+
"\t\t\t\tversion: '1.0.0',",
|
|
90
|
+
"\t\t\t},",
|
|
91
|
+
"\t\t} ),",
|
|
92
|
+
`\t\topenApiFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/api.openapi.json`)},`,
|
|
93
|
+
`\t\tslug: ${quoteTsString(variables.slugKebabCase)},`,
|
|
94
|
+
`\t\ttypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}/types.ts`)},`,
|
|
95
|
+
"\t},",
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
function buildCompoundChildConfigEntry(variables) {
|
|
99
|
+
return [
|
|
100
|
+
"\t{",
|
|
101
|
+
`\t\tslug: ${quoteTsString(`${variables.slugKebabCase}-item`)},`,
|
|
102
|
+
`\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}ItemAttributes`)},`,
|
|
103
|
+
`\t\ttypesFile: ${quoteTsString(`src/blocks/${variables.slugKebabCase}-item/types.ts`)},`,
|
|
104
|
+
"\t},",
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
|
107
|
+
function buildConfigEntries(templateId, variables) {
|
|
108
|
+
if (templateId === "basic" || templateId === "interactivity") {
|
|
109
|
+
return [buildSingleBlockConfigEntry(variables)];
|
|
110
|
+
}
|
|
111
|
+
if (templateId === "persistence") {
|
|
112
|
+
return [buildPersistenceBlockConfigEntry(variables)];
|
|
113
|
+
}
|
|
114
|
+
if (variables.compoundPersistenceEnabled === "true") {
|
|
115
|
+
return [
|
|
116
|
+
buildPersistenceBlockConfigEntry(variables),
|
|
117
|
+
buildCompoundChildConfigEntry(variables),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
return [
|
|
121
|
+
buildSingleBlockConfigEntry(variables),
|
|
122
|
+
buildCompoundChildConfigEntry(variables),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function buildMigrationBlocks(templateId, variables) {
|
|
126
|
+
if (templateId === "compound") {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
blockJsonFile: `src/blocks/${variables.slugKebabCase}/block.json`,
|
|
130
|
+
blockName: `${variables.namespace}/${variables.slugKebabCase}`,
|
|
131
|
+
key: variables.slugKebabCase,
|
|
132
|
+
manifestFile: `src/blocks/${variables.slugKebabCase}/typia.manifest.json`,
|
|
133
|
+
saveFile: `src/blocks/${variables.slugKebabCase}/save.tsx`,
|
|
134
|
+
typesFile: `src/blocks/${variables.slugKebabCase}/types.ts`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
blockJsonFile: `src/blocks/${variables.slugKebabCase}-item/block.json`,
|
|
138
|
+
blockName: `${variables.namespace}/${variables.slugKebabCase}-item`,
|
|
139
|
+
key: `${variables.slugKebabCase}-item`,
|
|
140
|
+
manifestFile: `src/blocks/${variables.slugKebabCase}-item/typia.manifest.json`,
|
|
141
|
+
saveFile: `src/blocks/${variables.slugKebabCase}-item/save.tsx`,
|
|
142
|
+
typesFile: `src/blocks/${variables.slugKebabCase}-item/types.ts`,
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
return [
|
|
147
|
+
{
|
|
148
|
+
blockJsonFile: `src/blocks/${variables.slugKebabCase}/block.json`,
|
|
149
|
+
blockName: `${variables.namespace}/${variables.slugKebabCase}`,
|
|
150
|
+
key: variables.slugKebabCase,
|
|
151
|
+
manifestFile: `src/blocks/${variables.slugKebabCase}/typia.manifest.json`,
|
|
152
|
+
saveFile: `src/blocks/${variables.slugKebabCase}/save.tsx`,
|
|
153
|
+
typesFile: `src/blocks/${variables.slugKebabCase}/types.ts`,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
async function ensureCollectionImport(filePath) {
|
|
158
|
+
await patchFile(filePath, (source) => {
|
|
159
|
+
if (source.includes(COLLECTION_IMPORT_LINE)) {
|
|
160
|
+
return source;
|
|
161
|
+
}
|
|
162
|
+
if (source.includes("import metadata from './block.json';")) {
|
|
163
|
+
return source.replace("import metadata from './block.json';", `${COLLECTION_IMPORT_LINE}\nimport metadata from './block.json';`);
|
|
164
|
+
}
|
|
165
|
+
return `${COLLECTION_IMPORT_LINE}\n${source}`;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async function copyTempDirectory(sourceDir, targetDir) {
|
|
169
|
+
await fsp.mkdir(path.dirname(targetDir), { recursive: true });
|
|
170
|
+
await fsp.cp(sourceDir, targetDir, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
async function addCollectionImportsForTemplate(projectDir, templateId, variables) {
|
|
173
|
+
if (templateId === "compound") {
|
|
174
|
+
await ensureCollectionImport(path.join(projectDir, "src", "blocks", variables.slugKebabCase, "index.tsx"));
|
|
175
|
+
await ensureCollectionImport(path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`, "index.tsx"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await ensureCollectionImport(path.join(projectDir, "src", "blocks", variables.slugKebabCase, "index.tsx"));
|
|
179
|
+
}
|
|
180
|
+
function ensureBlockConfigCanAddRestManifests(source) {
|
|
181
|
+
const importLine = "import { defineEndpointManifest } from '@wp-typia/block-runtime/metadata-core';";
|
|
182
|
+
if (REST_MANIFEST_IMPORT_PATTERN.test(source)) {
|
|
183
|
+
return source;
|
|
184
|
+
}
|
|
185
|
+
return `${importLine}\n\n${source}`;
|
|
186
|
+
}
|
|
187
|
+
async function appendBlockConfigEntries(projectDir, entries, needsRestManifestImport) {
|
|
188
|
+
await appendWorkspaceInventoryEntries(projectDir, {
|
|
189
|
+
blockEntries: entries,
|
|
190
|
+
transformSource: needsRestManifestImport ? ensureBlockConfigCanAddRestManifests : undefined,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function renderWorkspacePersistenceServerModule(projectDir, variables) {
|
|
194
|
+
const targetDir = path.join(projectDir, "src", "blocks", variables.slugKebabCase);
|
|
195
|
+
const templateDir = buildServerTemplateRoot(variables.persistencePolicy);
|
|
196
|
+
await copyInterpolatedDirectory(templateDir, targetDir, variables);
|
|
197
|
+
}
|
|
198
|
+
const COMPOUND_SHARED_SUPPORT_FILES = ["hooks.ts", "validator-toolkit.ts"];
|
|
199
|
+
const LEGACY_ASSERT_PATTERN = /assert:\s*typia\.createAssert</u;
|
|
200
|
+
const LEGACY_MANIFEST_PATTERN = /\r?\n[ \t]*manifest:\s*currentManifest,/u;
|
|
201
|
+
const LEGACY_TOOLKIT_CALL_PATTERN = /createTemplateValidatorToolkit<\s*(?<typeName>[A-Za-z0-9_]+)\s*>\s*\(\s*\{/u;
|
|
202
|
+
const LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN = /from\s*["']\.\.\/\.\.\/validator-toolkit["']/u;
|
|
203
|
+
const TYPIA_IMPORT_PATTERN = /^[\uFEFF \t]*import\s+typia\s+from\s*["']typia["'];?/mu;
|
|
204
|
+
const COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS = [
|
|
205
|
+
/interface\s+TemplateValidatorFunctions\s*<\s*T\s+extends\s+object\s*>\s*\{/u,
|
|
206
|
+
/\bassert\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']assert["']\s*\]/u,
|
|
207
|
+
/\bclone\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']clone["']\s*\]/u,
|
|
208
|
+
/\bis\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']is["']\s*\]/u,
|
|
209
|
+
/\bprune\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']prune["']\s*\]/u,
|
|
210
|
+
/\brandom\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']random["']\s*\]/u,
|
|
211
|
+
/\bvalidate\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']validate["']\s*\]/u,
|
|
212
|
+
/createTemplateValidatorToolkit\s*<\s*T\s+extends\s+object\s*>\s*\(\s*\{/u,
|
|
213
|
+
];
|
|
214
|
+
function shouldRefreshCompoundValidatorToolkit(source) {
|
|
215
|
+
return (source === null ||
|
|
216
|
+
!COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS.every((pattern) => pattern.test(source)));
|
|
217
|
+
}
|
|
218
|
+
function isLegacyCompoundValidatorSource(source) {
|
|
219
|
+
return (typeof source === "string" &&
|
|
220
|
+
LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN.test(source) &&
|
|
221
|
+
!LEGACY_ASSERT_PATTERN.test(source));
|
|
222
|
+
}
|
|
223
|
+
function hasTypiaImport(source) {
|
|
224
|
+
return TYPIA_IMPORT_PATTERN.test(source.replace(/\/\*[\s\S]*?\*\//gu, ""));
|
|
225
|
+
}
|
|
226
|
+
function upgradeLegacyCompoundValidatorSource(source) {
|
|
227
|
+
const typeNameMatch = source.match(LEGACY_TOOLKIT_CALL_PATTERN);
|
|
228
|
+
const typeName = typeNameMatch?.groups?.typeName;
|
|
229
|
+
if (!typeName) {
|
|
230
|
+
throw new Error("Unable to upgrade a legacy compound validator without a generated type import.");
|
|
231
|
+
}
|
|
232
|
+
let nextSource = source;
|
|
233
|
+
if (!hasTypiaImport(nextSource)) {
|
|
234
|
+
nextSource = `import typia from 'typia';\n${nextSource}`;
|
|
235
|
+
}
|
|
236
|
+
nextSource = nextSource.replace(LEGACY_TOOLKIT_CALL_PATTERN, [
|
|
237
|
+
`createTemplateValidatorToolkit< ${typeName} >( {`,
|
|
238
|
+
`\tassert: typia.createAssert< ${typeName} >(),`,
|
|
239
|
+
`\tclone: typia.misc.createClone< ${typeName} >() as (`,
|
|
240
|
+
`\t\tvalue: ${typeName},`,
|
|
241
|
+
`\t) => ${typeName},`,
|
|
242
|
+
`\tis: typia.createIs< ${typeName} >(),`,
|
|
243
|
+
].join("\n") + "\n");
|
|
244
|
+
const replacedManifest = nextSource.replace(LEGACY_MANIFEST_PATTERN, [
|
|
245
|
+
"",
|
|
246
|
+
"\tmanifest: currentManifest,",
|
|
247
|
+
`\tprune: typia.misc.createPrune< ${typeName} >(),`,
|
|
248
|
+
`\trandom: typia.createRandom< ${typeName} >() as (`,
|
|
249
|
+
"\t\t...args: unknown[]",
|
|
250
|
+
`\t) => ${typeName},`,
|
|
251
|
+
`\tvalidate: typia.createValidate< ${typeName} >(),`,
|
|
252
|
+
].join("\n"));
|
|
253
|
+
if (replacedManifest === nextSource) {
|
|
254
|
+
throw new Error("Unable to upgrade legacy compound validator: manifest anchor not found.");
|
|
255
|
+
}
|
|
256
|
+
return replacedManifest;
|
|
257
|
+
}
|
|
258
|
+
async function collectLegacyCompoundValidatorPaths(projectDir) {
|
|
259
|
+
const blocksDir = path.join(projectDir, "src", "blocks");
|
|
260
|
+
if (!fs.existsSync(blocksDir)) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const blockEntries = await fsp.readdir(blocksDir, { withFileTypes: true });
|
|
264
|
+
const validatorPaths = await Promise.all(blockEntries
|
|
265
|
+
.filter((entry) => entry.isDirectory())
|
|
266
|
+
.map(async (entry) => {
|
|
267
|
+
const validatorPath = path.join(blocksDir, entry.name, "validators.ts");
|
|
268
|
+
const validatorSource = await readOptionalFile(validatorPath);
|
|
269
|
+
return isLegacyCompoundValidatorSource(validatorSource) ? validatorPath : null;
|
|
270
|
+
}));
|
|
271
|
+
return validatorPaths.filter((validatorPath) => validatorPath !== null);
|
|
272
|
+
}
|
|
273
|
+
async function ensureCompoundWorkspaceSupportFiles(projectDir, tempProjectDir, legacyValidatorPaths) {
|
|
274
|
+
for (const fileName of COMPOUND_SHARED_SUPPORT_FILES) {
|
|
275
|
+
const sourcePath = path.join(tempProjectDir, "src", fileName);
|
|
276
|
+
if (!fs.existsSync(sourcePath)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const targetPath = path.join(projectDir, "src", fileName);
|
|
280
|
+
const currentSource = await readOptionalFile(targetPath);
|
|
281
|
+
if (fileName === "validator-toolkit.ts"
|
|
282
|
+
? shouldRefreshCompoundValidatorToolkit(currentSource)
|
|
283
|
+
: currentSource === null) {
|
|
284
|
+
await fsp.mkdir(path.dirname(targetPath), { recursive: true });
|
|
285
|
+
await fsp.copyFile(sourcePath, targetPath);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const validatorPath of legacyValidatorPaths) {
|
|
289
|
+
const currentSource = await readOptionalFile(validatorPath);
|
|
290
|
+
if (!isLegacyCompoundValidatorSource(currentSource)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
await fsp.writeFile(validatorPath, upgradeLegacyCompoundValidatorSource(currentSource), "utf8");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function copyScaffoldedBlockSlice(projectDir, templateId, tempProjectDir, variables, legacyValidatorPaths = []) {
|
|
297
|
+
if (templateId === "compound") {
|
|
298
|
+
await ensureCompoundWorkspaceSupportFiles(projectDir, tempProjectDir, legacyValidatorPaths);
|
|
299
|
+
await copyTempDirectory(path.join(tempProjectDir, "src", "blocks", variables.slugKebabCase), path.join(projectDir, "src", "blocks", variables.slugKebabCase));
|
|
300
|
+
await copyTempDirectory(path.join(tempProjectDir, "src", "blocks", `${variables.slugKebabCase}-item`), path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`));
|
|
301
|
+
if (variables.compoundPersistenceEnabled === "true") {
|
|
302
|
+
await renderWorkspacePersistenceServerModule(projectDir, variables);
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await copyTempDirectory(path.join(tempProjectDir, "src"), path.join(projectDir, "src", "blocks", variables.slugKebabCase));
|
|
307
|
+
if (templateId === "persistence") {
|
|
308
|
+
await renderWorkspacePersistenceServerModule(projectDir, variables);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function collectWorkspaceBlockPaths(projectDir, templateId, variables) {
|
|
312
|
+
if (templateId === "compound") {
|
|
313
|
+
return [
|
|
314
|
+
path.join(projectDir, "src", "blocks", variables.slugKebabCase),
|
|
315
|
+
path.join(projectDir, "src", "blocks", `${variables.slugKebabCase}-item`),
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
return [path.join(projectDir, "src", "blocks", variables.slugKebabCase)];
|
|
319
|
+
}
|
|
320
|
+
function assertBlockTargetsDoNotExist(projectDir, templateId, variables) {
|
|
321
|
+
for (const targetPath of collectWorkspaceBlockPaths(projectDir, templateId, variables)) {
|
|
322
|
+
if (fs.existsSync(targetPath)) {
|
|
323
|
+
throw new Error(`A block already exists at ${path.relative(projectDir, targetPath)}. Choose a different name.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function updateWorkspaceMigrationConfigIfPresent(projectDir, newBlocks) {
|
|
328
|
+
const configPath = path.join(projectDir, "src", "migrations", "config.ts");
|
|
329
|
+
if (!fs.existsSync(configPath)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const configSource = await fsp.readFile(configPath, "utf8");
|
|
333
|
+
const config = parseMigrationConfig(configSource);
|
|
334
|
+
const existingBlocks = Array.isArray(config.blocks) ? config.blocks : [];
|
|
335
|
+
const nextBlocks = [
|
|
336
|
+
...existingBlocks,
|
|
337
|
+
...newBlocks.filter((block) => !existingBlocks.some((existing) => existing.key === block.key)),
|
|
338
|
+
];
|
|
339
|
+
writeMigrationConfig(projectDir, {
|
|
340
|
+
...config,
|
|
341
|
+
blocks: nextBlocks,
|
|
342
|
+
});
|
|
343
|
+
snapshotProjectVersion(projectDir, config.currentMigrationVersion, {
|
|
344
|
+
skipConfigUpdate: true,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async function syncWorkspaceBlockMetadata(projectDir, slug, sourceTypeName, typesFile) {
|
|
348
|
+
await syncBlockMetadata({
|
|
349
|
+
blockJsonFile: path.join("src", "blocks", slug, "block.json"),
|
|
350
|
+
jsonSchemaFile: path.join("src", "blocks", slug, "typia.schema.json"),
|
|
351
|
+
manifestFile: path.join("src", "blocks", slug, "typia.manifest.json"),
|
|
352
|
+
openApiFile: path.join("src", "blocks", slug, "typia.openapi.json"),
|
|
353
|
+
projectRoot: projectDir,
|
|
354
|
+
sourceTypeName,
|
|
355
|
+
typesFile,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async function syncWorkspacePersistenceArtifacts(projectDir, variables) {
|
|
359
|
+
await syncPersistenceRestArtifacts({
|
|
360
|
+
apiTypesFile: path.join("src", "blocks", variables.slugKebabCase, "api-types.ts"),
|
|
361
|
+
outputDir: path.join("src", "blocks", variables.slugKebabCase),
|
|
362
|
+
projectDir,
|
|
363
|
+
variables,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async function syncWorkspaceAddedBlockArtifacts(projectDir, templateId, variables) {
|
|
367
|
+
await syncWorkspaceBlockMetadata(projectDir, variables.slugKebabCase, `${variables.pascalCase}Attributes`, path.join("src", "blocks", variables.slugKebabCase, "types.ts"));
|
|
368
|
+
if (templateId === "compound") {
|
|
369
|
+
await syncWorkspaceBlockMetadata(projectDir, `${variables.slugKebabCase}-item`, `${variables.pascalCase}ItemAttributes`, path.join("src", "blocks", `${variables.slugKebabCase}-item`, "types.ts"));
|
|
370
|
+
}
|
|
371
|
+
if (templateId === "persistence" ||
|
|
372
|
+
(templateId === "compound" && variables.compoundPersistenceEnabled === "true")) {
|
|
373
|
+
await syncWorkspacePersistenceArtifacts(projectDir, variables);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function assertPersistenceFlagsAllowed(templateId, options) {
|
|
377
|
+
const hasPersistenceFlags = typeof options.dataStorageMode === "string" ||
|
|
378
|
+
typeof options.persistencePolicy === "string";
|
|
379
|
+
if (!hasPersistenceFlags) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (templateId === "persistence" || templateId === "compound") {
|
|
383
|
+
if (typeof options.dataStorageMode === "string" &&
|
|
384
|
+
options.dataStorageMode !== "custom-table" &&
|
|
385
|
+
options.dataStorageMode !== "post-meta") {
|
|
386
|
+
throw new Error(`Unsupported data storage mode "${options.dataStorageMode}". Expected one of: post-meta, custom-table.`);
|
|
387
|
+
}
|
|
388
|
+
if (typeof options.persistencePolicy === "string" &&
|
|
389
|
+
options.persistencePolicy !== "authenticated" &&
|
|
390
|
+
options.persistencePolicy !== "public") {
|
|
391
|
+
throw new Error(`Unsupported persistence policy "${options.persistencePolicy}". Expected one of: authenticated, public.`);
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
throw new Error(`--data-storage and --persistence-policy are supported only for \`wp-typia add block --template persistence\` or \`--template compound\`.`);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Seeds an empty official workspace migration project before any blocks are added.
|
|
399
|
+
*
|
|
400
|
+
* @param projectDir Absolute path to the workspace root that will own the
|
|
401
|
+
* migration config and generated scaffold files.
|
|
402
|
+
* @param currentMigrationVersion Initial migration label to record as the
|
|
403
|
+
* current version in the seeded workspace.
|
|
404
|
+
*/
|
|
405
|
+
export async function seedWorkspaceMigrationProject(projectDir, currentMigrationVersion) {
|
|
406
|
+
writeMigrationConfig(projectDir, {
|
|
407
|
+
blocks: [],
|
|
408
|
+
currentMigrationVersion,
|
|
409
|
+
snapshotDir: "src/migrations/versions",
|
|
410
|
+
supportedMigrationVersions: [currentMigrationVersion],
|
|
411
|
+
});
|
|
412
|
+
ensureMigrationDirectories(projectDir, []);
|
|
413
|
+
writeInitialMigrationScaffold(projectDir, currentMigrationVersion, []);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Adds one built-in block slice to an official workspace project.
|
|
417
|
+
*
|
|
418
|
+
* @param options Command options for the built-in workspace block scaffold flow.
|
|
419
|
+
* @param options.blockName Human-entered block name that will be normalized
|
|
420
|
+
* into the generated workspace block slug.
|
|
421
|
+
* @param options.cwd Working directory used to resolve the nearest official
|
|
422
|
+
* workspace. Defaults to `process.cwd()`.
|
|
423
|
+
* @param options.dataStorageMode Optional storage mode for persistence-capable
|
|
424
|
+
* templates.
|
|
425
|
+
* @param options.persistencePolicy Optional persistence policy for
|
|
426
|
+
* persistence-capable templates.
|
|
427
|
+
* @param options.templateId Built-in block family to scaffold. Defaults to
|
|
428
|
+
* `"basic"`.
|
|
429
|
+
* @returns A promise that resolves with the created block slugs, the owning
|
|
430
|
+
* project directory, and the resolved template id after workspace mutation
|
|
431
|
+
* succeeds.
|
|
432
|
+
* @throws {Error} When the template id is unknown, persistence flags are used
|
|
433
|
+
* with unsupported templates, the command runs outside an official workspace,
|
|
434
|
+
* or target block paths already exist.
|
|
435
|
+
*/
|
|
436
|
+
export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataStorageMode, persistencePolicy, templateId = "basic", }) {
|
|
437
|
+
if (!isAddBlockTemplateId(templateId)) {
|
|
438
|
+
throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
|
|
439
|
+
}
|
|
440
|
+
const resolvedTemplateId = templateId;
|
|
441
|
+
assertPersistenceFlagsAllowed(resolvedTemplateId, { dataStorageMode, persistencePolicy });
|
|
442
|
+
const workspace = resolveWorkspaceProject(cwd);
|
|
443
|
+
const normalizedSlug = normalizeBlockSlug(blockName);
|
|
444
|
+
if (!normalizedSlug) {
|
|
445
|
+
throw new Error("Block name is required. Use `wp-typia add block <name> --template <family>`.");
|
|
446
|
+
}
|
|
447
|
+
const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
|
|
448
|
+
let tempRoot = "";
|
|
449
|
+
try {
|
|
450
|
+
tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-add-block-"));
|
|
451
|
+
const tempProjectDir = path.join(tempRoot, normalizedSlug);
|
|
452
|
+
const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
|
|
453
|
+
const migrationConfigPath = path.join(workspace.projectDir, "src", "migrations", "config.ts");
|
|
454
|
+
const blockPhpPrefix = buildWorkspacePhpPrefix(workspace.workspace.phpPrefix, normalizedSlug);
|
|
455
|
+
const migrationConfigSource = await readOptionalFile(migrationConfigPath);
|
|
456
|
+
const migrationConfig = migrationConfigSource === null ? null : parseMigrationConfig(migrationConfigSource);
|
|
457
|
+
const compoundSupportPaths = resolvedTemplateId === "compound"
|
|
458
|
+
? COMPOUND_SHARED_SUPPORT_FILES.map((fileName) => path.join(workspace.projectDir, "src", fileName))
|
|
459
|
+
: [];
|
|
460
|
+
const legacyCompoundValidatorPaths = resolvedTemplateId === "compound"
|
|
461
|
+
? await collectLegacyCompoundValidatorPaths(workspace.projectDir)
|
|
462
|
+
: [];
|
|
463
|
+
const result = await scaffoldProject({
|
|
464
|
+
answers: {
|
|
465
|
+
...defaults,
|
|
466
|
+
author: workspace.author,
|
|
467
|
+
namespace: workspace.workspace.namespace,
|
|
468
|
+
phpPrefix: blockPhpPrefix,
|
|
469
|
+
slug: normalizedSlug,
|
|
470
|
+
textDomain: workspace.workspace.textDomain,
|
|
471
|
+
title: defaults.title,
|
|
472
|
+
},
|
|
473
|
+
cwd: workspace.projectDir,
|
|
474
|
+
dataStorageMode: dataStorageMode,
|
|
475
|
+
noInstall: true,
|
|
476
|
+
packageManager: workspace.packageManager,
|
|
477
|
+
persistencePolicy: persistencePolicy,
|
|
478
|
+
projectDir: tempProjectDir,
|
|
479
|
+
templateId: resolvedTemplateId,
|
|
480
|
+
});
|
|
481
|
+
assertBlockTargetsDoNotExist(workspace.projectDir, resolvedTemplateId, result.variables);
|
|
482
|
+
const mutationSnapshot = {
|
|
483
|
+
fileSources: await snapshotWorkspaceFiles([
|
|
484
|
+
blockConfigPath,
|
|
485
|
+
migrationConfigPath,
|
|
486
|
+
...compoundSupportPaths,
|
|
487
|
+
...legacyCompoundValidatorPaths,
|
|
488
|
+
]),
|
|
489
|
+
snapshotDirs: migrationConfig === null
|
|
490
|
+
? []
|
|
491
|
+
: buildMigrationBlocks(resolvedTemplateId, result.variables).map((block) => path.join(workspace.projectDir, ...migrationConfig.snapshotDir.split("/"), migrationConfig.currentMigrationVersion, block.key)),
|
|
492
|
+
targetPaths: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables),
|
|
493
|
+
};
|
|
494
|
+
try {
|
|
495
|
+
await copyScaffoldedBlockSlice(workspace.projectDir, resolvedTemplateId, tempProjectDir, result.variables, legacyCompoundValidatorPaths);
|
|
496
|
+
await addCollectionImportsForTemplate(workspace.projectDir, resolvedTemplateId, result.variables);
|
|
497
|
+
await appendBlockConfigEntries(workspace.projectDir, buildConfigEntries(resolvedTemplateId, result.variables), resolvedTemplateId === "persistence" ||
|
|
498
|
+
(resolvedTemplateId === "compound" &&
|
|
499
|
+
result.variables.compoundPersistenceEnabled === "true"));
|
|
500
|
+
await syncWorkspaceAddedBlockArtifacts(workspace.projectDir, resolvedTemplateId, result.variables);
|
|
501
|
+
await updateWorkspaceMigrationConfigIfPresent(workspace.projectDir, buildMigrationBlocks(resolvedTemplateId, result.variables));
|
|
502
|
+
return {
|
|
503
|
+
blockSlugs: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables).map((targetPath) => path.basename(targetPath)),
|
|
504
|
+
projectDir: workspace.projectDir,
|
|
505
|
+
templateId: resolvedTemplateId,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
await rollbackWorkspaceMutation(mutationSnapshot);
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
if (tempRoot) {
|
|
515
|
+
await fsp.rm(tempRoot, { force: true, recursive: true });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type HookedBlockPositionId } from "./hooked-blocks.js";
|
|
2
|
+
import { type WorkspaceInventory } from "./workspace-inventory.js";
|
|
3
|
+
import { type WorkspaceProject } from "./workspace-project.js";
|
|
4
|
+
/**
|
|
5
|
+
* Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern", "binding-source", "hooked-block"];
|
|
8
|
+
export type AddKindId = (typeof ADD_KIND_IDS)[number];
|
|
9
|
+
/**
|
|
10
|
+
* Supported built-in block families accepted by `wp-typia add block --template`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ADD_BLOCK_TEMPLATE_IDS: readonly ["basic", "interactivity", "persistence", "compound"];
|
|
13
|
+
export type AddBlockTemplateId = (typeof ADD_BLOCK_TEMPLATE_IDS)[number];
|
|
14
|
+
export interface RunAddVariationCommandOptions {
|
|
15
|
+
blockName: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
variationName: string;
|
|
18
|
+
}
|
|
19
|
+
export interface RunAddPatternCommandOptions {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
patternName: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RunAddBindingSourceCommandOptions {
|
|
24
|
+
bindingSourceName: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RunAddHookedBlockCommandOptions {
|
|
28
|
+
anchorBlockName: string;
|
|
29
|
+
blockName: string;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
position: string;
|
|
32
|
+
}
|
|
33
|
+
export interface RunAddBlockCommandOptions {
|
|
34
|
+
blockName: string;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
dataStorageMode?: string;
|
|
37
|
+
persistencePolicy?: string;
|
|
38
|
+
templateId?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface WorkspaceMutationSnapshot {
|
|
41
|
+
/** Snapshots of file contents taken before the mutation starts. */
|
|
42
|
+
fileSources: Array<{
|
|
43
|
+
/** Absolute file path recorded for rollback. */
|
|
44
|
+
filePath: string;
|
|
45
|
+
/** Previous file contents, or `null` when the file did not exist. */
|
|
46
|
+
source: string | null;
|
|
47
|
+
}>;
|
|
48
|
+
/** Snapshot directories created while seeding migration history. */
|
|
49
|
+
snapshotDirs: string[];
|
|
50
|
+
/** Files or directories created by the mutation that should be removed on rollback. */
|
|
51
|
+
targetPaths: string[];
|
|
52
|
+
}
|
|
53
|
+
export declare function normalizeBlockSlug(input: string): string;
|
|
54
|
+
export declare function assertValidGeneratedSlug(label: string, slug: string, usage: string): string;
|
|
55
|
+
export declare function assertValidHookedBlockPosition(position: string): HookedBlockPositionId;
|
|
56
|
+
export declare function getWorkspaceBootstrapPath(workspace: WorkspaceProject): string;
|
|
57
|
+
export declare function buildWorkspacePhpPrefix(workspacePhpPrefix: string, slug: string): string;
|
|
58
|
+
export declare function isAddBlockTemplateId(value: string): value is AddBlockTemplateId;
|
|
59
|
+
export declare function quoteTsString(value: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Apply a text transform to an existing file only when the contents change.
|
|
62
|
+
*/
|
|
63
|
+
export declare function patchFile(filePath: string, transform: (source: string) => string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Read a file when it exists and otherwise return `null`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readOptionalFile(filePath: string): Promise<string | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Restore a file to its captured source, deleting it when the snapshot was `null`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function restoreOptionalFile(filePath: string, source: string | null): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Capture the current contents of a set of workspace files for rollback.
|
|
74
|
+
*/
|
|
75
|
+
export declare function snapshotWorkspaceFiles(filePaths: string[]): Promise<WorkspaceMutationSnapshot["fileSources"]>;
|
|
76
|
+
/**
|
|
77
|
+
* Undo a partially applied workspace mutation from a captured snapshot.
|
|
78
|
+
*/
|
|
79
|
+
export declare function rollbackWorkspaceMutation(snapshot: WorkspaceMutationSnapshot): Promise<void>;
|
|
80
|
+
export declare function resolveWorkspaceBlock(inventory: WorkspaceInventory, blockSlug: string): WorkspaceInventory["blocks"][number];
|
|
81
|
+
export declare function assertValidHookAnchor(anchorBlockName: string): string;
|
|
82
|
+
export declare function readWorkspaceBlockJson(projectDir: string, blockSlug: string): {
|
|
83
|
+
blockJson: Record<string, unknown>;
|
|
84
|
+
blockJsonPath: string;
|
|
85
|
+
};
|
|
86
|
+
export declare function getMutableBlockHooks(blockJson: Record<string, unknown>, blockJsonRelativePath: string): Record<string, string>;
|
|
87
|
+
export declare function assertVariationDoesNotExist(projectDir: string, blockSlug: string, variationSlug: string, inventory: WorkspaceInventory): void;
|
|
88
|
+
export declare function assertPatternDoesNotExist(projectDir: string, patternSlug: string, inventory: WorkspaceInventory): void;
|
|
89
|
+
export declare function assertBindingSourceDoesNotExist(projectDir: string, bindingSourceSlug: string, inventory: WorkspaceInventory): void;
|
|
90
|
+
/**
|
|
91
|
+
* Returns help text for the canonical `wp-typia add` subcommands.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatAddHelpText(): string;
|