@wp-typia/project-tools 0.16.7 → 0.16.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +26 -1
  2. package/dist/runtime/block-generator-service.d.ts +9 -1
  3. package/dist/runtime/block-generator-service.js +137 -16
  4. package/dist/runtime/block-generator-tool-contract.d.ts +93 -0
  5. package/dist/runtime/block-generator-tool-contract.js +157 -0
  6. package/dist/runtime/built-in-block-artifacts.js +171 -423
  7. package/dist/runtime/built-in-block-code-artifacts.js +96 -46
  8. package/dist/runtime/built-in-block-code-templates.d.ts +36 -0
  9. package/dist/runtime/built-in-block-code-templates.js +2233 -0
  10. package/dist/runtime/cli-add-block.d.ts +2 -1
  11. package/dist/runtime/cli-add-block.js +163 -25
  12. package/dist/runtime/cli-add-shared.d.ts +7 -0
  13. package/dist/runtime/cli-add-shared.js +4 -6
  14. package/dist/runtime/cli-add-workspace.js +56 -17
  15. package/dist/runtime/cli-core.d.ts +4 -0
  16. package/dist/runtime/cli-core.js +3 -0
  17. package/dist/runtime/cli-diagnostics.d.ts +58 -0
  18. package/dist/runtime/cli-diagnostics.js +101 -0
  19. package/dist/runtime/cli-doctor.d.ts +2 -1
  20. package/dist/runtime/cli-doctor.js +16 -5
  21. package/dist/runtime/cli-help.js +4 -4
  22. package/dist/runtime/cli-scaffold.d.ts +5 -1
  23. package/dist/runtime/cli-scaffold.js +138 -111
  24. package/dist/runtime/external-layer-selection.d.ts +14 -0
  25. package/dist/runtime/external-layer-selection.js +35 -0
  26. package/dist/runtime/index.d.ts +4 -2
  27. package/dist/runtime/index.js +2 -1
  28. package/dist/runtime/migration-render.d.ts +23 -1
  29. package/dist/runtime/migration-render.js +58 -10
  30. package/dist/runtime/migration-ui-capability.js +17 -8
  31. package/dist/runtime/migration-utils.d.ts +7 -6
  32. package/dist/runtime/migration-utils.js +76 -73
  33. package/dist/runtime/migrations.js +2 -2
  34. package/dist/runtime/object-utils.d.ts +8 -1
  35. package/dist/runtime/object-utils.js +21 -1
  36. package/dist/runtime/scaffold-apply-utils.d.ts +14 -2
  37. package/dist/runtime/scaffold-apply-utils.js +19 -6
  38. package/dist/runtime/scaffold-repository-reference.d.ts +22 -0
  39. package/dist/runtime/scaffold-repository-reference.js +119 -0
  40. package/dist/runtime/scaffold.d.ts +5 -1
  41. package/dist/runtime/scaffold.js +15 -37
  42. package/dist/runtime/template-builtins.d.ts +11 -1
  43. package/dist/runtime/template-builtins.js +118 -25
  44. package/dist/runtime/template-layers.d.ts +37 -0
  45. package/dist/runtime/template-layers.js +184 -0
  46. package/dist/runtime/template-render.d.ts +28 -2
  47. package/dist/runtime/template-render.js +122 -55
  48. package/dist/runtime/template-source.d.ts +23 -5
  49. package/dist/runtime/template-source.js +296 -217
  50. package/package.json +8 -3
  51. package/templates/_shared/base/src/validator-toolkit.ts.mustache +2 -2
  52. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +58 -12
  53. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +19 -47
  54. package/templates/_shared/migration-ui/common/src/migrations/index.ts +40 -11
@@ -1,25 +1,32 @@
1
1
  /// <reference path="./external-template-modules.d.ts" />
2
- import fs from "node:fs";
3
- import { promises as fsp } from "node:fs";
4
- import { createRequire } from "node:module";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { execFileSync } from "node:child_process";
8
- import { pathToFileURL } from "node:url";
9
- import npa from "npm-package-arg";
10
- import semver from "semver";
11
- import { x as extractTarball } from "tar";
12
- import { BUILTIN_TEMPLATE_IDS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, isBuiltInTemplateId, } from "./template-registry.js";
13
- import { isPlainObject } from "./object-utils.js";
14
- import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
15
- import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, resolveBuiltInTemplateSource, } from "./template-builtins.js";
16
- import { getPackageVersions } from "./package-versions.js";
17
- import { toSegmentPascalCase } from "./string-case.js";
18
- import { copyRawDirectory, copyRenderedDirectory } from "./template-render.js";
19
- const EXTERNAL_TEMPLATE_ENTRY_CANDIDATES = ["index.js", "index.cjs", "index.mjs"];
20
- const TEMPLATE_WARNING_MESSAGE = "wp-typia owns package/tooling/sync setup for generated projects, so this external template setting is ignored.";
2
+ import fs from 'node:fs';
3
+ import { promises as fsp } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { execFileSync } from 'node:child_process';
8
+ import { pathToFileURL } from 'node:url';
9
+ import npa from 'npm-package-arg';
10
+ import semver from 'semver';
11
+ import { x as extractTarball } from 'tar';
12
+ import { BUILTIN_TEMPLATE_IDS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, isBuiltInTemplateId, } from './template-registry.js';
13
+ import { isPlainObject } from './object-utils.js';
14
+ import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from './template-defaults.js';
15
+ import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, resolveBuiltInTemplateSource, } from './template-builtins.js';
16
+ import { loadExternalTemplateLayerManifest } from './template-layers.js';
17
+ import { getPackageVersions } from './package-versions.js';
18
+ import { toSegmentPascalCase } from './string-case.js';
19
+ import { copyRawDirectory, copyRenderedDirectory } from './template-render.js';
20
+ const EXTERNAL_TEMPLATE_ENTRY_CANDIDATES = [
21
+ 'index.js',
22
+ 'index.cjs',
23
+ 'index.mjs',
24
+ ];
25
+ const TEMPLATE_WARNING_MESSAGE = 'wp-typia owns package/tooling/sync setup for generated projects, so this external template setting is ignored.';
21
26
  function isTemplatePathLocator(templateId) {
22
- return path.isAbsolute(templateId) || templateId.startsWith("./") || templateId.startsWith("../");
27
+ return (path.isAbsolute(templateId) ||
28
+ templateId.startsWith('./') ||
29
+ templateId.startsWith('../'));
23
30
  }
24
31
  function getTemplateWarning(key) {
25
32
  return `Ignoring external template config key "${key}": ${TEMPLATE_WARNING_MESSAGE}`;
@@ -27,7 +34,7 @@ function getTemplateWarning(key) {
27
34
  function resolveSourceSubpath(sourceDir, relativePath) {
28
35
  const targetPath = path.resolve(sourceDir, relativePath);
29
36
  const relativeTarget = path.relative(sourceDir, targetPath);
30
- if (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget)) {
37
+ if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
31
38
  throw new Error(`Template path "${relativePath}" must stay within ${sourceDir}.`);
32
39
  }
33
40
  return targetPath;
@@ -42,38 +49,40 @@ function getExternalTemplateEntry(sourceDir) {
42
49
  return null;
43
50
  }
44
51
  function selectRegistryVersion(metadata, locator) {
45
- const distTags = isPlainObject(metadata["dist-tags"]) ? metadata["dist-tags"] : {};
52
+ const distTags = isPlainObject(metadata['dist-tags'])
53
+ ? metadata['dist-tags']
54
+ : {};
46
55
  const versions = isPlainObject(metadata.versions) ? metadata.versions : {};
47
56
  const versionKeys = Object.keys(versions);
48
- if (locator.type === "version") {
57
+ if (locator.type === 'version') {
49
58
  if (!versions[locator.fetchSpec]) {
50
59
  throw new Error(`npm template package version not found: ${locator.raw}`);
51
60
  }
52
61
  return locator.fetchSpec;
53
62
  }
54
- if (locator.type === "tag") {
63
+ if (locator.type === 'tag') {
55
64
  const taggedVersion = distTags[locator.fetchSpec];
56
- if (typeof taggedVersion !== "string") {
65
+ if (typeof taggedVersion !== 'string') {
57
66
  throw new Error(`npm template package tag not found: ${locator.raw}`);
58
67
  }
59
68
  return taggedVersion;
60
69
  }
61
- const range = locator.fetchSpec.trim().length > 0 ? locator.fetchSpec : "*";
70
+ const range = locator.fetchSpec.trim().length > 0 ? locator.fetchSpec : '*';
62
71
  const matchedVersion = semver.maxSatisfying(versionKeys, range);
63
72
  if (matchedVersion) {
64
73
  return matchedVersion;
65
74
  }
66
75
  if (locator.fetchSpec.trim().length > 0) {
67
- throw new Error(`Unable to resolve npm template version for ${locator.raw}. Requested "${locator.fetchSpec}" but available versions are: ${versionKeys.join(", ") || "(none)"}.`);
76
+ throw new Error(`Unable to resolve npm template version for ${locator.raw}. Requested "${locator.fetchSpec}" but available versions are: ${versionKeys.join(', ') || '(none)'}.`);
68
77
  }
69
78
  const latestVersion = distTags.latest;
70
- if (typeof latestVersion === "string" && versions[latestVersion]) {
79
+ if (typeof latestVersion === 'string' && versions[latestVersion]) {
71
80
  return latestVersion;
72
81
  }
73
82
  throw new Error(`Unable to resolve a published npm template version for ${locator.raw}.`);
74
83
  }
75
84
  async function fetchNpmTemplateSource(locator) {
76
- const registryBase = (process.env.NPM_CONFIG_REGISTRY ?? "https://registry.npmjs.org").replace(/\/$/, "");
85
+ const registryBase = (process.env.NPM_CONFIG_REGISTRY ?? 'https://registry.npmjs.org').replace(/\/$/, '');
77
86
  const metadataResponse = await fetch(`${registryBase}/${encodeURIComponent(locator.name)}`);
78
87
  if (!metadataResponse.ok) {
79
88
  throw new Error(`Failed to fetch npm template metadata for ${locator.raw}: ${metadataResponse.status}`);
@@ -86,10 +95,10 @@ async function fetchNpmTemplateSource(locator) {
86
95
  throw new Error(`npm template metadata is missing dist information for ${locator.raw}@${resolvedVersion}.`);
87
96
  }
88
97
  const tarballUrl = versionMetadata.dist.tarball;
89
- if (typeof tarballUrl !== "string" || tarballUrl.length === 0) {
98
+ if (typeof tarballUrl !== 'string' || tarballUrl.length === 0) {
90
99
  throw new Error(`npm template metadata is missing tarball URL for ${locator.raw}@${resolvedVersion}.`);
91
100
  }
92
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-template-source-"));
101
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-template-source-'));
93
102
  const cleanup = async () => {
94
103
  await fsp.rm(tempRoot, { force: true, recursive: true });
95
104
  };
@@ -98,8 +107,8 @@ async function fetchNpmTemplateSource(locator) {
98
107
  if (!tarballResponse.ok) {
99
108
  throw new Error(`Failed to download npm template tarball for ${locator.raw}: ${tarballResponse.status}`);
100
109
  }
101
- const tarballPath = path.join(tempRoot, "template.tgz");
102
- const unpackDir = path.join(tempRoot, "source");
110
+ const tarballPath = path.join(tempRoot, 'template.tgz');
111
+ const unpackDir = path.join(tempRoot, 'source');
103
112
  await fsp.mkdir(unpackDir, { recursive: true });
104
113
  await fsp.writeFile(tarballPath, Buffer.from(await tarballResponse.arrayBuffer()));
105
114
  await extractTarball({
@@ -120,14 +129,14 @@ async function fetchNpmTemplateSource(locator) {
120
129
  }
121
130
  }
122
131
  async function normalizeWpTypiaTemplateSeed(seed) {
123
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-template-source-"));
124
- const normalizedDir = path.join(tempRoot, "template");
132
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-template-source-'));
133
+ const normalizedDir = path.join(tempRoot, 'template');
125
134
  try {
126
135
  await copyRawDirectory(seed.blockDir, normalizedDir, {
127
136
  filter: (sourcePath, _targetPath, entry) => {
128
137
  const mustacheVariantPath = path.join(path.dirname(sourcePath), `${entry.name}.mustache`);
129
138
  return !(entry.isFile() &&
130
- (entry.name === "package.json" || entry.name === "README.md") &&
139
+ (entry.name === 'package.json' || entry.name === 'README.md') &&
131
140
  fs.existsSync(mustacheVariantPath));
132
141
  },
133
142
  });
@@ -154,21 +163,23 @@ async function normalizeWpTypiaTemplateSeed(seed) {
154
163
  * use an already-installed template without forcing a registry fetch.
155
164
  */
156
165
  function resolveInstalledNpmTemplateSource(locator, cwd) {
157
- if (locator.rawSpec !== "" && locator.rawSpec !== "*") {
166
+ if (locator.rawSpec !== '' && locator.rawSpec !== '*') {
158
167
  return null;
159
168
  }
160
- const workspacePackagesRoot = path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..");
169
+ const workspacePackagesRoot = path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, '..');
161
170
  if (fs.existsSync(workspacePackagesRoot)) {
162
- for (const entry of fs.readdirSync(workspacePackagesRoot, { withFileTypes: true })) {
171
+ for (const entry of fs.readdirSync(workspacePackagesRoot, {
172
+ withFileTypes: true,
173
+ })) {
163
174
  if (!entry.isDirectory()) {
164
175
  continue;
165
176
  }
166
177
  const packageDir = path.join(workspacePackagesRoot, entry.name);
167
- const packageJsonPath = path.join(packageDir, "package.json");
178
+ const packageJsonPath = path.join(packageDir, 'package.json');
168
179
  if (!fs.existsSync(packageJsonPath)) {
169
180
  continue;
170
181
  }
171
- const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
182
+ const manifest = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
172
183
  if (manifest.name === locator.name) {
173
184
  return {
174
185
  blockDir: packageDir,
@@ -177,7 +188,7 @@ function resolveInstalledNpmTemplateSource(locator, cwd) {
177
188
  }
178
189
  }
179
190
  }
180
- const workspaceRequire = createRequire(path.join(path.resolve(cwd), "__wp_typia_template_resolver__.cjs"));
191
+ const workspaceRequire = createRequire(path.join(path.resolve(cwd), '__wp_typia_template_resolver__.cjs'));
181
192
  try {
182
193
  const packageJsonPath = fs.realpathSync(workspaceRequire.resolve(`${locator.name}/package.json`));
183
194
  const sourceDir = path.dirname(packageJsonPath);
@@ -187,13 +198,14 @@ function resolveInstalledNpmTemplateSource(locator, cwd) {
187
198
  };
188
199
  }
189
200
  catch (error) {
190
- const errorCode = typeof error === "object" && error !== null && "code" in error
201
+ const errorCode = typeof error === 'object' && error !== null && 'code' in error
191
202
  ? String(error.code)
192
- : "";
193
- if (errorCode === "MODULE_NOT_FOUND" ||
194
- errorCode === "ERR_PACKAGE_PATH_NOT_EXPORTED") {
195
- for (const basePath of workspaceRequire.resolve.paths(locator.name) ?? []) {
196
- const packageJsonPath = path.join(basePath, locator.name, "package.json");
203
+ : '';
204
+ if (errorCode === 'MODULE_NOT_FOUND' ||
205
+ errorCode === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
206
+ for (const basePath of workspaceRequire.resolve.paths(locator.name) ??
207
+ []) {
208
+ const packageJsonPath = path.join(basePath, locator.name, 'package.json');
197
209
  if (!fs.existsSync(packageJsonPath)) {
198
210
  continue;
199
211
  }
@@ -209,12 +221,12 @@ function resolveInstalledNpmTemplateSource(locator, cwd) {
209
221
  }
210
222
  }
211
223
  function isOfficialWorkspaceTemplateSeed(seed) {
212
- const packageJsonPath = path.join(seed.rootDir, "package.json");
224
+ const packageJsonPath = path.join(seed.rootDir, 'package.json');
213
225
  if (!fs.existsSync(packageJsonPath)) {
214
226
  return false;
215
227
  }
216
228
  try {
217
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
229
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
218
230
  return packageJson.name === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE;
219
231
  }
220
232
  catch {
@@ -222,11 +234,13 @@ function isOfficialWorkspaceTemplateSeed(seed) {
222
234
  }
223
235
  }
224
236
  export function parseGitHubTemplateLocator(templateId) {
225
- if (!templateId.startsWith("github:")) {
237
+ if (!templateId.startsWith('github:')) {
226
238
  return null;
227
239
  }
228
- const [locatorBody, refSegment] = templateId.slice("github:".length).split("#", 2);
229
- const segments = locatorBody.split("/").filter(Boolean);
240
+ const [locatorBody, refSegment] = templateId
241
+ .slice('github:'.length)
242
+ .split('#', 2);
243
+ const segments = locatorBody.split('/').filter(Boolean);
230
244
  if (segments.length < 3) {
231
245
  throw new Error(`GitHub template locators must look like github:owner/repo/path[#ref]. Received: ${templateId}`);
232
246
  }
@@ -235,11 +249,13 @@ export function parseGitHubTemplateLocator(templateId) {
235
249
  owner,
236
250
  repo,
237
251
  ref: refSegment ?? null,
238
- sourcePath: sourcePathSegments.join("/"),
252
+ sourcePath: sourcePathSegments.join('/'),
239
253
  };
240
254
  }
241
255
  export function parseNpmTemplateLocator(templateId) {
242
- if (isBuiltInTemplateId(templateId) || isTemplatePathLocator(templateId) || templateId.startsWith("github:")) {
256
+ if (isBuiltInTemplateId(templateId) ||
257
+ isTemplatePathLocator(templateId) ||
258
+ templateId.startsWith('github:')) {
243
259
  return null;
244
260
  }
245
261
  try {
@@ -248,9 +264,11 @@ export function parseNpmTemplateLocator(templateId) {
248
264
  return null;
249
265
  }
250
266
  const parsedWithRawSpec = parsed;
251
- const rawSpec = typeof parsedWithRawSpec.rawSpec === "string" ? parsedWithRawSpec.rawSpec : "";
267
+ const rawSpec = typeof parsedWithRawSpec.rawSpec === 'string'
268
+ ? parsedWithRawSpec.rawSpec
269
+ : '';
252
270
  return {
253
- fetchSpec: typeof parsed.fetchSpec === "string" ? parsed.fetchSpec : "",
271
+ fetchSpec: typeof parsed.fetchSpec === 'string' ? parsed.fetchSpec : '',
254
272
  name: parsed.name,
255
273
  raw: templateId,
256
274
  rawSpec,
@@ -267,21 +285,22 @@ export function parseTemplateLocator(templateId) {
267
285
  }
268
286
  const githubLocator = parseGitHubTemplateLocator(templateId);
269
287
  if (githubLocator) {
270
- return { kind: "github", locator: githubLocator };
288
+ return { kind: 'github', locator: githubLocator };
271
289
  }
272
290
  if (isTemplatePathLocator(templateId)) {
273
- return { kind: "path", templatePath: templateId };
291
+ return { kind: 'path', templatePath: templateId };
274
292
  }
275
293
  const npmLocator = parseNpmTemplateLocator(templateId);
276
294
  if (npmLocator) {
277
- return { kind: "npm", locator: npmLocator };
295
+ return { kind: 'npm', locator: npmLocator };
278
296
  }
279
- throw new Error(`Unknown template "${templateId}". Expected one of: ${BUILTIN_TEMPLATE_IDS.join(", ")}, a local path, github:owner/repo/path[#ref], or an npm package spec.`);
297
+ throw new Error(`Unknown template "${templateId}". Expected one of: ${BUILTIN_TEMPLATE_IDS.join(', ')}, a local path, github:owner/repo/path[#ref], or an npm package spec.`);
280
298
  }
281
299
  function getDefaultCategoryFromBlockJson(blockJson) {
282
- return typeof blockJson.category === "string" && blockJson.category.trim().length > 0
300
+ return typeof blockJson.category === 'string' &&
301
+ blockJson.category.trim().length > 0
283
302
  ? blockJson.category.trim()
284
- : "widgets";
303
+ : 'widgets';
285
304
  }
286
305
  function getDefaultCategory(sourceDir) {
287
306
  try {
@@ -289,7 +308,7 @@ function getDefaultCategory(sourceDir) {
289
308
  return getDefaultCategoryFromBlockJson(blockJson);
290
309
  }
291
310
  catch {
292
- return "widgets";
311
+ return 'widgets';
293
312
  }
294
313
  }
295
314
  function getTemplateVariableContext(variables) {
@@ -312,31 +331,41 @@ function getTemplateVariableContext(variables) {
312
331
  };
313
332
  }
314
333
  async function detectTemplateSourceFormat(sourceDir) {
315
- if (fs.existsSync(path.join(sourceDir, "package.json.mustache"))) {
316
- return "wp-typia";
334
+ if (fs.existsSync(path.join(sourceDir, 'package.json.mustache'))) {
335
+ return 'wp-typia';
336
+ }
337
+ if (await loadExternalTemplateLayerManifest(sourceDir)) {
338
+ throw new Error(`Template source at ${sourceDir} is an external layer package. External layers currently compose only through built-in scaffolds via the runtime API, not as standalone template ids.`);
317
339
  }
318
340
  if (getExternalTemplateEntry(sourceDir)) {
319
- return "create-block-external";
341
+ return 'create-block-external';
320
342
  }
321
- const sourceRoot = fs.existsSync(path.join(sourceDir, "src")) ? path.join(sourceDir, "src") : sourceDir;
343
+ const sourceRoot = fs.existsSync(path.join(sourceDir, 'src'))
344
+ ? path.join(sourceDir, 'src')
345
+ : sourceDir;
322
346
  const blockJsonCandidates = [
323
- path.join(sourceDir, "block.json"),
324
- path.join(sourceRoot, "block.json"),
347
+ path.join(sourceDir, 'block.json'),
348
+ path.join(sourceRoot, 'block.json'),
325
349
  ];
326
350
  const hasBlockJson = blockJsonCandidates.some((candidate) => fs.existsSync(candidate));
327
- const hasIndexFile = ["index.js", "index.jsx", "index.ts", "index.tsx"].some((filename) => fs.existsSync(path.join(sourceRoot, filename)));
328
- const hasEditFile = ["edit.js", "edit.jsx", "edit.ts", "edit.tsx"].some((filename) => fs.existsSync(path.join(sourceRoot, filename)));
329
- const hasSaveFile = ["save.js", "save.jsx", "save.ts", "save.tsx"].some((filename) => fs.existsSync(path.join(sourceRoot, filename)));
351
+ const hasIndexFile = ['index.js', 'index.jsx', 'index.ts', 'index.tsx'].some((filename) => fs.existsSync(path.join(sourceRoot, filename)));
352
+ const hasEditFile = ['edit.js', 'edit.jsx', 'edit.ts', 'edit.tsx'].some((filename) => fs.existsSync(path.join(sourceRoot, filename)));
353
+ const hasSaveFile = ['save.js', 'save.jsx', 'save.ts', 'save.tsx'].some((filename) => fs.existsSync(path.join(sourceRoot, filename)));
330
354
  if (hasBlockJson && hasIndexFile && hasEditFile && hasSaveFile) {
331
- return "create-block-subset";
355
+ return 'create-block-subset';
332
356
  }
333
357
  throw new Error(`Unsupported template source at ${sourceDir}. Expected a wp-typia template directory, an official create-block external template config, or a create-block subset with block.json and src/index/edit/save files.`);
334
358
  }
335
359
  function readRemoteBlockJson(blockDir) {
336
- const sourceRoot = fs.existsSync(path.join(blockDir, "src")) ? path.join(blockDir, "src") : blockDir;
337
- for (const candidate of [path.join(blockDir, "block.json"), path.join(sourceRoot, "block.json")]) {
360
+ const sourceRoot = fs.existsSync(path.join(blockDir, 'src'))
361
+ ? path.join(blockDir, 'src')
362
+ : blockDir;
363
+ for (const candidate of [
364
+ path.join(blockDir, 'block.json'),
365
+ path.join(sourceRoot, 'block.json'),
366
+ ]) {
338
367
  if (fs.existsSync(candidate)) {
339
- return JSON.parse(fs.readFileSync(candidate, "utf8"));
368
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
340
369
  }
341
370
  }
342
371
  throw new Error(`Unable to locate block.json in ${blockDir}`);
@@ -354,72 +383,83 @@ async function assertNoSymlinks(sourceDir) {
354
383
  }
355
384
  }
356
385
  function renderTypeScriptLiteral(value) {
357
- if (typeof value === "string") {
386
+ if (typeof value === 'string') {
358
387
  return JSON.stringify(value);
359
388
  }
360
- if (typeof value === "number" || typeof value === "boolean") {
389
+ if (typeof value === 'number' || typeof value === 'boolean') {
361
390
  return String(value);
362
391
  }
363
- return "undefined";
392
+ return 'undefined';
364
393
  }
365
394
  function renderTagsForAttribute(attribute) {
366
395
  const tags = [];
367
- if (typeof attribute.default === "string" || typeof attribute.default === "number" || typeof attribute.default === "boolean") {
396
+ if (typeof attribute.default === 'string' ||
397
+ typeof attribute.default === 'number' ||
398
+ typeof attribute.default === 'boolean') {
368
399
  tags.push(`tags.Default<${renderTypeScriptLiteral(attribute.default)}>`);
369
400
  }
370
401
  return tags;
371
402
  }
372
403
  function renderAttributeBaseType(attributeName, attribute) {
373
404
  if (Array.isArray(attribute.enum) && attribute.enum.length > 0) {
374
- return attribute.enum.map((item) => renderTypeScriptLiteral(item)).join(" | ");
405
+ return attribute.enum
406
+ .map((item) => renderTypeScriptLiteral(item))
407
+ .join(' | ');
375
408
  }
376
409
  switch (attribute.type) {
377
- case "string":
378
- return "string";
379
- case "number":
380
- return "number";
381
- case "boolean":
382
- return "boolean";
383
- case "array":
384
- return "unknown[]";
385
- case "object":
386
- return "Record<string, unknown>";
410
+ case 'string':
411
+ return 'string';
412
+ case 'number':
413
+ return 'number';
414
+ case 'boolean':
415
+ return 'boolean';
416
+ case 'array':
417
+ return 'unknown[]';
418
+ case 'object':
419
+ return 'Record<string, unknown>';
387
420
  default:
388
- if (typeof attributeName === "string" && attributeName.toLowerCase().includes("class")) {
389
- return "string";
421
+ if (typeof attributeName === 'string' &&
422
+ attributeName.toLowerCase().includes('class')) {
423
+ return 'string';
390
424
  }
391
- return "unknown";
425
+ return 'unknown';
392
426
  }
393
427
  }
394
428
  function buildRemoteTypesSource(blockJson, context) {
395
429
  const attributes = (blockJson.attributes ?? {});
396
- const lines = ['import { tags } from "typia";', "", `export interface ${context.pascalCase}Attributes {`];
430
+ const lines = [
431
+ 'import { tags } from "typia";',
432
+ '',
433
+ `export interface ${context.pascalCase}Attributes {`,
434
+ ];
397
435
  for (const [name, attribute] of Object.entries(attributes)) {
398
436
  const baseType = renderAttributeBaseType(name, attribute);
399
437
  const tagList = renderTagsForAttribute(attribute);
400
- const baseTypeWithGrouping = tagList.length > 0 && baseType.includes(" | ") ? `(${baseType})` : baseType;
401
- const renderedType = [baseTypeWithGrouping, ...tagList].join(" & ");
438
+ const baseTypeWithGrouping = tagList.length > 0 && baseType.includes(' | ')
439
+ ? `(${baseType})`
440
+ : baseType;
441
+ const renderedType = [baseTypeWithGrouping, ...tagList].join(' & ');
402
442
  lines.push(` ${JSON.stringify(name)}?: ${renderedType};`);
403
443
  }
404
- lines.push("}", "");
405
- return lines.join("\n");
444
+ lines.push('}', '');
445
+ return lines.join('\n');
406
446
  }
407
447
  function buildRemoteBlockJsonTemplate(blockJson) {
408
448
  const merged = {
409
449
  ...blockJson,
410
- description: "{{description}}",
411
- name: "{{namespace}}/{{slug}}",
412
- textdomain: "{{textDomain}}",
413
- title: "{{title}}",
450
+ description: '{{description}}',
451
+ name: '{{namespace}}/{{slug}}',
452
+ textdomain: '{{textDomain}}',
453
+ title: '{{title}}',
414
454
  };
415
455
  if (!Array.isArray(merged.keywords) || merged.keywords.length === 0) {
416
- merged.keywords = ["{{keyword}}", "typia", "block"];
456
+ merged.keywords = ['{{keyword}}', 'typia', 'block'];
417
457
  }
418
- return `${JSON.stringify(merged, null, "\t")}\n`;
458
+ return `${JSON.stringify(merged, null, '\t')}\n`;
419
459
  }
420
460
  async function rewriteBlockJsonImports(directory) {
421
- const textExtensions = new Set([".js", ".jsx", ".ts", ".tsx"]);
422
- const targetBlockJsonPath = path.join(directory, "block.json");
461
+ const textExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
462
+ const targetBlockJsonPath = path.join(directory, 'block.json');
423
463
  async function visit(currentPath) {
424
464
  const stats = await fsp.stat(currentPath);
425
465
  if (stats.isDirectory()) {
@@ -432,32 +472,36 @@ async function rewriteBlockJsonImports(directory) {
432
472
  if (!textExtensions.has(path.extname(currentPath))) {
433
473
  return;
434
474
  }
435
- const content = await fsp.readFile(currentPath, "utf8");
436
- const relativeSpecifier = path.relative(path.dirname(currentPath), targetBlockJsonPath).replace(/\\/g, "/");
437
- const normalizedSpecifier = relativeSpecifier.startsWith(".") ? relativeSpecifier : `./${relativeSpecifier}`;
475
+ const content = await fsp.readFile(currentPath, 'utf8');
476
+ const relativeSpecifier = path
477
+ .relative(path.dirname(currentPath), targetBlockJsonPath)
478
+ .replace(/\\/g, '/');
479
+ const normalizedSpecifier = relativeSpecifier.startsWith('.')
480
+ ? relativeSpecifier
481
+ : `./${relativeSpecifier}`;
438
482
  const next = content.replace(/(['"])\.{1,2}\/[^'"]*block\.json\1/g, `$1${normalizedSpecifier}$1`);
439
483
  if (next !== content) {
440
- await fsp.writeFile(currentPath, next, "utf8");
484
+ await fsp.writeFile(currentPath, next, 'utf8');
441
485
  }
442
486
  }
443
487
  await visit(directory);
444
488
  }
445
489
  async function patchRemotePackageJson(templateDir, needsInteractivity) {
446
- const packageJsonPath = path.join(templateDir, "package.json.mustache");
447
- const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
490
+ const packageJsonPath = path.join(templateDir, 'package.json.mustache');
491
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, 'utf8'));
448
492
  const existingDependencies = { ...(packageJson.dependencies ?? {}) };
449
493
  const existingDevDependencies = { ...(packageJson.devDependencies ?? {}) };
450
- delete existingDependencies["@wp-typia/project-tools"];
451
- delete existingDevDependencies["@wp-typia/project-tools"];
494
+ delete existingDependencies['@wp-typia/project-tools'];
495
+ delete existingDevDependencies['@wp-typia/project-tools'];
452
496
  packageJson.devDependencies = {
453
- "@wp-typia/block-runtime": "{{blockRuntimePackageVersion}}",
454
- "@wp-typia/block-types": "{{blockTypesPackageVersion}}",
497
+ '@wp-typia/block-runtime': '{{blockRuntimePackageVersion}}',
498
+ '@wp-typia/block-types': '{{blockTypesPackageVersion}}',
455
499
  ...existingDevDependencies,
456
500
  };
457
501
  if (needsInteractivity) {
458
502
  packageJson.dependencies = {
459
503
  ...existingDependencies,
460
- "@wordpress/interactivity": "^6.29.0",
504
+ '@wordpress/interactivity': '^6.29.0',
461
505
  };
462
506
  }
463
507
  else if (Object.keys(existingDependencies).length > 0) {
@@ -466,13 +510,18 @@ async function patchRemotePackageJson(templateDir, needsInteractivity) {
466
510
  else {
467
511
  delete packageJson.dependencies;
468
512
  }
469
- await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
513
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
470
514
  }
471
515
  function getSeedSourceRoot(blockDir) {
472
- return fs.existsSync(path.join(blockDir, "src")) ? path.join(blockDir, "src") : blockDir;
516
+ return fs.existsSync(path.join(blockDir, 'src'))
517
+ ? path.join(blockDir, 'src')
518
+ : blockDir;
473
519
  }
474
520
  function findSeedRenderPhp(seed) {
475
- for (const candidate of [path.join(seed.blockDir, "render.php"), path.join(seed.rootDir, "render.php")]) {
521
+ for (const candidate of [
522
+ path.join(seed.blockDir, 'render.php'),
523
+ path.join(seed.rootDir, 'render.php'),
524
+ ]) {
476
525
  if (fs.existsSync(candidate)) {
477
526
  return candidate;
478
527
  }
@@ -481,49 +530,49 @@ function findSeedRenderPhp(seed) {
481
530
  }
482
531
  async function removeSeedEntryConflicts(templateDir) {
483
532
  for (const filename of [
484
- "block.json",
485
- "block.json.mustache",
486
- "edit.js",
487
- "edit.jsx",
488
- "edit.ts",
489
- "edit.tsx",
490
- "edit.tsx.mustache",
491
- "hooks.ts",
492
- "hooks.ts.mustache",
493
- "index.js",
494
- "index.jsx",
495
- "index.ts",
496
- "index.tsx",
497
- "index.tsx.mustache",
498
- "save.js",
499
- "save.jsx",
500
- "save.ts",
501
- "save.tsx",
502
- "save.tsx.mustache",
503
- "style.css",
504
- "style.scss",
505
- "style.scss.mustache",
506
- "types.ts",
507
- "types.ts.mustache",
508
- "validators.ts",
509
- "validators.ts.mustache",
510
- "view.js",
511
- "view.jsx",
512
- "view.ts",
513
- "view.tsx",
533
+ 'block.json',
534
+ 'block.json.mustache',
535
+ 'edit.js',
536
+ 'edit.jsx',
537
+ 'edit.ts',
538
+ 'edit.tsx',
539
+ 'edit.tsx.mustache',
540
+ 'hooks.ts',
541
+ 'hooks.ts.mustache',
542
+ 'index.js',
543
+ 'index.jsx',
544
+ 'index.ts',
545
+ 'index.tsx',
546
+ 'index.tsx.mustache',
547
+ 'save.js',
548
+ 'save.jsx',
549
+ 'save.ts',
550
+ 'save.tsx',
551
+ 'save.tsx.mustache',
552
+ 'style.css',
553
+ 'style.scss',
554
+ 'style.scss.mustache',
555
+ 'types.ts',
556
+ 'types.ts.mustache',
557
+ 'validators.ts',
558
+ 'validators.ts.mustache',
559
+ 'view.js',
560
+ 'view.jsx',
561
+ 'view.ts',
562
+ 'view.tsx',
514
563
  ]) {
515
- await fsp.rm(path.join(templateDir, "src", filename), { force: true });
564
+ await fsp.rm(path.join(templateDir, 'src', filename), { force: true });
516
565
  }
517
566
  }
518
567
  async function normalizeCreateBlockSubset(seed, context) {
519
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-remote-template-"));
520
- const templateDir = path.join(tempRoot, "template");
568
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-remote-template-'));
569
+ const templateDir = path.join(tempRoot, 'template');
521
570
  const blockJson = readRemoteBlockJson(seed.blockDir);
522
571
  const sourceRoot = getSeedSourceRoot(seed.blockDir);
523
572
  await fsp.mkdir(templateDir, { recursive: true });
524
- for (const layerDir of getBuiltInTemplateLayerDirs("basic")) {
573
+ for (const layerDir of getBuiltInTemplateLayerDirs('basic')) {
525
574
  if (!fs.existsSync(layerDir)) {
526
- if (isOmittableBuiltInTemplateLayerDir("basic", layerDir)) {
575
+ if (isOmittableBuiltInTemplateLayerDir('basic', layerDir)) {
527
576
  continue;
528
577
  }
529
578
  throw new Error(`Built-in template layer is missing: ${layerDir}`);
@@ -534,31 +583,41 @@ async function normalizeCreateBlockSubset(seed, context) {
534
583
  });
535
584
  }
536
585
  await removeSeedEntryConflicts(templateDir);
537
- await fsp.cp(sourceRoot, path.join(templateDir, "src"), { recursive: true, force: true });
586
+ await fsp.cp(sourceRoot, path.join(templateDir, 'src'), {
587
+ recursive: true,
588
+ force: true,
589
+ });
538
590
  const remoteRenderPath = findSeedRenderPhp(seed);
539
591
  if (remoteRenderPath) {
540
- await fsp.copyFile(remoteRenderPath, path.join(templateDir, "render.php"));
592
+ await fsp.copyFile(remoteRenderPath, path.join(templateDir, 'render.php'));
541
593
  }
542
594
  if (seed.assetsDir && fs.existsSync(seed.assetsDir)) {
543
- await fsp.cp(seed.assetsDir, path.join(templateDir, "assets"), { recursive: true, force: true });
544
- }
545
- await fsp.writeFile(path.join(templateDir, "src", "types.ts"), buildRemoteTypesSource(blockJson, context), "utf8");
546
- await fsp.writeFile(path.join(templateDir, "src", "block.json"), buildRemoteBlockJsonTemplate(blockJson), "utf8");
547
- await rewriteBlockJsonImports(path.join(templateDir, "src"));
548
- const needsInteractivity = typeof blockJson.viewScriptModule === "string" ||
549
- typeof blockJson.viewScript === "string" ||
550
- fs.existsSync(path.join(templateDir, "src", "view.js")) ||
551
- fs.existsSync(path.join(templateDir, "src", "view.ts")) ||
552
- fs.existsSync(path.join(templateDir, "src", "view.tsx")) ||
553
- fs.existsSync(path.join(templateDir, "src", "interactivity.js")) ||
554
- fs.existsSync(path.join(templateDir, "src", "interactivity.ts"));
595
+ await fsp.cp(seed.assetsDir, path.join(templateDir, 'assets'), {
596
+ recursive: true,
597
+ force: true,
598
+ });
599
+ }
600
+ await fsp.writeFile(path.join(templateDir, 'src', 'types.ts'), buildRemoteTypesSource(blockJson, context), 'utf8');
601
+ await fsp.writeFile(path.join(templateDir, 'src', 'block.json'), buildRemoteBlockJsonTemplate(blockJson), 'utf8');
602
+ await rewriteBlockJsonImports(path.join(templateDir, 'src'));
603
+ const needsInteractivity = typeof blockJson.viewScriptModule === 'string' ||
604
+ typeof blockJson.viewScript === 'string' ||
605
+ fs.existsSync(path.join(templateDir, 'src', 'view.js')) ||
606
+ fs.existsSync(path.join(templateDir, 'src', 'view.ts')) ||
607
+ fs.existsSync(path.join(templateDir, 'src', 'view.tsx')) ||
608
+ fs.existsSync(path.join(templateDir, 'src', 'interactivity.js')) ||
609
+ fs.existsSync(path.join(templateDir, 'src', 'interactivity.ts'));
555
610
  await patchRemotePackageJson(templateDir, needsInteractivity);
556
611
  return {
557
- id: "remote:create-block-subset",
612
+ id: 'remote:create-block-subset',
558
613
  defaultCategory: getDefaultCategoryFromBlockJson(blockJson),
559
- description: "A wp-typia scaffold normalized from a create-block subset source",
560
- features: ["Remote source", "create-block adapter", "Typia metadata pipeline"],
561
- format: "create-block-subset",
614
+ description: 'A wp-typia scaffold normalized from a create-block subset source',
615
+ features: [
616
+ 'Remote source',
617
+ 'create-block adapter',
618
+ 'Typia metadata pipeline',
619
+ ],
620
+ format: 'create-block-subset',
562
621
  selectedVariant: seed.selectedVariant ?? null,
563
622
  templateDir,
564
623
  warnings: seed.warnings ?? [],
@@ -583,15 +642,15 @@ async function loadExternalTemplateConfig(sourceDir) {
583
642
  }
584
643
  const warnings = [];
585
644
  for (const ignoredKey of [
586
- "pluginTemplatesPath",
587
- "wpScripts",
588
- "wpEnv",
589
- "customScripts",
590
- "npmDependencies",
591
- "npmDevDependencies",
592
- "customPackageJSON",
593
- "pluginReadme",
594
- "pluginHeader",
645
+ 'pluginTemplatesPath',
646
+ 'wpScripts',
647
+ 'wpEnv',
648
+ 'customScripts',
649
+ 'npmDependencies',
650
+ 'npmDevDependencies',
651
+ 'customPackageJSON',
652
+ 'pluginReadme',
653
+ 'pluginHeader',
595
654
  ]) {
596
655
  if (ignoredKey in loadedConfig) {
597
656
  warnings.push(getTemplateWarning(ignoredKey));
@@ -621,11 +680,11 @@ function getVariantConfig(config, requestedVariant) {
621
680
  }
622
681
  const selectedVariant = requestedVariant ?? variantKeys[0];
623
682
  if (!selectedVariant || !isPlainObject(config.variants?.[selectedVariant])) {
624
- throw new Error(`Unknown template variant "${requestedVariant}". Expected one of: ${variantKeys.join(", ")}`);
683
+ throw new Error(`Unknown template variant "${requestedVariant}". Expected one of: ${variantKeys.join(', ')}`);
625
684
  }
626
685
  return {
627
686
  selectedVariant,
628
- variantConfig: config.variants?.[selectedVariant],
687
+ variantConfig: config.variants?.[selectedVariant] ?? {},
629
688
  };
630
689
  }
631
690
  function extractVariantRenderValues(variantConfig) {
@@ -651,7 +710,7 @@ async function buildExternalTemplateView(context, config, selectedVariant, varia
651
710
  }
652
711
  const transformed = await config.transformer(mergedView);
653
712
  if (!isPlainObject(transformed)) {
654
- throw new Error("External template transformer(view) must return an object.");
713
+ throw new Error('External template transformer(view) must return an object.');
655
714
  }
656
715
  return {
657
716
  ...mergedView,
@@ -661,28 +720,35 @@ async function buildExternalTemplateView(context, config, selectedVariant, varia
661
720
  async function renderCreateBlockExternalTemplate(sourceDir, context, requestedVariant) {
662
721
  const { config, warnings } = await loadExternalTemplateConfig(sourceDir);
663
722
  const { selectedVariant, variantConfig } = getVariantConfig(config, requestedVariant);
664
- const blockTemplatesPath = (typeof variantConfig.blockTemplatesPath === "string" ? variantConfig.blockTemplatesPath : config.blockTemplatesPath) ??
665
- null;
723
+ const blockTemplatesPath = (typeof variantConfig.blockTemplatesPath === 'string'
724
+ ? variantConfig.blockTemplatesPath
725
+ : config.blockTemplatesPath) ?? null;
666
726
  if (!blockTemplatesPath) {
667
- throw new Error("External template config must define blockTemplatesPath.");
727
+ throw new Error('External template config must define blockTemplatesPath.');
668
728
  }
669
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-create-block-external-"));
729
+ const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-create-block-external-'));
670
730
  const cleanup = async () => {
671
731
  await fsp.rm(tempRoot, { force: true, recursive: true });
672
732
  };
673
733
  try {
674
- const renderedRoot = path.join(tempRoot, "rendered");
675
- const folderName = (typeof variantConfig.folderName === "string" ? variantConfig.folderName : config.folderName) || ".";
734
+ const renderedRoot = path.join(tempRoot, 'rendered');
735
+ const folderName = (typeof variantConfig.folderName === 'string'
736
+ ? variantConfig.folderName
737
+ : config.folderName) || '.';
676
738
  const blockDir = resolveSourceSubpath(renderedRoot, folderName);
677
739
  const view = await buildExternalTemplateView(context, config, selectedVariant, variantConfig);
678
740
  const blockTemplateDir = resolveSourceSubpath(sourceDir, blockTemplatesPath);
679
741
  await copyRenderedDirectory(blockTemplateDir, blockDir, view);
680
- const assetsPath = typeof variantConfig.assetsPath === "string" ? variantConfig.assetsPath : config.assetsPath;
681
- if (typeof assetsPath === "string" && assetsPath.trim().length > 0) {
682
- await copyRawDirectory(resolveSourceSubpath(sourceDir, assetsPath), path.join(tempRoot, "assets"));
742
+ const assetsPath = typeof variantConfig.assetsPath === 'string'
743
+ ? variantConfig.assetsPath
744
+ : config.assetsPath;
745
+ if (typeof assetsPath === 'string' && assetsPath.trim().length > 0) {
746
+ await copyRawDirectory(resolveSourceSubpath(sourceDir, assetsPath), path.join(tempRoot, 'assets'));
683
747
  }
684
748
  return {
685
- assetsDir: fs.existsSync(path.join(tempRoot, "assets")) ? path.join(tempRoot, "assets") : undefined,
749
+ assetsDir: fs.existsSync(path.join(tempRoot, 'assets'))
750
+ ? path.join(tempRoot, 'assets')
751
+ : undefined,
686
752
  blockDir,
687
753
  cleanup,
688
754
  rootDir: tempRoot,
@@ -696,22 +762,23 @@ async function renderCreateBlockExternalTemplate(sourceDir, context, requestedVa
696
762
  }
697
763
  }
698
764
  async function resolveGitHubTemplateSource(locator) {
699
- const remoteRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-template-source-"));
765
+ const remoteRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-template-source-'));
700
766
  const cleanup = async () => {
701
767
  await fsp.rm(remoteRoot, { force: true, recursive: true });
702
768
  };
703
- const checkoutDir = path.join(remoteRoot, "source");
769
+ const checkoutDir = path.join(remoteRoot, 'source');
704
770
  try {
705
- const args = ["clone", "--depth", "1"];
771
+ const args = ['clone', '--depth', '1'];
706
772
  if (locator.ref) {
707
- args.push("--branch", locator.ref);
773
+ args.push('--branch', locator.ref);
708
774
  }
709
775
  args.push(`https://github.com/${locator.owner}/${locator.repo}.git`, checkoutDir);
710
- execFileSync("git", args, { stdio: "ignore" });
776
+ execFileSync('git', args, { stdio: 'ignore' });
711
777
  const sourceDir = path.resolve(checkoutDir, locator.sourcePath);
712
778
  const relativeSourceDir = path.relative(checkoutDir, sourceDir);
713
- if (relativeSourceDir.startsWith("..") || path.isAbsolute(relativeSourceDir)) {
714
- throw new Error("GitHub template path must stay within the cloned repository.");
779
+ if (relativeSourceDir.startsWith('..') ||
780
+ path.isAbsolute(relativeSourceDir)) {
781
+ throw new Error('GitHub template path must stay within the cloned repository.');
715
782
  }
716
783
  if (!fs.existsSync(sourceDir)) {
717
784
  throw new Error(`GitHub template path does not exist: ${locator.sourcePath}`);
@@ -728,8 +795,16 @@ async function resolveGitHubTemplateSource(locator) {
728
795
  throw error;
729
796
  }
730
797
  }
731
- async function resolveTemplateSeed(locator, cwd) {
732
- if (locator.kind === "path") {
798
+ /**
799
+ * Resolves a template locator into a local seed source directory.
800
+ *
801
+ * @param locator Remote template locator describing a local path, GitHub source, or npm package.
802
+ * @param cwd Current working directory used to resolve local template paths.
803
+ * @returns A local seed source containing the resolved root and block directory, plus optional cleanup.
804
+ * @throws When the locator is invalid, the source cannot be fetched, or filesystem validation fails.
805
+ */
806
+ export async function resolveTemplateSeed(locator, cwd) {
807
+ if (locator.kind === 'path') {
733
808
  const sourceDir = path.resolve(cwd, locator.templatePath);
734
809
  if (!fs.existsSync(sourceDir)) {
735
810
  throw new Error(`Template path does not exist: ${sourceDir}`);
@@ -740,7 +815,7 @@ async function resolveTemplateSeed(locator, cwd) {
740
815
  rootDir: sourceDir,
741
816
  };
742
817
  }
743
- if (locator.kind === "github") {
818
+ if (locator.kind === 'github') {
744
819
  return resolveGitHubTemplateSource(locator.locator);
745
820
  }
746
821
  const installedSource = resolveInstalledNpmTemplateSource(locator.locator, cwd);
@@ -756,8 +831,8 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
756
831
  throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for built-in template "${templateId}".`);
757
832
  }
758
833
  return resolveBuiltInTemplateSource(templateId, {
759
- persistenceEnabled: variables.compoundPersistenceEnabled === "true",
760
- persistencePolicy: variables.persistencePolicy === "public" ? "public" : "authenticated",
834
+ persistenceEnabled: variables.compoundPersistenceEnabled === 'true',
835
+ persistencePolicy: variables.persistencePolicy === 'public' ? 'public' : 'authenticated',
761
836
  });
762
837
  }
763
838
  const locator = parseTemplateLocator(templateId);
@@ -767,7 +842,7 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
767
842
  let normalizedSeed = null;
768
843
  try {
769
844
  const format = await detectTemplateSourceFormat(seed.blockDir);
770
- if (format === "wp-typia") {
845
+ if (format === 'wp-typia') {
771
846
  if (variant) {
772
847
  throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for "${templateId}".`);
773
848
  }
@@ -775,8 +850,8 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
775
850
  return {
776
851
  id: templateId,
777
852
  defaultCategory: getDefaultCategory(seed.blockDir),
778
- description: "A remote wp-typia template source",
779
- features: ["Remote source", "wp-typia format"],
853
+ description: 'A remote wp-typia template source',
854
+ features: ['Remote source', 'wp-typia format'],
780
855
  format,
781
856
  isOfficialWorkspaceTemplate,
782
857
  templateDir: normalizedSeed.blockDir,
@@ -784,14 +859,14 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
784
859
  };
785
860
  }
786
861
  normalizedSeed =
787
- format === "create-block-external"
862
+ format === 'create-block-external'
788
863
  ? await renderCreateBlockExternalTemplate(seed.blockDir, context, variant)
789
864
  : variant
790
865
  ? (() => {
791
866
  throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for "${templateId}".`);
792
867
  })()
793
868
  : seed;
794
- if (format === "create-block-external") {
869
+ if (format === 'create-block-external') {
795
870
  const normalized = await normalizeCreateBlockSubset(normalizedSeed, context);
796
871
  return {
797
872
  ...normalized,
@@ -799,10 +874,14 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
799
874
  await normalized.cleanup?.();
800
875
  await seed.cleanup?.();
801
876
  },
802
- description: "A wp-typia scaffold normalized from an official create-block external template",
803
- features: ["Remote source", "official external template", "Typia metadata pipeline"],
877
+ description: 'A wp-typia scaffold normalized from an official create-block external template',
878
+ features: [
879
+ 'Remote source',
880
+ 'official external template',
881
+ 'Typia metadata pipeline',
882
+ ],
804
883
  format,
805
- id: "remote:create-block-external",
884
+ id: 'remote:create-block-external',
806
885
  isOfficialWorkspaceTemplate,
807
886
  selectedVariant: normalizedSeed.selectedVariant ?? null,
808
887
  warnings: normalizedSeed.warnings ?? [],