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