@wp-typia/project-tools 0.24.2 → 0.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/cli-scaffold-emission.d.ts +124 -0
- package/dist/runtime/cli-scaffold-emission.js +45 -0
- package/dist/runtime/cli-scaffold-files.d.ts +22 -0
- package/dist/runtime/cli-scaffold-files.js +69 -0
- package/dist/runtime/cli-scaffold-output.d.ts +79 -0
- package/dist/runtime/cli-scaffold-output.js +49 -0
- package/dist/runtime/cli-scaffold-validation.d.ts +82 -0
- package/dist/runtime/cli-scaffold-validation.js +190 -0
- package/dist/runtime/cli-scaffold.d.ts +10 -44
- package/dist/runtime/cli-scaffold.js +34 -327
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { scaffoldProject } from "./scaffold.js";
|
|
2
|
+
import type { PackageManagerId } from "./package-managers.js";
|
|
3
|
+
import type { ScaffoldProgressEvent } from "./scaffold.js";
|
|
4
|
+
type ScaffoldProjectOptions = Parameters<typeof scaffoldProject>[0];
|
|
5
|
+
/**
|
|
6
|
+
* Dependency installation hook accepted by scaffold emission.
|
|
7
|
+
*/
|
|
8
|
+
export type ScaffoldInstallDependencies = ScaffoldProjectOptions["installDependencies"];
|
|
9
|
+
/**
|
|
10
|
+
* Dry-run metadata returned after rendering a scaffold into a preview directory.
|
|
11
|
+
*/
|
|
12
|
+
export interface ScaffoldDryRunPlan {
|
|
13
|
+
/**
|
|
14
|
+
* Whether dependency installation would run or is skipped by --no-install.
|
|
15
|
+
*/
|
|
16
|
+
dependencyInstall: "skipped-by-flag" | "would-install";
|
|
17
|
+
/**
|
|
18
|
+
* Sorted project-relative paths that would be written by the scaffold.
|
|
19
|
+
*/
|
|
20
|
+
files: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalized scaffold emission options shared by real and dry-run flows.
|
|
24
|
+
*/
|
|
25
|
+
export interface ScaffoldEmissionOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Whether an existing target directory may be reused.
|
|
28
|
+
*/
|
|
29
|
+
allowExistingDir: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Optional alternate render target specification for supported templates.
|
|
32
|
+
*/
|
|
33
|
+
alternateRenderTargets?: ScaffoldProjectOptions["alternateRenderTargets"];
|
|
34
|
+
/**
|
|
35
|
+
* Resolved scaffold answers used by template variable generation.
|
|
36
|
+
*/
|
|
37
|
+
answers: ScaffoldProjectOptions["answers"];
|
|
38
|
+
/**
|
|
39
|
+
* Caller working directory used to resolve relative template inputs.
|
|
40
|
+
*/
|
|
41
|
+
cwd: string;
|
|
42
|
+
/**
|
|
43
|
+
* Persistence storage mode for templates that support storage options.
|
|
44
|
+
*/
|
|
45
|
+
dataStorageMode?: ScaffoldProjectOptions["dataStorageMode"];
|
|
46
|
+
/**
|
|
47
|
+
* Optional public root id selected from an external layer package.
|
|
48
|
+
*/
|
|
49
|
+
externalLayerId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Optional external layer source package, path, or locator.
|
|
52
|
+
*/
|
|
53
|
+
externalLayerSource?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Display label for the external layer source before path resolution.
|
|
56
|
+
*/
|
|
57
|
+
externalLayerSourceLabel?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Optional dependency installer override used by tests and callers.
|
|
60
|
+
*/
|
|
61
|
+
installDependencies?: ScaffoldProjectOptions["installDependencies"];
|
|
62
|
+
/**
|
|
63
|
+
* Whether generated projects should skip dependency installation.
|
|
64
|
+
*/
|
|
65
|
+
noInstall: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Optional callback for scaffold progress events.
|
|
68
|
+
*/
|
|
69
|
+
onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Package manager used for generated metadata and follow-up commands.
|
|
72
|
+
*/
|
|
73
|
+
packageManager: PackageManagerId;
|
|
74
|
+
/**
|
|
75
|
+
* Persistence access policy for templates that support server storage.
|
|
76
|
+
*/
|
|
77
|
+
persistencePolicy?: ScaffoldProjectOptions["persistencePolicy"];
|
|
78
|
+
/**
|
|
79
|
+
* Optional create profile that enables preset groups such as plugin QA.
|
|
80
|
+
*/
|
|
81
|
+
profile?: ScaffoldProjectOptions["profile"];
|
|
82
|
+
/**
|
|
83
|
+
* Absolute target directory for the generated project.
|
|
84
|
+
*/
|
|
85
|
+
projectDir: string;
|
|
86
|
+
/**
|
|
87
|
+
* Resolved template id or external template locator to render.
|
|
88
|
+
*/
|
|
89
|
+
templateId: string;
|
|
90
|
+
/**
|
|
91
|
+
* Optional built-in or external template variant id.
|
|
92
|
+
*/
|
|
93
|
+
variant?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Whether migration UI support should be added when supported.
|
|
96
|
+
*/
|
|
97
|
+
withMigrationUi: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Whether scaffold test presets should be emitted.
|
|
100
|
+
*/
|
|
101
|
+
withTestPreset: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Whether local wp-env support should be emitted.
|
|
104
|
+
*/
|
|
105
|
+
withWpEnv: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Emit scaffold files into the target project directory.
|
|
109
|
+
*
|
|
110
|
+
* @param options Normalized scaffold emission options.
|
|
111
|
+
* @returns The scaffold result produced by the runtime renderer.
|
|
112
|
+
*/
|
|
113
|
+
export declare function emitScaffoldProject(options: ScaffoldEmissionOptions): Promise<Awaited<ReturnType<typeof scaffoldProject>>>;
|
|
114
|
+
/**
|
|
115
|
+
* Build a dry-run scaffold plan without mutating the requested target directory.
|
|
116
|
+
*
|
|
117
|
+
* @param options Normalized scaffold emission options.
|
|
118
|
+
* @returns Preview metadata and the scaffold result from the temp render.
|
|
119
|
+
*/
|
|
120
|
+
export declare function buildScaffoldDryRunPlan(options: ScaffoldEmissionOptions): Promise<{
|
|
121
|
+
plan: ScaffoldDryRunPlan;
|
|
122
|
+
result: Awaited<ReturnType<typeof scaffoldProject>>;
|
|
123
|
+
}>;
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assertDryRunTargetDirectoryReady, listRelativeProjectFiles, } from "./cli-scaffold-files.js";
|
|
3
|
+
import { scaffoldProject } from "./scaffold.js";
|
|
4
|
+
import { createManagedTempRoot } from "./temp-roots.js";
|
|
5
|
+
/**
|
|
6
|
+
* Emit scaffold files into the target project directory.
|
|
7
|
+
*
|
|
8
|
+
* @param options Normalized scaffold emission options.
|
|
9
|
+
* @returns The scaffold result produced by the runtime renderer.
|
|
10
|
+
*/
|
|
11
|
+
export async function emitScaffoldProject(options) {
|
|
12
|
+
return scaffoldProject(options);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a dry-run scaffold plan without mutating the requested target directory.
|
|
16
|
+
*
|
|
17
|
+
* @param options Normalized scaffold emission options.
|
|
18
|
+
* @returns Preview metadata and the scaffold result from the temp render.
|
|
19
|
+
*/
|
|
20
|
+
export async function buildScaffoldDryRunPlan(options) {
|
|
21
|
+
await assertDryRunTargetDirectoryReady(options.projectDir, options.allowExistingDir);
|
|
22
|
+
const { path: tempRoot, cleanup } = await createManagedTempRoot("wp-typia-scaffold-plan-");
|
|
23
|
+
const previewProjectDir = path.join(tempRoot, "preview-project");
|
|
24
|
+
try {
|
|
25
|
+
const result = await emitScaffoldProject({
|
|
26
|
+
...options,
|
|
27
|
+
allowExistingDir: false,
|
|
28
|
+
noInstall: true,
|
|
29
|
+
projectDir: previewProjectDir,
|
|
30
|
+
});
|
|
31
|
+
const files = await listRelativeProjectFiles(previewProjectDir);
|
|
32
|
+
return {
|
|
33
|
+
plan: {
|
|
34
|
+
dependencyInstall: options.noInstall
|
|
35
|
+
? "skipped-by-flag"
|
|
36
|
+
: "would-install",
|
|
37
|
+
files,
|
|
38
|
+
},
|
|
39
|
+
result,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await cleanup();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List every file emitted under a scaffold project using POSIX-style paths.
|
|
3
|
+
*
|
|
4
|
+
* @param rootDir Root project directory to scan recursively.
|
|
5
|
+
* @returns Sorted relative file paths suitable for dry-run preview output.
|
|
6
|
+
*/
|
|
7
|
+
export declare function listRelativeProjectFiles(rootDir: string): Promise<string[]>;
|
|
8
|
+
/**
|
|
9
|
+
* Ensure a dry-run target would be legal before rendering into a temp preview.
|
|
10
|
+
*
|
|
11
|
+
* @param projectDir Real target project directory requested by the user.
|
|
12
|
+
* @param allowExistingDir Whether existing target directories are allowed.
|
|
13
|
+
* @throws Error when the target exists, is non-empty, and is not allowed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertDryRunTargetDirectoryReady(projectDir: string, allowExistingDir: boolean): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Read script names from the package manifest emitted by a scaffold run.
|
|
18
|
+
*
|
|
19
|
+
* @param projectDir Generated project directory containing package.json.
|
|
20
|
+
* @returns Script names, or undefined when the manifest is absent or invalid.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readGeneratedPackageScripts(projectDir: string): Promise<string[] | undefined>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { formatNonEmptyTargetDirectoryError } from "./scaffold-bootstrap.js";
|
|
4
|
+
import { pathExists } from "./fs-async.js";
|
|
5
|
+
import { readJsonFile } from "./json-utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* List every file emitted under a scaffold project using POSIX-style paths.
|
|
8
|
+
*
|
|
9
|
+
* @param rootDir Root project directory to scan recursively.
|
|
10
|
+
* @returns Sorted relative file paths suitable for dry-run preview output.
|
|
11
|
+
*/
|
|
12
|
+
export async function listRelativeProjectFiles(rootDir) {
|
|
13
|
+
const relativeFiles = [];
|
|
14
|
+
async function visit(currentDir) {
|
|
15
|
+
const entries = await fsp.readdir(currentDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
await visit(absolutePath);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
relativeFiles.push(path
|
|
23
|
+
.relative(rootDir, absolutePath)
|
|
24
|
+
.replace(path.sep === "\\" ? /\\/gu : /\//gu, "/"));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
await visit(rootDir);
|
|
28
|
+
return relativeFiles.sort((left, right) => left.localeCompare(right));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Ensure a dry-run target would be legal before rendering into a temp preview.
|
|
32
|
+
*
|
|
33
|
+
* @param projectDir Real target project directory requested by the user.
|
|
34
|
+
* @param allowExistingDir Whether existing target directories are allowed.
|
|
35
|
+
* @throws Error when the target exists, is non-empty, and is not allowed.
|
|
36
|
+
*/
|
|
37
|
+
export async function assertDryRunTargetDirectoryReady(projectDir, allowExistingDir) {
|
|
38
|
+
if (!(await pathExists(projectDir)) || allowExistingDir) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const entries = await fsp.readdir(projectDir);
|
|
42
|
+
if (entries.length > 0) {
|
|
43
|
+
throw new Error(formatNonEmptyTargetDirectoryError(projectDir));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Read script names from the package manifest emitted by a scaffold run.
|
|
48
|
+
*
|
|
49
|
+
* @param projectDir Generated project directory containing package.json.
|
|
50
|
+
* @returns Script names, or undefined when the manifest is absent or invalid.
|
|
51
|
+
*/
|
|
52
|
+
export async function readGeneratedPackageScripts(projectDir) {
|
|
53
|
+
try {
|
|
54
|
+
const parsedPackageJson = await readJsonFile(path.join(projectDir, "package.json"), {
|
|
55
|
+
context: "generated package manifest",
|
|
56
|
+
});
|
|
57
|
+
const scripts = parsedPackageJson.scripts &&
|
|
58
|
+
typeof parsedPackageJson.scripts === "object" &&
|
|
59
|
+
!Array.isArray(parsedPackageJson.scripts)
|
|
60
|
+
? parsedPackageJson.scripts
|
|
61
|
+
: {};
|
|
62
|
+
return Object.entries(scripts)
|
|
63
|
+
.filter(([, value]) => typeof value === "string")
|
|
64
|
+
.map(([scriptName]) => scriptName);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { PackageManagerId } from "./package-managers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Inputs used to build CLI next-step commands after scaffolding succeeds.
|
|
4
|
+
*/
|
|
5
|
+
export interface ScaffoldNextStepsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Whether install instructions should be printed because install was skipped.
|
|
8
|
+
*/
|
|
9
|
+
noInstall: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Package manager used to format install and run commands.
|
|
12
|
+
*/
|
|
13
|
+
packageManager: PackageManagerId;
|
|
14
|
+
/**
|
|
15
|
+
* Absolute scaffold target directory.
|
|
16
|
+
*/
|
|
17
|
+
projectDir: string;
|
|
18
|
+
/**
|
|
19
|
+
* Project path exactly as provided to the CLI.
|
|
20
|
+
*/
|
|
21
|
+
projectInput: string;
|
|
22
|
+
/**
|
|
23
|
+
* Resolved template id used to choose the primary development script.
|
|
24
|
+
*/
|
|
25
|
+
templateId: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Inputs used to compute optional onboarding guidance after scaffolding.
|
|
29
|
+
*/
|
|
30
|
+
export interface ScaffoldOptionalOnboardingOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Script names discovered from the generated package manifest.
|
|
33
|
+
*/
|
|
34
|
+
availableScripts?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Package manager used to format optional commands.
|
|
37
|
+
*/
|
|
38
|
+
packageManager: PackageManagerId;
|
|
39
|
+
/**
|
|
40
|
+
* Resolved template id used to select template-specific guidance.
|
|
41
|
+
*/
|
|
42
|
+
templateId: string;
|
|
43
|
+
/**
|
|
44
|
+
* Whether compound persistence support is present in generated variables.
|
|
45
|
+
*/
|
|
46
|
+
compoundPersistenceEnabled?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* User-facing optional onboarding copy and command list.
|
|
50
|
+
*/
|
|
51
|
+
export interface OptionalOnboardingGuidance {
|
|
52
|
+
/**
|
|
53
|
+
* Full note shown in the detailed completion output.
|
|
54
|
+
*/
|
|
55
|
+
note: string;
|
|
56
|
+
/**
|
|
57
|
+
* Short note shown in compact completion output.
|
|
58
|
+
*/
|
|
59
|
+
shortNote: string;
|
|
60
|
+
/**
|
|
61
|
+
* Optional follow-up commands to show after next steps.
|
|
62
|
+
*/
|
|
63
|
+
steps: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the printed next-step commands for a scaffolded project.
|
|
67
|
+
*
|
|
68
|
+
* @param options Project location and package-manager details used to format
|
|
69
|
+
* next-step commands.
|
|
70
|
+
* @returns Ordered shell commands shown after scaffolding succeeds.
|
|
71
|
+
*/
|
|
72
|
+
export declare function getNextSteps({ projectInput, projectDir, packageManager, noInstall, templateId, }: ScaffoldNextStepsOptions): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Compute optional onboarding guidance shown after scaffolding completes.
|
|
75
|
+
*
|
|
76
|
+
* @param options Package-manager and template context for optional guidance.
|
|
77
|
+
* @returns Optional onboarding note and step list.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getOptionalOnboarding({ availableScripts, packageManager, templateId, compoundPersistenceEnabled, }: ScaffoldOptionalOnboardingOptions): OptionalOnboardingGuidance;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
|
|
3
|
+
import { getOptionalOnboardingNote, getOptionalOnboardingShortNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
|
|
4
|
+
import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
|
|
5
|
+
function quoteShellValue(value) {
|
|
6
|
+
if (!value.startsWith("-") &&
|
|
7
|
+
/^[A-Za-z0-9._/@:-]+(?:\/[A-Za-z0-9._@:-]+)*$/.test(value)) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build the printed next-step commands for a scaffolded project.
|
|
14
|
+
*
|
|
15
|
+
* @param options Project location and package-manager details used to format
|
|
16
|
+
* next-step commands.
|
|
17
|
+
* @returns Ordered shell commands shown after scaffolding succeeds.
|
|
18
|
+
*/
|
|
19
|
+
export function getNextSteps({ projectInput, projectDir, packageManager, noInstall, templateId, }) {
|
|
20
|
+
const cdTarget = path.isAbsolute(projectInput) ? projectDir : projectInput;
|
|
21
|
+
const steps = [`cd ${quoteShellValue(cdTarget)}`];
|
|
22
|
+
if (noInstall) {
|
|
23
|
+
steps.push(formatInstallCommand(packageManager));
|
|
24
|
+
}
|
|
25
|
+
steps.push(formatRunScript(packageManager, getPrimaryDevelopmentScript(templateId)));
|
|
26
|
+
return steps;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compute optional onboarding guidance shown after scaffolding completes.
|
|
30
|
+
*
|
|
31
|
+
* @param options Package-manager and template context for optional guidance.
|
|
32
|
+
* @returns Optional onboarding note and step list.
|
|
33
|
+
*/
|
|
34
|
+
export function getOptionalOnboarding({ availableScripts, packageManager, templateId, compoundPersistenceEnabled = false, }) {
|
|
35
|
+
return {
|
|
36
|
+
note: getOptionalOnboardingNote(packageManager, templateId, {
|
|
37
|
+
availableScripts,
|
|
38
|
+
compoundPersistenceEnabled,
|
|
39
|
+
}),
|
|
40
|
+
shortNote: getOptionalOnboardingShortNote(packageManager, templateId, {
|
|
41
|
+
availableScripts,
|
|
42
|
+
compoundPersistenceEnabled,
|
|
43
|
+
}),
|
|
44
|
+
steps: getOptionalOnboardingSteps(packageManager, templateId, {
|
|
45
|
+
availableScripts,
|
|
46
|
+
compoundPersistenceEnabled,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the project directory argument before template resolution.
|
|
3
|
+
*
|
|
4
|
+
* @param projectInput Raw project directory input from the CLI.
|
|
5
|
+
* @throws Error when the input is empty or points at the current/parent directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateCreateProjectInput(projectInput: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Collect warnings for project directory names that are awkward in shells.
|
|
10
|
+
*
|
|
11
|
+
* @param projectDir Absolute target project directory.
|
|
12
|
+
* @returns User-facing warning messages for non-fatal directory concerns.
|
|
13
|
+
*/
|
|
14
|
+
export declare function collectProjectDirectoryWarnings(projectDir: string): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Determine whether a template should resolve persistence-related options.
|
|
17
|
+
*
|
|
18
|
+
* @param templateId Resolved template id.
|
|
19
|
+
* @param options Explicit persistence flags provided by the caller.
|
|
20
|
+
* @returns True when persistence defaults or prompts should be applied.
|
|
21
|
+
*/
|
|
22
|
+
export declare function templateUsesPersistenceSettings(templateId: string, options: {
|
|
23
|
+
dataStorageMode?: string;
|
|
24
|
+
persistencePolicy?: string;
|
|
25
|
+
}): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Collect warnings for flags that do not apply to the selected template.
|
|
28
|
+
*
|
|
29
|
+
* @param options Template id and raw CLI flags that may be ignored.
|
|
30
|
+
* @returns User-facing warnings for non-fatal capability mismatches.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectTemplateCapabilityWarnings(options: {
|
|
33
|
+
queryPostType?: string;
|
|
34
|
+
templateId: string;
|
|
35
|
+
withMigrationUi?: boolean;
|
|
36
|
+
}): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Validate create flags that depend on the selected template capability set.
|
|
39
|
+
*
|
|
40
|
+
* @param options Raw create flags plus the resolved template id.
|
|
41
|
+
* @throws Error when a flag is unsupported for the selected template.
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateCreateFlagContract(options: {
|
|
44
|
+
alternateRenderTargets?: string;
|
|
45
|
+
dataStorageMode?: string;
|
|
46
|
+
innerBlocksPreset?: string;
|
|
47
|
+
persistencePolicy?: string;
|
|
48
|
+
templateId: string;
|
|
49
|
+
variant?: string;
|
|
50
|
+
}): void;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve an optional string selection from explicit input, prompts, or defaults.
|
|
53
|
+
*
|
|
54
|
+
* @param options Selection configuration including allowed values and prompt hooks.
|
|
55
|
+
* @returns The resolved value, or undefined when resolution is disabled.
|
|
56
|
+
* @throws Error when an explicit value is not in the allowed set.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveOptionalSelection<T extends string>({ defaultValue, explicitValue, isInteractive, isValue, label, allowedValues, select, shouldResolve, yes, }: {
|
|
59
|
+
defaultValue: T;
|
|
60
|
+
explicitValue?: string;
|
|
61
|
+
isInteractive: boolean;
|
|
62
|
+
isValue: (input: string) => input is T;
|
|
63
|
+
label: string;
|
|
64
|
+
allowedValues: readonly T[];
|
|
65
|
+
select?: () => Promise<T>;
|
|
66
|
+
shouldResolve?: boolean;
|
|
67
|
+
yes: boolean;
|
|
68
|
+
}): Promise<T | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve an optional boolean flag from explicit input, prompts, or defaults.
|
|
71
|
+
*
|
|
72
|
+
* @param options Boolean selection configuration and optional prompt hook.
|
|
73
|
+
* @returns The resolved boolean value.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveOptionalBooleanFlag({ defaultValue, disabled, explicitValue, isInteractive, select, yes, }: {
|
|
76
|
+
defaultValue?: boolean;
|
|
77
|
+
disabled?: boolean;
|
|
78
|
+
explicitValue?: boolean;
|
|
79
|
+
isInteractive: boolean;
|
|
80
|
+
select?: () => Promise<boolean>;
|
|
81
|
+
yes: boolean;
|
|
82
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseAlternateRenderTargets } from "./alternate-render-targets.js";
|
|
3
|
+
import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
|
|
4
|
+
import { assertBuiltInTemplateVariantAllowed } from "./cli-validation.js";
|
|
5
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
|
|
6
|
+
/**
|
|
7
|
+
* Validate the project directory argument before template resolution.
|
|
8
|
+
*
|
|
9
|
+
* @param projectInput Raw project directory input from the CLI.
|
|
10
|
+
* @throws Error when the input is empty or points at the current/parent directory.
|
|
11
|
+
*/
|
|
12
|
+
export function validateCreateProjectInput(projectInput) {
|
|
13
|
+
const normalizedProjectInput = projectInput.trim();
|
|
14
|
+
if (normalizedProjectInput.length === 0) {
|
|
15
|
+
throw new Error("Project directory is required. Usage: wp-typia create <project-dir> (or wp-typia <project-dir> when <project-dir> is the only positional argument).");
|
|
16
|
+
}
|
|
17
|
+
const normalizedProjectPath = path.normalize(normalizedProjectInput).replace(/[\\/]+$/u, "") ||
|
|
18
|
+
path.normalize(normalizedProjectInput);
|
|
19
|
+
if (normalizedProjectPath === "." || normalizedProjectPath === "..") {
|
|
20
|
+
throw new Error("`wp-typia create` requires a new project directory. Use an explicit child directory instead of `.` or `..`.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Collect warnings for project directory names that are awkward in shells.
|
|
25
|
+
*
|
|
26
|
+
* @param projectDir Absolute target project directory.
|
|
27
|
+
* @returns User-facing warning messages for non-fatal directory concerns.
|
|
28
|
+
*/
|
|
29
|
+
export function collectProjectDirectoryWarnings(projectDir) {
|
|
30
|
+
const warnings = [];
|
|
31
|
+
const projectName = path.basename(projectDir);
|
|
32
|
+
if (/\s/u.test(projectName)) {
|
|
33
|
+
warnings.push(`Project directory "${projectName}" contains spaces. The generated next-step commands will be quoted, but a simple kebab-case directory name is usually easier to use with shells and downstream tooling.`);
|
|
34
|
+
}
|
|
35
|
+
const shellSensitiveCharacters = Array.from(new Set(projectName.match(/[^A-Za-z0-9._ -]/gu) ?? []));
|
|
36
|
+
if (shellSensitiveCharacters.length > 0) {
|
|
37
|
+
warnings.push(`Project directory "${projectName}" contains shell-sensitive characters (${shellSensitiveCharacters.join(", ")}). Prefer letters, numbers, ".", "_" and "-" when possible.`);
|
|
38
|
+
}
|
|
39
|
+
return warnings;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Determine whether a template should resolve persistence-related options.
|
|
43
|
+
*
|
|
44
|
+
* @param templateId Resolved template id.
|
|
45
|
+
* @param options Explicit persistence flags provided by the caller.
|
|
46
|
+
* @returns True when persistence defaults or prompts should be applied.
|
|
47
|
+
*/
|
|
48
|
+
export function templateUsesPersistenceSettings(templateId, options) {
|
|
49
|
+
if (templateId === "persistence") {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (templateId !== "compound") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return Boolean(options.dataStorageMode || options.persistencePolicy);
|
|
56
|
+
}
|
|
57
|
+
function templateSupportsPersistenceFlags(templateId) {
|
|
58
|
+
return templateId === "persistence" || templateId === "compound";
|
|
59
|
+
}
|
|
60
|
+
function templateSupportsCompoundInnerBlocksPreset(templateId) {
|
|
61
|
+
return templateId === "compound";
|
|
62
|
+
}
|
|
63
|
+
function createTemplateLabel(templateId) {
|
|
64
|
+
return templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
65
|
+
? "`--template workspace`"
|
|
66
|
+
: `"${templateId}"`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Collect warnings for flags that do not apply to the selected template.
|
|
70
|
+
*
|
|
71
|
+
* @param options Template id and raw CLI flags that may be ignored.
|
|
72
|
+
* @returns User-facing warnings for non-fatal capability mismatches.
|
|
73
|
+
*/
|
|
74
|
+
export function collectTemplateCapabilityWarnings(options) {
|
|
75
|
+
const warnings = [];
|
|
76
|
+
const trimmedQueryPostType = options.queryPostType?.trim();
|
|
77
|
+
if (trimmedQueryPostType &&
|
|
78
|
+
options.templateId !== "query-loop" &&
|
|
79
|
+
(isBuiltInTemplateId(options.templateId) ||
|
|
80
|
+
options.templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE)) {
|
|
81
|
+
warnings.push(`\`--query-post-type\` only applies to \`wp-typia create --template query-loop\`, which scaffolds a create-time \`core/query\` variation instead of a standalone block. ${createTemplateLabel(options.templateId)} will ignore "${trimmedQueryPostType}".`);
|
|
82
|
+
}
|
|
83
|
+
if (options.withMigrationUi === true &&
|
|
84
|
+
!isBuiltInTemplateId(options.templateId) &&
|
|
85
|
+
options.templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
|
|
86
|
+
warnings.push(`\`--with-migration-ui\` was ignored for ${createTemplateLabel(options.templateId)}. Migration UI currently scaffolds built-in templates and the official \`--template workspace\` flow; external templates still need to opt into that surface explicitly.`);
|
|
87
|
+
}
|
|
88
|
+
return warnings;
|
|
89
|
+
}
|
|
90
|
+
function templateSupportsAlternateRenderTargets(options) {
|
|
91
|
+
if (!options.alternateRenderTargets) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (options.templateId === "persistence") {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (options.templateId !== "compound") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return templateUsesPersistenceSettings(options.templateId, {
|
|
101
|
+
dataStorageMode: options.dataStorageMode,
|
|
102
|
+
persistencePolicy: options.persistencePolicy,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate create flags that depend on the selected template capability set.
|
|
107
|
+
*
|
|
108
|
+
* @param options Raw create flags plus the resolved template id.
|
|
109
|
+
* @throws Error when a flag is unsupported for the selected template.
|
|
110
|
+
*/
|
|
111
|
+
export function validateCreateFlagContract(options) {
|
|
112
|
+
const { alternateRenderTargets, dataStorageMode, innerBlocksPreset, persistencePolicy, templateId, variant, } = options;
|
|
113
|
+
if ((dataStorageMode || persistencePolicy) &&
|
|
114
|
+
!templateSupportsPersistenceFlags(templateId)) {
|
|
115
|
+
throw new Error("`--data-storage` and `--persistence-policy` are supported only for `wp-typia create --template persistence` or `--template compound`.");
|
|
116
|
+
}
|
|
117
|
+
if (alternateRenderTargets &&
|
|
118
|
+
!templateSupportsAlternateRenderTargets({
|
|
119
|
+
alternateRenderTargets,
|
|
120
|
+
dataStorageMode,
|
|
121
|
+
persistencePolicy,
|
|
122
|
+
templateId,
|
|
123
|
+
})) {
|
|
124
|
+
if (templateId === "compound") {
|
|
125
|
+
throw new Error("`--alternate-render-targets` on `wp-typia create --template compound` requires the persistence-enabled server render path. Add `--data-storage <post-meta|custom-table>` or `--persistence-policy <authenticated|public>` first.");
|
|
126
|
+
}
|
|
127
|
+
throw new Error("`--alternate-render-targets` is supported only for `wp-typia create --template persistence` or persistence-enabled `--template compound` scaffolds.");
|
|
128
|
+
}
|
|
129
|
+
parseAlternateRenderTargets(alternateRenderTargets);
|
|
130
|
+
if (innerBlocksPreset &&
|
|
131
|
+
!templateSupportsCompoundInnerBlocksPreset(templateId)) {
|
|
132
|
+
throw new Error("`--inner-blocks-preset` is supported only for `wp-typia create --template compound`.");
|
|
133
|
+
}
|
|
134
|
+
parseCompoundInnerBlocksPreset(innerBlocksPreset);
|
|
135
|
+
if (isBuiltInTemplateId(templateId)) {
|
|
136
|
+
assertBuiltInTemplateVariantAllowed({
|
|
137
|
+
templateId,
|
|
138
|
+
variant,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function parseSelectableValue(label, value, isValue, allowedValues) {
|
|
143
|
+
if (isValue(value)) {
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unsupported ${label} "${value}". Expected one of: ${allowedValues.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve an optional string selection from explicit input, prompts, or defaults.
|
|
150
|
+
*
|
|
151
|
+
* @param options Selection configuration including allowed values and prompt hooks.
|
|
152
|
+
* @returns The resolved value, or undefined when resolution is disabled.
|
|
153
|
+
* @throws Error when an explicit value is not in the allowed set.
|
|
154
|
+
*/
|
|
155
|
+
export async function resolveOptionalSelection({ defaultValue, explicitValue, isInteractive, isValue, label, allowedValues, select, shouldResolve = true, yes, }) {
|
|
156
|
+
if (!shouldResolve) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (explicitValue !== undefined) {
|
|
160
|
+
return parseSelectableValue(label, explicitValue, isValue, allowedValues);
|
|
161
|
+
}
|
|
162
|
+
if (yes) {
|
|
163
|
+
return defaultValue;
|
|
164
|
+
}
|
|
165
|
+
if (isInteractive && select) {
|
|
166
|
+
return select();
|
|
167
|
+
}
|
|
168
|
+
return defaultValue;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolve an optional boolean flag from explicit input, prompts, or defaults.
|
|
172
|
+
*
|
|
173
|
+
* @param options Boolean selection configuration and optional prompt hook.
|
|
174
|
+
* @returns The resolved boolean value.
|
|
175
|
+
*/
|
|
176
|
+
export async function resolveOptionalBooleanFlag({ defaultValue = false, disabled = false, explicitValue, isInteractive, select, yes, }) {
|
|
177
|
+
if (disabled) {
|
|
178
|
+
return defaultValue;
|
|
179
|
+
}
|
|
180
|
+
if (typeof explicitValue === "boolean") {
|
|
181
|
+
return explicitValue;
|
|
182
|
+
}
|
|
183
|
+
if (yes) {
|
|
184
|
+
return defaultValue;
|
|
185
|
+
}
|
|
186
|
+
if (isInteractive && select) {
|
|
187
|
+
return select();
|
|
188
|
+
}
|
|
189
|
+
return defaultValue;
|
|
190
|
+
}
|
|
@@ -1,30 +1,12 @@
|
|
|
1
|
-
import { collectScaffoldAnswers
|
|
2
|
-
import
|
|
1
|
+
import { collectScaffoldAnswers } from "./scaffold.js";
|
|
2
|
+
import { type ScaffoldEmissionOptions, type ScaffoldInstallDependencies } from "./cli-scaffold-emission.js";
|
|
3
|
+
import type { DataStorageMode, PersistencePolicy } from "./scaffold.js";
|
|
3
4
|
import type { PackageManagerId } from "./package-managers.js";
|
|
4
5
|
import { type ExternalLayerSelectionOption } from "./external-layer-selection.js";
|
|
5
6
|
import type { TemplateDefinition } from "./template-registry.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
projectDir: string;
|
|
10
|
-
projectInput: string;
|
|
11
|
-
templateId: string;
|
|
12
|
-
}
|
|
13
|
-
interface GetOptionalOnboardingOptions {
|
|
14
|
-
availableScripts?: string[];
|
|
15
|
-
packageManager: PackageManagerId;
|
|
16
|
-
templateId: string;
|
|
17
|
-
compoundPersistenceEnabled?: boolean;
|
|
18
|
-
}
|
|
19
|
-
interface OptionalOnboardingGuidance {
|
|
20
|
-
note: string;
|
|
21
|
-
shortNote: string;
|
|
22
|
-
steps: string[];
|
|
23
|
-
}
|
|
24
|
-
export interface ScaffoldDryRunPlan {
|
|
25
|
-
dependencyInstall: "skipped-by-flag" | "would-install";
|
|
26
|
-
files: string[];
|
|
27
|
-
}
|
|
7
|
+
export { getNextSteps, getOptionalOnboarding } from "./cli-scaffold-output.js";
|
|
8
|
+
export type { OptionalOnboardingGuidance, ScaffoldNextStepsOptions, ScaffoldOptionalOnboardingOptions, } from "./cli-scaffold-output.js";
|
|
9
|
+
export type { ScaffoldDryRunPlan } from "./cli-scaffold-emission.js";
|
|
28
10
|
interface RunScaffoldFlowOptions {
|
|
29
11
|
allowExistingDir?: boolean;
|
|
30
12
|
alternateRenderTargets?: string;
|
|
@@ -33,12 +15,12 @@ interface RunScaffoldFlowOptions {
|
|
|
33
15
|
dryRun?: boolean;
|
|
34
16
|
externalLayerId?: string;
|
|
35
17
|
externalLayerSource?: string;
|
|
36
|
-
installDependencies?:
|
|
18
|
+
installDependencies?: ScaffoldInstallDependencies;
|
|
37
19
|
innerBlocksPreset?: string;
|
|
38
20
|
isInteractive?: boolean;
|
|
39
21
|
namespace?: string;
|
|
40
22
|
noInstall?: boolean;
|
|
41
|
-
onProgress?:
|
|
23
|
+
onProgress?: ScaffoldEmissionOptions["onProgress"];
|
|
42
24
|
packageManager?: string;
|
|
43
25
|
phpPrefix?: string;
|
|
44
26
|
profile?: string;
|
|
@@ -62,21 +44,6 @@ interface RunScaffoldFlowOptions {
|
|
|
62
44
|
withWpEnv?: boolean;
|
|
63
45
|
yes?: boolean;
|
|
64
46
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Build the printed next-step commands for a scaffolded project.
|
|
67
|
-
*
|
|
68
|
-
* @param options Project location and package-manager details used to format
|
|
69
|
-
* next-step commands.
|
|
70
|
-
* @returns Ordered shell commands shown after scaffolding succeeds.
|
|
71
|
-
*/
|
|
72
|
-
export declare function getNextSteps({ projectInput, projectDir, packageManager, noInstall, templateId, }: GetNextStepsOptions): string[];
|
|
73
|
-
/**
|
|
74
|
-
* Compute optional onboarding guidance shown after scaffolding completes.
|
|
75
|
-
*
|
|
76
|
-
* @param options Package-manager and template context for optional guidance.
|
|
77
|
-
* @returns Optional onboarding note and step list.
|
|
78
|
-
*/
|
|
79
|
-
export declare function getOptionalOnboarding({ availableScripts, packageManager, templateId, compoundPersistenceEnabled, }: GetOptionalOnboardingOptions): OptionalOnboardingGuidance;
|
|
80
47
|
/**
|
|
81
48
|
* Resolve scaffold options, prompts, and follow-up steps for one CLI run.
|
|
82
49
|
*
|
|
@@ -86,8 +53,8 @@ export declare function getOptionalOnboarding({ availableScripts, packageManager
|
|
|
86
53
|
*/
|
|
87
54
|
export declare function runScaffoldFlow({ projectInput, cwd, templateId, alternateRenderTargets, dataStorageMode, dryRun, externalLayerId, externalLayerSource, innerBlocksPreset, persistencePolicy, packageManager, namespace, profile, textDomain, phpPrefix, queryPostType, yes, noInstall, onProgress, isInteractive, allowExistingDir, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }: RunScaffoldFlowOptions): Promise<{
|
|
88
55
|
dryRun: boolean;
|
|
89
|
-
optionalOnboarding: OptionalOnboardingGuidance;
|
|
90
|
-
plan: ScaffoldDryRunPlan | undefined;
|
|
56
|
+
optionalOnboarding: import("./cli-scaffold-output.js").OptionalOnboardingGuidance;
|
|
57
|
+
plan: import("./cli-scaffold-emission.js").ScaffoldDryRunPlan | undefined;
|
|
91
58
|
projectDir: string;
|
|
92
59
|
projectInput: string;
|
|
93
60
|
packageManager: PackageManagerId;
|
|
@@ -101,4 +68,3 @@ export declare function runScaffoldFlow({ projectInput, cwd, templateId, alterna
|
|
|
101
68
|
variables: import("./scaffold.js").ScaffoldTemplateVariables;
|
|
102
69
|
};
|
|
103
70
|
}>;
|
|
104
|
-
export {};
|
|
@@ -1,272 +1,15 @@
|
|
|
1
|
-
import { promises as fsp } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDataStorageMode, resolveCreateProfileId, isPersistencePolicy, resolvePackageManagerId, resolveTemplateId,
|
|
4
|
-
import { parseAlternateRenderTargets } from "./alternate-render-targets.js";
|
|
2
|
+
import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDataStorageMode, resolveCreateProfileId, isPersistencePolicy, resolvePackageManagerId, resolveTemplateId, } from "./scaffold.js";
|
|
5
3
|
import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
|
|
6
4
|
import { isCompoundPersistenceEnabled } from "./scaffold-template-variable-groups.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { formatNonEmptyTargetDirectoryError } from "./scaffold-bootstrap.js";
|
|
12
|
-
import { pathExists } from "./fs-async.js";
|
|
13
|
-
import { readJsonFile } from "./json-utils.js";
|
|
5
|
+
import { buildScaffoldDryRunPlan, emitScaffoldProject, } from "./cli-scaffold-emission.js";
|
|
6
|
+
import { readGeneratedPackageScripts } from "./cli-scaffold-files.js";
|
|
7
|
+
import { getNextSteps, getOptionalOnboarding, } from "./cli-scaffold-output.js";
|
|
8
|
+
import { collectProjectDirectoryWarnings, collectTemplateCapabilityWarnings, resolveOptionalBooleanFlag, resolveOptionalSelection, templateUsesPersistenceSettings, validateCreateFlagContract, validateCreateProjectInput, } from "./cli-scaffold-validation.js";
|
|
14
9
|
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
|
|
15
10
|
import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const relativeFiles = [];
|
|
19
|
-
async function visit(currentDir) {
|
|
20
|
-
const entries = await fsp.readdir(currentDir, { withFileTypes: true });
|
|
21
|
-
for (const entry of entries) {
|
|
22
|
-
const absolutePath = path.join(currentDir, entry.name);
|
|
23
|
-
if (entry.isDirectory()) {
|
|
24
|
-
await visit(absolutePath);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
relativeFiles.push(path
|
|
28
|
-
.relative(rootDir, absolutePath)
|
|
29
|
-
.replace(path.sep === "\\" ? /\\/gu : /\//gu, "/"));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
await visit(rootDir);
|
|
33
|
-
return relativeFiles.sort((left, right) => left.localeCompare(right));
|
|
34
|
-
}
|
|
35
|
-
async function assertDryRunTargetDirectoryReady(projectDir, allowExistingDir) {
|
|
36
|
-
if (!(await pathExists(projectDir)) || allowExistingDir) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const entries = await fsp.readdir(projectDir);
|
|
40
|
-
if (entries.length > 0) {
|
|
41
|
-
throw new Error(formatNonEmptyTargetDirectoryError(projectDir));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
async function buildScaffoldDryRunPlan({ allowExistingDir, alternateRenderTargets, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, externalLayerSourceLabel, installDependencies, noInstall, onProgress, packageManager, persistencePolicy, profile, projectDir, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }) {
|
|
45
|
-
await assertDryRunTargetDirectoryReady(projectDir, allowExistingDir);
|
|
46
|
-
const { path: tempRoot, cleanup } = await createManagedTempRoot("wp-typia-scaffold-plan-");
|
|
47
|
-
const previewProjectDir = path.join(tempRoot, "preview-project");
|
|
48
|
-
try {
|
|
49
|
-
const result = await scaffoldProject({
|
|
50
|
-
allowExistingDir: false,
|
|
51
|
-
alternateRenderTargets,
|
|
52
|
-
answers,
|
|
53
|
-
cwd,
|
|
54
|
-
dataStorageMode,
|
|
55
|
-
externalLayerId,
|
|
56
|
-
externalLayerSource,
|
|
57
|
-
externalLayerSourceLabel,
|
|
58
|
-
installDependencies,
|
|
59
|
-
noInstall: true,
|
|
60
|
-
onProgress,
|
|
61
|
-
packageManager,
|
|
62
|
-
persistencePolicy,
|
|
63
|
-
profile,
|
|
64
|
-
projectDir: previewProjectDir,
|
|
65
|
-
templateId,
|
|
66
|
-
variant,
|
|
67
|
-
withMigrationUi,
|
|
68
|
-
withTestPreset,
|
|
69
|
-
withWpEnv,
|
|
70
|
-
});
|
|
71
|
-
const files = await listRelativeProjectFiles(previewProjectDir);
|
|
72
|
-
return {
|
|
73
|
-
plan: {
|
|
74
|
-
dependencyInstall: noInstall ? "skipped-by-flag" : "would-install",
|
|
75
|
-
files,
|
|
76
|
-
},
|
|
77
|
-
result,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
await cleanup();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function validateCreateProjectInput(projectInput) {
|
|
85
|
-
const normalizedProjectInput = projectInput.trim();
|
|
86
|
-
if (normalizedProjectInput.length === 0) {
|
|
87
|
-
throw new Error("Project directory is required. Usage: wp-typia create <project-dir> (or wp-typia <project-dir> when <project-dir> is the only positional argument).");
|
|
88
|
-
}
|
|
89
|
-
const normalizedProjectPath = path.normalize(normalizedProjectInput).replace(/[\\/]+$/u, "") ||
|
|
90
|
-
path.normalize(normalizedProjectInput);
|
|
91
|
-
if (normalizedProjectPath === "." || normalizedProjectPath === "..") {
|
|
92
|
-
throw new Error("`wp-typia create` requires a new project directory. Use an explicit child directory instead of `.` or `..`.");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function collectProjectDirectoryWarnings(projectDir) {
|
|
96
|
-
const warnings = [];
|
|
97
|
-
const projectName = path.basename(projectDir);
|
|
98
|
-
if (/\s/u.test(projectName)) {
|
|
99
|
-
warnings.push(`Project directory "${projectName}" contains spaces. The generated next-step commands will be quoted, but a simple kebab-case directory name is usually easier to use with shells and downstream tooling.`);
|
|
100
|
-
}
|
|
101
|
-
const shellSensitiveCharacters = Array.from(new Set(projectName.match(/[^A-Za-z0-9._ -]/gu) ?? []));
|
|
102
|
-
if (shellSensitiveCharacters.length > 0) {
|
|
103
|
-
warnings.push(`Project directory "${projectName}" contains shell-sensitive characters (${shellSensitiveCharacters.join(", ")}). Prefer letters, numbers, ".", "_" and "-" when possible.`);
|
|
104
|
-
}
|
|
105
|
-
return warnings;
|
|
106
|
-
}
|
|
107
|
-
function templateUsesPersistenceSettings(templateId, options) {
|
|
108
|
-
if (templateId === "persistence") {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
if (templateId !== "compound") {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
return Boolean(options.dataStorageMode || options.persistencePolicy);
|
|
115
|
-
}
|
|
116
|
-
function templateSupportsPersistenceFlags(templateId) {
|
|
117
|
-
return templateId === "persistence" || templateId === "compound";
|
|
118
|
-
}
|
|
119
|
-
function templateSupportsCompoundInnerBlocksPreset(templateId) {
|
|
120
|
-
return templateId === "compound";
|
|
121
|
-
}
|
|
122
|
-
function createTemplateLabel(templateId) {
|
|
123
|
-
return templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
124
|
-
? "`--template workspace`"
|
|
125
|
-
: `"${templateId}"`;
|
|
126
|
-
}
|
|
127
|
-
function collectTemplateCapabilityWarnings(options) {
|
|
128
|
-
const warnings = [];
|
|
129
|
-
const trimmedQueryPostType = options.queryPostType?.trim();
|
|
130
|
-
if (trimmedQueryPostType &&
|
|
131
|
-
options.templateId !== "query-loop" &&
|
|
132
|
-
(isBuiltInTemplateId(options.templateId) ||
|
|
133
|
-
options.templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE)) {
|
|
134
|
-
warnings.push(`\`--query-post-type\` only applies to \`wp-typia create --template query-loop\`, which scaffolds a create-time \`core/query\` variation instead of a standalone block. ${createTemplateLabel(options.templateId)} will ignore "${trimmedQueryPostType}".`);
|
|
135
|
-
}
|
|
136
|
-
if (options.withMigrationUi === true &&
|
|
137
|
-
!isBuiltInTemplateId(options.templateId) &&
|
|
138
|
-
options.templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
|
|
139
|
-
warnings.push(`\`--with-migration-ui\` was ignored for ${createTemplateLabel(options.templateId)}. Migration UI currently scaffolds built-in templates and the official \`--template workspace\` flow; external templates still need to opt into that surface explicitly.`);
|
|
140
|
-
}
|
|
141
|
-
return warnings;
|
|
142
|
-
}
|
|
143
|
-
function templateSupportsAlternateRenderTargets(options) {
|
|
144
|
-
if (!options.alternateRenderTargets) {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
if (options.templateId === "persistence") {
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
if (options.templateId !== "compound") {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
return templateUsesPersistenceSettings(options.templateId, {
|
|
154
|
-
dataStorageMode: options.dataStorageMode,
|
|
155
|
-
persistencePolicy: options.persistencePolicy,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
function validateCreateFlagContract(options) {
|
|
159
|
-
const { alternateRenderTargets, dataStorageMode, innerBlocksPreset, persistencePolicy, templateId, variant, } = options;
|
|
160
|
-
if ((dataStorageMode || persistencePolicy) &&
|
|
161
|
-
!templateSupportsPersistenceFlags(templateId)) {
|
|
162
|
-
throw new Error("`--data-storage` and `--persistence-policy` are supported only for `wp-typia create --template persistence` or `--template compound`.");
|
|
163
|
-
}
|
|
164
|
-
if (alternateRenderTargets &&
|
|
165
|
-
!templateSupportsAlternateRenderTargets({
|
|
166
|
-
alternateRenderTargets,
|
|
167
|
-
dataStorageMode,
|
|
168
|
-
persistencePolicy,
|
|
169
|
-
templateId,
|
|
170
|
-
})) {
|
|
171
|
-
if (templateId === "compound") {
|
|
172
|
-
throw new Error("`--alternate-render-targets` on `wp-typia create --template compound` requires the persistence-enabled server render path. Add `--data-storage <post-meta|custom-table>` or `--persistence-policy <authenticated|public>` first.");
|
|
173
|
-
}
|
|
174
|
-
throw new Error("`--alternate-render-targets` is supported only for `wp-typia create --template persistence` or persistence-enabled `--template compound` scaffolds.");
|
|
175
|
-
}
|
|
176
|
-
parseAlternateRenderTargets(alternateRenderTargets);
|
|
177
|
-
if (innerBlocksPreset &&
|
|
178
|
-
!templateSupportsCompoundInnerBlocksPreset(templateId)) {
|
|
179
|
-
throw new Error("`--inner-blocks-preset` is supported only for `wp-typia create --template compound`.");
|
|
180
|
-
}
|
|
181
|
-
parseCompoundInnerBlocksPreset(innerBlocksPreset);
|
|
182
|
-
if (isBuiltInTemplateId(templateId)) {
|
|
183
|
-
assertBuiltInTemplateVariantAllowed({
|
|
184
|
-
templateId,
|
|
185
|
-
variant,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function parseSelectableValue(label, value, isValue, allowedValues) {
|
|
190
|
-
if (isValue(value)) {
|
|
191
|
-
return value;
|
|
192
|
-
}
|
|
193
|
-
throw new Error(`Unsupported ${label} "${value}". Expected one of: ${allowedValues.join(", ")}`);
|
|
194
|
-
}
|
|
195
|
-
async function resolveOptionalSelection({ defaultValue, explicitValue, isInteractive, isValue, label, allowedValues, select, shouldResolve = true, yes, }) {
|
|
196
|
-
if (!shouldResolve) {
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
if (explicitValue) {
|
|
200
|
-
return parseSelectableValue(label, explicitValue, isValue, allowedValues);
|
|
201
|
-
}
|
|
202
|
-
if (yes) {
|
|
203
|
-
return defaultValue;
|
|
204
|
-
}
|
|
205
|
-
if (isInteractive && select) {
|
|
206
|
-
return select();
|
|
207
|
-
}
|
|
208
|
-
return defaultValue;
|
|
209
|
-
}
|
|
210
|
-
async function resolveOptionalBooleanFlag({ defaultValue = false, disabled = false, explicitValue, isInteractive, select, yes, }) {
|
|
211
|
-
if (disabled) {
|
|
212
|
-
return defaultValue;
|
|
213
|
-
}
|
|
214
|
-
if (typeof explicitValue === "boolean") {
|
|
215
|
-
return explicitValue;
|
|
216
|
-
}
|
|
217
|
-
if (yes) {
|
|
218
|
-
return defaultValue;
|
|
219
|
-
}
|
|
220
|
-
if (isInteractive && select) {
|
|
221
|
-
return select();
|
|
222
|
-
}
|
|
223
|
-
return defaultValue;
|
|
224
|
-
}
|
|
225
|
-
function quoteShellValue(value) {
|
|
226
|
-
if (!value.startsWith("-") &&
|
|
227
|
-
/^[A-Za-z0-9._/@:-]+(?:\/[A-Za-z0-9._@:-]+)*$/.test(value)) {
|
|
228
|
-
return value;
|
|
229
|
-
}
|
|
230
|
-
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Build the printed next-step commands for a scaffolded project.
|
|
234
|
-
*
|
|
235
|
-
* @param options Project location and package-manager details used to format
|
|
236
|
-
* next-step commands.
|
|
237
|
-
* @returns Ordered shell commands shown after scaffolding succeeds.
|
|
238
|
-
*/
|
|
239
|
-
export function getNextSteps({ projectInput, projectDir, packageManager, noInstall, templateId, }) {
|
|
240
|
-
const cdTarget = path.isAbsolute(projectInput) ? projectDir : projectInput;
|
|
241
|
-
const steps = [`cd ${quoteShellValue(cdTarget)}`];
|
|
242
|
-
if (noInstall) {
|
|
243
|
-
steps.push(formatInstallCommand(packageManager));
|
|
244
|
-
}
|
|
245
|
-
steps.push(formatRunScript(packageManager, getPrimaryDevelopmentScript(templateId)));
|
|
246
|
-
return steps;
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Compute optional onboarding guidance shown after scaffolding completes.
|
|
250
|
-
*
|
|
251
|
-
* @param options Package-manager and template context for optional guidance.
|
|
252
|
-
* @returns Optional onboarding note and step list.
|
|
253
|
-
*/
|
|
254
|
-
export function getOptionalOnboarding({ availableScripts, packageManager, templateId, compoundPersistenceEnabled = false, }) {
|
|
255
|
-
return {
|
|
256
|
-
note: getOptionalOnboardingNote(packageManager, templateId, {
|
|
257
|
-
availableScripts,
|
|
258
|
-
compoundPersistenceEnabled,
|
|
259
|
-
}),
|
|
260
|
-
shortNote: getOptionalOnboardingShortNote(packageManager, templateId, {
|
|
261
|
-
availableScripts,
|
|
262
|
-
compoundPersistenceEnabled,
|
|
263
|
-
}),
|
|
264
|
-
steps: getOptionalOnboardingSteps(packageManager, templateId, {
|
|
265
|
-
availableScripts,
|
|
266
|
-
compoundPersistenceEnabled,
|
|
267
|
-
}),
|
|
268
|
-
};
|
|
269
|
-
}
|
|
11
|
+
import { resolveLocalCliPathOption, normalizeOptionalCliString, } from "./cli-validation.js";
|
|
12
|
+
export { getNextSteps, getOptionalOnboarding } from "./cli-scaffold-output.js";
|
|
270
13
|
/**
|
|
271
14
|
* Resolve scaffold options, prompts, and follow-up steps for one CLI run.
|
|
272
15
|
*
|
|
@@ -381,73 +124,37 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
|
|
|
381
124
|
if (resolvedTemplateId === "compound" && resolvedInnerBlocksPreset) {
|
|
382
125
|
answers.compoundInnerBlocksPreset = resolvedInnerBlocksPreset;
|
|
383
126
|
}
|
|
127
|
+
const emissionOptions = {
|
|
128
|
+
allowExistingDir,
|
|
129
|
+
alternateRenderTargets,
|
|
130
|
+
answers,
|
|
131
|
+
cwd,
|
|
132
|
+
dataStorageMode: resolvedDataStorage,
|
|
133
|
+
externalLayerId: resolvedExternalLayerSelection.externalLayerId,
|
|
134
|
+
externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
|
|
135
|
+
externalLayerSourceLabel: normalizedExternalLayerSource,
|
|
136
|
+
installDependencies,
|
|
137
|
+
noInstall,
|
|
138
|
+
onProgress,
|
|
139
|
+
packageManager: resolvedPackageManager,
|
|
140
|
+
persistencePolicy: resolvedPersistencePolicy,
|
|
141
|
+
profile: resolvedProfile,
|
|
142
|
+
projectDir,
|
|
143
|
+
templateId: resolvedTemplateId,
|
|
144
|
+
variant,
|
|
145
|
+
withMigrationUi: resolvedWithMigrationUi,
|
|
146
|
+
withTestPreset: resolvedWithTestPreset,
|
|
147
|
+
withWpEnv: resolvedWithWpEnv,
|
|
148
|
+
};
|
|
384
149
|
const resolvedResult = dryRun
|
|
385
|
-
? await buildScaffoldDryRunPlan(
|
|
386
|
-
allowExistingDir,
|
|
387
|
-
alternateRenderTargets,
|
|
388
|
-
answers,
|
|
389
|
-
cwd,
|
|
390
|
-
dataStorageMode: resolvedDataStorage,
|
|
391
|
-
externalLayerId: resolvedExternalLayerSelection.externalLayerId,
|
|
392
|
-
externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
|
|
393
|
-
externalLayerSourceLabel: normalizedExternalLayerSource,
|
|
394
|
-
installDependencies,
|
|
395
|
-
noInstall,
|
|
396
|
-
onProgress,
|
|
397
|
-
packageManager: resolvedPackageManager,
|
|
398
|
-
persistencePolicy: resolvedPersistencePolicy,
|
|
399
|
-
profile: resolvedProfile,
|
|
400
|
-
projectDir,
|
|
401
|
-
templateId: resolvedTemplateId,
|
|
402
|
-
variant,
|
|
403
|
-
withMigrationUi: resolvedWithMigrationUi,
|
|
404
|
-
withTestPreset: resolvedWithTestPreset,
|
|
405
|
-
withWpEnv: resolvedWithWpEnv,
|
|
406
|
-
})
|
|
150
|
+
? await buildScaffoldDryRunPlan(emissionOptions)
|
|
407
151
|
: {
|
|
408
152
|
plan: undefined,
|
|
409
|
-
result: await
|
|
410
|
-
alternateRenderTargets,
|
|
411
|
-
answers,
|
|
412
|
-
allowExistingDir,
|
|
413
|
-
cwd,
|
|
414
|
-
dataStorageMode: resolvedDataStorage,
|
|
415
|
-
externalLayerId: resolvedExternalLayerSelection.externalLayerId,
|
|
416
|
-
externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
|
|
417
|
-
externalLayerSourceLabel: normalizedExternalLayerSource,
|
|
418
|
-
installDependencies,
|
|
419
|
-
noInstall,
|
|
420
|
-
onProgress,
|
|
421
|
-
packageManager: resolvedPackageManager,
|
|
422
|
-
persistencePolicy: resolvedPersistencePolicy,
|
|
423
|
-
profile: resolvedProfile,
|
|
424
|
-
projectDir,
|
|
425
|
-
templateId: resolvedTemplateId,
|
|
426
|
-
variant,
|
|
427
|
-
withMigrationUi: resolvedWithMigrationUi,
|
|
428
|
-
withTestPreset: resolvedWithTestPreset,
|
|
429
|
-
withWpEnv: resolvedWithWpEnv,
|
|
430
|
-
}),
|
|
153
|
+
result: await emitScaffoldProject(emissionOptions),
|
|
431
154
|
};
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const parsedPackageJson = await readJsonFile(path.join(projectDir, "package.json"), {
|
|
436
|
-
context: "generated package manifest",
|
|
437
|
-
});
|
|
438
|
-
const scripts = parsedPackageJson.scripts &&
|
|
439
|
-
typeof parsedPackageJson.scripts === "object" &&
|
|
440
|
-
!Array.isArray(parsedPackageJson.scripts)
|
|
441
|
-
? parsedPackageJson.scripts
|
|
442
|
-
: {};
|
|
443
|
-
availableScripts = Object.entries(scripts)
|
|
444
|
-
.filter(([, value]) => typeof value === "string")
|
|
445
|
-
.map(([scriptName]) => scriptName);
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
availableScripts = undefined;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
155
|
+
const availableScripts = dryRun
|
|
156
|
+
? undefined
|
|
157
|
+
: await readGeneratedPackageScripts(projectDir);
|
|
451
158
|
return {
|
|
452
159
|
dryRun,
|
|
453
160
|
optionalOnboarding: getOptionalOnboarding({
|