@wp-typia/project-tools 0.16.13 → 0.17.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.
Files changed (38) hide show
  1. package/README.md +0 -1
  2. package/dist/runtime/block-generator-service-spec.d.ts +6 -0
  3. package/dist/runtime/block-generator-service-spec.js +27 -0
  4. package/dist/runtime/built-in-block-code-artifacts.js +14 -1
  5. package/dist/runtime/built-in-block-code-templates/query-loop.d.ts +1 -0
  6. package/dist/runtime/built-in-block-code-templates/query-loop.js +70 -0
  7. package/dist/runtime/built-in-block-code-templates.d.ts +1 -0
  8. package/dist/runtime/built-in-block-code-templates.js +1 -0
  9. package/dist/runtime/built-in-block-non-ts-artifacts.js +2 -0
  10. package/dist/runtime/cli-help.js +1 -0
  11. package/dist/runtime/cli-prompt.js +78 -19
  12. package/dist/runtime/cli-scaffold.d.ts +2 -1
  13. package/dist/runtime/cli-scaffold.js +2 -1
  14. package/dist/runtime/local-dev-presets.js +21 -11
  15. package/dist/runtime/scaffold-answer-resolution.d.ts +37 -0
  16. package/dist/runtime/scaffold-answer-resolution.js +163 -0
  17. package/dist/runtime/scaffold-apply-utils.d.ts +1 -16
  18. package/dist/runtime/scaffold-apply-utils.js +4 -128
  19. package/dist/runtime/scaffold-documents.d.ts +34 -0
  20. package/dist/runtime/scaffold-documents.js +143 -0
  21. package/dist/runtime/scaffold-onboarding.js +12 -0
  22. package/dist/runtime/scaffold-template-variables.d.ts +9 -0
  23. package/dist/runtime/scaffold-template-variables.js +117 -0
  24. package/dist/runtime/scaffold.d.ts +19 -9
  25. package/dist/runtime/scaffold.js +6 -202
  26. package/dist/runtime/template-defaults.d.ts +7 -0
  27. package/dist/runtime/template-defaults.js +4 -0
  28. package/dist/runtime/template-registry.d.ts +1 -1
  29. package/dist/runtime/template-registry.js +14 -1
  30. package/package.json +3 -3
  31. package/templates/query-loop/inc/query-runtime.php.mustache +85 -0
  32. package/templates/query-loop/package.json.mustache +32 -0
  33. package/templates/query-loop/src/patterns/grid.php.mustache +49 -0
  34. package/templates/query-loop/src/patterns/list.php.mustache +48 -0
  35. package/templates/query-loop/src/query-extension.ts.mustache +41 -0
  36. package/templates/query-loop/src/validator-toolkit.ts.mustache +1 -0
  37. package/templates/query-loop/webpack.config.js.mustache +16 -0
  38. package/templates/query-loop/{{slugKebabCase}}.php.mustache +84 -0
@@ -0,0 +1,163 @@
1
+ import { PACKAGE_MANAGER_IDS, getPackageManager, } from './package-managers.js';
2
+ import { normalizeBlockSlug, resolveScaffoldIdentifiers, validateBlockSlug, validateNamespace, } from './scaffold-identifiers.js';
3
+ import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
4
+ import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from './template-defaults.js';
5
+ import { toSnakeCase, toTitleCase, } from './string-case.js';
6
+ const WORKSPACE_TEMPLATE_ALIAS = 'workspace';
7
+ /**
8
+ * Detect the current author name from local Git config.
9
+ *
10
+ * @returns The configured Git author name, or `"Your Name"` when unavailable.
11
+ */
12
+ export function detectAuthor() {
13
+ try {
14
+ return (execSync('git config user.name', {
15
+ encoding: 'utf8',
16
+ stdio: ['ignore', 'pipe', 'ignore'],
17
+ }).trim() || 'Your Name');
18
+ }
19
+ catch {
20
+ return 'Your Name';
21
+ }
22
+ }
23
+ /**
24
+ * Compute the default scaffold answers for one project and template pair.
25
+ *
26
+ * @param projectName User-supplied project directory or block name seed.
27
+ * @param templateId Selected scaffold template identifier.
28
+ * @returns Normalized default answers for scaffold prompts and non-interactive flows.
29
+ */
30
+ export function getDefaultAnswers(projectName, templateId) {
31
+ const template = isBuiltInTemplateId(templateId) ? getTemplateById(templateId) : null;
32
+ const slugDefault = normalizeBlockSlug(projectName) || 'my-wp-typia-block';
33
+ return {
34
+ author: detectAuthor(),
35
+ dataStorageMode: templateId === 'persistence' ? 'custom-table' : undefined,
36
+ description: template?.description ?? 'A WordPress block scaffolded from a remote template',
37
+ namespace: slugDefault,
38
+ persistencePolicy: templateId === 'persistence' ? 'authenticated' : undefined,
39
+ phpPrefix: toSnakeCase(slugDefault),
40
+ queryPostType: templateId === 'query-loop' ? 'post' : undefined,
41
+ slug: slugDefault,
42
+ textDomain: slugDefault,
43
+ title: toTitleCase(slugDefault),
44
+ };
45
+ }
46
+ function validateQueryPostType(value) {
47
+ const normalizedValue = value.trim().toLowerCase();
48
+ if (normalizedValue.length === 0) {
49
+ return 'Query post type is required.';
50
+ }
51
+ if (!/^[a-z0-9_-]{1,20}$/u.test(normalizedValue)) {
52
+ return 'Query post type must be lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".';
53
+ }
54
+ return true;
55
+ }
56
+ function normalizeQueryPostType(value) {
57
+ if (typeof value !== 'string') {
58
+ return undefined;
59
+ }
60
+ const normalizedValue = value.trim().toLowerCase();
61
+ if (validateQueryPostType(normalizedValue) !== true) {
62
+ throw new Error('Query post type must be lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".');
63
+ }
64
+ return normalizedValue;
65
+ }
66
+ function normalizeTemplateSelection(templateId) {
67
+ return templateId === WORKSPACE_TEMPLATE_ALIAS
68
+ ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
69
+ : templateId;
70
+ }
71
+ /**
72
+ * Resolve the scaffold template id from flags, defaults, and interactive selection.
73
+ *
74
+ * @param options Template resolution options for interactive and non-interactive flows.
75
+ * @returns The normalized template identifier to scaffold.
76
+ */
77
+ export async function resolveTemplateId({ templateId, yes = false, isInteractive = false, selectTemplate, }) {
78
+ if (templateId) {
79
+ const normalizedTemplateId = normalizeTemplateSelection(templateId);
80
+ if (isRemovedBuiltInTemplateId(templateId)) {
81
+ throw new Error(getRemovedBuiltInTemplateMessage(templateId));
82
+ }
83
+ if (isBuiltInTemplateId(normalizedTemplateId)) {
84
+ return getTemplateById(normalizedTemplateId).id;
85
+ }
86
+ return normalizedTemplateId;
87
+ }
88
+ if (yes) {
89
+ return 'basic';
90
+ }
91
+ if (!isInteractive || !selectTemplate) {
92
+ throw new Error(`Template is required in non-interactive mode. Use --template <${TEMPLATE_IDS.join('|')}|./path|github:owner/repo/path[#ref]|npm-package>.`);
93
+ }
94
+ return normalizeTemplateSelection(await selectTemplate());
95
+ }
96
+ /**
97
+ * Resolve the package manager id from flags, defaults, and interactive selection.
98
+ *
99
+ * @param options Package manager resolution options for interactive and non-interactive flows.
100
+ * @returns The normalized package manager id.
101
+ */
102
+ export async function resolvePackageManagerId({ packageManager, yes = false, isInteractive = false, selectPackageManager, }) {
103
+ if (packageManager) {
104
+ return getPackageManager(packageManager).id;
105
+ }
106
+ if (yes) {
107
+ return 'npm';
108
+ }
109
+ if (!isInteractive || !selectPackageManager) {
110
+ throw new Error(`Package manager is required in non-interactive mode. Use --package-manager <${PACKAGE_MANAGER_IDS.join('|')}>.`);
111
+ }
112
+ return selectPackageManager();
113
+ }
114
+ /**
115
+ * Collect scaffold answers from defaults, CLI overrides, and optional prompts.
116
+ *
117
+ * @param options Answer collection inputs including prompt callbacks and explicit overrides.
118
+ * @returns The normalized scaffold answers used for rendering and file generation.
119
+ */
120
+ export async function collectScaffoldAnswers({ projectName, templateId, yes = false, dataStorageMode, namespace, persistencePolicy, phpPrefix, promptText, queryPostType, textDomain, }) {
121
+ const defaults = getDefaultAnswers(projectName, templateId);
122
+ if (yes) {
123
+ const identifiers = resolveScaffoldIdentifiers({
124
+ namespace: namespace ?? defaults.namespace,
125
+ phpPrefix,
126
+ slug: defaults.slug,
127
+ textDomain,
128
+ });
129
+ return {
130
+ ...defaults,
131
+ dataStorageMode: dataStorageMode ?? defaults.dataStorageMode,
132
+ namespace: identifiers.namespace,
133
+ persistencePolicy: persistencePolicy ?? defaults.persistencePolicy,
134
+ phpPrefix: identifiers.phpPrefix,
135
+ queryPostType: normalizeQueryPostType(queryPostType ?? defaults.queryPostType),
136
+ textDomain: identifiers.textDomain,
137
+ };
138
+ }
139
+ if (!promptText) {
140
+ throw new Error('Interactive answers require a promptText callback.');
141
+ }
142
+ const identifiers = resolveScaffoldIdentifiers({
143
+ namespace: namespace ?? (await promptText('Namespace', defaults.namespace, validateNamespace)),
144
+ phpPrefix,
145
+ slug: await promptText('Block slug', defaults.slug, validateBlockSlug),
146
+ textDomain,
147
+ });
148
+ return {
149
+ author: await promptText('Author', defaults.author),
150
+ dataStorageMode: dataStorageMode ?? defaults.dataStorageMode,
151
+ description: await promptText('Description', defaults.description),
152
+ namespace: identifiers.namespace,
153
+ persistencePolicy: persistencePolicy ?? defaults.persistencePolicy,
154
+ phpPrefix: identifiers.phpPrefix,
155
+ queryPostType: templateId === 'query-loop'
156
+ ? normalizeQueryPostType(await promptText('Query post type', queryPostType ?? defaults.queryPostType ?? 'post', validateQueryPostType))
157
+ : normalizeQueryPostType(queryPostType ?? defaults.queryPostType),
158
+ slug: identifiers.slug,
159
+ textDomain: identifiers.textDomain,
160
+ title: await promptText('Block title', toTitleCase(identifiers.slug)),
161
+ };
162
+ }
163
+ import { execSync } from 'node:child_process';
@@ -3,27 +3,12 @@ import type { BuiltInCodeArtifact } from "./built-in-block-code-artifacts.js";
3
3
  import { type BuiltInTemplateId } from "./template-registry.js";
4
4
  import type { PackageManagerId } from "./package-managers.js";
5
5
  import type { ScaffoldTemplateVariables } from "./scaffold.js";
6
+ export { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
6
7
  export interface InstallDependenciesOptions {
7
8
  packageManager: PackageManagerId;
8
9
  projectDir: string;
9
10
  }
10
11
  export declare function ensureDirectory(targetDir: string, allowExisting?: boolean): Promise<void>;
11
- /**
12
- * Builds the generated README markdown for one scaffolded project.
13
- *
14
- * @param templateId Scaffold template family or template identifier.
15
- * @param variables Interpolated scaffold variables used in generated content.
16
- * @param packageManager Package manager used to format install and script commands.
17
- * @param options Optional README sections enabled by scaffold presets.
18
- * @returns Markdown README content for the generated project root.
19
- */
20
- export declare function buildReadme(templateId: string, variables: ScaffoldTemplateVariables, packageManager: PackageManagerId, { withMigrationUi, withTestPreset, withWpEnv, }?: {
21
- withMigrationUi?: boolean;
22
- withTestPreset?: boolean;
23
- withWpEnv?: boolean;
24
- }): string;
25
- export declare function buildGitignore(): string;
26
- export declare function mergeTextLines(primaryContent: string, existingContent: string): string;
27
12
  export declare function writeStarterManifestFiles(targetDir: string, templateId: string, variables: ScaffoldTemplateVariables, artifacts?: readonly BuiltInBlockArtifact[]): Promise<void>;
28
13
  /**
29
14
  * Seed REST-derived persistence artifacts into a newly scaffolded built-in
@@ -3,18 +3,19 @@ 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 { fileURLToPath } from "node:url";
6
- import { applyGeneratedProjectDxPackageJson, applyLocalDevPresetFiles, getPrimaryDevelopmentScript, } from "./local-dev-presets.js";
6
+ import { applyGeneratedProjectDxPackageJson, applyLocalDevPresetFiles, } from "./local-dev-presets.js";
7
7
  import { applyMigrationUiCapability } from "./migration-ui-capability.js";
8
8
  import { getPackageVersions } from "./package-versions.js";
9
9
  import { ensureMigrationDirectories, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
10
10
  import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
11
- import { getCompoundExtensionWorkflowSection, getInitialCommitCommands, getInitialCommitNote, getOptionalOnboardingNote, getOptionalOnboardingSteps, getQuickStartWorkflowNote, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from "./scaffold-onboarding.js";
11
+ import { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
12
12
  import { getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
13
13
  import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
14
14
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, } from "./template-registry.js";
15
15
  import { copyInterpolatedDirectory } from "./template-render.js";
16
- import { formatInstallCommand, formatPackageExecCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
16
+ import { formatInstallCommand, formatPackageExecCommand, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
17
17
  import { replaceRepositoryReferencePlaceholders, resolveScaffoldRepositoryReference, } from "./scaffold-repository-reference.js";
18
+ export { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
18
19
  const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
19
20
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
21
  const LOCKFILES = {
@@ -36,131 +37,6 @@ export async function ensureDirectory(targetDir, allowExisting = false) {
36
37
  throw new Error(`Target directory is not empty: ${targetDir}`);
37
38
  }
38
39
  }
39
- /**
40
- * Builds the generated README markdown for one scaffolded project.
41
- *
42
- * @param templateId Scaffold template family or template identifier.
43
- * @param variables Interpolated scaffold variables used in generated content.
44
- * @param packageManager Package manager used to format install and script commands.
45
- * @param options Optional README sections enabled by scaffold presets.
46
- * @returns Markdown README content for the generated project root.
47
- */
48
- export function buildReadme(templateId, variables, packageManager, { withMigrationUi = false, withTestPreset = false, withWpEnv = false, } = {}) {
49
- const optionalOnboardingSteps = getOptionalOnboardingSteps(packageManager, templateId, {
50
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
51
- });
52
- const initialCommitCommands = getInitialCommitCommands();
53
- const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
54
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
55
- });
56
- const compoundPersistenceEnabled = variables.compoundPersistenceEnabled === "true";
57
- const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === "true"
58
- ? "Public persistence writes use signed short-lived tokens, per-request ids, and coarse rate limiting by default. Add application-specific abuse controls before using the same pattern for high-value metrics or experiments."
59
- : null;
60
- const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
61
- const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
62
- compoundPersistenceEnabled,
63
- slug: variables.slug,
64
- });
65
- const developmentScript = getPrimaryDevelopmentScript(templateId);
66
- const wpEnvSection = withWpEnv
67
- ? `## Local WordPress\n\n\`\`\`bash\n${formatRunScript(packageManager, "wp-env:start")}\n${formatRunScript(packageManager, "wp-env:stop")}\n${formatRunScript(packageManager, "wp-env:reset")}\n\`\`\``
68
- : "";
69
- const testPresetSection = withTestPreset
70
- ? `## Local Test Preset\n\n\`\`\`bash\n${formatRunScript(packageManager, "wp-env:start:test")}\n${formatRunScript(packageManager, "wp-env:wait:test")}\n${formatRunScript(packageManager, "test:e2e")}\n\`\`\`\n\nThe generated smoke test uses \`.wp-env.test.json\` and verifies that the scaffolded block registers in the WordPress editor.`
71
- : "";
72
- const migrationSection = withMigrationUi
73
- ? `## Migration UI\n\nThis scaffold already includes an initialized migration workspace at \`v1\`, generated deprecated/runtime artifacts, and an editor-embedded migration dashboard. Migration versions are schema lineage labels and are separate from your package or plugin release version. Use the existing CLI commands to snapshot, diff, scaffold, verify, and fuzz future schema changes.\n\n\`\`\`bash\n${formatRunScript(packageManager, "migration:doctor")}\n${formatRunScript(packageManager, "migration:verify")}\n${formatRunScript(packageManager, "migration:fuzz")}\n\`\`\`\n\nRun \`migration:init\` only when retrofitting migration support into an older project that was not scaffolded with \`--with-migration-ui\`.`
74
- : "";
75
- return `# ${variables.title}
76
-
77
- ${variables.description}
78
-
79
- ## Template
80
-
81
- ${templateId}
82
-
83
- ## Quick Start
84
-
85
- \`\`\`bash
86
- ${formatInstallCommand(packageManager)}
87
- ${formatRunScript(packageManager, developmentScript)}
88
- \`\`\`
89
-
90
- ${getQuickStartWorkflowNote(packageManager, templateId, {
91
- compoundPersistenceEnabled,
92
- })}
93
-
94
- ## Build and Verify
95
-
96
- \`\`\`bash
97
- ${formatRunScript(packageManager, "build")}
98
- ${formatRunScript(packageManager, "typecheck")}
99
- \`\`\`
100
-
101
- ## Advanced Sync
102
-
103
- \`\`\`bash
104
- ${optionalOnboardingSteps.join("\n")}
105
- \`\`\`
106
-
107
- ${getOptionalOnboardingNote(packageManager, templateId, {
108
- compoundPersistenceEnabled,
109
- })}
110
-
111
- ## Before First Commit
112
-
113
- \`\`\`bash
114
- ${initialCommitCommands.join("\n")}
115
- \`\`\`
116
-
117
- ${getInitialCommitNote()}
118
-
119
- ${sourceOfTruthNote}${publicPersistencePolicyNote ? `\n\n${publicPersistencePolicyNote}` : ""}${migrationSection ? `\n\n${migrationSection}` : ""}${compoundExtensionWorkflowSection ? `\n\n${compoundExtensionWorkflowSection}` : ""}${wpEnvSection ? `\n\n${wpEnvSection}` : ""}${testPresetSection ? `\n\n${testPresetSection}` : ""}${phpRestExtensionPointsSection ? `\n\n${phpRestExtensionPointsSection}` : ""}
120
- `;
121
- }
122
- export function buildGitignore() {
123
- return `# Dependencies
124
- node_modules/
125
- .yarn/
126
- .pnp.*
127
-
128
- # Build
129
- build/
130
- dist/
131
-
132
- # Editor
133
- .vscode/
134
- .idea/
135
-
136
- # OS
137
- .DS_Store
138
- Thumbs.db
139
-
140
- # WordPress
141
- *.log
142
- .wp-env/
143
- `;
144
- }
145
- export function mergeTextLines(primaryContent, existingContent) {
146
- const normalizedPrimary = primaryContent.replace(/\r\n/g, "\n").trimEnd();
147
- const normalizedExisting = existingContent.replace(/\r\n/g, "\n").trimEnd();
148
- const mergedLines = [];
149
- const seen = new Set();
150
- for (const line of [...normalizedPrimary.split("\n"), ...normalizedExisting.split("\n")]) {
151
- if (line.length === 0 && mergedLines[mergedLines.length - 1] === "") {
152
- continue;
153
- }
154
- if (line.length > 0 && seen.has(line)) {
155
- continue;
156
- }
157
- if (line.length > 0) {
158
- seen.add(line);
159
- }
160
- mergedLines.push(line);
161
- }
162
- return `${mergedLines.join("\n").replace(/\n{3,}/g, "\n\n")}\n`;
163
- }
164
40
  export async function writeStarterManifestFiles(targetDir, templateId, variables, artifacts) {
165
41
  const manifests = artifacts
166
42
  ? artifacts.map((artifact) => ({
@@ -0,0 +1,34 @@
1
+ import type { PackageManagerId } from './package-managers.js';
2
+ import type { ScaffoldTemplateVariables } from './scaffold.js';
3
+ /**
4
+ * Builds the generated README markdown for one scaffolded project.
5
+ *
6
+ * @param templateId Scaffold template family or template identifier.
7
+ * @param variables Interpolated scaffold variables used in generated content.
8
+ * @param packageManager Package manager used to format install and script commands.
9
+ * @param options Optional README sections enabled by scaffold presets.
10
+ * @returns Markdown README content for the generated project root.
11
+ */
12
+ export declare function buildReadme(templateId: string, variables: ScaffoldTemplateVariables, packageManager: PackageManagerId, { withMigrationUi, withTestPreset, withWpEnv, }?: {
13
+ withMigrationUi?: boolean;
14
+ withTestPreset?: boolean;
15
+ withWpEnv?: boolean;
16
+ }): string;
17
+ /**
18
+ * Build the default `.gitignore` contents for a scaffolded project.
19
+ *
20
+ * @returns A newline-terminated `.gitignore` string covering dependency, build, editor, OS, and WordPress artifacts.
21
+ */
22
+ export declare function buildGitignore(): string;
23
+ /**
24
+ * Merge generated and existing text files while keeping line order stable.
25
+ *
26
+ * Existing unique lines are appended after the primary content, duplicate
27
+ * non-empty lines are removed, and runs of more than two blank lines collapse
28
+ * to a single empty separator.
29
+ *
30
+ * @param primaryContent Newly generated text that should appear first.
31
+ * @param existingContent Existing file contents preserved after unique generated lines.
32
+ * @returns The merged text block with a trailing newline.
33
+ */
34
+ export declare function mergeTextLines(primaryContent: string, existingContent: string): string;
@@ -0,0 +1,143 @@
1
+ import { getPrimaryDevelopmentScript } from './local-dev-presets.js';
2
+ import { getCompoundExtensionWorkflowSection, getInitialCommitCommands, getInitialCommitNote, getOptionalOnboardingNote, getOptionalOnboardingSteps, getQuickStartWorkflowNote, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from './scaffold-onboarding.js';
3
+ import { formatInstallCommand, formatRunScript, } from './package-managers.js';
4
+ /**
5
+ * Builds the generated README markdown for one scaffolded project.
6
+ *
7
+ * @param templateId Scaffold template family or template identifier.
8
+ * @param variables Interpolated scaffold variables used in generated content.
9
+ * @param packageManager Package manager used to format install and script commands.
10
+ * @param options Optional README sections enabled by scaffold presets.
11
+ * @returns Markdown README content for the generated project root.
12
+ */
13
+ export function buildReadme(templateId, variables, packageManager, { withMigrationUi = false, withTestPreset = false, withWpEnv = false, } = {}) {
14
+ const optionalOnboardingSteps = getOptionalOnboardingSteps(packageManager, templateId, {
15
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === 'true',
16
+ });
17
+ const initialCommitCommands = getInitialCommitCommands();
18
+ const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
19
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === 'true',
20
+ });
21
+ const compoundPersistenceEnabled = variables.compoundPersistenceEnabled === 'true';
22
+ const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === 'true'
23
+ ? 'Public persistence writes use signed short-lived tokens, per-request ids, and coarse rate limiting by default. Add application-specific abuse controls before using the same pattern for high-value metrics or experiments.'
24
+ : null;
25
+ const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
26
+ const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
27
+ compoundPersistenceEnabled,
28
+ slug: variables.slug,
29
+ });
30
+ const developmentScript = getPrimaryDevelopmentScript(templateId);
31
+ const wpEnvSection = withWpEnv
32
+ ? `## Local WordPress\n\n\`\`\`bash\n${formatRunScript(packageManager, 'wp-env:start')}\n${formatRunScript(packageManager, 'wp-env:stop')}\n${formatRunScript(packageManager, 'wp-env:reset')}\n\`\`\``
33
+ : '';
34
+ const testPresetSection = withTestPreset
35
+ ? `## Local Test Preset\n\n\`\`\`bash\n${formatRunScript(packageManager, 'wp-env:start:test')}\n${formatRunScript(packageManager, 'wp-env:wait:test')}\n${formatRunScript(packageManager, 'test:e2e')}\n\`\`\`\n\nThe generated smoke test uses \`.wp-env.test.json\` and verifies that the scaffolded block registers in the WordPress editor.`
36
+ : '';
37
+ const migrationSection = withMigrationUi
38
+ ? `## Migration UI\n\nThis scaffold already includes an initialized migration workspace at \`v1\`, generated deprecated/runtime artifacts, and an editor-embedded migration dashboard. Migration versions are schema lineage labels and are separate from your package or plugin release version. Use the existing CLI commands to snapshot, diff, scaffold, verify, and fuzz future schema changes.\n\n\`\`\`bash\n${formatRunScript(packageManager, 'migration:doctor')}\n${formatRunScript(packageManager, 'migration:verify')}\n${formatRunScript(packageManager, 'migration:fuzz')}\n\`\`\`\n\nRun \`migration:init\` only when retrofitting migration support into an older project that was not scaffolded with \`--with-migration-ui\`.`
39
+ : '';
40
+ const advancedSyncSection = optionalOnboardingSteps.length > 0
41
+ ? `## Advanced Sync\n\n\`\`\`bash\n${optionalOnboardingSteps.join('\n')}\n\`\`\`\n\n${getOptionalOnboardingNote(packageManager, templateId, {
42
+ compoundPersistenceEnabled,
43
+ })}`
44
+ : `## Artifact Refresh\n\n${getOptionalOnboardingNote(packageManager, templateId, {
45
+ compoundPersistenceEnabled,
46
+ })}`;
47
+ return `# ${variables.title}
48
+
49
+ ${variables.description}
50
+
51
+ ## Template
52
+
53
+ ${templateId}
54
+
55
+ ## Quick Start
56
+
57
+ \`\`\`bash
58
+ ${formatInstallCommand(packageManager)}
59
+ ${formatRunScript(packageManager, developmentScript)}
60
+ \`\`\`
61
+
62
+ ${getQuickStartWorkflowNote(packageManager, templateId, {
63
+ compoundPersistenceEnabled,
64
+ })}
65
+
66
+ ## Build and Verify
67
+
68
+ \`\`\`bash
69
+ ${formatRunScript(packageManager, 'build')}
70
+ ${formatRunScript(packageManager, 'typecheck')}
71
+ \`\`\`
72
+
73
+ ${advancedSyncSection}
74
+
75
+ ## Before First Commit
76
+
77
+ \`\`\`bash
78
+ ${initialCommitCommands.join('\n')}
79
+ \`\`\`
80
+
81
+ ${getInitialCommitNote()}
82
+
83
+ ${sourceOfTruthNote}${publicPersistencePolicyNote ? `\n\n${publicPersistencePolicyNote}` : ''}${migrationSection ? `\n\n${migrationSection}` : ''}${compoundExtensionWorkflowSection ? `\n\n${compoundExtensionWorkflowSection}` : ''}${wpEnvSection ? `\n\n${wpEnvSection}` : ''}${testPresetSection ? `\n\n${testPresetSection}` : ''}${phpRestExtensionPointsSection ? `\n\n${phpRestExtensionPointsSection}` : ''}
84
+ `;
85
+ }
86
+ /**
87
+ * Build the default `.gitignore` contents for a scaffolded project.
88
+ *
89
+ * @returns A newline-terminated `.gitignore` string covering dependency, build, editor, OS, and WordPress artifacts.
90
+ */
91
+ export function buildGitignore() {
92
+ return `# Dependencies
93
+ node_modules/
94
+ .yarn/
95
+ .pnp.*
96
+
97
+ # Build
98
+ build/
99
+ dist/
100
+
101
+ # Editor
102
+ .vscode/
103
+ .idea/
104
+
105
+ # OS
106
+ .DS_Store
107
+ Thumbs.db
108
+
109
+ # WordPress
110
+ *.log
111
+ .wp-env/
112
+ `;
113
+ }
114
+ /**
115
+ * Merge generated and existing text files while keeping line order stable.
116
+ *
117
+ * Existing unique lines are appended after the primary content, duplicate
118
+ * non-empty lines are removed, and runs of more than two blank lines collapse
119
+ * to a single empty separator.
120
+ *
121
+ * @param primaryContent Newly generated text that should appear first.
122
+ * @param existingContent Existing file contents preserved after unique generated lines.
123
+ * @returns The merged text block with a trailing newline.
124
+ */
125
+ export function mergeTextLines(primaryContent, existingContent) {
126
+ const normalizedPrimary = primaryContent.replace(/\r\n/g, '\n').trimEnd();
127
+ const normalizedExisting = existingContent.replace(/\r\n/g, '\n').trimEnd();
128
+ const mergedLines = [];
129
+ const seen = new Set();
130
+ for (const line of [...normalizedPrimary.split('\n'), ...normalizedExisting.split('\n')]) {
131
+ if (line.length === 0 && mergedLines[mergedLines.length - 1] === '') {
132
+ continue;
133
+ }
134
+ if (line.length > 0 && seen.has(line)) {
135
+ continue;
136
+ }
137
+ if (line.length > 0) {
138
+ seen.add(line);
139
+ }
140
+ mergedLines.push(line);
141
+ }
142
+ return `${mergedLines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`;
143
+ }
@@ -13,6 +13,9 @@ function templateHasPersistenceSync(templateId, { compoundPersistenceEnabled = f
13
13
  * Returns the optional sync script names to suggest for a template.
14
14
  */
15
15
  export function getOptionalSyncScriptNames(templateId, options = {}) {
16
+ if (templateId === "query-loop") {
17
+ return [];
18
+ }
16
19
  const availableScripts = new Set(options.availableScripts ?? []);
17
20
  if (availableScripts.has("sync")) {
18
21
  return ["sync"];
@@ -37,6 +40,9 @@ export function getOptionalOnboardingSteps(packageManager, templateId, options =
37
40
  * Returns the quick-start note explaining the scaffold's primary local loop.
38
41
  */
39
42
  export function getQuickStartWorkflowNote(packageManager, templateId = "basic", options = {}) {
43
+ if (templateId === "query-loop") {
44
+ return `${formatRunScript(packageManager, "start")} runs the editor build/watch loop that registers your Query Loop variation in the block editor. This scaffold is editor-facing by design: update \`src/index.ts\` when you want to change the variation namespace, default query, allowed controls, or the minimal inline starter layout, update \`src/patterns/*.php\` when you want richer connected layout presets in the inserter, use \`src/query-extension.ts\` when the variation needs custom query params or optional editor-side hooks, and mirror frontend/editor preview query mapping in \`inc/query-runtime.php\`.`;
45
+ }
40
46
  const developmentScript = getPrimaryDevelopmentScript(templateId);
41
47
  const devCommand = formatRunScript(packageManager, developmentScript);
42
48
  const startCommand = formatRunScript(packageManager, "start");
@@ -55,6 +61,9 @@ export function getQuickStartWorkflowNote(packageManager, templateId = "basic",
55
61
  * Returns the onboarding note explaining when manual sync is optional.
56
62
  */
57
63
  export function getOptionalOnboardingNote(packageManager, templateId = "basic", options = {}) {
64
+ if (templateId === "query-loop") {
65
+ return `This scaffold does not generate \`block.json\` or Typia manifests. Edit \`src/index.ts\` to change the variation contract, edit \`src/patterns/*.php\` when you want richer connected layouts beyond the inline fallback, edit \`src/query-extension.ts\` when you need variation-specific query params or custom editor hooks, and edit \`inc/query-runtime.php\` when those params need frontend or editor preview parity. Then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "start")}, or ${formatRunScript(packageManager, "typecheck")} as needed.`;
66
+ }
58
67
  const optionalSyncScripts = getOptionalSyncScriptNames(templateId, options);
59
68
  const hasUnifiedSync = optionalSyncScripts.includes("sync");
60
69
  const syncSteps = optionalSyncScripts.map((scriptName) => formatRunScript(packageManager, scriptName));
@@ -100,6 +109,9 @@ export function getInitialCommitNote() {
100
109
  * Returns source-of-truth guidance for generated artifacts by template mode.
101
110
  */
102
111
  export function getTemplateSourceOfTruthNote(templateId, { compoundPersistenceEnabled = false } = {}) {
112
+ if (templateId === "query-loop") {
113
+ return "`src/index.ts` remains the source of truth for the Query Loop variation name, default query attributes, `allowedControls`, and the minimal inline starter `innerBlocks`. Use `src/patterns/*.php` for richer connected layout presets that stay tied to the same variation namespace, use `src/query-extension.ts` for custom query seed values or optional editor-only hook registration, and use `inc/query-runtime.php` to keep frontend and editor preview query mapping aligned for those custom keys. The generated plugin bootstrap should stay focused on script registration, pattern loading, and explicit runtime glue for the variation.";
114
+ }
103
115
  if (templateId === "compound") {
104
116
  const compoundBase = "`src/blocks/*/types.ts` files remain the source of truth for each block's `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include starter `typia.manifest.json` files so editor imports resolve before the first sync.";
105
117
  if (compoundPersistenceEnabled) {
@@ -0,0 +1,9 @@
1
+ import type { ScaffoldAnswers, ScaffoldTemplateVariables } from './scaffold.js';
2
+ /**
3
+ * Build the normalized template variables used by scaffold rendering.
4
+ *
5
+ * @param templateId Selected scaffold template identifier.
6
+ * @param answers Normalized scaffold answers collected from defaults, flags, and prompts.
7
+ * @returns Template variables ready for file interpolation and generated artifacts.
8
+ */
9
+ export declare function getTemplateVariables(templateId: string, answers: ScaffoldAnswers): ScaffoldTemplateVariables;