@wp-typia/project-tools 0.22.6 → 0.22.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/block-targets.d.ts +40 -0
- package/dist/runtime/block-targets.js +71 -0
- package/dist/runtime/built-in-block-artifact-types.js +2 -1
- package/dist/runtime/built-in-block-attribute-specs.js +2 -1
- package/dist/runtime/built-in-block-code-artifacts.js +2 -0
- package/dist/runtime/built-in-block-non-ts-family-artifacts.js +12 -9
- package/dist/runtime/built-in-block-non-ts-render-utils.js +2 -0
- package/dist/runtime/cli-add-block-config.js +2 -1
- package/dist/runtime/cli-add-block-json.d.ts +2 -2
- package/dist/runtime/cli-add-block-json.js +5 -4
- package/dist/runtime/cli-add-block.js +4 -3
- package/dist/runtime/cli-add-workspace-ability-scaffold.js +21 -15
- package/dist/runtime/cli-add-workspace-ability.js +2 -2
- package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +17 -13
- package/dist/runtime/cli-add-workspace-admin-view.js +2 -2
- package/dist/runtime/cli-add-workspace-ai.js +2 -2
- package/dist/runtime/cli-add-workspace-assets.js +42 -48
- package/dist/runtime/cli-add-workspace-rest.js +2 -2
- package/dist/runtime/cli-add-workspace.js +14 -49
- package/dist/runtime/cli-diagnostics.js +6 -0
- package/dist/runtime/cli-init-plan-presentation.d.ts +16 -0
- package/dist/runtime/cli-init-plan-presentation.js +74 -0
- package/dist/runtime/cli-init-plan.js +5 -77
- package/dist/runtime/cli-scaffold.js +2 -1
- package/dist/runtime/create-template-validation.d.ts +10 -0
- package/dist/runtime/create-template-validation.js +121 -0
- package/dist/runtime/package-versions.d.ts +1 -1
- package/dist/runtime/package-versions.js +16 -3
- package/dist/runtime/php-utils.js +151 -148
- package/dist/runtime/scaffold-answer-resolution.js +5 -108
- package/dist/runtime/scaffold-apply-utils.js +3 -2
- package/dist/runtime/scaffold-identifiers.js +4 -3
- package/dist/runtime/scaffold-template-assertions.d.ts +6 -0
- package/dist/runtime/scaffold-template-assertions.js +33 -0
- package/dist/runtime/scaffold-template-variable-groups.d.ts +2 -0
- package/dist/runtime/scaffold-template-variable-groups.js +7 -0
- package/dist/runtime/string-case.d.ts +2 -4
- package/dist/runtime/string-case.js +15 -4
- package/dist/runtime/template-source-cache-policy.d.ts +53 -0
- package/dist/runtime/template-source-cache-policy.js +135 -0
- package/dist/runtime/template-source-cache.d.ts +1 -45
- package/dist/runtime/template-source-cache.js +9 -152
- package/dist/runtime/ts-property-names.d.ts +11 -0
- package/dist/runtime/ts-property-names.js +16 -0
- package/dist/runtime/workspace-inventory.d.ts +13 -1
- package/dist/runtime/workspace-inventory.js +35 -9
- package/package.json +6 -1
|
@@ -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
|
|
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
|
|
56
|
+
throw createCliDiagnosticCodeError(CLI_DIAGNOSTIC_CODES.MISSING_ARGUMENT, `${options.label} is required. Use \`${options.usage}\`.`);
|
|
56
57
|
}
|
|
57
|
-
throw
|
|
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,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
|
-
*
|
|
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.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
// Keep this list fixed so generated slugs and file paths do not drift when
|
|
2
|
+
// project config changes. Domain-specific acronyms should use separators.
|
|
1
3
|
const COMMON_ACRONYM_PREFIXES = [
|
|
2
4
|
'HTML',
|
|
3
5
|
'HTTP',
|
|
4
6
|
'JSON',
|
|
5
7
|
'REST',
|
|
6
8
|
'UUID',
|
|
9
|
+
'AJAX',
|
|
7
10
|
'API',
|
|
11
|
+
'CPT',
|
|
8
12
|
'CSS',
|
|
9
13
|
'CTA',
|
|
10
14
|
'DOM',
|
|
@@ -18,12 +22,18 @@ const COMMON_ACRONYM_PREFIXES = [
|
|
|
18
22
|
'UI',
|
|
19
23
|
'WP',
|
|
20
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'];
|
|
21
28
|
function capitalizeSegment(segment) {
|
|
22
29
|
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
23
30
|
}
|
|
24
31
|
function findCommonAcronymPrefix(segment) {
|
|
25
32
|
return COMMON_ACRONYM_PREFIXES.find((prefix) => segment.startsWith(prefix));
|
|
26
33
|
}
|
|
34
|
+
function isCommonAcronymLowercaseSuffix(suffix) {
|
|
35
|
+
return COMMON_ACRONYM_LOWERCASE_SUFFIXES.includes(suffix);
|
|
36
|
+
}
|
|
27
37
|
function splitKnownAcronymSegment(segment) {
|
|
28
38
|
const prefixes = [];
|
|
29
39
|
let remaining = segment;
|
|
@@ -36,6 +46,9 @@ function splitKnownAcronymSegment(segment) {
|
|
|
36
46
|
if (/^[A-Z][a-z]/.test(suffix)) {
|
|
37
47
|
return [...prefixes, prefix, suffix].join('-');
|
|
38
48
|
}
|
|
49
|
+
if (/^[a-z]+$/.test(suffix) && isCommonAcronymLowercaseSuffix(suffix)) {
|
|
50
|
+
return [...prefixes, prefix, suffix].join('-');
|
|
51
|
+
}
|
|
39
52
|
if (!findCommonAcronymPrefix(suffix)) {
|
|
40
53
|
break;
|
|
41
54
|
}
|
|
@@ -50,10 +63,8 @@ function splitAcronymBoundary(value) {
|
|
|
50
63
|
/**
|
|
51
64
|
* Normalize arbitrary text into a kebab-case identifier.
|
|
52
65
|
* Common acronym runs stay grouped, with a boundary before the next
|
|
53
|
-
* capitalized word
|
|
54
|
-
*
|
|
55
|
-
* word because there is no PascalCase boundary marker before the lowercase
|
|
56
|
-
* 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.
|
|
57
68
|
*
|
|
58
69
|
* @param input Raw text that may contain spaces, punctuation, or camelCase.
|
|
59
70
|
* @returns A lowercase kebab-case string with collapsed separators.
|
|
@@ -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;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Environment variable that disables external template cache reads and writes.
|
|
5
|
+
*
|
|
6
|
+
* Set to `0`, `false`, `no`, or `off` to bypass the cache.
|
|
7
|
+
*/
|
|
8
|
+
export const EXTERNAL_TEMPLATE_CACHE_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE';
|
|
9
|
+
/**
|
|
10
|
+
* Environment variable that overrides the external template cache root.
|
|
11
|
+
*/
|
|
12
|
+
export const EXTERNAL_TEMPLATE_CACHE_DIR_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR';
|
|
13
|
+
/**
|
|
14
|
+
* Environment variable that enables TTL-based external template cache pruning.
|
|
15
|
+
*
|
|
16
|
+
* Unset, empty, zero, negative, and non-numeric values keep pruning disabled.
|
|
17
|
+
*/
|
|
18
|
+
export const EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_TTL_DAYS';
|
|
19
|
+
/**
|
|
20
|
+
* Environment variable that overrides how often TTL pruning may scan the cache.
|
|
21
|
+
*
|
|
22
|
+
* Unset values use the default interval. Zero, negative, and non-numeric values
|
|
23
|
+
* disable scan throttling.
|
|
24
|
+
*/
|
|
25
|
+
export const EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS';
|
|
26
|
+
/**
|
|
27
|
+
* Milliseconds in one TTL day.
|
|
28
|
+
*/
|
|
29
|
+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
30
|
+
/**
|
|
31
|
+
* Default minimum interval between full external template cache prune scans.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
34
|
+
/**
|
|
35
|
+
* Normalized environment values that disable the cache.
|
|
36
|
+
*/
|
|
37
|
+
const DISABLED_CACHE_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
38
|
+
/**
|
|
39
|
+
* Checks whether remote external template source caching is enabled.
|
|
40
|
+
*
|
|
41
|
+
* Caching is enabled by default. Set `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE` to
|
|
42
|
+
* `0`, `false`, `no`, or `off` to force uncached resolution.
|
|
43
|
+
*
|
|
44
|
+
* @param env Environment object to inspect, defaulting to `process.env`.
|
|
45
|
+
* @returns Whether external template source cache reads and writes are enabled.
|
|
46
|
+
*/
|
|
47
|
+
export function isExternalTemplateCacheEnabled(env = process.env) {
|
|
48
|
+
const rawValue = env[EXTERNAL_TEMPLATE_CACHE_ENV];
|
|
49
|
+
if (rawValue === undefined) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return !DISABLED_CACHE_VALUES.has(rawValue.trim().toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the external template source cache root directory.
|
|
56
|
+
*
|
|
57
|
+
* `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR` overrides the location. Without an
|
|
58
|
+
* override, wp-typia uses a per-user `wp-typia-template-source-cache-*`
|
|
59
|
+
* directory inside the operating system temp directory.
|
|
60
|
+
*
|
|
61
|
+
* @param env Environment object to inspect, defaulting to `process.env`.
|
|
62
|
+
* @returns Absolute cache root directory path.
|
|
63
|
+
*/
|
|
64
|
+
export function getExternalTemplateCacheRoot(env = process.env) {
|
|
65
|
+
const configuredCacheDir = env[EXTERNAL_TEMPLATE_CACHE_DIR_ENV]?.trim();
|
|
66
|
+
if (configuredCacheDir) {
|
|
67
|
+
return path.resolve(configuredCacheDir);
|
|
68
|
+
}
|
|
69
|
+
return path.join(os.tmpdir(), `wp-typia-template-source-cache-${getCurrentUserCacheSegment()}`);
|
|
70
|
+
}
|
|
71
|
+
function parseExternalTemplateCacheTtlDays(value) {
|
|
72
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const ttlDays = typeof value === 'number' ? value : Number(value.trim());
|
|
76
|
+
if (!Number.isFinite(ttlDays) || ttlDays <= 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return ttlDays;
|
|
80
|
+
}
|
|
81
|
+
export function resolveExternalTemplateCacheTtlMs(options = {}) {
|
|
82
|
+
const env = options.env ?? process.env;
|
|
83
|
+
const ttlDays = options.ttlDays === undefined
|
|
84
|
+
? parseExternalTemplateCacheTtlDays(env[EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV])
|
|
85
|
+
: parseExternalTemplateCacheTtlDays(options.ttlDays);
|
|
86
|
+
if (ttlDays === null) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const ttlMs = ttlDays * MILLISECONDS_PER_DAY;
|
|
90
|
+
return Number.isFinite(ttlMs) ? ttlMs : null;
|
|
91
|
+
}
|
|
92
|
+
function parseExternalTemplateCachePruneIntervalMs(value) {
|
|
93
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const intervalMs = typeof value === 'number' ? value : Number(value.trim());
|
|
97
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return intervalMs;
|
|
101
|
+
}
|
|
102
|
+
export function resolveExternalTemplateCachePruneIntervalMs(options = {}) {
|
|
103
|
+
if (options.pruneIntervalMs !== undefined) {
|
|
104
|
+
return parseExternalTemplateCachePruneIntervalMs(options.pruneIntervalMs);
|
|
105
|
+
}
|
|
106
|
+
const env = options.env ?? process.env;
|
|
107
|
+
const envValue = env[EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV];
|
|
108
|
+
if (envValue === undefined) {
|
|
109
|
+
return DEFAULT_CACHE_PRUNE_INTERVAL_MS;
|
|
110
|
+
}
|
|
111
|
+
return parseExternalTemplateCachePruneIntervalMs(envValue);
|
|
112
|
+
}
|
|
113
|
+
export function getExternalTemplateCacheNowMs(now) {
|
|
114
|
+
const nowMs = now instanceof Date
|
|
115
|
+
? now.getTime()
|
|
116
|
+
: typeof now === 'number'
|
|
117
|
+
? now
|
|
118
|
+
: Date.now();
|
|
119
|
+
return Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
120
|
+
}
|
|
121
|
+
function getCurrentUserCacheSegment() {
|
|
122
|
+
if (typeof process.getuid === 'function') {
|
|
123
|
+
return String(process.getuid());
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const safeUsername = os
|
|
127
|
+
.userInfo()
|
|
128
|
+
.username.trim()
|
|
129
|
+
.replace(/[^A-Za-z0-9._-]+/gu, '-');
|
|
130
|
+
return safeUsername.length > 0 ? safeUsername : 'user';
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return 'user';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -1,26 +1,4 @@
|
|
|
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";
|
|
1
|
+
export { EXTERNAL_TEMPLATE_CACHE_DIR_ENV, EXTERNAL_TEMPLATE_CACHE_ENV, EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV, EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, getExternalTemplateCacheRoot, isExternalTemplateCacheEnabled, } from './template-source-cache-policy.js';
|
|
24
2
|
/**
|
|
25
3
|
* Serializable metadata recorded in the cache marker for diagnostics.
|
|
26
4
|
*/
|
|
@@ -125,27 +103,6 @@ export interface ExternalTemplateCachePruneResult {
|
|
|
125
103
|
*/
|
|
126
104
|
ttlMs: number | null;
|
|
127
105
|
}
|
|
128
|
-
/**
|
|
129
|
-
* Checks whether remote external template source caching is enabled.
|
|
130
|
-
*
|
|
131
|
-
* Caching is enabled by default. Set `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE` to
|
|
132
|
-
* `0`, `false`, `no`, or `off` to force uncached resolution.
|
|
133
|
-
*
|
|
134
|
-
* @param env Environment object to inspect, defaulting to `process.env`.
|
|
135
|
-
* @returns Whether external template source cache reads and writes are enabled.
|
|
136
|
-
*/
|
|
137
|
-
export declare function isExternalTemplateCacheEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
138
|
-
/**
|
|
139
|
-
* Resolves the external template source cache root directory.
|
|
140
|
-
*
|
|
141
|
-
* `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR` overrides the location. Without an
|
|
142
|
-
* override, wp-typia uses a per-user `wp-typia-template-source-cache-*`
|
|
143
|
-
* directory inside the operating system temp directory.
|
|
144
|
-
*
|
|
145
|
-
* @param env Environment object to inspect, defaulting to `process.env`.
|
|
146
|
-
* @returns Absolute cache root directory path.
|
|
147
|
-
*/
|
|
148
|
-
export declare function getExternalTemplateCacheRoot(env?: NodeJS.ProcessEnv): string;
|
|
149
106
|
/**
|
|
150
107
|
* Creates a deterministic cache key from source identity and integrity parts.
|
|
151
108
|
*
|
|
@@ -187,4 +144,3 @@ export declare function findReusableExternalTemplateSourceCache(descriptor: Exte
|
|
|
187
144
|
* @returns Cache resolution details, or `null` when caching is disabled.
|
|
188
145
|
*/
|
|
189
146
|
export declare function resolveExternalTemplateSourceCache(descriptor: ExternalTemplateCacheDescriptor, populateSourceDir: (sourceDir: string) => Promise<void>): Promise<ExternalTemplateCacheResolution | null>;
|
|
190
|
-
export {};
|
|
@@ -1,31 +1,9 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
2
|
import { promises as fsp } from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
3
|
import path from 'node:path';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Set to `0`, `false`, `no`, or `off` to bypass the cache.
|
|
10
|
-
*/
|
|
11
|
-
export const EXTERNAL_TEMPLATE_CACHE_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE';
|
|
12
|
-
/**
|
|
13
|
-
* Environment variable that overrides the external template cache root.
|
|
14
|
-
*/
|
|
15
|
-
export const EXTERNAL_TEMPLATE_CACHE_DIR_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR';
|
|
16
|
-
/**
|
|
17
|
-
* Environment variable that enables TTL-based external template cache pruning.
|
|
18
|
-
*
|
|
19
|
-
* Unset, empty, zero, negative, and non-numeric values keep pruning disabled.
|
|
20
|
-
*/
|
|
21
|
-
export const EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_TTL_DAYS';
|
|
22
|
-
/**
|
|
23
|
-
* Environment variable that overrides how often TTL pruning may scan the cache.
|
|
24
|
-
*
|
|
25
|
-
* Unset values use the default interval. Zero, negative, and non-numeric values
|
|
26
|
-
* disable scan throttling.
|
|
27
|
-
*/
|
|
28
|
-
export const EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS';
|
|
4
|
+
import { getNodeErrorCode, pathExists } from './fs-async.js';
|
|
5
|
+
import { getExternalTemplateCacheNowMs, getExternalTemplateCacheRoot, isExternalTemplateCacheEnabled, resolveExternalTemplateCachePruneIntervalMs, resolveExternalTemplateCacheTtlMs, } from './template-source-cache-policy.js';
|
|
6
|
+
export { EXTERNAL_TEMPLATE_CACHE_DIR_ENV, EXTERNAL_TEMPLATE_CACHE_ENV, EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV, EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, getExternalTemplateCacheRoot, isExternalTemplateCacheEnabled, } from './template-source-cache-policy.js';
|
|
29
7
|
/**
|
|
30
8
|
* Marker file written after a cache entry is fully populated.
|
|
31
9
|
*/
|
|
@@ -34,14 +12,6 @@ const CACHE_MARKER_FILE = 'wp-typia-template-cache.json';
|
|
|
34
12
|
* Marker file written after a full TTL prune scan completes.
|
|
35
13
|
*/
|
|
36
14
|
const CACHE_PRUNE_MARKER_FILE = 'wp-typia-template-cache-prune.json';
|
|
37
|
-
/**
|
|
38
|
-
* Milliseconds in one TTL day.
|
|
39
|
-
*/
|
|
40
|
-
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
41
|
-
/**
|
|
42
|
-
* Default minimum interval between full external template cache prune scans.
|
|
43
|
-
*/
|
|
44
|
-
const DEFAULT_CACHE_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
45
15
|
/**
|
|
46
16
|
* Private directory mode used for cache roots and entries on POSIX platforms.
|
|
47
17
|
*/
|
|
@@ -50,10 +20,6 @@ const PRIVATE_CACHE_DIRECTORY_MODE = 0o700;
|
|
|
50
20
|
* Marker value used when URL-like metadata cannot be safely normalized.
|
|
51
21
|
*/
|
|
52
22
|
const REDACTED_CACHE_METADATA_VALUE = '[redacted]';
|
|
53
|
-
/**
|
|
54
|
-
* Normalized environment values that disable the cache.
|
|
55
|
-
*/
|
|
56
|
-
const DISABLED_CACHE_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
57
23
|
/**
|
|
58
24
|
* Filesystem errors that mean another writer published the same cache entry.
|
|
59
25
|
*/
|
|
@@ -80,80 +46,10 @@ const SAFE_CACHE_NAMESPACE_SEGMENT = /^[A-Za-z0-9_.-]+$/u;
|
|
|
80
46
|
* Cache entries are deterministic SHA-256 digest directory names.
|
|
81
47
|
*/
|
|
82
48
|
const SAFE_CACHE_ENTRY_SEGMENT = /^[a-f0-9]{64}$/u;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* `0`, `false`, `no`, or `off` to force uncached resolution.
|
|
88
|
-
*
|
|
89
|
-
* @param env Environment object to inspect, defaulting to `process.env`.
|
|
90
|
-
* @returns Whether external template source cache reads and writes are enabled.
|
|
91
|
-
*/
|
|
92
|
-
export function isExternalTemplateCacheEnabled(env = process.env) {
|
|
93
|
-
const rawValue = env[EXTERNAL_TEMPLATE_CACHE_ENV];
|
|
94
|
-
if (rawValue === undefined) {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
return !DISABLED_CACHE_VALUES.has(rawValue.trim().toLowerCase());
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Resolves the external template source cache root directory.
|
|
101
|
-
*
|
|
102
|
-
* `WP_TYPIA_EXTERNAL_TEMPLATE_CACHE_DIR` overrides the location. Without an
|
|
103
|
-
* override, wp-typia uses a per-user `wp-typia-template-source-cache-*`
|
|
104
|
-
* directory inside the operating system temp directory.
|
|
105
|
-
*
|
|
106
|
-
* @param env Environment object to inspect, defaulting to `process.env`.
|
|
107
|
-
* @returns Absolute cache root directory path.
|
|
108
|
-
*/
|
|
109
|
-
export function getExternalTemplateCacheRoot(env = process.env) {
|
|
110
|
-
const configuredCacheDir = env[EXTERNAL_TEMPLATE_CACHE_DIR_ENV]?.trim();
|
|
111
|
-
if (configuredCacheDir) {
|
|
112
|
-
return path.resolve(configuredCacheDir);
|
|
113
|
-
}
|
|
114
|
-
return path.join(os.tmpdir(), `wp-typia-template-source-cache-${getCurrentUserCacheSegment()}`);
|
|
115
|
-
}
|
|
116
|
-
function parseExternalTemplateCacheTtlDays(value) {
|
|
117
|
-
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
const ttlDays = typeof value === 'number' ? value : Number(value.trim());
|
|
121
|
-
if (!Number.isFinite(ttlDays) || ttlDays <= 0) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
return ttlDays;
|
|
125
|
-
}
|
|
126
|
-
function resolveExternalTemplateCacheTtlMs(options = {}) {
|
|
127
|
-
const env = options.env ?? process.env;
|
|
128
|
-
const ttlDays = options.ttlDays === undefined
|
|
129
|
-
? parseExternalTemplateCacheTtlDays(env[EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV])
|
|
130
|
-
: parseExternalTemplateCacheTtlDays(options.ttlDays);
|
|
131
|
-
if (ttlDays === null) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
const ttlMs = ttlDays * MILLISECONDS_PER_DAY;
|
|
135
|
-
return Number.isFinite(ttlMs) ? ttlMs : null;
|
|
136
|
-
}
|
|
137
|
-
function parseExternalTemplateCachePruneIntervalMs(value) {
|
|
138
|
-
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
const intervalMs = typeof value === 'number' ? value : Number(value.trim());
|
|
142
|
-
if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
return intervalMs;
|
|
146
|
-
}
|
|
147
|
-
function resolveExternalTemplateCachePruneIntervalMs(options = {}) {
|
|
148
|
-
if (options.pruneIntervalMs !== undefined) {
|
|
149
|
-
return parseExternalTemplateCachePruneIntervalMs(options.pruneIntervalMs);
|
|
150
|
-
}
|
|
151
|
-
const env = options.env ?? process.env;
|
|
152
|
-
const envValue = env[EXTERNAL_TEMPLATE_CACHE_PRUNE_INTERVAL_MS_ENV];
|
|
153
|
-
if (envValue === undefined) {
|
|
154
|
-
return DEFAULT_CACHE_PRUNE_INTERVAL_MS;
|
|
155
|
-
}
|
|
156
|
-
return parseExternalTemplateCachePruneIntervalMs(envValue);
|
|
49
|
+
function createTemporaryCacheEntryDirName(cacheKey) {
|
|
50
|
+
// Crypto randomness keeps concurrent cache populators from colliding on the
|
|
51
|
+
// staging directory before the final atomic rename publishes the cache entry.
|
|
52
|
+
return `.tmp-${cacheKey}-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
157
53
|
}
|
|
158
54
|
/**
|
|
159
55
|
* Creates a deterministic cache key from source identity and integrity parts.
|
|
@@ -166,15 +62,6 @@ export function createExternalTemplateCacheKey(keyParts) {
|
|
|
166
62
|
.update(JSON.stringify(keyParts))
|
|
167
63
|
.digest('hex');
|
|
168
64
|
}
|
|
169
|
-
async function pathExists(filePath) {
|
|
170
|
-
try {
|
|
171
|
-
await fsp.access(filePath, fs.constants.F_OK);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
65
|
async function isDirectoryPath(directory) {
|
|
179
66
|
try {
|
|
180
67
|
const stats = await fsp.lstat(directory);
|
|
@@ -184,11 +71,6 @@ async function isDirectoryPath(directory) {
|
|
|
184
71
|
return false;
|
|
185
72
|
}
|
|
186
73
|
}
|
|
187
|
-
function getNodeErrorCode(error) {
|
|
188
|
-
return typeof error === 'object' && error !== null && 'code' in error
|
|
189
|
-
? String(error.code)
|
|
190
|
-
: '';
|
|
191
|
-
}
|
|
192
74
|
async function removeTemporaryCacheEntry(entryDir) {
|
|
193
75
|
try {
|
|
194
76
|
await fsp.rm(entryDir, { force: true, recursive: true });
|
|
@@ -197,21 +79,6 @@ async function removeTemporaryCacheEntry(entryDir) {
|
|
|
197
79
|
// Cache cleanup is best-effort; the caller can still continue uncached.
|
|
198
80
|
}
|
|
199
81
|
}
|
|
200
|
-
function getCurrentUserCacheSegment() {
|
|
201
|
-
if (typeof process.getuid === 'function') {
|
|
202
|
-
return String(process.getuid());
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
const safeUsername = os
|
|
206
|
-
.userInfo()
|
|
207
|
-
.username.trim()
|
|
208
|
-
.replace(/[^A-Za-z0-9._-]+/gu, '-');
|
|
209
|
-
return safeUsername.length > 0 ? safeUsername : 'user';
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
return 'user';
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
82
|
function getCurrentUid() {
|
|
216
83
|
return typeof process.getuid === 'function' ? process.getuid() : null;
|
|
217
84
|
}
|
|
@@ -360,14 +227,6 @@ async function readCacheEntryMarker(markerPath) {
|
|
|
360
227
|
function cacheMetadataMatches(actual, expected) {
|
|
361
228
|
return Object.entries(expected).every(([key, value]) => actual[key] === value);
|
|
362
229
|
}
|
|
363
|
-
function getExternalTemplateCacheNowMs(now) {
|
|
364
|
-
const nowMs = now instanceof Date
|
|
365
|
-
? now.getTime()
|
|
366
|
-
: typeof now === 'number'
|
|
367
|
-
? now
|
|
368
|
-
: Date.now();
|
|
369
|
-
return Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
370
|
-
}
|
|
371
230
|
function isCacheEntryFreshForTtl(createdAtMs, nowMs, ttlMs) {
|
|
372
231
|
return ttlMs === null || createdAtMs >= nowMs - ttlMs;
|
|
373
232
|
}
|
|
@@ -673,9 +532,7 @@ export async function resolveExternalTemplateSourceCache(descriptor, populateSou
|
|
|
673
532
|
if (existingMarker) {
|
|
674
533
|
await removeCacheEntryWithinRoot(cacheRoot, entryDir);
|
|
675
534
|
}
|
|
676
|
-
const temporaryEntryDir = path.join(namespaceDir,
|
|
677
|
-
.toString(16)
|
|
678
|
-
.slice(2)}`);
|
|
535
|
+
const temporaryEntryDir = path.join(namespaceDir, createTemporaryCacheEntryDirName(cacheKey));
|
|
679
536
|
const temporarySourceDir = path.join(temporaryEntryDir, 'source');
|
|
680
537
|
let populateFailed = false;
|
|
681
538
|
try {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Extract the literal text for TypeScript property names this runtime supports.
|
|
4
|
+
*
|
|
5
|
+
* Computed property names are intentionally not resolved because their runtime
|
|
6
|
+
* values are not statically knowable from the syntax node alone.
|
|
7
|
+
*
|
|
8
|
+
* @param name TypeScript property name node.
|
|
9
|
+
* @returns Identifier, string-literal, or numeric-literal text; otherwise `null`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getPropertyNameText(name: ts.PropertyName): string | null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Extract the literal text for TypeScript property names this runtime supports.
|
|
4
|
+
*
|
|
5
|
+
* Computed property names are intentionally not resolved because their runtime
|
|
6
|
+
* values are not statically knowable from the syntax node alone.
|
|
7
|
+
*
|
|
8
|
+
* @param name TypeScript property name node.
|
|
9
|
+
* @returns Identifier, string-literal, or numeric-literal text; otherwise `null`.
|
|
10
|
+
*/
|
|
11
|
+
export function getPropertyNameText(name) {
|
|
12
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
13
|
+
return name.text;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|