@wp-typia/project-tools 0.22.10 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/cli-add-collision.d.ts +25 -0
- package/dist/runtime/cli-add-collision.js +76 -0
- package/dist/runtime/cli-add-help.js +11 -2
- package/dist/runtime/cli-add-kind-ids.d.ts +1 -1
- package/dist/runtime/cli-add-kind-ids.js +3 -0
- package/dist/runtime/cli-add-types.d.ts +117 -0
- package/dist/runtime/cli-add-types.js +26 -0
- package/dist/runtime/cli-add-validation.d.ts +90 -1
- package/dist/runtime/cli-add-validation.js +304 -1
- package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +74 -19
- package/dist/runtime/cli-add-workspace-admin-view-source.js +11 -2
- package/dist/runtime/cli-add-workspace-admin-view-templates.d.ts +20 -2
- package/dist/runtime/cli-add-workspace-admin-view-templates.js +359 -3
- package/dist/runtime/cli-add-workspace-admin-view-types.d.ts +21 -0
- package/dist/runtime/cli-add-workspace-admin-view-types.js +22 -0
- package/dist/runtime/cli-add-workspace-ai-anchors.js +121 -31
- package/dist/runtime/cli-add-workspace-contract-source-emitters.d.ts +15 -0
- package/dist/runtime/cli-add-workspace-contract-source-emitters.js +42 -0
- package/dist/runtime/cli-add-workspace-contract.d.ts +15 -0
- package/dist/runtime/cli-add-workspace-contract.js +65 -0
- package/dist/runtime/cli-add-workspace-integration-env.d.ts +24 -0
- package/dist/runtime/cli-add-workspace-integration-env.js +391 -0
- package/dist/runtime/cli-add-workspace-post-meta-anchors.d.ts +23 -0
- package/dist/runtime/cli-add-workspace-post-meta-anchors.js +244 -0
- package/dist/runtime/cli-add-workspace-post-meta-source-emitters.d.ts +63 -0
- package/dist/runtime/cli-add-workspace-post-meta-source-emitters.js +179 -0
- package/dist/runtime/cli-add-workspace-post-meta.d.ts +15 -0
- package/dist/runtime/cli-add-workspace-post-meta.js +107 -0
- package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +1 -0
- package/dist/runtime/cli-add-workspace-rest-anchors.js +285 -21
- package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +90 -2
- package/dist/runtime/cli-add-workspace-rest-source-emitters.js +302 -29
- package/dist/runtime/cli-add-workspace-rest.d.ts +15 -2
- package/dist/runtime/cli-add-workspace-rest.js +329 -21
- package/dist/runtime/cli-add-workspace.d.ts +15 -0
- package/dist/runtime/cli-add-workspace.js +15 -0
- package/dist/runtime/cli-add.d.ts +1 -1
- package/dist/runtime/cli-add.js +1 -1
- package/dist/runtime/cli-core.d.ts +2 -1
- package/dist/runtime/cli-core.js +2 -1
- package/dist/runtime/cli-doctor-environment.js +1 -3
- package/dist/runtime/cli-doctor-workspace-features.js +128 -10
- package/dist/runtime/cli-doctor-workspace-package.d.ts +25 -3
- package/dist/runtime/cli-doctor-workspace-package.js +35 -13
- package/dist/runtime/cli-doctor-workspace-shared.d.ts +2 -0
- package/dist/runtime/cli-doctor-workspace-shared.js +2 -0
- package/dist/runtime/cli-doctor-workspace.js +8 -3
- package/dist/runtime/cli-help.js +7 -0
- package/dist/runtime/cli-init-templates.js +11 -1
- package/dist/runtime/contract-artifacts.d.ts +14 -0
- package/dist/runtime/contract-artifacts.js +15 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/rest-resource-artifacts.d.ts +57 -1
- package/dist/runtime/rest-resource-artifacts.js +97 -1
- package/dist/runtime/template-render.d.ts +1 -1
- package/dist/runtime/template-render.js +1 -1
- package/dist/runtime/template-source-cache-markers.d.ts +37 -0
- package/dist/runtime/template-source-cache-markers.js +125 -0
- package/dist/runtime/template-source-cache.d.ts +1 -4
- package/dist/runtime/template-source-cache.js +16 -122
- package/dist/runtime/template-source-external.d.ts +4 -2
- package/dist/runtime/template-source-external.js +4 -2
- package/dist/runtime/template-source-remote.d.ts +8 -4
- package/dist/runtime/template-source-remote.js +8 -4
- package/dist/runtime/workspace-inventory-mutations.js +52 -3
- package/dist/runtime/workspace-inventory-parser.d.ts +3 -2
- package/dist/runtime/workspace-inventory-parser.js +126 -5
- package/dist/runtime/workspace-inventory-read.d.ts +9 -2
- package/dist/runtime/workspace-inventory-read.js +9 -2
- package/dist/runtime/workspace-inventory-templates.d.ts +16 -1
- package/dist/runtime/workspace-inventory-templates.js +74 -4
- package/dist/runtime/workspace-inventory-types.d.ts +51 -2
- package/dist/runtime/workspace-inventory.d.ts +2 -2
- package/dist/runtime/workspace-inventory.js +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { defineEndpointManifest, syncEndpointClient, syncRestOpenApi, syncTypeSchemas, } from "@wp-typia/block-runtime/metadata-core";
|
|
3
|
+
function resolveManualRestContractWordPressAuth(auth) {
|
|
4
|
+
if (auth === "authenticated") {
|
|
5
|
+
return {
|
|
6
|
+
mechanism: "rest-nonce",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
if (auth === "public-write-protected") {
|
|
10
|
+
return {
|
|
11
|
+
mechanism: "public-signed-token",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
3
16
|
/**
|
|
4
17
|
* Build the endpoint manifest for a workspace-level REST resource scaffold.
|
|
5
18
|
*
|
|
@@ -9,7 +22,12 @@ import { defineEndpointManifest, syncEndpointClient, syncRestOpenApi, syncTypeSc
|
|
|
9
22
|
*/
|
|
10
23
|
export function buildRestResourceEndpointManifest(variables, methods) {
|
|
11
24
|
const basePath = `/${variables.namespace}/${variables.slugKebabCase}`;
|
|
12
|
-
const
|
|
25
|
+
const routePattern = variables.routePattern == null
|
|
26
|
+
? `/${variables.slugKebabCase}/item`
|
|
27
|
+
: variables.routePattern.startsWith("/")
|
|
28
|
+
? variables.routePattern
|
|
29
|
+
: `/${variables.routePattern}`;
|
|
30
|
+
const itemPath = `/${variables.namespace}${routePattern}`;
|
|
13
31
|
const contracts = {};
|
|
14
32
|
const endpoints = [];
|
|
15
33
|
if (methods.includes("list")) {
|
|
@@ -124,6 +142,52 @@ export function buildRestResourceEndpointManifest(variables, methods) {
|
|
|
124
142
|
},
|
|
125
143
|
});
|
|
126
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Build the endpoint manifest for a type-only manual REST contract. Manual
|
|
147
|
+
* contracts describe routes owned by another integration without generating PHP
|
|
148
|
+
* route glue in the workspace.
|
|
149
|
+
*
|
|
150
|
+
* @param variables Template naming data used for contract names, route path,
|
|
151
|
+
* and OpenAPI info.
|
|
152
|
+
* @returns Endpoint manifest consumed by schema, OpenAPI, and client generators.
|
|
153
|
+
*/
|
|
154
|
+
export function buildManualRestContractEndpointManifest(variables) {
|
|
155
|
+
const contracts = {
|
|
156
|
+
query: {
|
|
157
|
+
sourceTypeName: variables.queryTypeName,
|
|
158
|
+
},
|
|
159
|
+
response: {
|
|
160
|
+
sourceTypeName: variables.responseTypeName,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
if (variables.bodyTypeName) {
|
|
164
|
+
contracts.request = {
|
|
165
|
+
sourceTypeName: variables.bodyTypeName,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const wordpressAuth = resolveManualRestContractWordPressAuth(variables.auth);
|
|
169
|
+
return defineEndpointManifest({
|
|
170
|
+
contracts,
|
|
171
|
+
endpoints: [
|
|
172
|
+
{
|
|
173
|
+
auth: variables.auth,
|
|
174
|
+
...(variables.bodyTypeName ? { bodyContract: "request" } : {}),
|
|
175
|
+
method: variables.method,
|
|
176
|
+
operationId: `call${variables.pascalCase}ManualRestContract`,
|
|
177
|
+
path: `/${variables.namespace}${variables.pathPattern}`,
|
|
178
|
+
queryContract: "query",
|
|
179
|
+
responseContract: "response",
|
|
180
|
+
summary: `Call external ${variables.title} REST route.`,
|
|
181
|
+
tags: [variables.title],
|
|
182
|
+
...(wordpressAuth ? { wordpressAuth } : {}),
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
info: {
|
|
186
|
+
title: `${variables.title} Manual REST Contract`,
|
|
187
|
+
version: "1.0.0",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
127
191
|
/**
|
|
128
192
|
* Synchronize generated schemas, OpenAPI output, and endpoint client code for
|
|
129
193
|
* a workspace-level REST resource scaffold.
|
|
@@ -156,3 +220,35 @@ export async function syncRestResourceArtifacts({ clientFile, methods, outputDir
|
|
|
156
220
|
validatorsFile,
|
|
157
221
|
});
|
|
158
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Synchronize generated schemas, OpenAPI output, and endpoint client code for a
|
|
225
|
+
* type-only manual REST contract.
|
|
226
|
+
*
|
|
227
|
+
* @param options Contract file paths and naming variables.
|
|
228
|
+
* @returns A promise that resolves after every generated artifact has refreshed.
|
|
229
|
+
*/
|
|
230
|
+
export async function syncManualRestContractArtifacts({ clientFile, outputDir, projectDir, typesFile, validatorsFile, variables, }) {
|
|
231
|
+
const manifest = buildManualRestContractEndpointManifest(variables);
|
|
232
|
+
for (const [baseName, contract] of Object.entries(manifest.contracts)) {
|
|
233
|
+
await syncTypeSchemas({
|
|
234
|
+
jsonSchemaFile: path.join(outputDir, "api-schemas", `${baseName}.schema.json`),
|
|
235
|
+
openApiFile: path.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
|
|
236
|
+
projectRoot: projectDir,
|
|
237
|
+
sourceTypeName: contract.sourceTypeName,
|
|
238
|
+
typesFile,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
await syncRestOpenApi({
|
|
242
|
+
manifest,
|
|
243
|
+
openApiFile: path.join(outputDir, "api.openapi.json"),
|
|
244
|
+
projectRoot: projectDir,
|
|
245
|
+
typesFile,
|
|
246
|
+
});
|
|
247
|
+
await syncEndpointClient({
|
|
248
|
+
clientFile,
|
|
249
|
+
manifest,
|
|
250
|
+
projectRoot: projectDir,
|
|
251
|
+
typesFile,
|
|
252
|
+
validatorsFile,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -55,4 +55,4 @@ export declare function copyInterpolatedDirectory(sourceDir: string, targetDir:
|
|
|
55
55
|
* preview root.
|
|
56
56
|
*/
|
|
57
57
|
export declare function listInterpolatedDirectoryOutputs(sourceDir: string, view: Record<string, string>): Promise<string[]>;
|
|
58
|
-
export declare function
|
|
58
|
+
export declare function pathExistsSync(targetPath: string): boolean;
|
|
@@ -191,6 +191,6 @@ export async function listInterpolatedDirectoryOutputs(sourceDir, view) {
|
|
|
191
191
|
});
|
|
192
192
|
return outputs.sort((left, right) => left.localeCompare(right));
|
|
193
193
|
}
|
|
194
|
-
export function
|
|
194
|
+
export function pathExistsSync(targetPath) {
|
|
195
195
|
return fs.existsSync(targetPath);
|
|
196
196
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker file written after a cache entry is fully populated.
|
|
3
|
+
*/
|
|
4
|
+
export declare const CACHE_MARKER_FILE = "wp-typia-template-cache.json";
|
|
5
|
+
/**
|
|
6
|
+
* Marker file written after a full TTL prune scan completes.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CACHE_PRUNE_MARKER_FILE = "wp-typia-template-cache-prune.json";
|
|
9
|
+
/**
|
|
10
|
+
* Serializable metadata recorded in cache markers for diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
export type ExternalTemplateCacheMetadata = Record<string, string | null>;
|
|
13
|
+
export interface ExternalTemplateCacheEntryMarker {
|
|
14
|
+
createdAtMs: number;
|
|
15
|
+
metadata: ExternalTemplateCacheMetadata;
|
|
16
|
+
}
|
|
17
|
+
export interface ExternalTemplateCachePruneMarker {
|
|
18
|
+
prunedAtMs: number;
|
|
19
|
+
pruneIntervalMs: number | null;
|
|
20
|
+
ttlMs: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function sanitizeExternalTemplateCacheMetadata(metadata: ExternalTemplateCacheMetadata): ExternalTemplateCacheMetadata;
|
|
23
|
+
export declare function parseExternalTemplateCacheEntryMarker(markerText: string): ExternalTemplateCacheEntryMarker | null;
|
|
24
|
+
export declare function externalTemplateCacheMetadataMatches(actual: ExternalTemplateCacheMetadata, expected: ExternalTemplateCacheMetadata): boolean;
|
|
25
|
+
export declare function isExternalTemplateCacheEntryFreshForTtl(createdAtMs: number, nowMs: number, ttlMs: number | null): boolean;
|
|
26
|
+
export declare function parseExternalTemplateCachePruneMarker(markerText: string): ExternalTemplateCachePruneMarker | null;
|
|
27
|
+
export declare function formatExternalTemplateCacheEntryMarker({ cacheKey, createdAt, metadata, namespace, }: {
|
|
28
|
+
cacheKey: string;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
metadata: ExternalTemplateCacheMetadata;
|
|
31
|
+
namespace: string;
|
|
32
|
+
}): string;
|
|
33
|
+
export declare function formatExternalTemplateCachePruneMarker({ nowMs, pruneIntervalMs, ttlMs, }: {
|
|
34
|
+
nowMs: number;
|
|
35
|
+
pruneIntervalMs: number | null;
|
|
36
|
+
ttlMs: number;
|
|
37
|
+
}): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker file written after a cache entry is fully populated.
|
|
3
|
+
*/
|
|
4
|
+
export const CACHE_MARKER_FILE = 'wp-typia-template-cache.json';
|
|
5
|
+
/**
|
|
6
|
+
* Marker file written after a full TTL prune scan completes.
|
|
7
|
+
*/
|
|
8
|
+
export const CACHE_PRUNE_MARKER_FILE = 'wp-typia-template-cache-prune.json';
|
|
9
|
+
/**
|
|
10
|
+
* Marker value used when URL-like metadata cannot be safely normalized.
|
|
11
|
+
*/
|
|
12
|
+
const REDACTED_CACHE_METADATA_VALUE = '[redacted]';
|
|
13
|
+
/**
|
|
14
|
+
* Metadata fields that may contain credentialed or signed URLs.
|
|
15
|
+
*/
|
|
16
|
+
const URL_LIKE_METADATA_KEY = /(url|uri|registry|tarball)/iu;
|
|
17
|
+
function sanitizeExternalTemplateCacheMetadataValue(key, value) {
|
|
18
|
+
if (!URL_LIKE_METADATA_KEY.test(key)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(value);
|
|
23
|
+
url.username = '';
|
|
24
|
+
url.password = '';
|
|
25
|
+
url.search = '';
|
|
26
|
+
url.hash = '';
|
|
27
|
+
return url.toString();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return REDACTED_CACHE_METADATA_VALUE;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function sanitizeExternalTemplateCacheMetadata(metadata) {
|
|
34
|
+
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [
|
|
35
|
+
key,
|
|
36
|
+
value === null
|
|
37
|
+
? null
|
|
38
|
+
: sanitizeExternalTemplateCacheMetadataValue(key, value),
|
|
39
|
+
]));
|
|
40
|
+
}
|
|
41
|
+
export function parseExternalTemplateCacheEntryMarker(markerText) {
|
|
42
|
+
let marker;
|
|
43
|
+
try {
|
|
44
|
+
marker = JSON.parse(markerText);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (typeof marker !== 'object' || marker === null || Array.isArray(marker)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const rawMetadata = marker.metadata;
|
|
53
|
+
if (typeof rawMetadata !== 'object' ||
|
|
54
|
+
rawMetadata === null ||
|
|
55
|
+
Array.isArray(rawMetadata)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const metadata = {};
|
|
59
|
+
for (const [key, value] of Object.entries(rawMetadata)) {
|
|
60
|
+
if (typeof value !== 'string' && value !== null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
metadata[key] = value;
|
|
64
|
+
}
|
|
65
|
+
const rawCreatedAt = marker.createdAt;
|
|
66
|
+
const createdAtMs = typeof rawCreatedAt === 'string' ? Date.parse(rawCreatedAt) : 0;
|
|
67
|
+
return {
|
|
68
|
+
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0,
|
|
69
|
+
metadata,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function externalTemplateCacheMetadataMatches(actual, expected) {
|
|
73
|
+
return Object.entries(expected).every(([key, value]) => actual[key] === value);
|
|
74
|
+
}
|
|
75
|
+
export function isExternalTemplateCacheEntryFreshForTtl(createdAtMs, nowMs, ttlMs) {
|
|
76
|
+
return ttlMs === null || createdAtMs >= nowMs - ttlMs;
|
|
77
|
+
}
|
|
78
|
+
export function parseExternalTemplateCachePruneMarker(markerText) {
|
|
79
|
+
let marker;
|
|
80
|
+
try {
|
|
81
|
+
marker = JSON.parse(markerText);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (typeof marker !== 'object' || marker === null || Array.isArray(marker)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const rawPrunedAt = marker.prunedAt;
|
|
90
|
+
const prunedAtMs = typeof rawPrunedAt === 'string' ? Date.parse(rawPrunedAt) : Number.NaN;
|
|
91
|
+
const rawPruneIntervalMs = marker
|
|
92
|
+
.pruneIntervalMs;
|
|
93
|
+
const rawTtlMs = marker.ttlMs;
|
|
94
|
+
if (typeof rawTtlMs !== 'number' || !Number.isFinite(rawTtlMs)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (!Number.isFinite(prunedAtMs)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (rawPruneIntervalMs !== null &&
|
|
101
|
+
(typeof rawPruneIntervalMs !== 'number' ||
|
|
102
|
+
!Number.isFinite(rawPruneIntervalMs))) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
prunedAtMs,
|
|
107
|
+
pruneIntervalMs: rawPruneIntervalMs ?? null,
|
|
108
|
+
ttlMs: rawTtlMs,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function formatExternalTemplateCacheEntryMarker({ cacheKey, createdAt, metadata, namespace, }) {
|
|
112
|
+
return `${JSON.stringify({
|
|
113
|
+
createdAt: createdAt.toISOString(),
|
|
114
|
+
key: cacheKey,
|
|
115
|
+
metadata: sanitizeExternalTemplateCacheMetadata(metadata),
|
|
116
|
+
namespace,
|
|
117
|
+
}, null, 2)}\n`;
|
|
118
|
+
}
|
|
119
|
+
export function formatExternalTemplateCachePruneMarker({ nowMs, pruneIntervalMs, ttlMs, }) {
|
|
120
|
+
return `${JSON.stringify({
|
|
121
|
+
prunedAt: new Date(nowMs).toISOString(),
|
|
122
|
+
pruneIntervalMs,
|
|
123
|
+
ttlMs,
|
|
124
|
+
}, null, 2)}\n`;
|
|
125
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
+
import { type ExternalTemplateCacheMetadata } from './template-source-cache-markers.js';
|
|
1
2
|
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';
|
|
2
|
-
/**
|
|
3
|
-
* Serializable metadata recorded in the cache marker for diagnostics.
|
|
4
|
-
*/
|
|
5
|
-
type ExternalTemplateCacheMetadata = Record<string, string | null>;
|
|
6
3
|
/**
|
|
7
4
|
* Describes a deterministic external template cache entry.
|
|
8
5
|
*
|
|
@@ -2,24 +2,13 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { promises as fsp } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getNodeErrorCode, pathExists } from './fs-async.js';
|
|
5
|
+
import { CACHE_MARKER_FILE, CACHE_PRUNE_MARKER_FILE, externalTemplateCacheMetadataMatches, formatExternalTemplateCacheEntryMarker, formatExternalTemplateCachePruneMarker, isExternalTemplateCacheEntryFreshForTtl, parseExternalTemplateCacheEntryMarker, parseExternalTemplateCachePruneMarker, } from './template-source-cache-markers.js';
|
|
5
6
|
import { getExternalTemplateCacheNowMs, getExternalTemplateCacheRoot, isExternalTemplateCacheEnabled, resolveExternalTemplateCachePruneIntervalMs, resolveExternalTemplateCacheTtlMs, } from './template-source-cache-policy.js';
|
|
6
7
|
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';
|
|
7
|
-
/**
|
|
8
|
-
* Marker file written after a cache entry is fully populated.
|
|
9
|
-
*/
|
|
10
|
-
const CACHE_MARKER_FILE = 'wp-typia-template-cache.json';
|
|
11
|
-
/**
|
|
12
|
-
* Marker file written after a full TTL prune scan completes.
|
|
13
|
-
*/
|
|
14
|
-
const CACHE_PRUNE_MARKER_FILE = 'wp-typia-template-cache-prune.json';
|
|
15
8
|
/**
|
|
16
9
|
* Private directory mode used for cache roots and entries on POSIX platforms.
|
|
17
10
|
*/
|
|
18
11
|
const PRIVATE_CACHE_DIRECTORY_MODE = 0o700;
|
|
19
|
-
/**
|
|
20
|
-
* Marker value used when URL-like metadata cannot be safely normalized.
|
|
21
|
-
*/
|
|
22
|
-
const REDACTED_CACHE_METADATA_VALUE = '[redacted]';
|
|
23
12
|
/**
|
|
24
13
|
* Filesystem errors that mean another writer published the same cache entry.
|
|
25
14
|
*/
|
|
@@ -34,10 +23,6 @@ const CACHE_UNAVAILABLE_ERROR_CODES = new Set([
|
|
|
34
23
|
'EPERM',
|
|
35
24
|
'EROFS',
|
|
36
25
|
]);
|
|
37
|
-
/**
|
|
38
|
-
* Metadata fields that may contain credentialed or signed URLs.
|
|
39
|
-
*/
|
|
40
|
-
const URL_LIKE_METADATA_KEY = /(url|uri|registry|tarball)/iu;
|
|
41
26
|
/**
|
|
42
27
|
* Cache namespaces must stay within one path segment under the cache root.
|
|
43
28
|
*/
|
|
@@ -126,28 +111,6 @@ async function ensurePrivateCacheDirectory(directory) {
|
|
|
126
111
|
return false;
|
|
127
112
|
}
|
|
128
113
|
}
|
|
129
|
-
function sanitizeCacheMetadataValue(key, value) {
|
|
130
|
-
if (!URL_LIKE_METADATA_KEY.test(key)) {
|
|
131
|
-
return value;
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
const url = new URL(value);
|
|
135
|
-
url.username = '';
|
|
136
|
-
url.password = '';
|
|
137
|
-
url.search = '';
|
|
138
|
-
url.hash = '';
|
|
139
|
-
return url.toString();
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return REDACTED_CACHE_METADATA_VALUE;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function sanitizeCacheMetadata(metadata) {
|
|
146
|
-
return Object.fromEntries(Object.entries(metadata).map(([key, value]) => [
|
|
147
|
-
key,
|
|
148
|
-
value === null ? null : sanitizeCacheMetadataValue(key, value),
|
|
149
|
-
]));
|
|
150
|
-
}
|
|
151
114
|
function resolveCacheNamespaceDir(cacheRoot, namespace) {
|
|
152
115
|
if (namespace === '.' ||
|
|
153
116
|
namespace === '..' ||
|
|
@@ -185,51 +148,14 @@ async function isReusableCacheEntry(entryDir, markerPath, sourceDir) {
|
|
|
185
148
|
(await pathExists(markerPath)) &&
|
|
186
149
|
(await isDirectoryPath(sourceDir)));
|
|
187
150
|
}
|
|
188
|
-
function parseCacheMarkerMetadata(markerText) {
|
|
189
|
-
let marker;
|
|
190
|
-
try {
|
|
191
|
-
marker = JSON.parse(markerText);
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
if (typeof marker !== 'object' || marker === null || Array.isArray(marker)) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
const rawMetadata = marker.metadata;
|
|
200
|
-
if (typeof rawMetadata !== 'object' ||
|
|
201
|
-
rawMetadata === null ||
|
|
202
|
-
Array.isArray(rawMetadata)) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
const metadata = {};
|
|
206
|
-
for (const [key, value] of Object.entries(rawMetadata)) {
|
|
207
|
-
if (typeof value !== 'string' && value !== null) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
metadata[key] = value;
|
|
211
|
-
}
|
|
212
|
-
const rawCreatedAt = marker.createdAt;
|
|
213
|
-
const createdAtMs = typeof rawCreatedAt === 'string' ? Date.parse(rawCreatedAt) : 0;
|
|
214
|
-
return {
|
|
215
|
-
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0,
|
|
216
|
-
metadata,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
151
|
async function readCacheEntryMarker(markerPath) {
|
|
220
152
|
try {
|
|
221
|
-
return
|
|
153
|
+
return parseExternalTemplateCacheEntryMarker(await fsp.readFile(markerPath, 'utf8'));
|
|
222
154
|
}
|
|
223
155
|
catch {
|
|
224
156
|
return null;
|
|
225
157
|
}
|
|
226
158
|
}
|
|
227
|
-
function cacheMetadataMatches(actual, expected) {
|
|
228
|
-
return Object.entries(expected).every(([key, value]) => actual[key] === value);
|
|
229
|
-
}
|
|
230
|
-
function isCacheEntryFreshForTtl(createdAtMs, nowMs, ttlMs) {
|
|
231
|
-
return ttlMs === null || createdAtMs >= nowMs - ttlMs;
|
|
232
|
-
}
|
|
233
159
|
async function getReusableCacheEntryMarker(entryDir, markerPath, sourceDir) {
|
|
234
160
|
if (!(await isReusableCacheEntry(entryDir, markerPath, sourceDir))) {
|
|
235
161
|
return null;
|
|
@@ -238,7 +164,8 @@ async function getReusableCacheEntryMarker(entryDir, markerPath, sourceDir) {
|
|
|
238
164
|
}
|
|
239
165
|
async function isReusableFreshCacheEntry(entryDir, markerPath, sourceDir, nowMs, ttlMs) {
|
|
240
166
|
const marker = await getReusableCacheEntryMarker(entryDir, markerPath, sourceDir);
|
|
241
|
-
return (marker !== null &&
|
|
167
|
+
return (marker !== null &&
|
|
168
|
+
isExternalTemplateCacheEntryFreshForTtl(marker.createdAtMs, nowMs, ttlMs));
|
|
242
169
|
}
|
|
243
170
|
function isPathInsideDirectory(directory, candidatePath) {
|
|
244
171
|
const relativePath = path.relative(directory, candidatePath);
|
|
@@ -261,39 +188,6 @@ async function removeCacheEntryWithinRoot(cacheRoot, entryDir) {
|
|
|
261
188
|
function getCachePruneMarkerPath(cacheRoot) {
|
|
262
189
|
return path.join(cacheRoot, CACHE_PRUNE_MARKER_FILE);
|
|
263
190
|
}
|
|
264
|
-
function parseCachePruneMarker(markerText) {
|
|
265
|
-
let marker;
|
|
266
|
-
try {
|
|
267
|
-
marker = JSON.parse(markerText);
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
if (typeof marker !== 'object' || marker === null || Array.isArray(marker)) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
const rawPrunedAt = marker.prunedAt;
|
|
276
|
-
const prunedAtMs = typeof rawPrunedAt === 'string' ? Date.parse(rawPrunedAt) : Number.NaN;
|
|
277
|
-
const rawPruneIntervalMs = marker
|
|
278
|
-
.pruneIntervalMs;
|
|
279
|
-
const rawTtlMs = marker.ttlMs;
|
|
280
|
-
if (typeof rawTtlMs !== 'number' || !Number.isFinite(rawTtlMs)) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
if (!Number.isFinite(prunedAtMs)) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
if (rawPruneIntervalMs !== null &&
|
|
287
|
-
(typeof rawPruneIntervalMs !== 'number' ||
|
|
288
|
-
!Number.isFinite(rawPruneIntervalMs))) {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
prunedAtMs,
|
|
293
|
-
pruneIntervalMs: rawPruneIntervalMs ?? null,
|
|
294
|
-
ttlMs: rawTtlMs,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
191
|
async function shouldSkipExternalTemplateCachePrune({ cacheRoot, force, nowMs, pruneIntervalMs, ttlMs, }) {
|
|
298
192
|
if (force || pruneIntervalMs === null) {
|
|
299
193
|
return false;
|
|
@@ -305,7 +199,7 @@ async function shouldSkipExternalTemplateCachePrune({ cacheRoot, force, nowMs, p
|
|
|
305
199
|
catch {
|
|
306
200
|
return false;
|
|
307
201
|
}
|
|
308
|
-
const marker =
|
|
202
|
+
const marker = parseExternalTemplateCachePruneMarker(markerText);
|
|
309
203
|
if (!marker ||
|
|
310
204
|
marker.ttlMs !== ttlMs ||
|
|
311
205
|
marker.pruneIntervalMs !== pruneIntervalMs) {
|
|
@@ -316,11 +210,11 @@ async function shouldSkipExternalTemplateCachePrune({ cacheRoot, force, nowMs, p
|
|
|
316
210
|
}
|
|
317
211
|
async function writeExternalTemplateCachePruneMarker({ cacheRoot, nowMs, pruneIntervalMs, ttlMs, }) {
|
|
318
212
|
try {
|
|
319
|
-
await fsp.writeFile(getCachePruneMarkerPath(cacheRoot),
|
|
320
|
-
|
|
213
|
+
await fsp.writeFile(getCachePruneMarkerPath(cacheRoot), formatExternalTemplateCachePruneMarker({
|
|
214
|
+
nowMs,
|
|
321
215
|
pruneIntervalMs,
|
|
322
216
|
ttlMs,
|
|
323
|
-
},
|
|
217
|
+
}), 'utf8');
|
|
324
218
|
}
|
|
325
219
|
catch {
|
|
326
220
|
// Prune markers are an optimization; failing to write one keeps TTL safe.
|
|
@@ -476,8 +370,8 @@ export async function findReusableExternalTemplateSourceCache(descriptor) {
|
|
|
476
370
|
const sourceDir = path.join(entryDir, 'source');
|
|
477
371
|
const marker = await getReusableCacheEntryMarker(entryDir, markerPath, sourceDir);
|
|
478
372
|
if (!marker ||
|
|
479
|
-
!
|
|
480
|
-
!
|
|
373
|
+
!isExternalTemplateCacheEntryFreshForTtl(marker.createdAtMs, nowMs, ttlMs) ||
|
|
374
|
+
!externalTemplateCacheMetadataMatches(marker.metadata, descriptor.metadata)) {
|
|
481
375
|
continue;
|
|
482
376
|
}
|
|
483
377
|
if (!bestEntry || marker.createdAtMs > bestEntry.createdAtMs) {
|
|
@@ -523,7 +417,7 @@ export async function resolveExternalTemplateSourceCache(descriptor, populateSou
|
|
|
523
417
|
await pruneExternalTemplateCache();
|
|
524
418
|
const existingMarker = await getReusableCacheEntryMarker(entryDir, markerPath, sourceDir);
|
|
525
419
|
if (existingMarker &&
|
|
526
|
-
|
|
420
|
+
isExternalTemplateCacheEntryFreshForTtl(existingMarker.createdAtMs, nowMs, ttlMs)) {
|
|
527
421
|
return {
|
|
528
422
|
cacheHit: true,
|
|
529
423
|
sourceDir,
|
|
@@ -550,12 +444,12 @@ export async function resolveExternalTemplateSourceCache(descriptor, populateSou
|
|
|
550
444
|
populateFailed = true;
|
|
551
445
|
throw error;
|
|
552
446
|
}
|
|
553
|
-
await fsp.writeFile(path.join(temporaryEntryDir, CACHE_MARKER_FILE),
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
metadata:
|
|
447
|
+
await fsp.writeFile(path.join(temporaryEntryDir, CACHE_MARKER_FILE), formatExternalTemplateCacheEntryMarker({
|
|
448
|
+
cacheKey,
|
|
449
|
+
createdAt: new Date(),
|
|
450
|
+
metadata: descriptor.metadata,
|
|
557
451
|
namespace: descriptor.namespace,
|
|
558
|
-
},
|
|
452
|
+
}), 'utf8');
|
|
559
453
|
await fsp.rename(temporaryEntryDir, entryDir);
|
|
560
454
|
return {
|
|
561
455
|
cacheHit: false,
|
|
@@ -7,8 +7,10 @@ export declare const EXTERNAL_TEMPLATE_TRUST_WARNING = "External template config
|
|
|
7
7
|
/**
|
|
8
8
|
* Search a source directory for the first supported external template entry.
|
|
9
9
|
*
|
|
10
|
-
* @deprecated Use `findExternalTemplateEntry()` from async
|
|
11
|
-
* paths.
|
|
10
|
+
* @deprecated Since 0.22.10. Use `findExternalTemplateEntry()` from async
|
|
11
|
+
* template-source paths. Removal target: not currently scheduled; this sync
|
|
12
|
+
* compatibility helper remains available until release notes announce a
|
|
13
|
+
* versioned target.
|
|
12
14
|
*
|
|
13
15
|
* @param sourceDir Directory that may contain an external template config entry.
|
|
14
16
|
* @returns The first matching entry path, or null when no supported entry exists.
|
|
@@ -33,8 +33,10 @@ function resolveSourceSubpath(sourceDir, relativePath) {
|
|
|
33
33
|
/**
|
|
34
34
|
* Search a source directory for the first supported external template entry.
|
|
35
35
|
*
|
|
36
|
-
* @deprecated Use `findExternalTemplateEntry()` from async
|
|
37
|
-
* paths.
|
|
36
|
+
* @deprecated Since 0.22.10. Use `findExternalTemplateEntry()` from async
|
|
37
|
+
* template-source paths. Removal target: not currently scheduled; this sync
|
|
38
|
+
* compatibility helper remains available until release notes announce a
|
|
39
|
+
* versioned target.
|
|
38
40
|
*
|
|
39
41
|
* @param sourceDir Directory that may contain an external template config entry.
|
|
40
42
|
* @returns The first matching entry path, or null when no supported entry exists.
|
|
@@ -2,8 +2,10 @@ import type { ResolvedTemplateSource, SeedSource, TemplateVariableContext } from
|
|
|
2
2
|
/**
|
|
3
3
|
* Read a remote block source and return its default block category.
|
|
4
4
|
*
|
|
5
|
-
* @deprecated Use `getDefaultCategoryAsync()` from async
|
|
6
|
-
*
|
|
5
|
+
* @deprecated Since 0.22.10. Use `getDefaultCategoryAsync()` from async
|
|
6
|
+
* template-source paths. Removal target: not currently scheduled; this sync
|
|
7
|
+
* compatibility helper remains available until release notes announce a
|
|
8
|
+
* versioned target.
|
|
7
9
|
*
|
|
8
10
|
* @param sourceDir Block source directory that may contain a block.json file.
|
|
9
11
|
* @returns The declared block category, or "widgets" when detection fails.
|
|
@@ -20,8 +22,10 @@ export declare function getDefaultCategoryAsync(sourceDir: string): Promise<stri
|
|
|
20
22
|
* Read `wpTypia.projectType` from a rendered or source template package
|
|
21
23
|
* manifest and return it when present.
|
|
22
24
|
*
|
|
23
|
-
* @deprecated Use `getTemplateProjectTypeAsync()` from async
|
|
24
|
-
* paths.
|
|
25
|
+
* @deprecated Since 0.22.10. Use `getTemplateProjectTypeAsync()` from async
|
|
26
|
+
* template-source paths. Removal target: not currently scheduled; this sync
|
|
27
|
+
* compatibility helper remains available until release notes announce a
|
|
28
|
+
* versioned target.
|
|
25
29
|
*/
|
|
26
30
|
export declare function getTemplateProjectType(sourceDir: string): string | null;
|
|
27
31
|
/**
|
|
@@ -59,8 +59,10 @@ async function readRemoteBlockJsonAsync(blockDir) {
|
|
|
59
59
|
/**
|
|
60
60
|
* Read a remote block source and return its default block category.
|
|
61
61
|
*
|
|
62
|
-
* @deprecated Use `getDefaultCategoryAsync()` from async
|
|
63
|
-
*
|
|
62
|
+
* @deprecated Since 0.22.10. Use `getDefaultCategoryAsync()` from async
|
|
63
|
+
* template-source paths. Removal target: not currently scheduled; this sync
|
|
64
|
+
* compatibility helper remains available until release notes announce a
|
|
65
|
+
* versioned target.
|
|
64
66
|
*
|
|
65
67
|
* @param sourceDir Block source directory that may contain a block.json file.
|
|
66
68
|
* @returns The declared block category, or "widgets" when detection fails.
|
|
@@ -143,8 +145,10 @@ async function readTemplatePackageJsonAsync(sourceDir) {
|
|
|
143
145
|
* Read `wpTypia.projectType` from a rendered or source template package
|
|
144
146
|
* manifest and return it when present.
|
|
145
147
|
*
|
|
146
|
-
* @deprecated Use `getTemplateProjectTypeAsync()` from async
|
|
147
|
-
* paths.
|
|
148
|
+
* @deprecated Since 0.22.10. Use `getTemplateProjectTypeAsync()` from async
|
|
149
|
+
* template-source paths. Removal target: not currently scheduled; this sync
|
|
150
|
+
* compatibility helper remains available until release notes announce a
|
|
151
|
+
* versioned target.
|
|
148
152
|
*/
|
|
149
153
|
export function getTemplateProjectType(sourceDir) {
|
|
150
154
|
const packageJsonEntry = readTemplatePackageJson(sourceDir);
|