@wp-typia/project-tools 0.16.8 → 0.16.10

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 (50) hide show
  1. package/README.md +14 -4
  2. package/dist/runtime/block-generator-service.d.ts +5 -1
  3. package/dist/runtime/block-generator-service.js +7 -3
  4. package/dist/runtime/built-in-block-artifacts.js +388 -572
  5. package/dist/runtime/built-in-block-code-artifacts.js +96 -46
  6. package/dist/runtime/built-in-block-code-templates.d.ts +36 -0
  7. package/dist/runtime/built-in-block-code-templates.js +2234 -0
  8. package/dist/runtime/cli-add-block.d.ts +2 -1
  9. package/dist/runtime/cli-add-block.js +163 -25
  10. package/dist/runtime/cli-add-shared.d.ts +7 -0
  11. package/dist/runtime/cli-add-shared.js +4 -6
  12. package/dist/runtime/cli-add-workspace.js +56 -17
  13. package/dist/runtime/cli-core.d.ts +4 -0
  14. package/dist/runtime/cli-core.js +3 -0
  15. package/dist/runtime/cli-diagnostics.d.ts +58 -0
  16. package/dist/runtime/cli-diagnostics.js +101 -0
  17. package/dist/runtime/cli-doctor.d.ts +2 -1
  18. package/dist/runtime/cli-doctor.js +16 -5
  19. package/dist/runtime/cli-help.js +4 -4
  20. package/dist/runtime/cli-scaffold.d.ts +5 -1
  21. package/dist/runtime/cli-scaffold.js +138 -111
  22. package/dist/runtime/external-layer-selection.d.ts +14 -0
  23. package/dist/runtime/external-layer-selection.js +35 -0
  24. package/dist/runtime/index.d.ts +2 -2
  25. package/dist/runtime/index.js +1 -1
  26. package/dist/runtime/migration-render.d.ts +23 -1
  27. package/dist/runtime/migration-render.js +58 -10
  28. package/dist/runtime/migration-ui-capability.js +17 -8
  29. package/dist/runtime/migration-utils.d.ts +7 -6
  30. package/dist/runtime/migration-utils.js +76 -73
  31. package/dist/runtime/migrations.js +2 -2
  32. package/dist/runtime/object-utils.d.ts +8 -1
  33. package/dist/runtime/object-utils.js +21 -1
  34. package/dist/runtime/scaffold-apply-utils.d.ts +14 -2
  35. package/dist/runtime/scaffold-apply-utils.js +19 -6
  36. package/dist/runtime/scaffold-repository-reference.d.ts +22 -0
  37. package/dist/runtime/scaffold-repository-reference.js +119 -0
  38. package/dist/runtime/scaffold.d.ts +5 -1
  39. package/dist/runtime/scaffold.js +15 -37
  40. package/dist/runtime/template-layers.d.ts +6 -0
  41. package/dist/runtime/template-layers.js +20 -7
  42. package/dist/runtime/template-render.d.ts +13 -2
  43. package/dist/runtime/template-render.js +102 -71
  44. package/dist/runtime/template-source.d.ts +6 -5
  45. package/dist/runtime/template-source.js +284 -217
  46. package/package.json +8 -3
  47. package/templates/_shared/base/src/validator-toolkit.ts.mustache +2 -2
  48. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +61 -16
  49. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +19 -47
  50. package/templates/_shared/migration-ui/common/src/migrations/index.ts +40 -11
@@ -14,6 +14,7 @@ import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.j
14
14
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, } from "./template-registry.js";
15
15
  import { copyInterpolatedDirectory } from "./template-render.js";
16
16
  import { formatInstallCommand, formatPackageExecCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
17
+ import { replaceRepositoryReferencePlaceholders, resolveScaffoldRepositoryReference, } from "./scaffold-repository-reference.js";
17
18
  const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
18
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
20
  const LOCKFILES = {
@@ -265,7 +266,11 @@ export async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
265
266
  }
266
267
  }));
267
268
  }
268
- export async function replaceTextRecursively(targetDir, packageManagerId) {
269
+ /**
270
+ * Recursively normalizes generated text files for the selected package manager
271
+ * and repository reference.
272
+ */
273
+ export async function replaceTextRecursively(targetDir, packageManagerId, { repositoryManifestPaths, repositoryReference, } = {}) {
269
274
  const textExtensions = new Set([
270
275
  ".css",
271
276
  ".js",
@@ -278,6 +283,10 @@ export async function replaceTextRecursively(targetDir, packageManagerId) {
278
283
  ".tsx",
279
284
  ".txt",
280
285
  ]);
286
+ const resolvedRepositoryReference = repositoryReference ??
287
+ resolveScaffoldRepositoryReference({
288
+ manifestPaths: repositoryManifestPaths,
289
+ });
281
290
  async function visit(currentPath) {
282
291
  const stats = await fsp.stat(currentPath);
283
292
  if (stats.isDirectory()) {
@@ -291,9 +300,7 @@ export async function replaceTextRecursively(targetDir, packageManagerId) {
291
300
  return;
292
301
  }
293
302
  const content = await fsp.readFile(currentPath, "utf8");
294
- const nextContent = transformPackageManagerText(content, packageManagerId)
295
- .replace(/yourusername\/wp-typia-boilerplate/g, "imjlk/wp-typia")
296
- .replace(/yourusername\/wp-typia/g, "imjlk/wp-typia");
303
+ const nextContent = replaceRepositoryReferencePlaceholders(transformPackageManagerText(content, packageManagerId), resolvedRepositoryReference);
297
304
  if (nextContent !== content) {
298
305
  await fsp.writeFile(currentPath, nextContent, "utf8");
299
306
  }
@@ -344,7 +351,11 @@ export async function applyWorkspaceMigrationCapability(projectDir, packageManag
344
351
  ensureMigrationDirectories(projectDir, []);
345
352
  writeInitialMigrationScaffold(projectDir, "v1", []);
346
353
  }
347
- export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, }) {
354
+ /**
355
+ * Applies a built-in scaffold into the target directory, including generated
356
+ * code artifacts, starter manifests, preset files, and placeholder rewrites.
357
+ */
358
+ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, repositoryReference, }) {
348
359
  await ensureDirectory(projectDir, allowExistingDir);
349
360
  await copyInterpolatedDirectory(templateDir, projectDir, variables);
350
361
  if (codeArtifacts && codeArtifacts.length > 0) {
@@ -394,7 +405,9 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
394
405
  });
395
406
  await normalizePackageManagerFiles(projectDir, packageManager);
396
407
  await removeUnexpectedLockfiles(projectDir, packageManager);
397
- await replaceTextRecursively(projectDir, packageManager);
408
+ await replaceTextRecursively(projectDir, packageManager, {
409
+ repositoryReference,
410
+ });
398
411
  if (!noInstall) {
399
412
  const installer = installDependencies ?? defaultInstallDependencies;
400
413
  await installer({
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Default scaffold repository reference used when no GitHub repository metadata
3
+ * can be resolved from the current runtime package manifests.
4
+ */
5
+ export declare const DEFAULT_SCAFFOLD_REPOSITORY_REFERENCE = "imjlk/wp-typia";
6
+ /**
7
+ * Resolves the canonical scaffold repository reference in `owner/repo` format.
8
+ *
9
+ * The resolver checks candidate package manifests in priority order, extracts
10
+ * their `repository` field, and returns the first GitHub slug it can parse.
11
+ * When no candidate yields a GitHub repository reference, the fallback value
12
+ * is returned instead.
13
+ */
14
+ export declare function resolveScaffoldRepositoryReference({ fallback, manifestPaths, }?: {
15
+ fallback?: string;
16
+ manifestPaths?: readonly string[];
17
+ }): string;
18
+ /**
19
+ * Replaces legacy scaffold repository placeholders with a concrete
20
+ * `owner/repo` reference.
21
+ */
22
+ export declare function replaceRepositoryReferencePlaceholders(source: string, repositoryReference: string): string;
@@ -0,0 +1,119 @@
1
+ import fs from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { PROJECT_TOOLS_PACKAGE_ROOT } from "./template-registry.js";
5
+ const require = createRequire(import.meta.url);
6
+ /**
7
+ * Default scaffold repository reference used when no GitHub repository metadata
8
+ * can be resolved from the current runtime package manifests.
9
+ */
10
+ export const DEFAULT_SCAFFOLD_REPOSITORY_REFERENCE = "imjlk/wp-typia";
11
+ function getErrorCode(error) {
12
+ return typeof error === "object" && error !== null && "code" in error
13
+ ? String(error.code)
14
+ : undefined;
15
+ }
16
+ function readRepositoryPackageManifest(packageJsonPath) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
19
+ }
20
+ catch (error) {
21
+ if (getErrorCode(error) === "ENOENT") {
22
+ return null;
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+ function resolveInstalledPackageManifestPath(packageName) {
28
+ try {
29
+ return require.resolve(`${packageName}/package.json`);
30
+ }
31
+ catch (error) {
32
+ if (getErrorCode(error) === "MODULE_NOT_FOUND") {
33
+ return null;
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+ function getRepositoryFieldValue(manifest) {
39
+ if (!manifest?.repository) {
40
+ return undefined;
41
+ }
42
+ return typeof manifest.repository === "string"
43
+ ? manifest.repository
44
+ : manifest.repository.url;
45
+ }
46
+ function parseRepositoryReference(value) {
47
+ const trimmed = value?.trim();
48
+ if (!trimmed) {
49
+ return null;
50
+ }
51
+ const githubShorthandMatch = trimmed.match(/^github:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:#.*)?$/u);
52
+ if (githubShorthandMatch) {
53
+ return `${githubShorthandMatch[1]}/${githubShorthandMatch[2]}`;
54
+ }
55
+ if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(trimmed)) {
56
+ return trimmed;
57
+ }
58
+ const githubScpMatch = trimmed.match(/^git@github\.com:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?(?:#.*)?$/u);
59
+ if (githubScpMatch) {
60
+ return `${githubScpMatch[1]}/${githubScpMatch[2]}`;
61
+ }
62
+ const normalizedValue = trimmed
63
+ .replace(/^git\+/u, "")
64
+ .replace(/#.*$/u, "");
65
+ let parsedUrl;
66
+ try {
67
+ parsedUrl = new URL(normalizedValue);
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ if (parsedUrl.hostname !== "github.com") {
73
+ return null;
74
+ }
75
+ const pathSegments = parsedUrl.pathname
76
+ .replace(/^\/+/u, "")
77
+ .replace(/\.git$/u, "")
78
+ .split("/");
79
+ if (pathSegments.length < 2 || !pathSegments[0] || !pathSegments[1]) {
80
+ return null;
81
+ }
82
+ return `${pathSegments[0]}/${pathSegments[1]}`;
83
+ }
84
+ function getDefaultRepositoryManifestPaths() {
85
+ const candidatePaths = [
86
+ path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", "..", "package.json"),
87
+ path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", "wp-typia", "package.json"),
88
+ path.join(PROJECT_TOOLS_PACKAGE_ROOT, "package.json"),
89
+ resolveInstalledPackageManifestPath("wp-typia"),
90
+ resolveInstalledPackageManifestPath("@wp-typia/project-tools"),
91
+ ].filter((candidatePath) => Boolean(candidatePath));
92
+ return candidatePaths.filter((candidatePath, index, allPaths) => allPaths.indexOf(candidatePath) === index);
93
+ }
94
+ /**
95
+ * Resolves the canonical scaffold repository reference in `owner/repo` format.
96
+ *
97
+ * The resolver checks candidate package manifests in priority order, extracts
98
+ * their `repository` field, and returns the first GitHub slug it can parse.
99
+ * When no candidate yields a GitHub repository reference, the fallback value
100
+ * is returned instead.
101
+ */
102
+ export function resolveScaffoldRepositoryReference({ fallback = DEFAULT_SCAFFOLD_REPOSITORY_REFERENCE, manifestPaths = getDefaultRepositoryManifestPaths(), } = {}) {
103
+ for (const manifestPath of manifestPaths) {
104
+ const repositoryReference = parseRepositoryReference(getRepositoryFieldValue(readRepositoryPackageManifest(manifestPath)));
105
+ if (repositoryReference) {
106
+ return repositoryReference;
107
+ }
108
+ }
109
+ return fallback;
110
+ }
111
+ /**
112
+ * Replaces legacy scaffold repository placeholders with a concrete
113
+ * `owner/repo` reference.
114
+ */
115
+ export function replaceRepositoryReferencePlaceholders(source, repositoryReference) {
116
+ return source
117
+ .replace(/yourusername\/wp-typia-boilerplate/g, repositoryReference)
118
+ .replace(/yourusername\/wp-typia/g, repositoryReference);
119
+ }
@@ -115,11 +115,15 @@ interface ScaffoldProjectOptions {
115
115
  answers: ScaffoldAnswers;
116
116
  cwd?: string;
117
117
  dataStorageMode?: DataStorageMode;
118
+ externalLayerId?: string;
119
+ externalLayerSource?: string;
120
+ externalLayerSourceLabel?: string;
118
121
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
119
122
  noInstall?: boolean;
120
123
  packageManager: PackageManagerId;
121
124
  persistencePolicy?: PersistencePolicy;
122
125
  projectDir: string;
126
+ repositoryReference?: string;
123
127
  templateId: string;
124
128
  variant?: string;
125
129
  withMigrationUi?: boolean;
@@ -153,5 +157,5 @@ export declare function resolveTemplateId({ templateId, yes, isInteractive, sele
153
157
  export declare function resolvePackageManagerId({ packageManager, yes, isInteractive, selectPackageManager, }: ResolvePackageManagerOptions): Promise<PackageManagerId>;
154
158
  export declare function collectScaffoldAnswers({ projectName, templateId, yes, dataStorageMode, namespace, persistencePolicy, phpPrefix, promptText, textDomain, }: CollectScaffoldAnswersOptions): Promise<ScaffoldAnswers>;
155
159
  export declare function getTemplateVariables(templateId: string, answers: ScaffoldAnswers): ScaffoldTemplateVariables;
156
- export declare function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, cwd, allowExistingDir, noInstall, installDependencies, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
160
+ export declare function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd, allowExistingDir, noInstall, installDependencies, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
157
161
  export {};
@@ -3,6 +3,7 @@ import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { execSync } from "node:child_process";
5
5
  import { PACKAGE_MANAGER_IDS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
6
+ import { replaceTextRecursively } from "./scaffold-apply-utils.js";
6
7
  import { applyGeneratedProjectDxPackageJson, applyLocalDevPresetFiles, getPrimaryDevelopmentScript, } from "./local-dev-presets.js";
7
8
  import { applyMigrationUiCapability } from "./migration-ui-capability.js";
8
9
  import { getPackageVersions } from "./package-versions.js";
@@ -563,41 +564,6 @@ async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
563
564
  }
564
565
  }));
565
566
  }
566
- async function replaceTextRecursively(targetDir, packageManagerId) {
567
- const textExtensions = new Set([
568
- ".css",
569
- ".js",
570
- ".json",
571
- ".jsx",
572
- ".md",
573
- ".php",
574
- ".scss",
575
- ".ts",
576
- ".tsx",
577
- ".txt",
578
- ]);
579
- async function visit(currentPath) {
580
- const stats = await fsp.stat(currentPath);
581
- if (stats.isDirectory()) {
582
- const entries = await fsp.readdir(currentPath);
583
- for (const entry of entries) {
584
- await visit(path.join(currentPath, entry));
585
- }
586
- return;
587
- }
588
- if (path.basename(currentPath) === "package.json" || !textExtensions.has(path.extname(currentPath))) {
589
- return;
590
- }
591
- const content = await fsp.readFile(currentPath, "utf8");
592
- const nextContent = transformPackageManagerText(content, packageManagerId)
593
- .replace(/yourusername\/wp-typia-boilerplate/g, "imjlk/wp-typia")
594
- .replace(/yourusername\/wp-typia/g, "imjlk/wp-typia");
595
- if (nextContent !== content) {
596
- await fsp.writeFile(currentPath, nextContent, "utf8");
597
- }
598
- }
599
- await visit(targetDir);
600
- }
601
567
  async function defaultInstallDependencies({ projectDir, packageManager, }) {
602
568
  execSync(formatInstallCommand(packageManager), {
603
569
  cwd: projectDir,
@@ -642,10 +608,13 @@ async function applyWorkspaceMigrationCapability(projectDir, packageManager) {
642
608
  ensureMigrationDirectories(projectDir, []);
643
609
  writeInitialMigrationScaffold(projectDir, "v1", []);
644
610
  }
645
- export async function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
611
+ export async function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
646
612
  const resolvedTemplateId = normalizeTemplateSelection(templateId);
647
613
  const resolvedPackageManager = getPackageManager(packageManager).id;
648
614
  const isBuiltInTemplate = isBuiltInTemplateId(resolvedTemplateId);
615
+ if (externalLayerId && !externalLayerSource) {
616
+ throw new Error("externalLayerId requires externalLayerSource when composing built-in template layers.");
617
+ }
649
618
  if (isBuiltInTemplate) {
650
619
  const blockGeneratorService = new BlockGeneratorService();
651
620
  const plan = await blockGeneratorService.plan({
@@ -653,10 +622,14 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
653
622
  answers,
654
623
  cwd,
655
624
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
625
+ externalLayerId,
626
+ externalLayerSource,
627
+ externalLayerSourceLabel,
656
628
  noInstall,
657
629
  packageManager: resolvedPackageManager,
658
630
  persistencePolicy: persistencePolicy ?? answers.persistencePolicy,
659
631
  projectDir,
632
+ repositoryReference,
660
633
  templateId: resolvedTemplateId,
661
634
  variant,
662
635
  withMigrationUi,
@@ -670,6 +643,9 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
670
643
  rendered,
671
644
  });
672
645
  }
646
+ if (externalLayerSource || externalLayerId) {
647
+ throw new Error("External template layers currently compose only with built-in templates via `wp-typia create --template <basic|interactivity|persistence|compound>` or `wp-typia add block --template <family>`.");
648
+ }
673
649
  const variables = getTemplateVariables(resolvedTemplateId, {
674
650
  ...answers,
675
651
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
@@ -738,7 +714,9 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
738
714
  }
739
715
  await normalizePackageManagerFiles(projectDir, resolvedPackageManager);
740
716
  await removeUnexpectedLockfiles(projectDir, resolvedPackageManager);
741
- await replaceTextRecursively(projectDir, resolvedPackageManager);
717
+ await replaceTextRecursively(projectDir, resolvedPackageManager, {
718
+ repositoryReference,
719
+ });
742
720
  if (!noInstall) {
743
721
  const installer = installDependencies ?? defaultInstallDependencies;
744
722
  await installer({
@@ -18,7 +18,13 @@ export interface ResolvedExternalTemplateLayers {
18
18
  entries: ResolvedTemplateLayerEntry[];
19
19
  selectedLayerId: string;
20
20
  }
21
+ export interface SelectableExternalTemplateLayer {
22
+ description?: string;
23
+ extends: string[];
24
+ id: string;
25
+ }
21
26
  export declare function loadExternalTemplateLayerManifest(sourceRoot: string): Promise<ExternalTemplateLayerManifest | null>;
27
+ export declare function listSelectableExternalTemplateLayers(sourceRoot: string): Promise<SelectableExternalTemplateLayer[]>;
22
28
  export declare function resolveExternalTemplateLayers({ externalLayerId, sourceRoot, }: {
23
29
  externalLayerId?: string;
24
30
  sourceRoot: string;
@@ -77,11 +77,8 @@ export async function loadExternalTemplateLayerManifest(sourceRoot) {
77
77
  version: TEMPLATE_LAYER_MANIFEST_VERSION,
78
78
  };
79
79
  }
80
- function getDefaultExternalLayerId(manifest) {
80
+ function getSelectableExternalLayers(manifest) {
81
81
  const layerIds = Object.keys(manifest.layers);
82
- if (layerIds.length === 1) {
83
- return layerIds[0];
84
- }
85
82
  const referencedExternalLayerIds = new Set();
86
83
  for (const definition of Object.values(manifest.layers)) {
87
84
  for (const ancestorId of definition.extends ?? []) {
@@ -91,10 +88,26 @@ function getDefaultExternalLayerId(manifest) {
91
88
  }
92
89
  }
93
90
  const publicLayerIds = layerIds.filter((layerId) => !referencedExternalLayerIds.has(layerId));
94
- if (publicLayerIds.length === 1) {
95
- return publicLayerIds[0];
91
+ return publicLayerIds.map((layerId) => ({
92
+ description: manifest.layers[layerId]?.description,
93
+ extends: [...(manifest.layers[layerId]?.extends ?? [])],
94
+ id: layerId,
95
+ }));
96
+ }
97
+ function getDefaultExternalLayerId(manifest) {
98
+ const selectableLayers = getSelectableExternalLayers(manifest);
99
+ if (selectableLayers.length === 1) {
100
+ return selectableLayers[0].id;
101
+ }
102
+ const layerIds = Object.keys(manifest.layers);
103
+ throw new Error(`External layer package defines multiple selectable layers (${layerIds.join(", ")}). Pass an explicit externalLayerId or rerun through the interactive CLI selector.`);
104
+ }
105
+ export async function listSelectableExternalTemplateLayers(sourceRoot) {
106
+ const manifest = await loadExternalTemplateLayerManifest(sourceRoot);
107
+ if (!manifest) {
108
+ throw new Error(`No ${TEMPLATE_LAYER_MANIFEST_FILENAME} manifest found in ${sourceRoot}.`);
96
109
  }
97
- throw new Error(`External layer package defines multiple selectable layers (${layerIds.join(", ")}). Pass an explicit externalLayerId until a final CLI selection UX exists.`);
110
+ return getSelectableExternalLayers(manifest);
98
111
  }
99
112
  export async function resolveExternalTemplateLayers({ externalLayerId, sourceRoot, }) {
100
113
  const manifest = await loadExternalTemplateLayerManifest(sourceRoot);
@@ -16,15 +16,26 @@ export interface CopyRawDirectoryOptions {
16
16
  filter?: (sourcePath: string, targetPath: string, entry: fs.Dirent) => boolean | Promise<boolean>;
17
17
  }
18
18
  /**
19
- * Render a Mustache template while keeping HTML escaping disabled only for the
20
- * current render call.
19
+ * Render a Mustache template with full Mustache semantics while leaving values
20
+ * unescaped for scaffold source generation.
21
21
  */
22
22
  export declare function renderMustacheTemplateString(template: string, view: TemplateRenderView): string;
23
23
  /**
24
24
  * Recursively copies a directory tree without rendering template contents.
25
25
  */
26
26
  export declare function copyRawDirectory(sourceDir: string, targetDir: string, options?: CopyRawDirectoryOptions): Promise<void>;
27
+ /**
28
+ * Copy a template directory using full Mustache semantics for filenames and
29
+ * text-file contents while leaving rendered values unescaped.
30
+ */
27
31
  export declare function copyRenderedDirectory(sourceDir: string, targetDir: string, view: TemplateRenderView): Promise<void>;
32
+ /**
33
+ * Copy a template directory using direct `{{key}}` replacement only.
34
+ *
35
+ * This intentionally preserves literal Mustache sections, partials, and other
36
+ * advanced constructs so built-in scaffold copy paths keep their historical
37
+ * interpolation behavior.
38
+ */
28
39
  export declare function copyInterpolatedDirectory(sourceDir: string, targetDir: string, view: Record<string, string>): Promise<void>;
29
40
  /**
30
41
  * Lists the output file paths produced by an interpolated template directory
@@ -19,25 +19,32 @@ const BINARY_EXTENSIONS = new Set([
19
19
  ".woff",
20
20
  ".woff2",
21
21
  ]);
22
+ const IDENTITY_MUSTACHE_ESCAPE = (value) => value;
22
23
  /**
23
- * Render a Mustache template while keeping HTML escaping disabled only for the
24
- * current render call.
24
+ * Render a Mustache template with full Mustache semantics while leaving values
25
+ * unescaped for scaffold source generation.
25
26
  */
26
27
  export function renderMustacheTemplateString(template, view) {
27
- const originalEscape = Mustache.escape;
28
- Mustache.escape = (value) => value;
29
- try {
30
- return Mustache.render(template, view);
31
- }
32
- finally {
33
- Mustache.escape = originalEscape;
34
- }
28
+ const mustacheModule = Mustache;
29
+ const writer = new mustacheModule.Writer();
30
+ return writer.render(template, view, undefined, {
31
+ escape: IDENTITY_MUSTACHE_ESCAPE,
32
+ });
35
33
  }
36
34
  function escapeRegExp(value) {
37
35
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
38
36
  }
37
+ /**
38
+ * Render direct `{{key}}` placeholders without enabling sections, partials, or
39
+ * any other Mustache features.
40
+ */
39
41
  function renderInterpolatedString(template, view) {
40
- return Object.entries(view).reduce((output, [key, value]) => output.replace(new RegExp(`{{${escapeRegExp(key)}}}`, "g"), value), template);
42
+ const keys = Object.keys(view);
43
+ if (keys.length === 0) {
44
+ return template;
45
+ }
46
+ const placeholderPattern = new RegExp(keys.map((key) => `({{${escapeRegExp(key)}}})`).join("|"), "g");
47
+ return template.replace(placeholderPattern, (match) => view[match.slice(2, -2)] ?? match);
41
48
  }
42
49
  function isBinaryTemplateFile(filePath) {
43
50
  return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase());
@@ -51,6 +58,59 @@ function resolveRenderedPath(targetDir, destinationName) {
51
58
  }
52
59
  return resolvedDestinationPath;
53
60
  }
61
+ function stripTemplateExtension(entryName) {
62
+ return entryName.endsWith(".mustache")
63
+ ? entryName.slice(0, -".mustache".length)
64
+ : entryName;
65
+ }
66
+ function renderTemplateDestinationName(entryName, view, renderString) {
67
+ return renderString(stripTemplateExtension(entryName), view);
68
+ }
69
+ async function traverseTemplateDirectory({ prepareDirectory, renderString, sourceDir, targetDir, view, visitFile, }) {
70
+ const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ const sourcePath = path.join(sourceDir, entry.name);
73
+ const destinationName = renderTemplateDestinationName(entry.name, view, renderString);
74
+ const destinationPath = resolveRenderedPath(targetDir, destinationName);
75
+ if (entry.isDirectory()) {
76
+ await prepareDirectory?.(destinationPath);
77
+ await traverseTemplateDirectory({
78
+ prepareDirectory,
79
+ renderString,
80
+ sourceDir: sourcePath,
81
+ targetDir: destinationPath,
82
+ view,
83
+ visitFile,
84
+ });
85
+ continue;
86
+ }
87
+ await visitFile({
88
+ destinationPath,
89
+ sourcePath,
90
+ });
91
+ }
92
+ }
93
+ async function copyTemplateDirectory({ renderString, sourceDir, targetDir, view, }) {
94
+ await fsp.mkdir(targetDir, { recursive: true });
95
+ await traverseTemplateDirectory({
96
+ prepareDirectory: async (directoryPath) => {
97
+ await fsp.mkdir(directoryPath, { recursive: true });
98
+ },
99
+ renderString,
100
+ sourceDir,
101
+ targetDir,
102
+ view,
103
+ visitFile: async ({ destinationPath, sourcePath }) => {
104
+ await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
105
+ if (isBinaryTemplateFile(sourcePath)) {
106
+ await fsp.copyFile(sourcePath, destinationPath);
107
+ return;
108
+ }
109
+ const content = await fsp.readFile(sourcePath, "utf8");
110
+ await fsp.writeFile(destinationPath, renderString(content, view), "utf8");
111
+ },
112
+ });
113
+ }
54
114
  /**
55
115
  * Recursively copies a directory tree without rendering template contents.
56
116
  */
@@ -70,53 +130,32 @@ export async function copyRawDirectory(sourceDir, targetDir, options = {}) {
70
130
  await fsp.copyFile(sourcePath, targetPath);
71
131
  }
72
132
  }
133
+ /**
134
+ * Copy a template directory using full Mustache semantics for filenames and
135
+ * text-file contents while leaving rendered values unescaped.
136
+ */
73
137
  export async function copyRenderedDirectory(sourceDir, targetDir, view) {
74
- const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
75
- for (const entry of entries) {
76
- const sourcePath = path.join(sourceDir, entry.name);
77
- const destinationNameTemplate = entry.name.endsWith(".mustache")
78
- ? entry.name.slice(0, -".mustache".length)
79
- : entry.name;
80
- const destinationName = renderMustacheTemplateString(destinationNameTemplate, view);
81
- const destinationPath = resolveRenderedPath(targetDir, destinationName);
82
- if (entry.isDirectory()) {
83
- await fsp.mkdir(destinationPath, { recursive: true });
84
- await copyRenderedDirectory(sourcePath, destinationPath, view);
85
- continue;
86
- }
87
- if (isBinaryTemplateFile(sourcePath)) {
88
- await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
89
- await fsp.copyFile(sourcePath, destinationPath);
90
- continue;
91
- }
92
- const content = await fsp.readFile(sourcePath, "utf8");
93
- await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
94
- await fsp.writeFile(destinationPath, renderMustacheTemplateString(content, view), "utf8");
95
- }
138
+ await copyTemplateDirectory({
139
+ renderString: renderMustacheTemplateString,
140
+ sourceDir,
141
+ targetDir,
142
+ view,
143
+ });
96
144
  }
145
+ /**
146
+ * Copy a template directory using direct `{{key}}` replacement only.
147
+ *
148
+ * This intentionally preserves literal Mustache sections, partials, and other
149
+ * advanced constructs so built-in scaffold copy paths keep their historical
150
+ * interpolation behavior.
151
+ */
97
152
  export async function copyInterpolatedDirectory(sourceDir, targetDir, view) {
98
- const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
99
- for (const entry of entries) {
100
- const sourcePath = path.join(sourceDir, entry.name);
101
- const destinationNameTemplate = entry.name.endsWith(".mustache")
102
- ? entry.name.slice(0, -".mustache".length)
103
- : entry.name;
104
- const destinationName = renderInterpolatedString(destinationNameTemplate, view);
105
- const destinationPath = resolveRenderedPath(targetDir, destinationName);
106
- if (entry.isDirectory()) {
107
- await fsp.mkdir(destinationPath, { recursive: true });
108
- await copyInterpolatedDirectory(sourcePath, destinationPath, view);
109
- continue;
110
- }
111
- if (isBinaryTemplateFile(sourcePath)) {
112
- await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
113
- await fsp.copyFile(sourcePath, destinationPath);
114
- continue;
115
- }
116
- const content = await fsp.readFile(sourcePath, "utf8");
117
- await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
118
- await fsp.writeFile(destinationPath, renderInterpolatedString(content, view), "utf8");
119
- }
153
+ await copyTemplateDirectory({
154
+ renderString: renderInterpolatedString,
155
+ sourceDir,
156
+ targetDir,
157
+ view,
158
+ });
120
159
  }
121
160
  /**
122
161
  * Lists the output file paths produced by an interpolated template directory
@@ -135,23 +174,15 @@ export async function copyInterpolatedDirectory(sourceDir, targetDir, view) {
135
174
  export async function listInterpolatedDirectoryOutputs(sourceDir, view) {
136
175
  const virtualRoot = path.resolve("/wp-typia-template-preview");
137
176
  const outputs = [];
138
- async function visit(currentSourceDir, currentTargetDir) {
139
- const entries = await fsp.readdir(currentSourceDir, { withFileTypes: true });
140
- for (const entry of entries) {
141
- const sourcePath = path.join(currentSourceDir, entry.name);
142
- const destinationNameTemplate = entry.name.endsWith(".mustache")
143
- ? entry.name.slice(0, -".mustache".length)
144
- : entry.name;
145
- const destinationName = renderInterpolatedString(destinationNameTemplate, view);
146
- const destinationPath = resolveRenderedPath(currentTargetDir, destinationName);
147
- if (entry.isDirectory()) {
148
- await visit(sourcePath, destinationPath);
149
- continue;
150
- }
177
+ await traverseTemplateDirectory({
178
+ renderString: renderInterpolatedString,
179
+ sourceDir,
180
+ targetDir: virtualRoot,
181
+ view,
182
+ visitFile: async ({ destinationPath }) => {
151
183
  outputs.push(path.relative(virtualRoot, destinationPath).replace(/\\/g, "/"));
152
- }
153
- }
154
- await visit(sourceDir, virtualRoot);
184
+ },
185
+ });
155
186
  return outputs.sort((left, right) => left.localeCompare(right));
156
187
  }
157
188
  export function pathExists(targetPath) {
@@ -1,9 +1,10 @@
1
- type TemplateSourceFormat = "wp-typia" | "create-block-external" | "create-block-subset";
1
+ import { type UnknownRecord } from './object-utils.js';
2
+ type TemplateSourceFormat = 'wp-typia' | 'create-block-external' | 'create-block-subset';
2
3
  /**
3
4
  * Public template variables exposed to external template seeds before wp-typia
4
5
  * normalizes them into a scaffold project.
5
6
  */
6
- export interface TemplateVariableContext extends Record<string, unknown> {
7
+ export interface TemplateVariableContext extends UnknownRecord {
7
8
  /** Version string for `@wp-typia/api-client` used in generated dependencies. */
8
9
  apiClientPackageVersion: string;
9
10
  /** Version string for `@wp-typia/block-runtime` used in generated dependencies. */
@@ -61,13 +62,13 @@ interface SeedSource {
61
62
  warnings?: string[];
62
63
  }
63
64
  type RemoteTemplateLocator = {
64
- kind: "github";
65
+ kind: 'github';
65
66
  locator: GitHubTemplateLocator;
66
67
  } | {
67
- kind: "npm";
68
+ kind: 'npm';
68
69
  locator: NpmTemplateLocator;
69
70
  } | {
70
- kind: "path";
71
+ kind: 'path';
71
72
  templatePath: string;
72
73
  };
73
74
  export declare function parseGitHubTemplateLocator(templateId: string): GitHubTemplateLocator | null;