@wp-typia/project-tools 0.22.7 → 0.22.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.
Files changed (63) hide show
  1. package/dist/runtime/block-targets.d.ts +40 -0
  2. package/dist/runtime/block-targets.js +71 -0
  3. package/dist/runtime/built-in-block-artifact-types.js +2 -1
  4. package/dist/runtime/built-in-block-attribute-specs.js +2 -1
  5. package/dist/runtime/built-in-block-code-artifacts.js +2 -0
  6. package/dist/runtime/built-in-block-non-ts-family-artifacts.js +12 -9
  7. package/dist/runtime/built-in-block-non-ts-render-utils.js +2 -0
  8. package/dist/runtime/cli-add-block-config.js +2 -1
  9. package/dist/runtime/cli-add-block.js +16 -4
  10. package/dist/runtime/cli-add-types.d.ts +8 -0
  11. package/dist/runtime/cli-add-types.js +11 -0
  12. package/dist/runtime/cli-add-workspace-ability-scaffold.js +21 -15
  13. package/dist/runtime/cli-add-workspace-ability.js +2 -2
  14. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +17 -13
  15. package/dist/runtime/cli-add-workspace-admin-view.js +2 -2
  16. package/dist/runtime/cli-add-workspace-ai.js +2 -2
  17. package/dist/runtime/cli-add-workspace-assets.js +42 -48
  18. package/dist/runtime/cli-add-workspace-rest.js +2 -2
  19. package/dist/runtime/cli-add-workspace.js +6 -38
  20. package/dist/runtime/cli-add.d.ts +3 -2
  21. package/dist/runtime/cli-add.js +2 -2
  22. package/dist/runtime/cli-core.d.ts +4 -2
  23. package/dist/runtime/cli-core.js +3 -2
  24. package/dist/runtime/cli-diagnostics.js +6 -0
  25. package/dist/runtime/cli-doctor-workspace.js +2 -0
  26. package/dist/runtime/cli-scaffold.js +5 -4
  27. package/dist/runtime/create-template-validation.d.ts +10 -0
  28. package/dist/runtime/create-template-validation.js +95 -0
  29. package/dist/runtime/id-suggestions.d.ts +21 -0
  30. package/dist/runtime/id-suggestions.js +48 -0
  31. package/dist/runtime/index.d.ts +5 -2
  32. package/dist/runtime/index.js +3 -1
  33. package/dist/runtime/migration-maintenance-verify.js +2 -0
  34. package/dist/runtime/package-versions.js +15 -2
  35. package/dist/runtime/php-utils.js +66 -0
  36. package/dist/runtime/scaffold-answer-resolution.js +5 -108
  37. package/dist/runtime/scaffold-apply-utils.js +3 -2
  38. package/dist/runtime/scaffold-identifiers.js +4 -3
  39. package/dist/runtime/scaffold-template-assertions.d.ts +6 -0
  40. package/dist/runtime/scaffold-template-assertions.js +33 -0
  41. package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
  42. package/dist/runtime/scaffold-template-variable-groups.js +7 -0
  43. package/dist/runtime/scaffold.js +3 -3
  44. package/dist/runtime/string-case.d.ts +2 -4
  45. package/dist/runtime/string-case.js +13 -4
  46. package/dist/runtime/template-builtins.js +11 -8
  47. package/dist/runtime/template-layers.js +2 -2
  48. package/dist/runtime/template-source-cache-policy.d.ts +53 -0
  49. package/dist/runtime/template-source-cache-policy.js +135 -0
  50. package/dist/runtime/template-source-cache.d.ts +1 -45
  51. package/dist/runtime/template-source-cache.js +9 -152
  52. package/dist/runtime/template-source-external.d.ts +3 -0
  53. package/dist/runtime/template-source-external.js +5 -2
  54. package/dist/runtime/template-source-remote.d.ts +6 -0
  55. package/dist/runtime/template-source-remote.js +8 -2
  56. package/dist/runtime/template-source-seeds.d.ts +13 -0
  57. package/dist/runtime/template-source-seeds.js +36 -8
  58. package/dist/runtime/template-source.js +2 -2
  59. package/dist/runtime/ts-property-names.d.ts +11 -0
  60. package/dist/runtime/ts-property-names.js +16 -0
  61. package/dist/runtime/workspace-inventory.d.ts +33 -7
  62. package/dist/runtime/workspace-inventory.js +94 -41
  63. package/package.json +11 -1
@@ -5,6 +5,7 @@
5
5
  * CLI while keeping reusable project logic out of the CLI package itself.
6
6
  * Consumers should prefer these exports for scaffold, add, migrate, doctor,
7
7
  * and workspace-aware helpers such as `getWorkspaceBlockSelectOptions`,
8
+ * `getWorkspaceBlockSelectOptionsAsync`,
8
9
  * `runAddBlockCommand`, `runAddBlockStyleCommand`,
9
10
  * `runAddBlockTransformCommand`, `runAddVariationCommand`,
10
11
  * `runAddPatternCommand`, `runAddBindingSourceCommand`,
@@ -29,11 +30,13 @@ export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJso
29
30
  export { buildCompoundChildStarterManifestDocument, getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
30
31
  export type { EndpointAuthIntent, EndpointOpenApiAuthMode, EndpointOpenApiContractDocument, EndpointOpenApiDocumentOptions, EndpointOpenApiEndpointDefinition, EndpointOpenApiMethod, EndpointWordPressAuthDefinition, EndpointWordPressAuthMechanism, JsonSchemaDocument, JsonSchemaProjectionProfile, JsonSchemaObject, NormalizedEndpointAuthDefinition, OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter, OpenApiPathItem, OpenApiSecurityScheme, } from "./schema-core.js";
31
32
  export { PACKAGE_MANAGER_IDS, PACKAGE_MANAGERS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, getPackageManagerSelectOptions, transformPackageManagerText, } from "./package-managers.js";
33
+ export { suggestCloseId } from "./id-suggestions.js";
34
+ export type { SuggestCloseIdOptions } from "./id-suggestions.js";
32
35
  export { clearPackageVersionsCache, getPackageVersions, invalidatePackageVersionsCache, } from "./package-versions.js";
33
36
  export type { PackageVersions } from "./package-versions.js";
34
37
  export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOptions, listTemplates, } from "./template-registry.js";
35
38
  export { EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, pruneExternalTemplateCache, } from "./template-source-cache.js";
36
39
  export type { ExternalTemplateCachePruneOptions, ExternalTemplateCachePruneResult, } from "./template-source-cache.js";
37
40
  export { STALE_TEMP_ROOT_MAX_AGE_MS, WP_TYPIA_TEMP_ROOT_PREFIX, cleanupManagedTempRoot, cleanupStaleTempRoots, createManagedTempRoot, getTrackedTempRoots, } from "./temp-roots.js";
38
- export { createReadlinePrompt, createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatAddHelpText, formatDoctorCheckLine, formatDoctorSummaryLine, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getDoctorFailureDetailLines, getFailingDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, HOOKED_BLOCK_POSITION_IDS, EDITOR_PLUGIN_SLOT_IDS, isCliDiagnosticError, runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
39
- export type { CliDiagnosticCode, CliDiagnosticCodeError, CliDiagnosticMessage, DoctorCheck, EditorPluginSlotId, HookedBlockPositionId, ReadlinePrompt, } from "./cli-core.js";
41
+ export { createReadlinePrompt, createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatAddHelpText, formatDoctorCheckLine, formatDoctorSummaryLine, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getDoctorFailureDetailLines, getFailingDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, HOOKED_BLOCK_POSITION_IDS, EDITOR_PLUGIN_SLOT_IDS, isCliDiagnosticError, runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
42
+ export type { CliDiagnosticCode, CliDiagnosticCodeError, CliDiagnosticMessage, DoctorCheck, EditorPluginSlotId, HookedBlockPositionId, ReadlinePrompt, WorkspaceBlockSelectOption, } from "./cli-core.js";
@@ -5,6 +5,7 @@
5
5
  * CLI while keeping reusable project logic out of the CLI package itself.
6
6
  * Consumers should prefer these exports for scaffold, add, migrate, doctor,
7
7
  * and workspace-aware helpers such as `getWorkspaceBlockSelectOptions`,
8
+ * `getWorkspaceBlockSelectOptionsAsync`,
8
9
  * `runAddBlockCommand`, `runAddBlockStyleCommand`,
9
10
  * `runAddBlockTransformCommand`, `runAddVariationCommand`,
10
11
  * `runAddPatternCommand`, `runAddBindingSourceCommand`,
@@ -23,8 +24,9 @@ export { parseWorkspacePackageManagerId, resolveWorkspaceProject, tryResolveWork
23
24
  export { manifestAttributeToJsonSchema, projectJsonSchemaDocument, manifestToJsonSchema, manifestToOpenApi, normalizeEndpointAuthDefinition, } from "./schema-core.js";
24
25
  export { buildCompoundChildStarterManifestDocument, getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
25
26
  export { PACKAGE_MANAGER_IDS, PACKAGE_MANAGERS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, getPackageManagerSelectOptions, transformPackageManagerText, } from "./package-managers.js";
27
+ export { suggestCloseId } from "./id-suggestions.js";
26
28
  export { clearPackageVersionsCache, getPackageVersions, invalidatePackageVersionsCache, } from "./package-versions.js";
27
29
  export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOptions, listTemplates, } from "./template-registry.js";
28
30
  export { EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, pruneExternalTemplateCache, } from "./template-source-cache.js";
29
31
  export { STALE_TEMP_ROOT_MAX_AGE_MS, WP_TYPIA_TEMP_ROOT_PREFIX, cleanupManagedTempRoot, cleanupStaleTempRoots, createManagedTempRoot, getTrackedTempRoots, } from "./temp-roots.js";
30
- export { createReadlinePrompt, createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatAddHelpText, formatDoctorCheckLine, formatDoctorSummaryLine, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getDoctorFailureDetailLines, getFailingDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, HOOKED_BLOCK_POSITION_IDS, EDITOR_PLUGIN_SLOT_IDS, isCliDiagnosticError, runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
32
+ export { createReadlinePrompt, createCliCommandError, createCliDiagnosticCodeError, CliDiagnosticError, CLI_DIAGNOSTIC_CODES, formatCliDiagnosticError, formatAddHelpText, formatDoctorCheckLine, formatDoctorSummaryLine, formatHelpText, formatTemplateDetails, formatTemplateFeatures, formatTemplateSummary, getDoctorChecks, getDoctorFailureDetailLines, getFailingDoctorChecks, getNextSteps, getOptionalOnboarding, getWorkspaceBlockSelectOptions, getWorkspaceBlockSelectOptionsAsync, HOOKED_BLOCK_POSITION_IDS, EDITOR_PLUGIN_SLOT_IDS, isCliDiagnosticError, runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runDoctor, runAddVariationCommand, runScaffoldFlow, } from "./cli-core.js";
@@ -76,6 +76,8 @@ function recordWorkspaceMigrationTargetAlignment(projectDir, state, recordCheck)
76
76
  return;
77
77
  }
78
78
  try {
79
+ // Maintenance verification is intentionally synchronous so it can compare
80
+ // migration state against a single inventory snapshot.
79
81
  const inventory = readWorkspaceInventory(workspace.projectDir);
80
82
  const expectedTargets = inventory.blocks.map((block) => `${workspace.workspace.namespace}/${block.slug}`);
81
83
  const configuredTargets = state.blocks.map((block) => block.blockName);
@@ -58,10 +58,23 @@ function createContentFingerprint(source) {
58
58
  }
59
59
  return (hash >>> 0).toString(16);
60
60
  }
61
+ function readPackageManifestFile(packageJsonPath) {
62
+ const fileDescriptor = fs.openSync(packageJsonPath, 'r');
63
+ try {
64
+ // Keep cache metadata and manifest contents tied to one opened file, so a
65
+ // concurrent path replacement cannot mix stats from one manifest with
66
+ // contents from another. The content fingerprint remains the final guard.
67
+ const stats = fs.fstatSync(fileDescriptor);
68
+ const source = fs.readFileSync(fileDescriptor, 'utf8');
69
+ return { source, stats };
70
+ }
71
+ finally {
72
+ fs.closeSync(fileDescriptor);
73
+ }
74
+ }
61
75
  function resolvePackageManifestLocation(packageJsonPath) {
62
76
  try {
63
- const stats = fs.statSync(packageJsonPath);
64
- const source = fs.readFileSync(packageJsonPath, 'utf8');
77
+ const { source, stats } = readPackageManifestFile(packageJsonPath);
65
78
  return {
66
79
  cacheKey: `file:${packageJsonPath}:${stats.ino}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.size}:${createContentFingerprint(source)}`,
67
80
  packageJsonPath,
@@ -142,6 +142,9 @@ function matchesPhpFunctionCallAt(source, index, functionName) {
142
142
  function createPhpScannerState() {
143
143
  return {
144
144
  heredocDelimiter: "",
145
+ interpolationComment: "",
146
+ interpolationDepth: 0,
147
+ interpolationQuote: "",
145
148
  mode: "code",
146
149
  };
147
150
  }
@@ -165,11 +168,74 @@ function advancePhpScanner(source, index, state) {
165
168
  if (character === "\\") {
166
169
  return { ambiguous: false, inCode: false, index: index + 2 };
167
170
  }
171
+ if (state.mode === "double-quoted" &&
172
+ character === "{" &&
173
+ source[index + 1] === "$") {
174
+ state.mode = "double-quoted-interpolation";
175
+ state.interpolationComment = "";
176
+ state.interpolationDepth = 1;
177
+ state.interpolationQuote = "";
178
+ return { ambiguous: false, inCode: false, index: index + 2 };
179
+ }
168
180
  if (character === quote) {
169
181
  state.mode = "code";
170
182
  }
171
183
  return { ambiguous: false, inCode: false, index: index + 1 };
172
184
  }
185
+ if (state.mode === "double-quoted-interpolation") {
186
+ if (state.interpolationQuote) {
187
+ if (character === "\\") {
188
+ return { ambiguous: false, inCode: false, index: index + 2 };
189
+ }
190
+ if (character === state.interpolationQuote) {
191
+ state.interpolationQuote = "";
192
+ }
193
+ return { ambiguous: false, inCode: false, index: index + 1 };
194
+ }
195
+ if (state.interpolationComment === "line") {
196
+ if (character === "\r" || character === "\n") {
197
+ state.interpolationComment = "";
198
+ }
199
+ return { ambiguous: false, inCode: false, index: index + 1 };
200
+ }
201
+ if (state.interpolationComment === "block") {
202
+ if (character === "*" && source[index + 1] === "/") {
203
+ state.interpolationComment = "";
204
+ return { ambiguous: false, inCode: false, index: index + 2 };
205
+ }
206
+ return { ambiguous: false, inCode: false, index: index + 1 };
207
+ }
208
+ if (character === "/" && source[index + 1] === "/") {
209
+ state.interpolationComment = "line";
210
+ return { ambiguous: false, inCode: false, index: index + 2 };
211
+ }
212
+ if (character === "#" && source[index + 1] !== "[") {
213
+ state.interpolationComment = "line";
214
+ return { ambiguous: false, inCode: false, index: index + 1 };
215
+ }
216
+ if (character === "/" && source[index + 1] === "*") {
217
+ state.interpolationComment = "block";
218
+ return { ambiguous: false, inCode: false, index: index + 2 };
219
+ }
220
+ if (character === "'" || character === '"') {
221
+ state.interpolationQuote = character;
222
+ return { ambiguous: false, inCode: false, index: index + 1 };
223
+ }
224
+ if (character === "{") {
225
+ state.interpolationDepth += 1;
226
+ return { ambiguous: false, inCode: false, index: index + 1 };
227
+ }
228
+ if (character === "}") {
229
+ state.interpolationDepth -= 1;
230
+ if (state.interpolationDepth <= 0) {
231
+ state.interpolationComment = "";
232
+ state.interpolationDepth = 0;
233
+ state.mode = "double-quoted";
234
+ }
235
+ return { ambiguous: false, inCode: false, index: index + 1 };
236
+ }
237
+ return { ambiguous: false, inCode: false, index: index + 1 };
238
+ }
173
239
  if (state.mode === "line-comment") {
174
240
  if (character === "\r" || character === "\n") {
175
241
  state.mode = "code";
@@ -1,23 +1,11 @@
1
1
  import { execSync } from 'node:child_process';
2
- import path from 'node:path';
3
2
  import { PACKAGE_MANAGER_IDS, getPackageManager, } from './package-managers.js';
4
3
  import { normalizeBlockSlug, resolveScaffoldIdentifiers, validateBlockSlug, validateNamespace, } from './scaffold-identifiers.js';
5
4
  import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from './cli-diagnostics.js';
6
- import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
7
- import { getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from './template-defaults.js';
8
- import { parseNpmTemplateLocator } from './template-source-locators.js';
5
+ import { getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
6
+ import { CREATE_TEMPLATE_SELECTION_HINT, validateExplicitCreateTemplateId, } from './create-template-validation.js';
9
7
  import { toSnakeCase, toTitleCase, } from './string-case.js';
10
- const WORKSPACE_TEMPLATE_ALIAS = 'workspace';
11
- const TEMPLATE_SELECTION_HINT = `--template <${[
12
- ...TEMPLATE_IDS,
13
- WORKSPACE_TEMPLATE_ALIAS,
14
- ].join('|')}|./path|github:owner/repo/path[#ref]|npm-package>`;
15
- const TEMPLATE_SUGGESTION_IDS = [...TEMPLATE_IDS, WORKSPACE_TEMPLATE_ALIAS];
16
8
  const QUERY_POST_TYPE_RULE = 'Use lowercase, 1-20 chars, and only a-z, 0-9, "_" or "-".';
17
- const USER_FACING_TEMPLATE_IDS = [
18
- ...TEMPLATE_IDS,
19
- WORKSPACE_TEMPLATE_ALIAS,
20
- ];
21
9
  /**
22
10
  * Detect the current author name from local Git config.
23
11
  *
@@ -80,80 +68,6 @@ function normalizeQueryPostType(value) {
80
68
  }
81
69
  return value.trim().toLowerCase();
82
70
  }
83
- function normalizeTemplateSelection(templateId) {
84
- return templateId === WORKSPACE_TEMPLATE_ALIAS
85
- ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
86
- : templateId;
87
- }
88
- function looksLikeWindowsAbsoluteTemplatePath(templateId) {
89
- return /^[a-z]:[\\/]/iu.test(templateId) || /^\\\\[^\\]+\\[^\\]+/u.test(templateId);
90
- }
91
- function looksLikeExplicitNonNpmExternalTemplateLocator(templateId) {
92
- return (path.isAbsolute(templateId) ||
93
- looksLikeWindowsAbsoluteTemplatePath(templateId) ||
94
- templateId.startsWith('./') ||
95
- templateId.startsWith('../') ||
96
- templateId.startsWith('@') ||
97
- templateId.startsWith('github:') ||
98
- templateId.includes('/'));
99
- }
100
- function looksLikeExplicitExternalTemplateLocator(templateId) {
101
- return (looksLikeExplicitNonNpmExternalTemplateLocator(templateId) ||
102
- parseNpmTemplateLocator(templateId) !== null);
103
- }
104
- function getEditDistance(left, right) {
105
- const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
106
- const current = new Array(right.length + 1);
107
- for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
108
- current[0] = leftIndex + 1;
109
- for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
110
- const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
111
- current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
112
- }
113
- for (let index = 0; index < current.length; index += 1) {
114
- previous[index] = current[index];
115
- }
116
- }
117
- return previous[right.length];
118
- }
119
- function findMistypedBuiltInTemplateSuggestion(templateId) {
120
- const normalizedTemplateId = templateId.trim().toLowerCase();
121
- if (normalizedTemplateId.length === 0 ||
122
- looksLikeExplicitNonNpmExternalTemplateLocator(normalizedTemplateId)) {
123
- return null;
124
- }
125
- let bestCandidate = null;
126
- for (const candidateId of TEMPLATE_SUGGESTION_IDS) {
127
- const distance = getEditDistance(normalizedTemplateId, candidateId);
128
- if (bestCandidate === null ||
129
- distance < bestCandidate.distance) {
130
- bestCandidate = {
131
- distance,
132
- id: candidateId,
133
- };
134
- }
135
- }
136
- return bestCandidate && bestCandidate.distance <= 2
137
- ? bestCandidate.id
138
- : null;
139
- }
140
- function getMistypedBuiltInTemplateMessage(templateId) {
141
- const suggestion = findMistypedBuiltInTemplateSuggestion(templateId);
142
- if (!suggestion) {
143
- return null;
144
- }
145
- const suggestionDescription = suggestion === WORKSPACE_TEMPLATE_ALIAS
146
- ? 'official workspace scaffold'
147
- : 'built-in scaffold';
148
- return `Unknown template "${templateId}". Did you mean "${suggestion}"? Use \`--template ${suggestion}\` for the ${suggestionDescription}, or pass a local path, \`github:owner/repo/path[#ref]\`, or an npm package spec for an external template.`;
149
- }
150
- function getUnknownTemplateMessage(templateId) {
151
- return [
152
- `Unknown template "${templateId}". Expected one of: ${USER_FACING_TEMPLATE_IDS.join(', ')}.`,
153
- 'Run `wp-typia templates list` to inspect available templates.',
154
- 'Pass an explicit external template locator such as `./path`, `github:owner/repo/path[#ref]`, or `@scope/template` for custom templates.',
155
- ].join(' ');
156
- }
157
71
  /**
158
72
  * Resolve the scaffold template id from flags, defaults, and interactive selection.
159
73
  *
@@ -162,32 +76,15 @@ function getUnknownTemplateMessage(templateId) {
162
76
  */
163
77
  export async function resolveTemplateId({ templateId, yes = false, isInteractive = false, selectTemplate, }) {
164
78
  if (templateId) {
165
- const normalizedTemplateId = normalizeTemplateSelection(templateId);
166
- if (isRemovedBuiltInTemplateId(templateId)) {
167
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getRemovedBuiltInTemplateMessage(templateId));
168
- }
169
- if (normalizedTemplateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
170
- return normalizedTemplateId;
171
- }
172
- if (isBuiltInTemplateId(normalizedTemplateId)) {
173
- return getTemplateById(normalizedTemplateId).id;
174
- }
175
- const mistypedBuiltInTemplateMessage = getMistypedBuiltInTemplateMessage(templateId);
176
- if (mistypedBuiltInTemplateMessage) {
177
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, mistypedBuiltInTemplateMessage);
178
- }
179
- if (!looksLikeExplicitExternalTemplateLocator(normalizedTemplateId)) {
180
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.UNKNOWN_TEMPLATE, getUnknownTemplateMessage(templateId));
181
- }
182
- return normalizedTemplateId;
79
+ return validateExplicitCreateTemplateId(templateId);
183
80
  }
184
81
  if (yes) {
185
82
  return 'basic';
186
83
  }
187
84
  if (!isInteractive || !selectTemplate) {
188
- throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `Template is required in non-interactive mode. Use ${TEMPLATE_SELECTION_HINT}.`);
85
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `Template is required in non-interactive mode. Use ${CREATE_TEMPLATE_SELECTION_HINT}.`);
189
86
  }
190
- return normalizeTemplateSelection(await selectTemplate());
87
+ return validateExplicitCreateTemplateId(await selectTemplate());
191
88
  }
192
89
  /**
193
90
  * Resolve the package manager id from flags, defaults, and interactive selection.
@@ -15,6 +15,7 @@ import { pathExists, readOptionalUtf8File, } from "./fs-async.js";
15
15
  import { normalizePackageJson } from "./scaffold-package-manager-files.js";
16
16
  export { applyWorkspaceMigrationCapability, isOfficialWorkspaceProject, } from "./scaffold-bootstrap.js";
17
17
  import { replaceRepositoryReferencePlaceholders, resolveScaffoldRepositoryReference, } from "./scaffold-repository-reference.js";
18
+ import { isCompoundPersistenceEnabled } from "./scaffold-template-variable-groups.js";
18
19
  export { buildGitignore, buildReadme, mergeTextLines } from "./scaffold-documents.js";
19
20
  async function reportScaffoldProgress(onProgress, event) {
20
21
  await onProgress?.(event);
@@ -103,7 +104,7 @@ async function withEphemeralScaffoldNodeModules(targetDir, callback) {
103
104
  */
104
105
  export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, variables) {
105
106
  const needsPersistenceArtifacts = templateId === "persistence" ||
106
- (templateId === "compound" && variables.compoundPersistenceEnabled === "true");
107
+ (templateId === "compound" && isCompoundPersistenceEnabled(variables));
107
108
  if (!needsPersistenceArtifacts) {
108
109
  return;
109
110
  }
@@ -256,7 +257,7 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
256
257
  await fsp.writeFile(gitignorePath, mergeTextLines(gitignoreContent ?? buildGitignore(), existingGitignore), "utf8");
257
258
  await normalizePackageJson(projectDir, packageManager);
258
259
  await applyGeneratedProjectDxPackageJson({
259
- compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
260
+ compoundPersistenceEnabled: isCompoundPersistenceEnabled(variables),
260
261
  packageManager,
261
262
  projectDir,
262
263
  templateId,
@@ -1,3 +1,4 @@
1
+ import { CLI_DIAGNOSTIC_CODES, createCliDiagnosticCodeError, } from "./cli-diagnostics.js";
1
2
  import { toKebabCase, toSnakeCase, } from "./string-case.js";
2
3
  const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
3
4
  const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
@@ -27,7 +28,7 @@ export function validatePhpPrefix(input) {
27
28
  export function assertValidIdentifier(label, value, validate) {
28
29
  const result = validate(value);
29
30
  if (result !== true) {
30
- throw new Error(typeof result === "string" ? `${label}: ${result}` : `${label} is invalid`);
31
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, typeof result === "string" ? `${label}: ${result}` : `${label} is invalid`);
31
32
  }
32
33
  return value;
33
34
  }
@@ -52,9 +53,9 @@ export function resolveNonEmptyNormalizedBlockSlug(options) {
52
53
  return normalizedSlug;
53
54
  }
54
55
  if (options.input.trim().length === 0) {
55
- throw new Error(`${options.label} is required. Use \`${options.usage}\`.`);
56
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `${options.label} is required. Use \`${options.usage}\`.`);
56
57
  }
57
- throw new Error(`${options.label} "${options.input.trim()}" normalizes to an empty slug. Use letters or numbers so wp-typia can generate a block slug.`);
58
+ throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.INVALID_ARGUMENT, `${options.label} "${options.input.trim()}" normalizes to an empty slug. Use letters or numbers so wp-typia can generate a block slug.`);
58
59
  }
59
60
  export function resolveValidatedBlockSlug(value) {
60
61
  return assertValidIdentifier("Block slug", normalizeBlockSlug(value), validateBlockSlug);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Revalidates target-language identifiers at the built-in template builder
3
+ * boundary so generated PHP and TypeScript never rely only on upstream CLI
4
+ * normalization.
5
+ */
6
+ export declare function assertScaffoldTemplateCodeIdentifiers(view: Record<string, unknown>): void;
@@ -0,0 +1,33 @@
1
+ const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/u;
2
+ const PHP_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_]*$/u;
3
+ const PHP_CONSTANT_IDENTIFIER_PATTERN = /^[A-Z_][A-Z0-9_]*$/u;
4
+ const JAVASCRIPT_IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
5
+ const QUERY_POST_TYPE_PATTERN = /^[a-z0-9_-]{1,20}$/u;
6
+ function assertOptionalStringPattern(view, key, pattern, description) {
7
+ const value = view[key];
8
+ if (typeof value === "undefined") {
9
+ return;
10
+ }
11
+ if (typeof value !== "string" || !pattern.test(value)) {
12
+ throw new Error(`Unsafe scaffold template variable "${key}" for ${description}: ${JSON.stringify(value)}.`);
13
+ }
14
+ }
15
+ /**
16
+ * Revalidates target-language identifiers at the built-in template builder
17
+ * boundary so generated PHP and TypeScript never rely only on upstream CLI
18
+ * normalization.
19
+ */
20
+ export function assertScaffoldTemplateCodeIdentifiers(view) {
21
+ assertOptionalStringPattern(view, "namespace", BLOCK_SLUG_PATTERN, "block namespace");
22
+ assertOptionalStringPattern(view, "slug", BLOCK_SLUG_PATTERN, "block slug");
23
+ assertOptionalStringPattern(view, "slugKebabCase", BLOCK_SLUG_PATTERN, "block slug");
24
+ assertOptionalStringPattern(view, "textDomain", BLOCK_SLUG_PATTERN, "text domain");
25
+ assertOptionalStringPattern(view, "textdomain", BLOCK_SLUG_PATTERN, "text domain");
26
+ assertOptionalStringPattern(view, "queryPostType", QUERY_POST_TYPE_PATTERN, "query post type");
27
+ assertOptionalStringPattern(view, "phpPrefix", PHP_IDENTIFIER_PATTERN, "PHP identifier");
28
+ assertOptionalStringPattern(view, "slugSnakeCase", PHP_IDENTIFIER_PATTERN, "PHP identifier");
29
+ assertOptionalStringPattern(view, "phpPrefixUpper", PHP_CONSTANT_IDENTIFIER_PATTERN, "PHP constant identifier");
30
+ assertOptionalStringPattern(view, "pascalCase", JAVASCRIPT_IDENTIFIER_PATTERN, "JavaScript identifier");
31
+ assertOptionalStringPattern(view, "slugCamelCase", JAVASCRIPT_IDENTIFIER_PATTERN, "JavaScript identifier");
32
+ assertOptionalStringPattern(view, "titleCase", JAVASCRIPT_IDENTIFIER_PATTERN, "JavaScript identifier");
33
+ }
@@ -153,4 +153,6 @@ export type PersistenceScaffoldTemplateVariablesLike = ScaffoldTemplateVariableG
153
153
  };
154
154
  export declare function attachScaffoldTemplateVariableGroups<TVariables extends Record<string, string>>(variables: TVariables, groups: ScaffoldTemplateVariableGroups): TVariables & ScaffoldTemplateVariableGroupsCarrier;
155
155
  export declare function getScaffoldTemplateVariableGroups(variables: ScaffoldTemplateVariableGroupsCarrier): ScaffoldTemplateVariableGroups;
156
+ export declare function isCompoundPersistenceEnabled(variables: ScaffoldTemplateVariableGroupsCarrier): boolean;
157
+ export declare function getScaffoldAlternateRenderTargets(variables: ScaffoldTemplateVariableGroupsCarrier): ScaffoldAlternateRenderTargetVariableGroup;
156
158
  export {};
@@ -11,3 +11,10 @@ export function attachScaffoldTemplateVariableGroups(variables, groups) {
11
11
  export function getScaffoldTemplateVariableGroups(variables) {
12
12
  return variables[SCAFFOLD_TEMPLATE_VARIABLE_GROUPS];
13
13
  }
14
+ export function isCompoundPersistenceEnabled(variables) {
15
+ const compound = getScaffoldTemplateVariableGroups(variables).compound;
16
+ return compound.enabled && compound.persistenceEnabled;
17
+ }
18
+ export function getScaffoldAlternateRenderTargets(variables) {
19
+ return getScaffoldTemplateVariableGroups(variables).alternateRenderTargets;
20
+ }
@@ -1,4 +1,3 @@
1
- import fs from "node:fs";
2
1
  import { promises as fsp } from "node:fs";
3
2
  import path from "node:path";
4
3
  import { getPackageManager } from "./package-managers.js";
@@ -14,6 +13,7 @@ import { BlockGeneratorService, } from "./block-generator-service.js";
14
13
  import { getTemplateVariables } from "./scaffold-template-variables.js";
15
14
  import { getScaffoldTemplateVariableGroups } from "./scaffold-template-variable-groups.js";
16
15
  import { assertExternalLayerCompositionOptions, } from "./cli-validation.js";
16
+ import { pathExists } from "./fs-async.js";
17
17
  export const DATA_STORAGE_MODES = ["post-meta", "custom-table"];
18
18
  export const PERSISTENCE_POLICIES = ["authenticated", "public"];
19
19
  export { buildBlockCssClassName } from "./scaffold-identifiers.js";
@@ -149,7 +149,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, alterna
149
149
  title: "Finalizing scaffold output",
150
150
  });
151
151
  const readmePath = path.join(projectDir, "README.md");
152
- if (!fs.existsSync(readmePath)) {
152
+ if (!(await pathExists(readmePath))) {
153
153
  await fsp.writeFile(readmePath, buildReadme(resolvedTemplateId, variables, resolvedPackageManager, {
154
154
  withMigrationUi: isBuiltInTemplate || isWorkspace ? withMigrationUi : false,
155
155
  withTestPreset: isBuiltInTemplate ? withTestPreset : false,
@@ -157,7 +157,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, alterna
157
157
  }), "utf8");
158
158
  }
159
159
  const gitignorePath = path.join(projectDir, ".gitignore");
160
- const existingGitignore = fs.existsSync(gitignorePath)
160
+ const existingGitignore = (await pathExists(gitignorePath))
161
161
  ? await fsp.readFile(gitignorePath, "utf8")
162
162
  : "";
163
163
  await fsp.writeFile(gitignorePath, mergeTextLines(buildGitignore(), existingGitignore), "utf8");
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Normalize arbitrary text into a kebab-case identifier.
3
3
  * Common acronym runs stay grouped, with a boundary before the next
4
- * capitalized word.
5
- * Ambiguous acronym+lowercase inputs like `URLslug` intentionally stay as one
6
- * word because there is no PascalCase boundary marker before the lowercase
7
- * suffix.
4
+ * capitalized word or before a narrow allow-list of lowercase WordPress slug
5
+ * terms. Naturalized words such as `RESTful` intentionally stay as one word.
8
6
  *
9
7
  * @param input Raw text that may contain spaces, punctuation, or camelCase.
10
8
  * @returns A lowercase kebab-case string with collapsed separators.
@@ -6,7 +6,9 @@ const COMMON_ACRONYM_PREFIXES = [
6
6
  'JSON',
7
7
  'REST',
8
8
  'UUID',
9
+ 'AJAX',
9
10
  'API',
11
+ 'CPT',
10
12
  'CSS',
11
13
  'CTA',
12
14
  'DOM',
@@ -20,12 +22,18 @@ const COMMON_ACRONYM_PREFIXES = [
20
22
  'UI',
21
23
  'WP',
22
24
  ];
25
+ // Keep lowercase suffix splitting narrow so naturalized words such as
26
+ // `RESTful` remain stable while WordPress slug terms like `URLslug` read well.
27
+ const COMMON_ACRONYM_LOWERCASE_SUFFIXES = ['slug'];
23
28
  function capitalizeSegment(segment) {
24
29
  return segment.charAt(0).toUpperCase() + segment.slice(1);
25
30
  }
26
31
  function findCommonAcronymPrefix(segment) {
27
32
  return COMMON_ACRONYM_PREFIXES.find((prefix) => segment.startsWith(prefix));
28
33
  }
34
+ function isCommonAcronymLowercaseSuffix(suffix) {
35
+ return COMMON_ACRONYM_LOWERCASE_SUFFIXES.includes(suffix);
36
+ }
29
37
  function splitKnownAcronymSegment(segment) {
30
38
  const prefixes = [];
31
39
  let remaining = segment;
@@ -38,6 +46,9 @@ function splitKnownAcronymSegment(segment) {
38
46
  if (/^[A-Z][a-z]/.test(suffix)) {
39
47
  return [...prefixes, prefix, suffix].join('-');
40
48
  }
49
+ if (/^[a-z]+$/.test(suffix) && isCommonAcronymLowercaseSuffix(suffix)) {
50
+ return [...prefixes, prefix, suffix].join('-');
51
+ }
41
52
  if (!findCommonAcronymPrefix(suffix)) {
42
53
  break;
43
54
  }
@@ -52,10 +63,8 @@ function splitAcronymBoundary(value) {
52
63
  /**
53
64
  * Normalize arbitrary text into a kebab-case identifier.
54
65
  * Common acronym runs stay grouped, with a boundary before the next
55
- * capitalized word.
56
- * Ambiguous acronym+lowercase inputs like `URLslug` intentionally stay as one
57
- * word because there is no PascalCase boundary marker before the lowercase
58
- * suffix.
66
+ * capitalized word or before a narrow allow-list of lowercase WordPress slug
67
+ * terms. Naturalized words such as `RESTful` intentionally stay as one word.
59
68
  *
60
69
  * @param input Raw text that may contain spaces, punctuation, or camelCase.
61
70
  * @returns A lowercase kebab-case string with collapsed separators.
@@ -1,6 +1,6 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import { promises as fsp } from "node:fs";
3
+ import { pathExists } from "./fs-async.js";
4
4
  import { createManagedTempRoot } from "./temp-roots.js";
5
5
  import { getTemplateById, SHARED_BASE_TEMPLATE_ROOT, SHARED_COMPOUND_TEMPLATE_ROOT, SHARED_MIGRATION_UI_TEMPLATE_ROOT, SHARED_PERSISTENCE_TEMPLATE_ROOT, SHARED_PRESET_TEMPLATE_ROOT, SHARED_REST_HELPER_TEMPLATE_ROOT, SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
6
6
  const BUILT_IN_SHARED_TEMPLATE_LAYERS = Object.freeze([
@@ -138,20 +138,23 @@ export function isOmittableBuiltInTemplateLayerDir(templateId, layerDir) {
138
138
  return (OMITTABLE_BUILT_IN_OVERLAY_TEMPLATE_IDS.has(templateId) &&
139
139
  layerDir === getTemplateById(templateId).templateDir);
140
140
  }
141
- function resolveMaterializedTemplateLayerDirs(templateId, layerDirs) {
142
- return layerDirs.flatMap((layerDir) => {
143
- if (fs.existsSync(layerDir)) {
144
- return [layerDir];
141
+ async function resolveMaterializedTemplateLayerDirs(templateId, layerDirs) {
142
+ const materializedLayerDirs = [];
143
+ for (const layerDir of layerDirs) {
144
+ if (await pathExists(layerDir)) {
145
+ materializedLayerDirs.push(layerDir);
146
+ continue;
145
147
  }
146
148
  if (isOmittableBuiltInTemplateLayerDir(templateId, layerDir)) {
147
- return [];
149
+ continue;
148
150
  }
149
151
  throw new Error(`Built-in template layer is missing: ${layerDir}`);
150
- });
152
+ }
153
+ return materializedLayerDirs;
151
154
  }
152
155
  async function materializeBuiltInTemplateSource(templateId, layerDirs) {
153
156
  const template = getTemplateById(templateId);
154
- const materializedLayerDirs = resolveMaterializedTemplateLayerDirs(templateId, layerDirs);
157
+ const materializedLayerDirs = await resolveMaterializedTemplateLayerDirs(templateId, layerDirs);
155
158
  const { path: tempRoot, cleanup } = await createManagedTempRoot("wp-typia-template-");
156
159
  const templateDir = path.join(tempRoot, templateId);
157
160
  try {
@@ -1,6 +1,6 @@
1
- import fs from "node:fs";
2
1
  import path from "node:path";
3
2
  import { promises as fsp } from "node:fs";
3
+ import { pathExists } from "./fs-async.js";
4
4
  import { isPlainObject } from "./object-utils.js";
5
5
  import { getBuiltInSharedTemplateLayerDir, isBuiltInSharedTemplateLayerId, } from "./template-builtins.js";
6
6
  import { listInterpolatedDirectoryOutputs } from "./template-render.js";
@@ -56,7 +56,7 @@ function parseLayerDefinition(layerId, value) {
56
56
  }
57
57
  export async function loadExternalTemplateLayerManifest(sourceRoot) {
58
58
  const manifestPath = path.join(sourceRoot, TEMPLATE_LAYER_MANIFEST_FILENAME);
59
- if (!fs.existsSync(manifestPath)) {
59
+ if (!(await pathExists(manifestPath))) {
60
60
  return null;
61
61
  }
62
62
  const raw = JSON.parse(await fsp.readFile(manifestPath, "utf8"));
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Environment variable that disables external template cache reads and writes.
3
+ *
4
+ * Set to `0`, `false`, `no`, or `off` to bypass the cache.
5
+ */
6
+ export declare const EXTERNAL_TEMPLATE_CACHE_ENV = "WP_TYPIA_EXTERNAL_TEMPLATE_CACHE";
7
+ /**
8
+ * Environment variable that overrides the external template cache root.
9
+ */
10
+ export declare const EXTERNAL_TEMPLATE_CACHE_DIR_ENV = "WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR";
11
+ /**
12
+ * Environment variable that enables TTL-based external template cache pruning.
13
+ *
14
+ * Unset, empty, zero, negative, and non-numeric values keep pruning disabled.
15
+ */
16
+ export declare const EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV = "WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_TTL_DAYS";
17
+ /**
18
+ * Environment variable that overrides how often TTL pruning may scan the cache.
19
+ *
20
+ * Unset values use the default interval. Zero, negative, and non-numeric values
21
+ * disable scan throttling.
22
+ */
23
+ export declare const EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV = "WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS";
24
+ /**
25
+ * Checks whether remote external template source caching is enabled.
26
+ *
27
+ * Caching is enabled by default. Set `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE` to
28
+ * `0`, `false`, `no`, or `off` to force uncached resolution.
29
+ *
30
+ * @param env Environment object to inspect, defaulting to `process.env`.
31
+ * @returns Whether external template source cache reads and writes are enabled.
32
+ */
33
+ export declare function isExternalTemplateCacheEnabled(env?: NodeJS.ProcessEnv): boolean;
34
+ /**
35
+ * Resolves the external template source cache root directory.
36
+ *
37
+ * `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR` overrides the location. Without an
38
+ * override, wp-typia uses a per-user `wp-typia-template-source-cache-*`
39
+ * directory inside the operating system temp directory.
40
+ *
41
+ * @param env Environment object to inspect, defaulting to `process.env`.
42
+ * @returns Absolute cache root directory path.
43
+ */
44
+ export declare function getExternalTemplateCacheRoot(env?: NodeJS.ProcessEnv): string;
45
+ export declare function resolveExternalTemplateCacheTtlMs(options?: {
46
+ env?: NodeJS.ProcessEnv;
47
+ ttlDays?: number;
48
+ }): number | null;
49
+ export declare function resolveExternalTemplateCachePruneIntervalMs(options?: {
50
+ env?: NodeJS.ProcessEnv;
51
+ pruneIntervalMs?: number;
52
+ }): number | null;
53
+ export declare function getExternalTemplateCacheNowMs(now: Date | number | undefined): number;