@wp-typia/project-tools 0.19.2 → 0.20.0
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 +23 -0
- package/dist/runtime/ability-spec.d.ts +90 -0
- package/dist/runtime/ability-spec.js +51 -0
- package/dist/runtime/ai-artifacts.d.ts +39 -0
- package/dist/runtime/ai-artifacts.js +68 -0
- package/dist/runtime/ai-feature-artifacts.d.ts +85 -0
- package/dist/runtime/ai-feature-artifacts.js +139 -0
- package/dist/runtime/ai-feature-capability.d.ts +114 -0
- package/dist/runtime/ai-feature-capability.js +150 -0
- package/dist/runtime/block-generator-service-spec.js +6 -0
- package/dist/runtime/cli-add-shared.d.ts +32 -1
- package/dist/runtime/cli-add-shared.js +44 -0
- package/dist/runtime/cli-add-workspace-ability.d.ts +8 -0
- package/dist/runtime/cli-add-workspace-ability.js +810 -0
- package/dist/runtime/cli-add-workspace-ai-anchors.d.ts +22 -0
- package/dist/runtime/cli-add-workspace-ai-anchors.js +277 -0
- package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +28 -0
- package/dist/runtime/cli-add-workspace-ai-source-emitters.js +346 -0
- package/dist/runtime/cli-add-workspace-ai.d.ts +14 -0
- package/dist/runtime/cli-add-workspace-ai.js +484 -0
- package/dist/runtime/cli-add-workspace.d.ts +10 -0
- package/dist/runtime/cli-add-workspace.js +10 -0
- package/dist/runtime/cli-add.d.ts +1 -1
- package/dist/runtime/cli-add.js +1 -1
- package/dist/runtime/cli-core.d.ts +3 -1
- package/dist/runtime/cli-core.js +3 -1
- package/dist/runtime/cli-diagnostics.d.ts +2 -0
- package/dist/runtime/cli-diagnostics.js +10 -1
- package/dist/runtime/cli-doctor-workspace.js +140 -1
- package/dist/runtime/cli-help.js +7 -0
- package/dist/runtime/cli-init.d.ts +49 -0
- package/dist/runtime/cli-init.js +354 -0
- package/dist/runtime/cli-scaffold.d.ts +1 -0
- package/dist/runtime/cli-scaffold.js +5 -1
- package/dist/runtime/cli-templates.js +36 -6
- package/dist/runtime/external-template-guards.d.ts +31 -0
- package/dist/runtime/external-template-guards.js +132 -0
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/package-managers.d.ts +8 -0
- package/dist/runtime/package-managers.js +13 -0
- package/dist/runtime/package-versions.d.ts +8 -0
- package/dist/runtime/package-versions.js +84 -19
- package/dist/runtime/scaffold-compatibility.d.ts +65 -0
- package/dist/runtime/scaffold-compatibility.js +152 -0
- package/dist/runtime/scaffold-documents.js +19 -1
- package/dist/runtime/scaffold-onboarding.d.ts +4 -0
- package/dist/runtime/scaffold-onboarding.js +25 -1
- package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
- package/dist/runtime/scaffold-template-variables.js +6 -0
- package/dist/runtime/scaffold.d.ts +3 -0
- package/dist/runtime/scaffold.js +2 -5
- package/dist/runtime/template-registry.d.ts +23 -1
- package/dist/runtime/template-registry.js +37 -3
- package/dist/runtime/template-source-external.js +9 -3
- package/dist/runtime/template-source-remote.js +5 -0
- package/dist/runtime/template-source-seeds.js +40 -6
- package/dist/runtime/typia-llm.d.ts +213 -0
- package/dist/runtime/typia-llm.js +348 -0
- package/dist/runtime/wordpress-ai.d.ts +122 -0
- package/dist/runtime/wordpress-ai.js +177 -0
- package/dist/runtime/workspace-inventory.d.ts +51 -4
- package/dist/runtime/workspace-inventory.js +157 -4
- package/package.json +17 -2
- package/templates/_shared/base/package.json.mustache +0 -1
- package/templates/_shared/base/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/_shared/compound/core/package.json.mustache +0 -1
- package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/_shared/compound/persistence/package.json.mustache +0 -1
- package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/_shared/persistence/core/package.json.mustache +0 -1
- package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +3 -3
- package/templates/interactivity/package.json.mustache +0 -1
- package/templates/query-loop/{{slugKebabCase}}.php.mustache +3 -3
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility policy helpers for generated scaffold outputs.
|
|
3
|
+
*
|
|
4
|
+
* The policy keeps plugin headers, runtime gates, and workspace inventory
|
|
5
|
+
* metadata aligned when optional or required AI-capable features are added.
|
|
6
|
+
*/
|
|
7
|
+
import { AI_FEATURE_DEFINITIONS, resolveAiFeatureCapabilityPlan, } from "./ai-feature-capability.js";
|
|
8
|
+
/**
|
|
9
|
+
* Baseline headers used by scaffold output before optional features are added.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_SCAFFOLD_COMPATIBILITY = {
|
|
12
|
+
requiresAtLeast: "6.7",
|
|
13
|
+
requiresPhp: "8.0",
|
|
14
|
+
testedUpTo: "6.9",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Optional WordPress AI Client surface used by server-only AI feature scaffold.
|
|
18
|
+
*/
|
|
19
|
+
export const OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY = [
|
|
20
|
+
{
|
|
21
|
+
featureId: AI_FEATURE_DEFINITIONS.wordpressAiClient.id,
|
|
22
|
+
mode: "optional",
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Required Abilities API surface used by typed workflow ability scaffold.
|
|
27
|
+
*/
|
|
28
|
+
export const REQUIRED_WORKSPACE_ABILITY_COMPATIBILITY = [
|
|
29
|
+
{
|
|
30
|
+
featureId: AI_FEATURE_DEFINITIONS.wordpressServerAbilities.id,
|
|
31
|
+
mode: "required",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
featureId: AI_FEATURE_DEFINITIONS.wordpressCoreAbilities.id,
|
|
35
|
+
mode: "required",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
function parseVersionFloorParts(value) {
|
|
39
|
+
return value.split(".").map((part, index) => {
|
|
40
|
+
if (!/^\d+$/u.test(part)) {
|
|
41
|
+
throw new Error(`parseVersionFloorParts received an invalid version floor "${value}" at segment ${index + 1}.`);
|
|
42
|
+
}
|
|
43
|
+
return Number.parseInt(part, 10);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function compareVersionFloors(left, right) {
|
|
47
|
+
const leftParts = parseVersionFloorParts(left);
|
|
48
|
+
const rightParts = parseVersionFloorParts(right);
|
|
49
|
+
const length = Math.max(leftParts.length, rightParts.length);
|
|
50
|
+
for (let index = 0; index < length; index += 1) {
|
|
51
|
+
const leftValue = leftParts[index] ?? 0;
|
|
52
|
+
const rightValue = rightParts[index] ?? 0;
|
|
53
|
+
if (leftValue > rightValue) {
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
if (leftValue < rightValue) {
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
function pickHigherVersionFloor(current, candidate) {
|
|
63
|
+
if (!candidate) {
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
return compareVersionFloors(current, candidate) >= 0 ? current : candidate;
|
|
67
|
+
}
|
|
68
|
+
function pickHigherHeaderVersionFloor(policyValue, currentValue) {
|
|
69
|
+
const normalizedCurrentValue = currentValue.trim();
|
|
70
|
+
if (!normalizedCurrentValue) {
|
|
71
|
+
return policyValue;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return pickHigherVersionFloor(policyValue, normalizedCurrentValue);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return policyValue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function formatRuntimeGate(feature) {
|
|
81
|
+
return (feature.runtimeGates ?? []).map((gate) => `${feature.label}: ${gate.kind} ${gate.value}`);
|
|
82
|
+
}
|
|
83
|
+
function getPolicyMode(capabilityPlan) {
|
|
84
|
+
if (capabilityPlan.requiredFeatures.length > 0) {
|
|
85
|
+
return "required";
|
|
86
|
+
}
|
|
87
|
+
if (capabilityPlan.optionalFeatures.length > 0) {
|
|
88
|
+
return "optional";
|
|
89
|
+
}
|
|
90
|
+
return "baseline";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve plugin header floors and capability gates for scaffold selections.
|
|
94
|
+
*/
|
|
95
|
+
export function resolveScaffoldCompatibilityPolicy(selections, { baseline = DEFAULT_SCAFFOLD_COMPATIBILITY, } = {}) {
|
|
96
|
+
const capabilityPlan = resolveAiFeatureCapabilityPlan(selections);
|
|
97
|
+
const requiresAtLeast = pickHigherVersionFloor(baseline.requiresAtLeast, capabilityPlan.hardMinimums.wordpress);
|
|
98
|
+
const requiresPhp = pickHigherVersionFloor(baseline.requiresPhp, capabilityPlan.hardMinimums.php);
|
|
99
|
+
const testedUpTo = pickHigherVersionFloor(baseline.testedUpTo, requiresAtLeast);
|
|
100
|
+
return {
|
|
101
|
+
capabilityPlan,
|
|
102
|
+
pluginHeader: {
|
|
103
|
+
requiresAtLeast,
|
|
104
|
+
requiresPhp,
|
|
105
|
+
testedUpTo,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Convert a resolved policy into workspace-inventory-safe JSON metadata.
|
|
111
|
+
*/
|
|
112
|
+
export function createScaffoldCompatibilityConfig(policy) {
|
|
113
|
+
const { capabilityPlan } = policy;
|
|
114
|
+
return {
|
|
115
|
+
hardMinimums: capabilityPlan.hardMinimums,
|
|
116
|
+
mode: getPolicyMode(capabilityPlan),
|
|
117
|
+
optionalFeatures: capabilityPlan.optionalFeatures.map((feature) => feature.label),
|
|
118
|
+
requiredFeatures: capabilityPlan.requiredFeatures.map((feature) => feature.label),
|
|
119
|
+
runtimeGates: [
|
|
120
|
+
...capabilityPlan.requiredFeatures.flatMap(formatRuntimeGate),
|
|
121
|
+
...capabilityPlan.optionalFeatures.flatMap(formatRuntimeGate),
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render compatibility metadata as formatted TypeScript object literal JSON.
|
|
127
|
+
*/
|
|
128
|
+
export function renderScaffoldCompatibilityConfig(policy, indent = "\t\t") {
|
|
129
|
+
const config = createScaffoldCompatibilityConfig(policy);
|
|
130
|
+
return JSON.stringify(config, null, "\t")
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line, index) => (index === 0 ? line : `${indent}${line}`))
|
|
133
|
+
.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function replacePluginHeaderVersionFloor(source, pattern, policyValue) {
|
|
136
|
+
return source.replace(pattern, (_match, prefix, currentValue, lineEnding) => {
|
|
137
|
+
const versionPrefix = prefix.endsWith(":") ? `${prefix} ` : prefix;
|
|
138
|
+
return `${versionPrefix}${pickHigherHeaderVersionFloor(policyValue, currentValue)}${lineEnding}`;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Patch a generated plugin bootstrap header without lowering custom floors.
|
|
143
|
+
*
|
|
144
|
+
* Preserves the original header line endings while replacing empty or invalid
|
|
145
|
+
* version strings with the policy values.
|
|
146
|
+
*/
|
|
147
|
+
export function updatePluginHeaderCompatibility(source, policy) {
|
|
148
|
+
const { pluginHeader } = policy;
|
|
149
|
+
const nextSource = replacePluginHeaderVersionFloor(source, /(\* Requires at least:[^\S\r\n]*)([^\r\n]*)(\r?)/u, pluginHeader.requiresAtLeast);
|
|
150
|
+
const nextSourceWithTestedUpTo = replacePluginHeaderVersionFloor(nextSource, /(\* Tested up to:[^\S\r\n]*)([^\r\n]*)(\r?)/u, pluginHeader.testedUpTo);
|
|
151
|
+
return replacePluginHeaderVersionFloor(nextSourceWithTestedUpTo, /(\* Requires PHP:[^\S\r\n]*)([^\r\n]*)(\r?)/u, pluginHeader.requiresPhp);
|
|
152
|
+
}
|
|
@@ -3,6 +3,24 @@ import { getCompoundExtensionWorkflowSection, getInitialCommitCommands, getIniti
|
|
|
3
3
|
import { formatPackageExecCommand, formatInstallCommand, formatRunScript, } from './package-managers.js';
|
|
4
4
|
import { getPackageVersions } from './package-versions.js';
|
|
5
5
|
import { getScaffoldTemplateVariableGroups } from './scaffold-template-variable-groups.js';
|
|
6
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_ALIAS, OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, normalizeTemplateLookupId, } from './template-registry.js';
|
|
7
|
+
function formatReadmeTemplateIdentity(templateId) {
|
|
8
|
+
const normalizedTemplateId = normalizeTemplateLookupId(templateId);
|
|
9
|
+
if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
|
|
10
|
+
return [
|
|
11
|
+
`- Alias: ${OFFICIAL_WORKSPACE_TEMPLATE_ALIAS}`,
|
|
12
|
+
`- Package: ${OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE}`,
|
|
13
|
+
'- Type: official workspace scaffold',
|
|
14
|
+
].join('\n');
|
|
15
|
+
}
|
|
16
|
+
if (normalizedTemplateId === 'query-loop') {
|
|
17
|
+
return ['- Family: query-loop', '- Type: create-time core/query variation scaffold'].join('\n');
|
|
18
|
+
}
|
|
19
|
+
if (isBuiltInTemplateId(normalizedTemplateId)) {
|
|
20
|
+
return [`- Family: ${normalizedTemplateId}`, '- Type: built-in block scaffold'].join('\n');
|
|
21
|
+
}
|
|
22
|
+
return [`- Template id: ${templateId}`, '- Type: custom or external scaffold'].join('\n');
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* Builds the generated README markdown for one scaffolded project.
|
|
8
26
|
*
|
|
@@ -73,7 +91,7 @@ ${variables.description}
|
|
|
73
91
|
|
|
74
92
|
## Template
|
|
75
93
|
|
|
76
|
-
${templateId}
|
|
94
|
+
${formatReadmeTemplateIdentity(templateId)}
|
|
77
95
|
|
|
78
96
|
## Quick Start
|
|
79
97
|
|
|
@@ -22,6 +22,10 @@ export declare function getQuickStartWorkflowNote(packageManager: PackageManager
|
|
|
22
22
|
* Returns the onboarding note explaining when manual sync is optional.
|
|
23
23
|
*/
|
|
24
24
|
export declare function getOptionalOnboardingNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
|
|
25
|
+
/**
|
|
26
|
+
* Returns a shorter optional onboarding note suitable for create completion output.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getOptionalOnboardingShortNote(packageManager: PackageManagerId, templateId?: string, options?: SyncOnboardingOptions): string;
|
|
25
29
|
/**
|
|
26
30
|
* Returns the recommended version-control commands for a fresh scaffold.
|
|
27
31
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatPackageExecCommand, formatRunScript, } from "./package-managers.js";
|
|
2
2
|
import { getPackageVersions } from "./package-versions.js";
|
|
3
3
|
import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
|
|
4
|
-
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
|
|
4
|
+
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, normalizeTemplateLookupId, } from "./template-registry.js";
|
|
5
5
|
const INITIAL_COMMIT_COMMANDS = [
|
|
6
6
|
"git init",
|
|
7
7
|
"git add .",
|
|
@@ -99,6 +99,30 @@ export function getOptionalOnboardingNote(packageManager, templateId = "basic",
|
|
|
99
99
|
}
|
|
100
100
|
return `You usually do not need to run ${syncCommand} during a normal ${formatRunScript(packageManager, developmentScript)} session. Run ${syncCommand} before ${formatRunScript(packageManager, "build")}, ${typecheckCommand}, or ${doctorCommand} when you want a reviewable refresh. ${syncTypesCommand} stays warn-only by default; use \`${failOnLossySyncCommand}\` or \`${strictSyncCommand}\` for stricter CI checks.${advancedPersistenceNote} Generated syncs do not create migration history, so refresh before your first commit if this directory is new.`;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Returns a shorter optional onboarding note suitable for create completion output.
|
|
104
|
+
*/
|
|
105
|
+
export function getOptionalOnboardingShortNote(packageManager, templateId = "basic", options = {}) {
|
|
106
|
+
const normalizedTemplateId = normalizeTemplateLookupId(templateId);
|
|
107
|
+
const doctorCommand = getDoctorVerificationCommand(packageManager);
|
|
108
|
+
if (normalizedTemplateId === "query-loop") {
|
|
109
|
+
return `No sync step is generated for this Query Loop scaffold. Edit the variation files directly, then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "typecheck")}, or ${doctorCommand} when you want a review pass.`;
|
|
110
|
+
}
|
|
111
|
+
const optionalSyncScripts = getOptionalSyncScriptNames(normalizedTemplateId, options);
|
|
112
|
+
const developmentScript = getPrimaryDevelopmentScript(normalizedTemplateId);
|
|
113
|
+
const devCommand = formatRunScript(packageManager, developmentScript);
|
|
114
|
+
const isCustomTemplate = !isBuiltInTemplateId(normalizedTemplateId) &&
|
|
115
|
+
normalizedTemplateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE;
|
|
116
|
+
if (isCustomTemplate && optionalSyncScripts.length === 0) {
|
|
117
|
+
return `Follow the template's own artifact-refresh guidance, then use ${doctorCommand} for a quick environment and workspace sanity check.`;
|
|
118
|
+
}
|
|
119
|
+
if (isCustomTemplate && optionalSyncScripts.length > 0) {
|
|
120
|
+
const syncSteps = optionalSyncScripts.map((scriptName) => formatRunScript(packageManager, scriptName));
|
|
121
|
+
return `Run ${syncSteps.join(" then ")} before build, typecheck, or ${doctorCommand} when you want a reviewable refresh.`;
|
|
122
|
+
}
|
|
123
|
+
const syncCommand = formatRunScript(packageManager, optionalSyncScripts.includes("sync") ? "sync" : "sync-types");
|
|
124
|
+
return `Skip ${syncCommand} during normal ${devCommand} work. Re-run it before build, typecheck, or ${doctorCommand} when you want a reviewable refresh.`;
|
|
125
|
+
}
|
|
102
126
|
/**
|
|
103
127
|
* Returns the recommended version-control commands for a fresh scaffold.
|
|
104
128
|
*/
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { ScaffoldPluginHeaderCompatibility } from "./scaffold-compatibility.js";
|
|
1
2
|
export declare const SCAFFOLD_TEMPLATE_VARIABLE_GROUPS: unique symbol;
|
|
2
3
|
export type ScaffoldTemplateFamily = "basic" | "interactivity" | "persistence" | "compound" | "query-loop" | "external";
|
|
3
4
|
export interface ScaffoldSharedTemplateVariableGroup {
|
|
4
5
|
author: string;
|
|
5
6
|
blockMetadataVersion: string;
|
|
6
7
|
category: string;
|
|
8
|
+
compatibility: ScaffoldPluginHeaderCompatibility;
|
|
7
9
|
cssClassName: string;
|
|
8
10
|
description: string;
|
|
9
11
|
descriptionJson: string;
|
|
@@ -6,6 +6,7 @@ import { DEFAULT_COMPOUND_INNER_BLOCKS_PRESET_ID, getCompoundInnerBlocksPresetDe
|
|
|
6
6
|
import { getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
|
|
7
7
|
import { toPascalCase, toSnakeCase, } from './string-case.js';
|
|
8
8
|
import { attachScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
|
|
9
|
+
import { resolveScaffoldCompatibilityPolicy } from "./scaffold-compatibility.js";
|
|
9
10
|
/**
|
|
10
11
|
* Build the normalized template variables used by scaffold rendering.
|
|
11
12
|
*
|
|
@@ -58,6 +59,7 @@ export function getTemplateVariables(templateId, answers) {
|
|
|
58
59
|
const persistencePolicy = templateId === 'persistence' || compoundPersistenceEnabled
|
|
59
60
|
? answers.persistencePolicy ?? 'authenticated'
|
|
60
61
|
: 'authenticated';
|
|
62
|
+
const compatibility = resolveScaffoldCompatibilityPolicy([]);
|
|
61
63
|
const flatVariables = {
|
|
62
64
|
alternateRenderTargetsCsv: '',
|
|
63
65
|
alternateRenderTargetsJson: '[]',
|
|
@@ -92,6 +94,8 @@ export function getTemplateVariables(templateId, answers) {
|
|
|
92
94
|
hasAlternatePlainTextRenderTarget: 'false',
|
|
93
95
|
hasAlternateRenderTargets: 'false',
|
|
94
96
|
projectToolsPackageVersion,
|
|
97
|
+
requiresAtLeast: compatibility.pluginHeader.requiresAtLeast,
|
|
98
|
+
requiresPhp: compatibility.pluginHeader.requiresPhp,
|
|
95
99
|
cssClassName,
|
|
96
100
|
dataStorageMode,
|
|
97
101
|
dashCase: slug,
|
|
@@ -118,6 +122,7 @@ export function getTemplateVariables(templateId, answers) {
|
|
|
118
122
|
phpPrefix,
|
|
119
123
|
phpPrefixUpper,
|
|
120
124
|
restPackageVersion,
|
|
125
|
+
testedUpTo: compatibility.pluginHeader.testedUpTo,
|
|
121
126
|
publicWriteRequestIdDeclaration: persistencePolicy === 'public'
|
|
122
127
|
? "publicWriteRequestId: string & tags.MinLength< 1 > & tags.MaxLength< 128 >;"
|
|
123
128
|
: "publicWriteRequestId?: string & tags.MinLength< 1 > & tags.MaxLength< 128 >;",
|
|
@@ -162,6 +167,7 @@ export function getTemplateVariables(templateId, answers) {
|
|
|
162
167
|
author: answers.author.trim(),
|
|
163
168
|
blockMetadataVersion: BUILTIN_BLOCK_METADATA_VERSION,
|
|
164
169
|
category: metadataDefaults?.category ?? template?.defaultCategory ?? 'widgets',
|
|
170
|
+
compatibility: compatibility.pluginHeader,
|
|
165
171
|
cssClassName,
|
|
166
172
|
description,
|
|
167
173
|
descriptionJson: JSON.stringify(description),
|
|
@@ -58,6 +58,8 @@ export interface FlatScaffoldTemplateVariables extends Record<string, string> {
|
|
|
58
58
|
hasAlternatePlainTextRenderTarget: "false" | "true";
|
|
59
59
|
hasAlternateRenderTargets: "false" | "true";
|
|
60
60
|
projectToolsPackageVersion: string;
|
|
61
|
+
requiresAtLeast: string;
|
|
62
|
+
requiresPhp: string;
|
|
61
63
|
cssClassName: string;
|
|
62
64
|
dashCase: string;
|
|
63
65
|
dataStorageMode: DataStorageMode;
|
|
@@ -90,6 +92,7 @@ export interface FlatScaffoldTemplateVariables extends Record<string, string> {
|
|
|
90
92
|
slugSnakeCase: string;
|
|
91
93
|
textDomain: string;
|
|
92
94
|
textdomain: string;
|
|
95
|
+
testedUpTo: string;
|
|
93
96
|
title: string;
|
|
94
97
|
titleJson: string;
|
|
95
98
|
titleCase: string;
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -8,13 +8,12 @@ import { applyMigrationUiCapability } from "./migration-ui-capability.js";
|
|
|
8
8
|
import { applyWorkspaceMigrationCapability, ensureScaffoldDirectory, isWorkspaceProject, seedBuiltInPersistenceArtifacts, writeStarterManifestFiles, } from "./scaffold-bootstrap.js";
|
|
9
9
|
import { defaultInstallDependencies, normalizePackageJson, normalizePackageManagerFiles, removeUnexpectedLockfiles, } from "./scaffold-package-manager-files.js";
|
|
10
10
|
import { copyInterpolatedDirectory } from "./template-render.js";
|
|
11
|
-
import {
|
|
11
|
+
import { isBuiltInTemplateId, normalizeTemplateLookupId, } from "./template-registry.js";
|
|
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
15
|
import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
|
|
16
16
|
import { assertExternalLayerCompositionOptions, } from "./cli-validation.js";
|
|
17
|
-
const WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
18
17
|
export const DATA_STORAGE_MODES = ["post-meta", "custom-table"];
|
|
19
18
|
export const PERSISTENCE_POLICIES = ["authenticated", "public"];
|
|
20
19
|
export { buildBlockCssClassName } from "./scaffold-identifiers.js";
|
|
@@ -28,9 +27,7 @@ export function isPersistencePolicy(value) {
|
|
|
28
27
|
return PERSISTENCE_POLICIES.includes(value);
|
|
29
28
|
}
|
|
30
29
|
function normalizeTemplateSelection(templateId) {
|
|
31
|
-
return templateId
|
|
32
|
-
? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
33
|
-
: templateId;
|
|
30
|
+
return normalizeTemplateLookupId(templateId);
|
|
34
31
|
}
|
|
35
32
|
async function reportScaffoldProgress(onProgress, event) {
|
|
36
33
|
await onProgress?.(event);
|
|
@@ -24,6 +24,7 @@ export declare const SHARED_TEST_PRESET_TEMPLATE_ROOT: string;
|
|
|
24
24
|
export declare const SHARED_WP_ENV_PRESET_TEMPLATE_ROOT: string;
|
|
25
25
|
export declare const BUILTIN_TEMPLATE_IDS: readonly ["basic", "interactivity", "persistence", "compound", "query-loop"];
|
|
26
26
|
export declare const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
27
|
+
export declare const OFFICIAL_WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
27
28
|
export type BuiltInTemplateId = (typeof BUILTIN_TEMPLATE_IDS)[number];
|
|
28
29
|
export type PersistencePolicy = "authenticated" | "public";
|
|
29
30
|
export interface TemplateDefinition {
|
|
@@ -36,10 +37,31 @@ export interface TemplateDefinition {
|
|
|
36
37
|
export declare const TEMPLATE_REGISTRY: readonly TemplateDefinition[];
|
|
37
38
|
export declare const TEMPLATE_IDS: BuiltInTemplateId[];
|
|
38
39
|
export declare function isBuiltInTemplateId(templateId: string): templateId is BuiltInTemplateId;
|
|
40
|
+
/**
|
|
41
|
+
* Returns whether a template id matches the user-facing official workspace alias.
|
|
42
|
+
*
|
|
43
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
44
|
+
* @returns `true` when the id is the `workspace` alias.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isOfficialWorkspaceTemplateAlias(templateId: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Converts user-facing template aliases into the registry lookup id.
|
|
49
|
+
*
|
|
50
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
51
|
+
* @returns The registry id used for template resolution.
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeTemplateLookupId(templateId: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Converts internal template ids into the id shown in human-facing output.
|
|
56
|
+
*
|
|
57
|
+
* @param templateId Template id stored in the registry.
|
|
58
|
+
* @returns The user-facing template id or alias for display.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getUserFacingTemplateId(templateId: string): string;
|
|
39
61
|
export declare function listTemplates(): readonly TemplateDefinition[];
|
|
40
62
|
export declare function getTemplateById(templateId: string): TemplateDefinition;
|
|
41
63
|
export declare function getTemplateSelectOptions(): Array<{
|
|
42
64
|
label: string;
|
|
43
|
-
value:
|
|
65
|
+
value: string;
|
|
44
66
|
hint: string;
|
|
45
67
|
}>;
|
|
@@ -81,6 +81,7 @@ export const BUILTIN_TEMPLATE_IDS = [
|
|
|
81
81
|
"query-loop",
|
|
82
82
|
];
|
|
83
83
|
export const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
84
|
+
export const OFFICIAL_WORKSPACE_TEMPLATE_ALIAS = "workspace";
|
|
84
85
|
export const TEMPLATE_REGISTRY = Object.freeze([
|
|
85
86
|
{
|
|
86
87
|
id: "basic",
|
|
@@ -129,14 +130,47 @@ export const TEMPLATE_IDS = [...BUILTIN_TEMPLATE_IDS];
|
|
|
129
130
|
export function isBuiltInTemplateId(templateId) {
|
|
130
131
|
return BUILTIN_TEMPLATE_IDS.includes(templateId);
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns whether a template id matches the user-facing official workspace alias.
|
|
135
|
+
*
|
|
136
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
137
|
+
* @returns `true` when the id is the `workspace` alias.
|
|
138
|
+
*/
|
|
139
|
+
export function isOfficialWorkspaceTemplateAlias(templateId) {
|
|
140
|
+
return templateId === OFFICIAL_WORKSPACE_TEMPLATE_ALIAS;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Converts user-facing template aliases into the registry lookup id.
|
|
144
|
+
*
|
|
145
|
+
* @param templateId Template id or alias supplied by a caller.
|
|
146
|
+
* @returns The registry id used for template resolution.
|
|
147
|
+
*/
|
|
148
|
+
export function normalizeTemplateLookupId(templateId) {
|
|
149
|
+
return isOfficialWorkspaceTemplateAlias(templateId)
|
|
150
|
+
? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
151
|
+
: templateId;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Converts internal template ids into the id shown in human-facing output.
|
|
155
|
+
*
|
|
156
|
+
* @param templateId Template id stored in the registry.
|
|
157
|
+
* @returns The user-facing template id or alias for display.
|
|
158
|
+
*/
|
|
159
|
+
export function getUserFacingTemplateId(templateId) {
|
|
160
|
+
return templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
|
|
161
|
+
? OFFICIAL_WORKSPACE_TEMPLATE_ALIAS
|
|
162
|
+
: templateId;
|
|
163
|
+
}
|
|
132
164
|
export function listTemplates() {
|
|
133
165
|
return TEMPLATE_REGISTRY;
|
|
134
166
|
}
|
|
135
167
|
export function getTemplateById(templateId) {
|
|
136
|
-
const
|
|
168
|
+
const normalizedTemplateId = normalizeTemplateLookupId(templateId);
|
|
169
|
+
const template = TEMPLATE_REGISTRY.find((entry) => entry.id === normalizedTemplateId);
|
|
137
170
|
if (!template) {
|
|
138
171
|
throw new Error(`Unknown template "${templateId}". Expected one of: ${[
|
|
139
172
|
...TEMPLATE_IDS,
|
|
173
|
+
OFFICIAL_WORKSPACE_TEMPLATE_ALIAS,
|
|
140
174
|
OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE,
|
|
141
175
|
].join(", ")}`);
|
|
142
176
|
}
|
|
@@ -144,8 +178,8 @@ export function getTemplateById(templateId) {
|
|
|
144
178
|
}
|
|
145
179
|
export function getTemplateSelectOptions() {
|
|
146
180
|
return TEMPLATE_REGISTRY.map((template) => ({
|
|
147
|
-
label: template.id,
|
|
148
|
-
value: template.id,
|
|
181
|
+
label: getUserFacingTemplateId(template.id),
|
|
182
|
+
value: getUserFacingTemplateId(template.id),
|
|
149
183
|
hint: template.features.join(", "),
|
|
150
184
|
}));
|
|
151
185
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { assertExternalTemplateFileSize, getExternalTemplateConfigMaxBytes, withExternalTemplateTimeout, } from './external-template-guards.js';
|
|
5
6
|
import { isPlainObject } from './object-utils.js';
|
|
6
7
|
import { toSegmentPascalCase } from './string-case.js';
|
|
7
8
|
import { createManagedTempRoot } from './temp-roots.js';
|
|
@@ -47,8 +48,12 @@ async function loadExternalTemplateConfig(sourceDir) {
|
|
|
47
48
|
if (!entryPath) {
|
|
48
49
|
throw new Error(`No external template config entry found in ${sourceDir}.`);
|
|
49
50
|
}
|
|
51
|
+
assertExternalTemplateFileSize(entryPath, {
|
|
52
|
+
label: `External template config "${entryPath}"`,
|
|
53
|
+
maxBytes: getExternalTemplateConfigMaxBytes(),
|
|
54
|
+
});
|
|
50
55
|
const moduleUrl = `${pathToFileURL(entryPath).href}?mtime=${fs.statSync(entryPath).mtimeMs}`;
|
|
51
|
-
const loadedModule = (await import(moduleUrl));
|
|
56
|
+
const loadedModule = (await withExternalTemplateTimeout(`loading external template config "${entryPath}"`, () => import(moduleUrl)));
|
|
52
57
|
const loadedConfig = loadedModule.default ?? loadedModule;
|
|
53
58
|
if (!isPlainObject(loadedConfig)) {
|
|
54
59
|
throw new Error(`External template config must export an object: ${entryPath}`);
|
|
@@ -150,10 +155,11 @@ async function buildExternalTemplateView(context, config, selectedVariant, varia
|
|
|
150
155
|
mergedView.variant = selectedVariant;
|
|
151
156
|
mergedView[getVariantFlagName(selectedVariant)] = true;
|
|
152
157
|
}
|
|
153
|
-
|
|
158
|
+
const transformer = config.transformer;
|
|
159
|
+
if (!transformer) {
|
|
154
160
|
return mergedView;
|
|
155
161
|
}
|
|
156
|
-
const transformed = await
|
|
162
|
+
const transformed = await withExternalTemplateTimeout(`running external template transformer for "${context.slug}"`, () => Promise.resolve(transformer(mergedView)));
|
|
157
163
|
if (!isPlainObject(transformed)) {
|
|
158
164
|
throw new Error('External template transformer(view) must return an object.');
|
|
159
165
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { promises as fsp } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { assertExternalTemplateFileSize, getExternalTemplatePackageJsonMaxBytes, } from './external-template-guards.js';
|
|
4
5
|
import { getBuiltInTemplateLayerDirs, isOmittableBuiltInTemplateLayerDir, } from './template-builtins.js';
|
|
5
6
|
import { copyRawDirectory } from './template-render.js';
|
|
6
7
|
import { createManagedTempRoot } from './temp-roots.js';
|
|
@@ -66,6 +67,10 @@ function readTemplatePackageJson(sourceDir) {
|
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
try {
|
|
70
|
+
assertExternalTemplateFileSize(candidate, {
|
|
71
|
+
label: `Template metadata file "${candidate}"`,
|
|
72
|
+
maxBytes: getExternalTemplatePackageJsonMaxBytes(),
|
|
73
|
+
});
|
|
69
74
|
return {
|
|
70
75
|
packageJson: JSON.parse(fs.readFileSync(candidate, 'utf8')),
|
|
71
76
|
sourcePath: candidate,
|
|
@@ -2,9 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { promises as fsp } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
6
|
import semver from 'semver';
|
|
7
7
|
import { x as extractTarball } from 'tar';
|
|
8
|
+
import { createExternalTemplateTimeoutError, fetchWithExternalTemplateTimeout, getExternalTemplateMetadataMaxBytes, getExternalTemplateTarballMaxBytes, getExternalTemplateTimeoutMs, readBufferResponseWithLimit, readJsonResponseWithLimit, } from './external-template-guards.js';
|
|
8
9
|
import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, PROJECT_TOOLS_PACKAGE_ROOT, } from './template-registry.js';
|
|
9
10
|
import { isPlainObject } from './object-utils.js';
|
|
10
11
|
import { createManagedTempRoot } from './temp-roots.js';
|
|
@@ -43,11 +44,17 @@ function selectRegistryVersion(metadata, locator) {
|
|
|
43
44
|
}
|
|
44
45
|
async function fetchNpmTemplateSource(locator) {
|
|
45
46
|
const registryBase = (process.env.NPM_CONFIG_REGISTRY ?? 'https://registry.npmjs.org').replace(/\/$/, '');
|
|
46
|
-
const
|
|
47
|
+
const metadataLabel = `fetching npm template metadata for ${locator.raw}`;
|
|
48
|
+
const metadataResponse = await fetchWithExternalTemplateTimeout(`${registryBase}/${encodeURIComponent(locator.name)}`, {
|
|
49
|
+
label: metadataLabel,
|
|
50
|
+
});
|
|
47
51
|
if (!metadataResponse.ok) {
|
|
48
52
|
throw new Error(`Failed to fetch npm template metadata for ${locator.raw}: ${metadataResponse.status}`);
|
|
49
53
|
}
|
|
50
|
-
const metadata =
|
|
54
|
+
const metadata = await readJsonResponseWithLimit(metadataResponse, {
|
|
55
|
+
label: `npm template metadata for ${locator.raw}`,
|
|
56
|
+
maxBytes: getExternalTemplateMetadataMaxBytes(),
|
|
57
|
+
});
|
|
51
58
|
const resolvedVersion = selectRegistryVersion(metadata, locator);
|
|
52
59
|
const versions = isPlainObject(metadata.versions) ? metadata.versions : {};
|
|
53
60
|
const versionMetadata = versions[resolvedVersion];
|
|
@@ -60,14 +67,19 @@ async function fetchNpmTemplateSource(locator) {
|
|
|
60
67
|
}
|
|
61
68
|
const { path: tempRoot, cleanup } = await createManagedTempRoot('wp-typia-template-source-');
|
|
62
69
|
try {
|
|
63
|
-
const tarballResponse = await
|
|
70
|
+
const tarballResponse = await fetchWithExternalTemplateTimeout(tarballUrl, {
|
|
71
|
+
label: `downloading npm template tarball for ${locator.raw}@${resolvedVersion}`,
|
|
72
|
+
});
|
|
64
73
|
if (!tarballResponse.ok) {
|
|
65
74
|
throw new Error(`Failed to download npm template tarball for ${locator.raw}: ${tarballResponse.status}`);
|
|
66
75
|
}
|
|
67
76
|
const tarballPath = path.join(tempRoot, 'template.tgz');
|
|
68
77
|
const unpackDir = path.join(tempRoot, 'source');
|
|
69
78
|
await fsp.mkdir(unpackDir, { recursive: true });
|
|
70
|
-
await fsp.writeFile(tarballPath,
|
|
79
|
+
await fsp.writeFile(tarballPath, await readBufferResponseWithLimit(tarballResponse, {
|
|
80
|
+
label: `npm template tarball for ${locator.raw}@${resolvedVersion}`,
|
|
81
|
+
maxBytes: getExternalTemplateTarballMaxBytes(),
|
|
82
|
+
}));
|
|
71
83
|
await extractTarball({
|
|
72
84
|
cwd: unpackDir,
|
|
73
85
|
file: tarballPath,
|
|
@@ -183,7 +195,29 @@ async function resolveGitHubTemplateSource(locator) {
|
|
|
183
195
|
args.push('--branch', locator.ref);
|
|
184
196
|
}
|
|
185
197
|
args.push(`https://github.com/${locator.owner}/${locator.repo}.git`, checkoutDir);
|
|
186
|
-
|
|
198
|
+
const cloneTimeoutMs = getExternalTemplateTimeoutMs();
|
|
199
|
+
const cloneResult = spawnSync('git', args, {
|
|
200
|
+
stdio: 'ignore',
|
|
201
|
+
timeout: cloneTimeoutMs,
|
|
202
|
+
});
|
|
203
|
+
if (cloneResult.error) {
|
|
204
|
+
const errorCode = typeof cloneResult.error === 'object' &&
|
|
205
|
+
cloneResult.error !== null &&
|
|
206
|
+
'code' in cloneResult.error
|
|
207
|
+
? String(cloneResult.error.code)
|
|
208
|
+
: '';
|
|
209
|
+
if (errorCode === 'ETIMEDOUT') {
|
|
210
|
+
throw createExternalTemplateTimeoutError(`cloning GitHub template ${locator.owner}/${locator.repo}`, cloneTimeoutMs);
|
|
211
|
+
}
|
|
212
|
+
throw cloneResult.error;
|
|
213
|
+
}
|
|
214
|
+
if (cloneResult.signal === 'SIGTERM' ||
|
|
215
|
+
cloneResult.signal === 'SIGKILL') {
|
|
216
|
+
throw createExternalTemplateTimeoutError(`cloning GitHub template ${locator.owner}/${locator.repo}`, cloneTimeoutMs);
|
|
217
|
+
}
|
|
218
|
+
if (cloneResult.status !== 0) {
|
|
219
|
+
throw new Error(`Failed to clone GitHub template source ${locator.owner}/${locator.repo}.`);
|
|
220
|
+
}
|
|
187
221
|
const sourceDir = path.resolve(checkoutDir, locator.sourcePath);
|
|
188
222
|
const relativeSourceDir = path.relative(checkoutDir, sourceDir);
|
|
189
223
|
if (relativeSourceDir.startsWith('..') ||
|