@wp-typia/project-tools 0.18.0 → 0.19.1

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 (62) hide show
  1. package/dist/runtime/alternate-render-targets.d.ts +5 -0
  2. package/dist/runtime/alternate-render-targets.js +29 -0
  3. package/dist/runtime/block-generator-service-core.d.ts +1 -1
  4. package/dist/runtime/block-generator-service-core.js +11 -7
  5. package/dist/runtime/block-generator-service-spec.d.ts +8 -1
  6. package/dist/runtime/block-generator-service-spec.js +248 -2
  7. package/dist/runtime/built-in-block-artifacts.js +3 -1
  8. package/dist/runtime/built-in-block-code-artifacts.js +3 -1
  9. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +1 -1
  10. package/dist/runtime/built-in-block-code-templates/compound-child.js +14 -9
  11. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +2 -2
  12. package/dist/runtime/built-in-block-code-templates/compound-parent.js +100 -43
  13. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +1 -1
  14. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +11 -8
  15. package/dist/runtime/built-in-block-non-ts-artifacts.js +505 -2
  16. package/dist/runtime/cli-add-block.d.ts +4 -1
  17. package/dist/runtime/cli-add-block.js +66 -31
  18. package/dist/runtime/cli-add-shared.d.ts +3 -1
  19. package/dist/runtime/cli-add-shared.js +12 -12
  20. package/dist/runtime/cli-core.d.ts +2 -0
  21. package/dist/runtime/cli-core.js +1 -0
  22. package/dist/runtime/cli-diagnostics.d.ts +26 -0
  23. package/dist/runtime/cli-diagnostics.js +107 -0
  24. package/dist/runtime/cli-doctor-workspace.js +4 -4
  25. package/dist/runtime/cli-help.js +4 -3
  26. package/dist/runtime/cli-scaffold.d.ts +3 -1
  27. package/dist/runtime/cli-scaffold.js +91 -15
  28. package/dist/runtime/cli-templates.js +26 -1
  29. package/dist/runtime/cli-validation.d.ts +66 -0
  30. package/dist/runtime/cli-validation.js +92 -0
  31. package/dist/runtime/compound-inner-blocks.d.ts +78 -0
  32. package/dist/runtime/compound-inner-blocks.js +88 -0
  33. package/dist/runtime/index.d.ts +5 -1
  34. package/dist/runtime/index.js +3 -1
  35. package/dist/runtime/migration-command-surface.js +2 -0
  36. package/dist/runtime/package-versions.d.ts +1 -0
  37. package/dist/runtime/package-versions.js +12 -0
  38. package/dist/runtime/scaffold-answer-resolution.js +10 -6
  39. package/dist/runtime/scaffold-bootstrap.js +5 -1
  40. package/dist/runtime/scaffold-documents.js +29 -6
  41. package/dist/runtime/scaffold-identifiers.d.ts +17 -0
  42. package/dist/runtime/scaffold-identifiers.js +22 -0
  43. package/dist/runtime/scaffold-onboarding.js +21 -13
  44. package/dist/runtime/scaffold-template-variable-groups.d.ts +154 -0
  45. package/dist/runtime/scaffold-template-variable-groups.js +13 -0
  46. package/dist/runtime/scaffold-template-variables.js +80 -1
  47. package/dist/runtime/scaffold.d.ts +21 -2
  48. package/dist/runtime/scaffold.js +12 -5
  49. package/dist/runtime/temp-roots.d.ts +44 -0
  50. package/dist/runtime/temp-roots.js +129 -0
  51. package/dist/runtime/template-builtins.js +4 -6
  52. package/dist/runtime/template-registry.d.ts +8 -0
  53. package/dist/runtime/template-registry.js +34 -1
  54. package/dist/runtime/template-source-external.d.ts +1 -0
  55. package/dist/runtime/template-source-external.js +4 -7
  56. package/dist/runtime/template-source-remote.js +44 -23
  57. package/dist/runtime/template-source-seeds.js +3 -9
  58. package/dist/runtime/template-source.d.ts +2 -3
  59. package/dist/runtime/template-source.js +13 -5
  60. package/dist/runtime/workspace-project.js +1 -1
  61. package/package.json +12 -2
  62. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +318 -18
@@ -1,4 +1,5 @@
1
1
  import type { PackageManagerId } from "./package-managers.js";
2
+ import type { ScaffoldTemplateVariableGroupsCarrier } from "./scaffold-template-variable-groups.js";
2
3
  /**
3
4
  * User-facing scaffold answers before template rendering.
4
5
  *
@@ -8,6 +9,7 @@ import type { PackageManagerId } from "./package-managers.js";
8
9
  */
9
10
  export interface ScaffoldAnswers {
10
11
  author: string;
12
+ compoundInnerBlocksPreset?: import("./compound-inner-blocks.js").CompoundInnerBlocksPresetId;
11
13
  dataStorageMode?: DataStorageMode;
12
14
  description: string;
13
15
  /** Block namespace used in generated block names such as `namespace/slug`. */
@@ -28,7 +30,9 @@ export type PersistencePolicy = (typeof PERSISTENCE_POLICIES)[number];
28
30
  /**
29
31
  * Normalized template variables shared by built-in and remote scaffold flows.
30
32
  */
31
- export interface ScaffoldTemplateVariables extends Record<string, string> {
33
+ export interface FlatScaffoldTemplateVariables extends Record<string, string> {
34
+ alternateRenderTargetsCsv: string;
35
+ alternateRenderTargetsJson: string;
32
36
  apiClientPackageVersion: string;
33
37
  author: string;
34
38
  blockRuntimePackageVersion: string;
@@ -42,6 +46,17 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
42
46
  compoundChildIcon: string;
43
47
  compoundChildTitleJson: string;
44
48
  compoundPersistenceEnabled: "false" | "true";
49
+ compoundInnerBlocksDirectInsert: "false" | "true";
50
+ compoundInnerBlocksOrientation: "" | "horizontal" | "vertical";
51
+ compoundInnerBlocksOrientationExpression: string;
52
+ compoundInnerBlocksPreset: string;
53
+ compoundInnerBlocksPresetDescription: string;
54
+ compoundInnerBlocksPresetLabel: string;
55
+ compoundInnerBlocksTemplateLockExpression: string;
56
+ hasAlternateEmailRenderTarget: "false" | "true";
57
+ hasAlternateMjmlRenderTarget: "false" | "true";
58
+ hasAlternatePlainTextRenderTarget: "false" | "true";
59
+ hasAlternateRenderTargets: "false" | "true";
45
60
  projectToolsPackageVersion: string;
46
61
  cssClassName: string;
47
62
  dashCase: string;
@@ -80,6 +95,8 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
80
95
  titleCase: string;
81
96
  persistencePolicy: PersistencePolicy;
82
97
  }
98
+ export interface ScaffoldTemplateVariables extends FlatScaffoldTemplateVariables, ScaffoldTemplateVariableGroupsCarrier {
99
+ }
83
100
  /**
84
101
  * Resolve scaffold template input from either built-in template ids or custom
85
102
  * template identifiers such as local paths, GitHub refs, and npm packages.
@@ -131,6 +148,7 @@ export interface ScaffoldProgressEvent {
131
148
  }
132
149
  interface ScaffoldProjectOptions {
133
150
  allowExistingDir?: boolean;
151
+ alternateRenderTargets?: string;
134
152
  answers: ScaffoldAnswers;
135
153
  cwd?: string;
136
154
  dataStorageMode?: DataStorageMode;
@@ -161,6 +179,7 @@ export interface ScaffoldProjectResult {
161
179
  export { buildBlockCssClassName } from "./scaffold-identifiers.js";
162
180
  export { collectScaffoldAnswers, detectAuthor, getDefaultAnswers, resolvePackageManagerId, resolveTemplateId, } from "./scaffold-answer-resolution.js";
163
181
  export { getTemplateVariables } from "./scaffold-template-variables.js";
182
+ export { getScaffoldTemplateVariableGroups, type BasicScaffoldTemplateVariableGroups, type CompoundScaffoldTemplateVariableGroups, type ExternalScaffoldTemplateVariableGroups, type InteractivityScaffoldTemplateVariableGroups, type PersistenceScaffoldTemplateVariableGroups, type QueryLoopScaffoldTemplateVariableGroups, type ScaffoldTemplateFamily, type ScaffoldTemplateVariableGroups, type ScaffoldTemplateVariableGroupsCarrier, } from "./scaffold-template-variable-groups.js";
164
183
  export declare function isDataStorageMode(value: string): value is DataStorageMode;
165
184
  export declare function isPersistencePolicy(value: string): value is PersistencePolicy;
166
- export declare function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd, allowExistingDir, noInstall, installDependencies, onProgress, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
185
+ export declare function scaffoldProject({ projectDir, templateId, answers, alternateRenderTargets, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd, allowExistingDir, noInstall, installDependencies, onProgress, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
@@ -12,12 +12,15 @@ import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./tem
12
12
  import { resolveTemplateSource } from "./template-source.js";
13
13
  import { BlockGeneratorService, } from "./block-generator-service.js";
14
14
  import { getTemplateVariables } from "./scaffold-template-variables.js";
15
+ import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
16
+ import { assertExternalLayerCompositionOptions, } from "./cli-validation.js";
15
17
  const WORKSPACE_TEMPLATE_ALIAS = "workspace";
16
18
  export const DATA_STORAGE_MODES = ["post-meta", "custom-table"];
17
19
  export const PERSISTENCE_POLICIES = ["authenticated", "public"];
18
20
  export { buildBlockCssClassName } from "./scaffold-identifiers.js";
19
21
  export { collectScaffoldAnswers, detectAuthor, getDefaultAnswers, resolvePackageManagerId, resolveTemplateId, } from "./scaffold-answer-resolution.js";
20
22
  export { getTemplateVariables } from "./scaffold-template-variables.js";
23
+ export { getScaffoldTemplateVariableGroups, } from "./scaffold-template-variable-groups.js";
21
24
  export function isDataStorageMode(value) {
22
25
  return DATA_STORAGE_MODES.includes(value);
23
26
  }
@@ -32,13 +35,14 @@ function normalizeTemplateSelection(templateId) {
32
35
  async function reportScaffoldProgress(onProgress, event) {
33
36
  await onProgress?.(event);
34
37
  }
35
- export async function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, onProgress = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
38
+ export async function scaffoldProject({ projectDir, templateId, answers, alternateRenderTargets, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, onProgress = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
36
39
  const resolvedTemplateId = normalizeTemplateSelection(templateId);
37
40
  const resolvedPackageManager = getPackageManager(packageManager).id;
38
41
  const isBuiltInTemplate = isBuiltInTemplateId(resolvedTemplateId);
39
- if (externalLayerId && !externalLayerSource) {
40
- throw new Error("externalLayerId requires externalLayerSource when composing built-in template layers.");
41
- }
42
+ assertExternalLayerCompositionOptions({
43
+ externalLayerId,
44
+ externalLayerSource,
45
+ });
42
46
  if (isBuiltInTemplate) {
43
47
  const blockGeneratorService = new BlockGeneratorService();
44
48
  await reportScaffoldProgress(onProgress, {
@@ -48,6 +52,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
48
52
  });
49
53
  const plan = await blockGeneratorService.plan({
50
54
  allowExistingDir,
55
+ alternateRenderTargets,
51
56
  answers,
52
57
  cwd,
53
58
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
@@ -161,8 +166,10 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
161
166
  await fsp.writeFile(gitignorePath, mergeTextLines(buildGitignore(), existingGitignore), "utf8");
162
167
  await normalizePackageJson(projectDir, resolvedPackageManager);
163
168
  if (isBuiltInTemplate) {
169
+ const variableGroups = getScaffoldTemplateVariableGroups(variables);
164
170
  await applyGeneratedProjectDxPackageJson({
165
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
171
+ compoundPersistenceEnabled: variableGroups.compound.enabled &&
172
+ variableGroups.compound.persistenceEnabled,
166
173
  packageManager: resolvedPackageManager,
167
174
  projectDir,
168
175
  templateId: resolvedTemplateId,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Required prefix for managed wp-typia temporary directories.
3
+ */
4
+ export declare const WP_TYPIA_TEMP_ROOT_PREFIX = "wp-typia-";
5
+ /**
6
+ * Default age threshold for pruning stale wp-typia temp roots.
7
+ */
8
+ export declare const STALE_TEMP_ROOT_MAX_AGE_MS: number;
9
+ type TempRootOptions = {
10
+ maxAgeMs?: number;
11
+ now?: number;
12
+ tmpDir?: string;
13
+ };
14
+ /**
15
+ * Remove a managed temp root and stop tracking it for process-level cleanup.
16
+ *
17
+ * @param tempRoot Absolute temporary directory path to remove.
18
+ */
19
+ export declare function cleanupManagedTempRoot(tempRoot: string): Promise<void>;
20
+ /**
21
+ * Remove stale `wp-typia-*` temp directories from the target temp root.
22
+ *
23
+ * @param options Optional temp directory, age threshold, and clock override.
24
+ * @returns Absolute temp-root paths removed during the cleanup pass.
25
+ */
26
+ export declare function cleanupStaleTempRoots({ maxAgeMs, now, tmpDir, }?: TempRootOptions): Promise<string[]>;
27
+ /**
28
+ * Create a managed wp-typia temp root and install process cleanup handlers.
29
+ *
30
+ * @param prefix Temp directory prefix. Must start with `wp-typia-`.
31
+ * @param options Optional temp directory override.
32
+ * @returns The created temp-root path plus an async cleanup helper.
33
+ */
34
+ export declare function createManagedTempRoot(prefix: string, options?: Pick<TempRootOptions, "tmpDir">): Promise<{
35
+ cleanup: () => Promise<void>;
36
+ path: string;
37
+ }>;
38
+ /**
39
+ * Snapshot the currently tracked temp roots for diagnostics and tests.
40
+ *
41
+ * @returns Absolute paths for temp roots currently registered for cleanup.
42
+ */
43
+ export declare function getTrackedTempRoots(): string[];
44
+ export {};
@@ -0,0 +1,129 @@
1
+ import fs from "node:fs";
2
+ import * as fsp from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ /**
6
+ * Required prefix for managed wp-typia temporary directories.
7
+ */
8
+ export const WP_TYPIA_TEMP_ROOT_PREFIX = "wp-typia-";
9
+ /**
10
+ * Default age threshold for pruning stale wp-typia temp roots.
11
+ */
12
+ export const STALE_TEMP_ROOT_MAX_AGE_MS = 1000 * 60 * 60 * 24;
13
+ const trackedTempRoots = new Set();
14
+ let cleanupHandlersInstalled = false;
15
+ let staleCleanupRan = false;
16
+ let signalCleanupInProgress = false;
17
+ function getTempDir(tmpDir) {
18
+ return tmpDir ?? os.tmpdir();
19
+ }
20
+ function cleanupTrackedTempRootsSync() {
21
+ for (const tempRoot of [...trackedTempRoots]) {
22
+ trackedTempRoots.delete(tempRoot);
23
+ try {
24
+ fs.rmSync(tempRoot, { force: true, recursive: true });
25
+ }
26
+ catch { }
27
+ }
28
+ }
29
+ function installCleanupHandlers() {
30
+ if (cleanupHandlersInstalled) {
31
+ return;
32
+ }
33
+ cleanupHandlersInstalled = true;
34
+ process.on("exit", cleanupTrackedTempRootsSync);
35
+ for (const [signal, exitCode] of [
36
+ ["SIGHUP", 129],
37
+ ["SIGINT", 130],
38
+ ["SIGTERM", 143],
39
+ ]) {
40
+ process.once(signal, () => {
41
+ if (signalCleanupInProgress) {
42
+ return;
43
+ }
44
+ signalCleanupInProgress = true;
45
+ cleanupTrackedTempRootsSync();
46
+ process.exitCode = exitCode;
47
+ process.exit();
48
+ });
49
+ }
50
+ }
51
+ /**
52
+ * Remove a managed temp root and stop tracking it for process-level cleanup.
53
+ *
54
+ * @param tempRoot Absolute temporary directory path to remove.
55
+ */
56
+ export async function cleanupManagedTempRoot(tempRoot) {
57
+ trackedTempRoots.delete(tempRoot);
58
+ await fsp.rm(tempRoot, { force: true, recursive: true });
59
+ }
60
+ /**
61
+ * Remove stale `wp-typia-*` temp directories from the target temp root.
62
+ *
63
+ * @param options Optional temp directory, age threshold, and clock override.
64
+ * @returns Absolute temp-root paths removed during the cleanup pass.
65
+ */
66
+ export async function cleanupStaleTempRoots({ maxAgeMs = STALE_TEMP_ROOT_MAX_AGE_MS, now = Date.now(), tmpDir, } = {}) {
67
+ const resolvedTmpDir = getTempDir(tmpDir);
68
+ const removedRoots = [];
69
+ const entries = await fsp.readdir(resolvedTmpDir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (!entry.isDirectory() ||
72
+ !entry.name.startsWith(WP_TYPIA_TEMP_ROOT_PREFIX)) {
73
+ continue;
74
+ }
75
+ const tempRoot = path.join(resolvedTmpDir, entry.name);
76
+ if (trackedTempRoots.has(tempRoot)) {
77
+ continue;
78
+ }
79
+ let stats;
80
+ try {
81
+ stats = await fsp.stat(tempRoot);
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ if (now - stats.mtimeMs < maxAgeMs) {
87
+ continue;
88
+ }
89
+ try {
90
+ await fsp.rm(tempRoot, { force: true, recursive: true });
91
+ removedRoots.push(tempRoot);
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ }
97
+ return removedRoots;
98
+ }
99
+ /**
100
+ * Create a managed wp-typia temp root and install process cleanup handlers.
101
+ *
102
+ * @param prefix Temp directory prefix. Must start with `wp-typia-`.
103
+ * @param options Optional temp directory override.
104
+ * @returns The created temp-root path plus an async cleanup helper.
105
+ */
106
+ export async function createManagedTempRoot(prefix, options = {}) {
107
+ if (!prefix.startsWith(WP_TYPIA_TEMP_ROOT_PREFIX)) {
108
+ throw new Error(`Managed wp-typia temp roots must use the "${WP_TYPIA_TEMP_ROOT_PREFIX}" prefix.`);
109
+ }
110
+ installCleanupHandlers();
111
+ if (!staleCleanupRan) {
112
+ staleCleanupRan = true;
113
+ await cleanupStaleTempRoots({ tmpDir: options.tmpDir });
114
+ }
115
+ const tempRoot = await fsp.mkdtemp(path.join(getTempDir(options.tmpDir), prefix));
116
+ trackedTempRoots.add(tempRoot);
117
+ return {
118
+ cleanup: async () => cleanupManagedTempRoot(tempRoot),
119
+ path: tempRoot,
120
+ };
121
+ }
122
+ /**
123
+ * Snapshot the currently tracked temp roots for diagnostics and tests.
124
+ *
125
+ * @returns Absolute paths for temp roots currently registered for cleanup.
126
+ */
127
+ export function getTrackedTempRoots() {
128
+ return [...trackedTempRoots];
129
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import { promises as fsp } from "node:fs";
4
+ import { createManagedTempRoot } from "./temp-roots.js";
5
5
  import { getTemplateById, SHARED_BASE_TEMPLATE_ROOT, SHARED_COMPOUND_TEMPLATE_ROOT, SHARED_MIGRATION_UI_TEMPLATE_ROOT, SHARED_PERSISTENCE_TEMPLATE_ROOT, SHARED_PRESET_TEMPLATE_ROOT, SHARED_REST_HELPER_TEMPLATE_ROOT, SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
6
6
  const BUILT_IN_SHARED_TEMPLATE_LAYERS = Object.freeze([
7
7
  {
@@ -152,7 +152,7 @@ function resolveMaterializedTemplateLayerDirs(templateId, layerDirs) {
152
152
  async function materializeBuiltInTemplateSource(templateId, layerDirs) {
153
153
  const template = getTemplateById(templateId);
154
154
  const materializedLayerDirs = resolveMaterializedTemplateLayerDirs(templateId, layerDirs);
155
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-template-"));
155
+ const { path: tempRoot, cleanup } = await createManagedTempRoot("wp-typia-template-");
156
156
  const templateDir = path.join(tempRoot, templateId);
157
157
  try {
158
158
  await fsp.mkdir(templateDir, { recursive: true });
@@ -164,7 +164,7 @@ async function materializeBuiltInTemplateSource(templateId, layerDirs) {
164
164
  }
165
165
  }
166
166
  catch (error) {
167
- await fsp.rm(tempRoot, { force: true, recursive: true });
167
+ await cleanup();
168
168
  throw error;
169
169
  }
170
170
  return {
@@ -174,9 +174,7 @@ async function materializeBuiltInTemplateSource(templateId, layerDirs) {
174
174
  features: template.features,
175
175
  format: "wp-typia",
176
176
  templateDir,
177
- cleanup: async () => {
178
- await fsp.rm(tempRoot, { force: true, recursive: true });
179
- },
177
+ cleanup,
180
178
  };
181
179
  }
182
180
  export async function resolveBuiltInTemplateSourceFromLayerDirs(templateId, layerDirs) {
@@ -1,3 +1,11 @@
1
+ /**
2
+ * Resolve the canonical `@wp-typia/project-tools` package root.
3
+ *
4
+ * When `WP_TYPIA_PROJECT_TOOLS_PACKAGE_ROOT` is set, the override is only
5
+ * accepted if it points at a readable package manifest whose `name` matches
6
+ * `@wp-typia/project-tools`. Invalid or stale overrides are ignored and normal
7
+ * upward package-root discovery continues.
8
+ */
1
9
  export declare function resolvePackageRoot(startDir: string): string;
2
10
  export declare const PROJECT_TOOLS_PACKAGE_ROOT: string;
3
11
  export declare const TEMPLATE_ROOT: string;
@@ -3,14 +3,47 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { getBuiltInTemplateMetadataDefaults } from "./template-defaults.js";
5
5
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const PROJECT_TOOLS_PACKAGE_ROOT_ENV = "WP_TYPIA_PROJECT_TOOLS_PACKAGE_ROOT";
7
+ const PROJECT_TOOLS_PACKAGE_NAME = "@wp-typia/project-tools";
8
+ function resolveValidProjectToolsPackageRoot(candidateRoot) {
9
+ const packageJsonPath = path.join(candidateRoot, "package.json");
10
+ if (!fs.existsSync(packageJsonPath)) {
11
+ return undefined;
12
+ }
13
+ try {
14
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
15
+ return packageJson.name === PROJECT_TOOLS_PACKAGE_NAME
16
+ ? candidateRoot
17
+ : undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ /**
24
+ * Resolve the canonical `@wp-typia/project-tools` package root.
25
+ *
26
+ * When `WP_TYPIA_PROJECT_TOOLS_PACKAGE_ROOT` is set, the override is only
27
+ * accepted if it points at a readable package manifest whose `name` matches
28
+ * `@wp-typia/project-tools`. Invalid or stale overrides are ignored and normal
29
+ * upward package-root discovery continues.
30
+ */
6
31
  export function resolvePackageRoot(startDir) {
32
+ const overriddenPackageRoot = process.env[PROJECT_TOOLS_PACKAGE_ROOT_ENV]?.trim();
33
+ if (overriddenPackageRoot) {
34
+ const resolvedOverride = path.resolve(overriddenPackageRoot);
35
+ const validOverride = resolveValidProjectToolsPackageRoot(resolvedOverride);
36
+ if (validOverride) {
37
+ return validOverride;
38
+ }
39
+ }
7
40
  let currentDir = startDir;
8
41
  while (true) {
9
42
  const packageJsonPath = path.join(currentDir, "package.json");
10
43
  if (fs.existsSync(packageJsonPath)) {
11
44
  try {
12
45
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
13
- if (packageJson.name === "@wp-typia/project-tools") {
46
+ if (packageJson.name === PROJECT_TOOLS_PACKAGE_NAME) {
14
47
  return currentDir;
15
48
  }
16
49
  }
@@ -3,6 +3,7 @@ import type { SeedSource, TemplateVariableContext } from './template-source-cont
3
3
  * Candidate filenames for official external template config entrypoints.
4
4
  */
5
5
  export declare const EXTERNAL_TEMPLATE_ENTRY_CANDIDATES: readonly ["index.js", "index.cjs", "index.mjs"];
6
+ export declare const EXTERNAL_TEMPLATE_TRUST_WARNING = "External template configs execute trusted JavaScript during scaffolding. Review the template source before using local paths, GitHub repos, or npm packages you do not already trust.";
6
7
  /**
7
8
  * Search a source directory for the first supported external template entry.
8
9
  *
@@ -1,11 +1,10 @@
1
1
  /// <reference path="./external-template-modules.d.ts" />
2
2
  import fs from 'node:fs';
3
- import { promises as fsp } from 'node:fs';
4
- import os from 'node:os';
5
3
  import path from 'node:path';
6
4
  import { pathToFileURL } from 'node:url';
7
5
  import { isPlainObject } from './object-utils.js';
8
6
  import { toSegmentPascalCase } from './string-case.js';
7
+ import { createManagedTempRoot } from './temp-roots.js';
9
8
  import { copyRawDirectory, copyRenderedDirectory } from './template-render.js';
10
9
  /**
11
10
  * Candidate filenames for official external template config entrypoints.
@@ -15,6 +14,7 @@ export const EXTERNAL_TEMPLATE_ENTRY_CANDIDATES = [
15
14
  'index.cjs',
16
15
  'index.mjs',
17
16
  ];
17
+ export const EXTERNAL_TEMPLATE_TRUST_WARNING = 'External template configs execute trusted JavaScript during scaffolding. Review the template source before using local paths, GitHub repos, or npm packages you do not already trust.';
18
18
  const TEMPLATE_WARNING_MESSAGE = 'wp-typia owns package/tooling/sync setup for generated projects, so this external template setting is ignored.';
19
19
  function getTemplateWarning(key) {
20
20
  return `Ignoring external template config key "${key}": ${TEMPLATE_WARNING_MESSAGE}`;
@@ -53,7 +53,7 @@ async function loadExternalTemplateConfig(sourceDir) {
53
53
  if (!isPlainObject(loadedConfig)) {
54
54
  throw new Error(`External template config must export an object: ${entryPath}`);
55
55
  }
56
- const warnings = [];
56
+ const warnings = [EXTERNAL_TEMPLATE_TRUST_WARNING];
57
57
  for (const ignoredKey of [
58
58
  'wpScripts',
59
59
  'wpEnv',
@@ -174,10 +174,7 @@ export async function renderCreateBlockExternalTemplate(sourceDir, context, requ
174
174
  const { config, warnings } = await loadExternalTemplateConfig(sourceDir);
175
175
  const { selectedVariant, variantConfig } = getVariantConfig(config, requestedVariant);
176
176
  const { folderName, formatHint, templatePath } = resolveConfiguredTemplatePath(config, variantConfig);
177
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-create-block-external-'));
178
- const cleanup = async () => {
179
- await fsp.rm(tempRoot, { force: true, recursive: true });
180
- };
177
+ const { path: tempRoot, cleanup } = await createManagedTempRoot('wp-typia-create-block-external-');
181
178
  try {
182
179
  const renderedRoot = path.join(tempRoot, 'rendered');
183
180
  const blockDir = resolveSourceSubpath(renderedRoot, folderName);
@@ -1,9 +1,27 @@
1
1
  import fs from 'node:fs';
2
2
  import { promises as fsp } from 'node:fs';
3
- import os from 'node:os';
4
3
  import path from 'node:path';
5
4
  import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, } from './template-builtins.js';
6
5
  import { copyRawDirectory } from './template-render.js';
6
+ import { createManagedTempRoot } from './temp-roots.js';
7
+ async function cleanupSeedRootPair(cleanup, seedCleanup) {
8
+ let cleanupError;
9
+ try {
10
+ await cleanup();
11
+ }
12
+ catch (error) {
13
+ cleanupError = error;
14
+ }
15
+ try {
16
+ await seedCleanup?.();
17
+ }
18
+ catch (error) {
19
+ cleanupError ?? (cleanupError = error);
20
+ }
21
+ if (cleanupError !== undefined) {
22
+ throw cleanupError;
23
+ }
24
+ }
7
25
  function getDefaultCategoryFromBlockJson(blockJson) {
8
26
  return typeof blockJson.category === 'string' &&
9
27
  blockJson.category.trim().length > 0
@@ -48,10 +66,14 @@ function readTemplatePackageJson(sourceDir) {
48
66
  continue;
49
67
  }
50
68
  try {
51
- return JSON.parse(fs.readFileSync(candidate, 'utf8'));
69
+ return {
70
+ packageJson: JSON.parse(fs.readFileSync(candidate, 'utf8')),
71
+ sourcePath: candidate,
72
+ };
52
73
  }
53
- catch {
54
- continue;
74
+ catch (error) {
75
+ const message = error instanceof Error ? error.message : 'Unknown parse failure';
76
+ throw new Error(`Failed to parse template metadata file "${candidate}": ${message}`);
55
77
  }
56
78
  }
57
79
  return null;
@@ -61,10 +83,18 @@ function readTemplatePackageJson(sourceDir) {
61
83
  * manifest and return it when present.
62
84
  */
63
85
  export function getTemplateProjectType(sourceDir) {
64
- const packageJson = readTemplatePackageJson(sourceDir);
65
- return typeof packageJson?.wpTypia?.projectType === 'string'
66
- ? packageJson.wpTypia.projectType
67
- : null;
86
+ const packageJsonEntry = readTemplatePackageJson(sourceDir);
87
+ if (!packageJsonEntry) {
88
+ return null;
89
+ }
90
+ const projectType = packageJsonEntry.packageJson.wpTypia?.projectType;
91
+ if (projectType === undefined) {
92
+ return null;
93
+ }
94
+ if (typeof projectType !== 'string' || projectType.trim().length === 0) {
95
+ throw new Error(`Template metadata file "${packageJsonEntry.sourcePath}" defines wpTypia.projectType, but it must be a non-empty string.`);
96
+ }
97
+ return projectType;
68
98
  }
69
99
  /**
70
100
  * Copy a wp-typia seed into a normalized temporary template directory.
@@ -73,7 +103,7 @@ export function getTemplateProjectType(sourceDir) {
73
103
  * @returns A cloned seed whose cleanup removes the temp root and original seed.
74
104
  */
75
105
  export async function normalizeWpTypiaTemplateSeed(seed) {
76
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-template-source-'));
106
+ const { path: tempRoot, cleanup } = await createManagedTempRoot('wp-typia-template-source-');
77
107
  const normalizedDir = path.join(tempRoot, 'template');
78
108
  try {
79
109
  await copyRawDirectory(seed.blockDir, normalizedDir, {
@@ -92,15 +122,12 @@ export async function normalizeWpTypiaTemplateSeed(seed) {
92
122
  }
93
123
  }
94
124
  catch (error) {
95
- await fsp.rm(tempRoot, { force: true, recursive: true });
125
+ await Promise.allSettled([cleanup(), seed.cleanup?.()]);
96
126
  throw error;
97
127
  }
98
128
  return {
99
129
  blockDir: normalizedDir,
100
- cleanup: async () => {
101
- await fsp.rm(tempRoot, { force: true, recursive: true });
102
- await seed.cleanup?.();
103
- },
130
+ cleanup: async () => cleanupSeedRootPair(cleanup, seed.cleanup),
104
131
  rootDir: normalizedDir,
105
132
  selectedVariant: seed.selectedVariant,
106
133
  warnings: seed.warnings,
@@ -296,7 +323,7 @@ async function removeSeedEntryConflicts(templateDir) {
296
323
  * @returns A normalized template source rooted in a temporary directory.
297
324
  */
298
325
  export async function normalizeCreateBlockSubset(seed, context) {
299
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-remote-template-'));
326
+ const { path: tempRoot, cleanup } = await createManagedTempRoot('wp-typia-remote-template-');
300
327
  try {
301
328
  const templateDir = path.join(tempRoot, 'template');
302
329
  const blockJson = readRemoteBlockJson(seed.blockDir);
@@ -353,17 +380,11 @@ export async function normalizeCreateBlockSubset(seed, context) {
353
380
  selectedVariant: seed.selectedVariant ?? null,
354
381
  templateDir,
355
382
  warnings: seed.warnings ?? [],
356
- cleanup: async () => {
357
- await fsp.rm(tempRoot, { force: true, recursive: true });
358
- if (seed.cleanup) {
359
- await seed.cleanup();
360
- }
361
- },
383
+ cleanup: async () => cleanupSeedRootPair(cleanup, seed.cleanup),
362
384
  };
363
385
  }
364
386
  catch (error) {
365
- await fsp.rm(tempRoot, { force: true, recursive: true });
366
- await seed.cleanup?.();
387
+ await Promise.allSettled([cleanup(), seed.cleanup?.()]);
367
388
  throw error;
368
389
  }
369
390
  }
@@ -1,13 +1,13 @@
1
1
  import fs from 'node:fs';
2
2
  import { promises as fsp } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
- import os from 'node:os';
5
4
  import path from 'node:path';
6
5
  import { execFileSync } from 'node:child_process';
7
6
  import semver from 'semver';
8
7
  import { x as extractTarball } from 'tar';
9
8
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, } from './template-registry.js';
10
9
  import { isPlainObject } from './object-utils.js';
10
+ import { createManagedTempRoot } from './temp-roots.js';
11
11
  function selectRegistryVersion(metadata, locator) {
12
12
  const distTags = isPlainObject(metadata['dist-tags'])
13
13
  ? metadata['dist-tags']
@@ -58,10 +58,7 @@ async function fetchNpmTemplateSource(locator) {
58
58
  if (typeof tarballUrl !== 'string' || tarballUrl.length === 0) {
59
59
  throw new Error(`npm template metadata is missing tarball URL for ${locator.raw}@${resolvedVersion}.`);
60
60
  }
61
- const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-template-source-'));
62
- const cleanup = async () => {
63
- await fsp.rm(tempRoot, { force: true, recursive: true });
64
- };
61
+ const { path: tempRoot, cleanup } = await createManagedTempRoot('wp-typia-template-source-');
65
62
  try {
66
63
  const tarballResponse = await fetch(tarballUrl);
67
64
  if (!tarballResponse.ok) {
@@ -178,10 +175,7 @@ export async function assertNoSymlinks(sourceDir) {
178
175
  }
179
176
  }
180
177
  async function resolveGitHubTemplateSource(locator) {
181
- const remoteRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-template-source-'));
182
- const cleanup = async () => {
183
- await fsp.rm(remoteRoot, { force: true, recursive: true });
184
- };
178
+ const { path: remoteRoot, cleanup } = await createManagedTempRoot('wp-typia-template-source-');
185
179
  const checkoutDir = path.join(remoteRoot, 'source');
186
180
  try {
187
181
  const args = ['clone', '--depth', '1'];
@@ -1,7 +1,6 @@
1
1
  import type { ResolvedTemplateSource } from './template-source-contracts.js';
2
+ import type { ScaffoldTemplateVariables } from './scaffold.js';
2
3
  export type { GitHubTemplateLocator, NpmTemplateLocator, RemoteTemplateLocator, ResolvedTemplateSource, TemplateSourceFormat, TemplateVariableContext, } from './template-source-contracts.js';
3
4
  export { parseGitHubTemplateLocator, parseNpmTemplateLocator, parseTemplateLocator, } from './template-source-locators.js';
4
5
  export { resolveTemplateSeed } from './template-source-seeds.js';
5
- export declare function resolveTemplateSource(templateId: string, cwd: string, variables: {
6
- [key: string]: string;
7
- }, variant?: string): Promise<ResolvedTemplateSource>;
6
+ export declare function resolveTemplateSource(templateId: string, cwd: string, variables: ScaffoldTemplateVariables, variant?: string): Promise<ResolvedTemplateSource>;
@@ -3,16 +3,24 @@ import { resolveBuiltInTemplateSource } from './template-builtins.js';
3
3
  import { parseTemplateLocator, } from './template-source-locators.js';
4
4
  import { detectTemplateSourceFormat, getTemplateProjectType, getDefaultCategory, getTemplateVariableContext, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, renderCreateBlockExternalTemplate, } from './template-source-normalization.js';
5
5
  import { isOfficialWorkspaceTemplateSeed, resolveTemplateSeed, } from './template-source-seeds.js';
6
+ import { assertBuiltInTemplateVariantAllowed, } from './cli-validation.js';
7
+ import { getScaffoldTemplateVariableGroups } from './scaffold-template-variable-groups.js';
6
8
  export { parseGitHubTemplateLocator, parseNpmTemplateLocator, parseTemplateLocator, } from './template-source-locators.js';
7
9
  export { resolveTemplateSeed } from './template-source-seeds.js';
8
10
  export async function resolveTemplateSource(templateId, cwd, variables, variant) {
9
11
  if (isBuiltInTemplateId(templateId)) {
10
- if (variant) {
11
- throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for built-in template "${templateId}".`);
12
- }
12
+ const variableGroups = getScaffoldTemplateVariableGroups(variables);
13
+ assertBuiltInTemplateVariantAllowed({
14
+ templateId,
15
+ variant,
16
+ });
13
17
  return resolveBuiltInTemplateSource(templateId, {
14
- persistenceEnabled: variables.compoundPersistenceEnabled === 'true',
15
- persistencePolicy: variables.persistencePolicy === 'public' ? 'public' : 'authenticated',
18
+ persistenceEnabled: variableGroups.compound.enabled &&
19
+ variableGroups.compound.persistenceEnabled,
20
+ persistencePolicy: variableGroups.persistence.enabled &&
21
+ variableGroups.persistence.policy === 'public'
22
+ ? 'public'
23
+ : 'authenticated',
16
24
  });
17
25
  }
18
26
  const locator = parseTemplateLocator(templateId);
@@ -148,5 +148,5 @@ export function resolveWorkspaceProject(startDir) {
148
148
  if (workspace) {
149
149
  return workspace;
150
150
  }
151
- throw new Error(`This command must run inside a ${WORKSPACE_TEMPLATE_PACKAGE} project. Create one with \`wp-typia create my-plugin --template ${WORKSPACE_TEMPLATE_PACKAGE}\` first.`);
151
+ throw new Error(`This command must run inside an official wp-typia workspace. Create one with \`wp-typia create my-plugin --template workspace\` first (the short alias for \`${WORKSPACE_TEMPLATE_PACKAGE}\`).`);
152
152
  }