@wp-typia/project-tools 0.22.3 → 0.22.4
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-block-json.d.ts +31 -0
- package/dist/runtime/cli-add-block-json.js +65 -0
- package/dist/runtime/cli-add-collision.d.ts +129 -0
- package/dist/runtime/cli-add-collision.js +293 -0
- package/dist/runtime/cli-add-filesystem.d.ts +29 -0
- package/dist/runtime/cli-add-filesystem.js +77 -0
- package/dist/runtime/cli-add-help.d.ts +4 -0
- package/dist/runtime/cli-add-help.js +41 -0
- package/dist/runtime/cli-add-shared.d.ts +6 -304
- package/dist/runtime/cli-add-shared.js +6 -524
- package/dist/runtime/cli-add-types.d.ts +247 -0
- package/dist/runtime/cli-add-types.js +64 -0
- package/dist/runtime/cli-add-validation.d.ts +87 -0
- package/dist/runtime/cli-add-validation.js +147 -0
- package/dist/runtime/cli-add-workspace-ability-scaffold.js +46 -72
- package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +35 -61
- package/dist/runtime/cli-add-workspace-ai-scaffold.js +53 -57
- package/dist/runtime/cli-add-workspace-ai-templates.js +2 -0
- package/dist/runtime/cli-add-workspace-mutation.d.ts +30 -0
- package/dist/runtime/cli-add-workspace-mutation.js +60 -0
- package/dist/runtime/cli-add-workspace.js +1 -79
- package/dist/runtime/cli-add.d.ts +2 -2
- package/dist/runtime/cli-add.js +2 -2
- package/dist/runtime/cli-doctor-workspace-blocks.js +1 -66
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/migration-utils.d.ts +2 -1
- package/dist/runtime/migration-utils.js +3 -11
- package/dist/runtime/package-managers.d.ts +19 -0
- package/dist/runtime/package-managers.js +62 -0
- package/dist/runtime/template-source-cache.d.ts +59 -0
- package/dist/runtime/template-source-cache.js +160 -0
- package/dist/runtime/ts-source-masking.d.ts +28 -0
- package/dist/runtime/ts-source-masking.js +104 -0
- package/dist/runtime/typia-llm.d.ts +9 -1
- package/dist/runtime/typia-llm.js +20 -5
- package/dist/runtime/workspace-inventory.js +116 -59
- package/dist/runtime/workspace-project.d.ts +1 -1
- package/dist/runtime/workspace-project.js +2 -10
- package/package.json +2 -2
package/dist/runtime/cli-add.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The canonical CLI surface stays stable here while the implementation lives
|
|
5
5
|
* in focused internal modules:
|
|
6
|
-
* - `cli-add-shared`
|
|
6
|
+
* - `cli-add-shared` as a compatibility barrel around focused add helpers
|
|
7
7
|
* - `cli-add-block` for built-in block scaffolding
|
|
8
8
|
* - `cli-add-workspace` for workspace mutation commands
|
|
9
9
|
*/
|
|
10
|
-
export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, } from "./cli-add-shared.js";
|
|
10
|
+
export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, isAddBlockTemplateId, } from "./cli-add-shared.js";
|
|
11
11
|
export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
|
|
12
12
|
export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
|
|
13
13
|
export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
|
|
4
4
|
import { checkExistingFiles, createDoctorCheck, WORKSPACE_FULL_BLOCK_NAME_PATTERN, WORKSPACE_GENERATED_BLOCK_ARTIFACTS, } from "./cli-doctor-workspace-shared.js";
|
|
5
5
|
import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
|
|
6
|
+
import { hasExecutablePattern, hasUncommentedPattern, maskTypeScriptCommentsAndLiterals, } from "./ts-source-masking.js";
|
|
6
7
|
const WORKSPACE_COLLECTION_IMPORT_LINE = "import '../../collection';";
|
|
7
8
|
const WORKSPACE_COLLECTION_IMPORT_PATTERN = /^\s*import\s+["']\.\.\/\.\.\/collection["']\s*;?\s*$/m;
|
|
8
9
|
const WORKSPACE_VARIATIONS_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceVariations\s*\}\s*from\s*["']\.\/variations["']\s*;?\s*$/mu;
|
|
@@ -40,72 +41,6 @@ const WORKSPACE_BLOCK_LOCAL_STYLE_FILES = [
|
|
|
40
41
|
];
|
|
41
42
|
const WORKSPACE_BLOCK_IFRAME_GLOBAL_DOM_PATTERN = /\b(?:document|window)\b|\b(?:parent|top)\b(?!\s*:)/gu;
|
|
42
43
|
const WORKSPACE_BLOCK_PROPS_PATTERN = /\buse(?:Block|InnerBlocks)Props(?:\.save)?\s*\(/u;
|
|
43
|
-
function maskSourceSegment(segment) {
|
|
44
|
-
return segment.replace(/[^\n\r]/gu, " ");
|
|
45
|
-
}
|
|
46
|
-
function maskTypeScriptComments(source) {
|
|
47
|
-
return source
|
|
48
|
-
.replace(/\/\*[\s\S]*?\*\//gu, maskSourceSegment)
|
|
49
|
-
.replace(/\/\/[^\n\r]*/gu, maskSourceSegment);
|
|
50
|
-
}
|
|
51
|
-
// Preserve offsets while hiding non-executable text from hook checks.
|
|
52
|
-
function maskTypeScriptCommentsAndLiterals(source) {
|
|
53
|
-
let maskedSource = "";
|
|
54
|
-
let index = 0;
|
|
55
|
-
while (index < source.length) {
|
|
56
|
-
const current = source[index];
|
|
57
|
-
const next = source[index + 1];
|
|
58
|
-
if (current === "/" && next === "/") {
|
|
59
|
-
const start = index;
|
|
60
|
-
index += 2;
|
|
61
|
-
while (index < source.length &&
|
|
62
|
-
source[index] !== "\n" &&
|
|
63
|
-
source[index] !== "\r") {
|
|
64
|
-
index += 1;
|
|
65
|
-
}
|
|
66
|
-
maskedSource += maskSourceSegment(source.slice(start, index));
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (current === "/" && next === "*") {
|
|
70
|
-
const start = index;
|
|
71
|
-
index += 2;
|
|
72
|
-
while (index < source.length &&
|
|
73
|
-
!(source[index] === "*" && source[index + 1] === "/")) {
|
|
74
|
-
index += 1;
|
|
75
|
-
}
|
|
76
|
-
index = Math.min(index + 2, source.length);
|
|
77
|
-
maskedSource += maskSourceSegment(source.slice(start, index));
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (current === "'" || current === '"' || current === "`") {
|
|
81
|
-
const start = index;
|
|
82
|
-
const quote = current;
|
|
83
|
-
index += 1;
|
|
84
|
-
while (index < source.length) {
|
|
85
|
-
const char = source[index];
|
|
86
|
-
if (char === "\\") {
|
|
87
|
-
index += 2;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
index += 1;
|
|
91
|
-
if (char === quote) {
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
maskedSource += maskSourceSegment(source.slice(start, index));
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
maskedSource += current;
|
|
99
|
-
index += 1;
|
|
100
|
-
}
|
|
101
|
-
return maskedSource;
|
|
102
|
-
}
|
|
103
|
-
function hasUncommentedPattern(source, pattern) {
|
|
104
|
-
return pattern.test(maskTypeScriptComments(source));
|
|
105
|
-
}
|
|
106
|
-
function hasExecutablePattern(source, pattern) {
|
|
107
|
-
return pattern.test(maskTypeScriptCommentsAndLiterals(source));
|
|
108
|
-
}
|
|
109
44
|
function normalizePathSeparators(relativePath) {
|
|
110
45
|
return relativePath.split(path.sep).join("/");
|
|
111
46
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export { PACKAGE_MANAGER_IDS, PACKAGE_MANAGERS, formatPackageExecCommand, format
|
|
|
32
32
|
export { clearPackageVersionsCache, getPackageVersions, invalidatePackageVersionsCache, } from "./package-versions.js";
|
|
33
33
|
export type { PackageVersions } from "./package-versions.js";
|
|
34
34
|
export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOptions, listTemplates, } from "./template-registry.js";
|
|
35
|
+
export { EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, pruneExternalTemplateCache, } from "./template-source-cache.js";
|
|
36
|
+
export type { ExternalTemplateCachePruneOptions, ExternalTemplateCachePruneResult, } from "./template-source-cache.js";
|
|
35
37
|
export { STALE_TEMP_ROOT_MAX_AGE_MS, WP_TYPIA_TEMP_ROOT_PREFIX, cleanupManagedTempRoot, cleanupStaleTempRoots, createManagedTempRoot, getTrackedTempRoots, } from "./temp-roots.js";
|
|
36
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";
|
|
37
39
|
export type { CliDiagnosticCode, CliDiagnosticCodeError, CliDiagnosticMessage, DoctorCheck, EditorPluginSlotId, HookedBlockPositionId, ReadlinePrompt, } from "./cli-core.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -25,5 +25,6 @@ export { buildCompoundChildStarterManifestDocument, getStarterManifestFiles, str
|
|
|
25
25
|
export { PACKAGE_MANAGER_IDS, PACKAGE_MANAGERS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, getPackageManagerSelectOptions, transformPackageManagerText, } from "./package-managers.js";
|
|
26
26
|
export { clearPackageVersionsCache, getPackageVersions, invalidatePackageVersionsCache, } from "./package-versions.js";
|
|
27
27
|
export { TEMPLATE_IDS, TEMPLATE_REGISTRY, getTemplateById, getTemplateSelectOptions, listTemplates, } from "./template-registry.js";
|
|
28
|
+
export { EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV, pruneExternalTemplateCache, } from "./template-source-cache.js";
|
|
28
29
|
export { STALE_TEMP_ROOT_MAX_AGE_MS, WP_TYPIA_TEMP_ROOT_PREFIX, cleanupManagedTempRoot, cleanupStaleTempRoots, createManagedTempRoot, getTrackedTempRoots, } from "./temp-roots.js";
|
|
29
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";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PackageManagerId } from './package-managers.js';
|
|
1
2
|
import type { JsonValue, ManifestAttribute, JsonObject } from './migration-types.js';
|
|
2
3
|
import { type UnknownRecord } from './object-utils.js';
|
|
3
4
|
export { cloneJsonValue } from './json-utils.js';
|
|
@@ -12,7 +13,7 @@ export declare function copyFile(sourcePath: string, targetPath: string): void;
|
|
|
12
13
|
export declare function sanitizeSaveSnapshotSource(source: string): string;
|
|
13
14
|
export declare function sanitizeSnapshotBlockJson(blockJson: JsonObject): JsonObject;
|
|
14
15
|
export declare function runProjectScriptIfPresent(projectDir: string, scriptName: string): void;
|
|
15
|
-
export declare function detectPackageManagerId(projectDir: string):
|
|
16
|
+
export declare function detectPackageManagerId(projectDir: string): PackageManagerId;
|
|
16
17
|
export declare function getLocalTsxBinary(projectDir: string): string;
|
|
17
18
|
/**
|
|
18
19
|
* Returns whether isInteractiveTerminal() is running with both stdin and stdout
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
import { formatRunScript } from './package-managers.js';
|
|
4
|
+
import { formatRunScript, inferPackageManagerId, parsePackageManagerField, } from './package-managers.js';
|
|
5
5
|
import { isPlainObject } from './object-utils.js';
|
|
6
6
|
export { cloneJsonValue } from './json-utils.js';
|
|
7
7
|
const MIGRATION_VERSION_LABEL_PATTERN = /^v([1-9]\d*)$/;
|
|
@@ -141,16 +141,8 @@ export function runProjectScriptIfPresent(projectDir, scriptName) {
|
|
|
141
141
|
}
|
|
142
142
|
export function detectPackageManagerId(projectDir) {
|
|
143
143
|
const packageJson = readJson(path.join(projectDir, 'package.json'));
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return 'bun';
|
|
147
|
-
if (field.startsWith('npm@'))
|
|
148
|
-
return 'npm';
|
|
149
|
-
if (field.startsWith('pnpm@'))
|
|
150
|
-
return 'pnpm';
|
|
151
|
-
if (field.startsWith('yarn@'))
|
|
152
|
-
return 'yarn';
|
|
153
|
-
return 'bun';
|
|
144
|
+
return (parsePackageManagerField(packageJson.packageManager) ??
|
|
145
|
+
inferPackageManagerId(projectDir));
|
|
154
146
|
}
|
|
155
147
|
export function getLocalTsxBinary(projectDir) {
|
|
156
148
|
const filename = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
|
|
@@ -9,6 +9,25 @@ export interface PackageManagerDefinition {
|
|
|
9
9
|
export declare const PACKAGE_MANAGER_IDS: PackageManagerId[];
|
|
10
10
|
export declare const PACKAGE_MANAGERS: Readonly<Record<PackageManagerId, PackageManagerDefinition>>;
|
|
11
11
|
export declare function getPackageManager(id: string): PackageManagerDefinition;
|
|
12
|
+
/**
|
|
13
|
+
* Parse a normalized package-manager id from a package.json `packageManager` field.
|
|
14
|
+
*
|
|
15
|
+
* @param packageManagerField Raw field value such as `pnpm@8.3.1`.
|
|
16
|
+
* @returns A supported package manager id, or null when the field is missing or unsupported.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parsePackageManagerField(packageManagerField: string | undefined): PackageManagerId | null;
|
|
19
|
+
/**
|
|
20
|
+
* Infer the package manager used by a project from package.json and lockfile signals.
|
|
21
|
+
*
|
|
22
|
+
* `packageManager` fields take precedence over lockfiles, then lockfile and PnP
|
|
23
|
+
* markers are checked in Bun, pnpm, Yarn, npm order. npm remains the fallback so
|
|
24
|
+
* generated projects without explicit metadata keep the historical CLI default.
|
|
25
|
+
*
|
|
26
|
+
* @param projectDir Project root to inspect.
|
|
27
|
+
* @param packageManagerField Optional already-read package.json field.
|
|
28
|
+
* @returns The inferred package manager id.
|
|
29
|
+
*/
|
|
30
|
+
export declare function inferPackageManagerId(projectDir: string, packageManagerField?: string): PackageManagerId;
|
|
12
31
|
export declare function getPackageManagerSelectOptions(): Array<{
|
|
13
32
|
label: string;
|
|
14
33
|
value: PackageManagerId;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
const PACKAGE_MANAGER_DATA = [
|
|
2
4
|
{
|
|
3
5
|
id: "bun",
|
|
@@ -28,6 +30,15 @@ const PACKAGE_MANAGER_DATA = [
|
|
|
28
30
|
frozenInstallCommand: "yarn install --frozen-lockfile",
|
|
29
31
|
},
|
|
30
32
|
];
|
|
33
|
+
const PACKAGE_MANAGER_LOCKFILE_SIGNALS = [
|
|
34
|
+
{ id: "bun", filenames: ["bun.lock", "bun.lockb"] },
|
|
35
|
+
{ id: "pnpm", filenames: ["pnpm-lock.yaml"] },
|
|
36
|
+
{
|
|
37
|
+
id: "yarn",
|
|
38
|
+
filenames: ["yarn.lock", ".pnp.cjs", ".pnp.loader.mjs", ".yarnrc.yml"],
|
|
39
|
+
},
|
|
40
|
+
{ id: "npm", filenames: ["package-lock.json", "npm-shrinkwrap.json"] },
|
|
41
|
+
];
|
|
31
42
|
export const PACKAGE_MANAGER_IDS = PACKAGE_MANAGER_DATA.map((manager) => manager.id);
|
|
32
43
|
export const PACKAGE_MANAGERS = Object.freeze(Object.fromEntries(PACKAGE_MANAGER_DATA.map((manager) => [manager.id, manager])));
|
|
33
44
|
const DEV_INSTALL_FLAGS = {
|
|
@@ -44,6 +55,57 @@ export function getPackageManager(id) {
|
|
|
44
55
|
}
|
|
45
56
|
return manager;
|
|
46
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse a normalized package-manager id from a package.json `packageManager` field.
|
|
60
|
+
*
|
|
61
|
+
* @param packageManagerField Raw field value such as `pnpm@8.3.1`.
|
|
62
|
+
* @returns A supported package manager id, or null when the field is missing or unsupported.
|
|
63
|
+
*/
|
|
64
|
+
export function parsePackageManagerField(packageManagerField) {
|
|
65
|
+
const packageManagerId = packageManagerField?.split("@", 1)[0];
|
|
66
|
+
return PACKAGE_MANAGER_IDS.includes(packageManagerId)
|
|
67
|
+
? packageManagerId
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
function readPackageManagerField(projectDir) {
|
|
71
|
+
try {
|
|
72
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
73
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
77
|
+
return typeof manifest.packageManager === "string"
|
|
78
|
+
? manifest.packageManager
|
|
79
|
+
: undefined;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Infer the package manager used by a project from package.json and lockfile signals.
|
|
87
|
+
*
|
|
88
|
+
* `packageManager` fields take precedence over lockfiles, then lockfile and PnP
|
|
89
|
+
* markers are checked in Bun, pnpm, Yarn, npm order. npm remains the fallback so
|
|
90
|
+
* generated projects without explicit metadata keep the historical CLI default.
|
|
91
|
+
*
|
|
92
|
+
* @param projectDir Project root to inspect.
|
|
93
|
+
* @param packageManagerField Optional already-read package.json field.
|
|
94
|
+
* @returns The inferred package manager id.
|
|
95
|
+
*/
|
|
96
|
+
export function inferPackageManagerId(projectDir, packageManagerField) {
|
|
97
|
+
const fieldPackageManager = parsePackageManagerField(packageManagerField) ??
|
|
98
|
+
parsePackageManagerField(readPackageManagerField(projectDir));
|
|
99
|
+
if (fieldPackageManager) {
|
|
100
|
+
return fieldPackageManager;
|
|
101
|
+
}
|
|
102
|
+
for (const signal of PACKAGE_MANAGER_LOCKFILE_SIGNALS) {
|
|
103
|
+
if (signal.filenames.some((filename) => fs.existsSync(path.join(projectDir, filename)))) {
|
|
104
|
+
return signal.id;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return "npm";
|
|
108
|
+
}
|
|
47
109
|
export function getPackageManagerSelectOptions() {
|
|
48
110
|
return PACKAGE_MANAGER_DATA.map((manager) => ({
|
|
49
111
|
label: manager.label,
|
|
@@ -8,6 +8,12 @@ export declare const EXTERNAL_TEMPLATE_CACHE_ENV = "WP_TYPIA_EXTERNAL_TEMPLATE_C
|
|
|
8
8
|
* Environment variable that overrides the external template cache root.
|
|
9
9
|
*/
|
|
10
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";
|
|
11
17
|
/**
|
|
12
18
|
* Serializable metadata recorded in the cache marker for diagnostics.
|
|
13
19
|
*/
|
|
@@ -58,6 +64,48 @@ export interface ExternalTemplateCacheLookupDescriptor {
|
|
|
58
64
|
*/
|
|
59
65
|
namespace: string;
|
|
60
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Options for best-effort external template cache pruning.
|
|
69
|
+
*/
|
|
70
|
+
export interface ExternalTemplateCachePruneOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Environment object to inspect, defaulting to `process.env`.
|
|
73
|
+
*/
|
|
74
|
+
env?: NodeJS.ProcessEnv;
|
|
75
|
+
/**
|
|
76
|
+
* Clock override for deterministic tests.
|
|
77
|
+
*/
|
|
78
|
+
now?: Date | number;
|
|
79
|
+
/**
|
|
80
|
+
* TTL override in days. When omitted, the TTL environment variable is used.
|
|
81
|
+
*/
|
|
82
|
+
ttlDays?: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Summary returned after external template cache pruning.
|
|
86
|
+
*/
|
|
87
|
+
export interface ExternalTemplateCachePruneResult {
|
|
88
|
+
/**
|
|
89
|
+
* Absolute cache root inspected by the pruning helper.
|
|
90
|
+
*/
|
|
91
|
+
cacheRoot: string;
|
|
92
|
+
/**
|
|
93
|
+
* Entries removed because their marker timestamp exceeded the TTL.
|
|
94
|
+
*/
|
|
95
|
+
prunedEntries: number;
|
|
96
|
+
/**
|
|
97
|
+
* Candidate cache entry directories inspected.
|
|
98
|
+
*/
|
|
99
|
+
scannedEntries: number;
|
|
100
|
+
/**
|
|
101
|
+
* Candidate directories skipped because they were malformed or unsafe.
|
|
102
|
+
*/
|
|
103
|
+
skippedEntries: number;
|
|
104
|
+
/**
|
|
105
|
+
* Resolved TTL in milliseconds, or `null` when pruning was disabled.
|
|
106
|
+
*/
|
|
107
|
+
ttlMs: number | null;
|
|
108
|
+
}
|
|
61
109
|
/**
|
|
62
110
|
* Checks whether remote external template source caching is enabled.
|
|
63
111
|
*
|
|
@@ -86,6 +134,17 @@ export declare function getExternalTemplateCacheRoot(env?: NodeJS.ProcessEnv): s
|
|
|
86
134
|
* @returns SHA-256 hex digest of the JSON-serialized key parts.
|
|
87
135
|
*/
|
|
88
136
|
export declare function createExternalTemplateCacheKey(keyParts: readonly string[]): string;
|
|
137
|
+
/**
|
|
138
|
+
* Removes stale external template cache entries when a positive TTL is configured.
|
|
139
|
+
*
|
|
140
|
+
* The helper is best-effort: malformed cache directories are skipped, cache
|
|
141
|
+
* roots must remain private and non-symlinked, and deletes are constrained to
|
|
142
|
+
* deterministic entry directories under the configured cache root.
|
|
143
|
+
*
|
|
144
|
+
* @param options Optional TTL, clock, and environment overrides.
|
|
145
|
+
* @returns Pruning summary with counts for inspected, skipped, and removed entries.
|
|
146
|
+
*/
|
|
147
|
+
export declare function pruneExternalTemplateCache(options?: ExternalTemplateCachePruneOptions): Promise<ExternalTemplateCachePruneResult>;
|
|
89
148
|
/**
|
|
90
149
|
* Finds a reusable cache entry whose marker metadata includes the expected fields.
|
|
91
150
|
*
|
|
@@ -13,10 +13,20 @@ export const EXTERNAL_TEMPLATE_CACHE_ENV = 'WP_TYPIA_EXTERNAL_TEMPLATE_CACHE';
|
|
|
13
13
|
* Environment variable that overrides the external template cache root.
|
|
14
14
|
*/
|
|
15
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';
|
|
16
22
|
/**
|
|
17
23
|
* Marker file written after a cache entry is fully populated.
|
|
18
24
|
*/
|
|
19
25
|
const CACHE_MARKER_FILE = 'wp-typia-template-cache.json';
|
|
26
|
+
/**
|
|
27
|
+
* Milliseconds in one TTL day.
|
|
28
|
+
*/
|
|
29
|
+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
20
30
|
/**
|
|
21
31
|
* Private directory mode used for cache roots and entries on POSIX platforms.
|
|
22
32
|
*/
|
|
@@ -51,6 +61,10 @@ const URL_LIKE_METADATA_KEY = /(url|uri|registry|tarball)/iu;
|
|
|
51
61
|
* Cache namespaces must stay within one path segment under the cache root.
|
|
52
62
|
*/
|
|
53
63
|
const SAFE_CACHE_NAMESPACE_SEGMENT = /^[A-Za-z0-9_.-]+$/u;
|
|
64
|
+
/**
|
|
65
|
+
* Cache entries are deterministic SHA-256 digest directory names.
|
|
66
|
+
*/
|
|
67
|
+
const SAFE_CACHE_ENTRY_SEGMENT = /^[a-f0-9]{64}$/u;
|
|
54
68
|
/**
|
|
55
69
|
* Checks whether remote external template source caching is enabled.
|
|
56
70
|
*
|
|
@@ -84,6 +98,27 @@ export function getExternalTemplateCacheRoot(env = process.env) {
|
|
|
84
98
|
}
|
|
85
99
|
return path.join(os.tmpdir(), `wp-typia-template-source-cache-${getCurrentUserCacheSegment()}`);
|
|
86
100
|
}
|
|
101
|
+
function parseExternalTemplateCacheTtlDays(value) {
|
|
102
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const ttlDays = typeof value === 'number' ? value : Number(value.trim());
|
|
106
|
+
if (!Number.isFinite(ttlDays) || ttlDays <= 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return ttlDays;
|
|
110
|
+
}
|
|
111
|
+
function resolveExternalTemplateCacheTtlMs(options = {}) {
|
|
112
|
+
const env = options.env ?? process.env;
|
|
113
|
+
const ttlDays = options.ttlDays === undefined
|
|
114
|
+
? parseExternalTemplateCacheTtlDays(env[EXTERNAL_TEMPLATE_CACHE_TTL_DAYS_ENV])
|
|
115
|
+
: parseExternalTemplateCacheTtlDays(options.ttlDays);
|
|
116
|
+
if (ttlDays === null) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const ttlMs = ttlDays * MILLISECONDS_PER_DAY;
|
|
120
|
+
return Number.isFinite(ttlMs) ? ttlMs : null;
|
|
121
|
+
}
|
|
87
122
|
/**
|
|
88
123
|
* Creates a deterministic cache key from source identity and integrity parts.
|
|
89
124
|
*
|
|
@@ -281,6 +316,129 @@ function parseCacheMarkerMetadata(markerText) {
|
|
|
281
316
|
function cacheMetadataMatches(actual, expected) {
|
|
282
317
|
return Object.entries(expected).every(([key, value]) => actual[key] === value);
|
|
283
318
|
}
|
|
319
|
+
function getExternalTemplateCacheNowMs(now) {
|
|
320
|
+
const nowMs = now instanceof Date
|
|
321
|
+
? now.getTime()
|
|
322
|
+
: typeof now === 'number'
|
|
323
|
+
? now
|
|
324
|
+
: Date.now();
|
|
325
|
+
return Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
326
|
+
}
|
|
327
|
+
function isPathInsideDirectory(directory, candidatePath) {
|
|
328
|
+
const relativePath = path.relative(directory, candidatePath);
|
|
329
|
+
return (relativePath.length > 0 &&
|
|
330
|
+
!relativePath.startsWith('..') &&
|
|
331
|
+
!path.isAbsolute(relativePath));
|
|
332
|
+
}
|
|
333
|
+
async function removeCacheEntryWithinRoot(cacheRoot, entryDir) {
|
|
334
|
+
if (!isPathInsideDirectory(cacheRoot, entryDir)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
await fsp.rm(entryDir, { force: true, recursive: true });
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Removes stale external template cache entries when a positive TTL is configured.
|
|
347
|
+
*
|
|
348
|
+
* The helper is best-effort: malformed cache directories are skipped, cache
|
|
349
|
+
* roots must remain private and non-symlinked, and deletes are constrained to
|
|
350
|
+
* deterministic entry directories under the configured cache root.
|
|
351
|
+
*
|
|
352
|
+
* @param options Optional TTL, clock, and environment overrides.
|
|
353
|
+
* @returns Pruning summary with counts for inspected, skipped, and removed entries.
|
|
354
|
+
*/
|
|
355
|
+
export async function pruneExternalTemplateCache(options = {}) {
|
|
356
|
+
const env = options.env ?? process.env;
|
|
357
|
+
const cacheRoot = getExternalTemplateCacheRoot(env);
|
|
358
|
+
const ttlMs = resolveExternalTemplateCacheTtlMs({
|
|
359
|
+
env,
|
|
360
|
+
ttlDays: options.ttlDays,
|
|
361
|
+
});
|
|
362
|
+
const result = {
|
|
363
|
+
cacheRoot,
|
|
364
|
+
prunedEntries: 0,
|
|
365
|
+
scannedEntries: 0,
|
|
366
|
+
skippedEntries: 0,
|
|
367
|
+
ttlMs,
|
|
368
|
+
};
|
|
369
|
+
if (ttlMs === null || !(await isPrivateCacheDirectory(cacheRoot))) {
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
let namespaceEntries;
|
|
373
|
+
try {
|
|
374
|
+
namespaceEntries = await fsp.readdir(cacheRoot, { withFileTypes: true });
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
const expiresBeforeMs = getExternalTemplateCacheNowMs(options.now) - ttlMs;
|
|
380
|
+
for (const namespaceEntry of namespaceEntries) {
|
|
381
|
+
if (!namespaceEntry.isDirectory()) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const namespaceDir = resolveCacheNamespaceDir(cacheRoot, namespaceEntry.name);
|
|
385
|
+
if (!namespaceDir || !(await isPrivateCacheDirectory(namespaceDir))) {
|
|
386
|
+
result.skippedEntries += 1;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
let cacheEntries;
|
|
390
|
+
try {
|
|
391
|
+
cacheEntries = await fsp.readdir(namespaceDir, { withFileTypes: true });
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
result.skippedEntries += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
for (const cacheEntry of cacheEntries) {
|
|
398
|
+
if (!cacheEntry.isDirectory()) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (!SAFE_CACHE_ENTRY_SEGMENT.test(cacheEntry.name)) {
|
|
402
|
+
result.skippedEntries += 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const entryDir = path.join(namespaceDir, cacheEntry.name);
|
|
406
|
+
result.scannedEntries += 1;
|
|
407
|
+
if (!isPathInsideDirectory(cacheRoot, entryDir)) {
|
|
408
|
+
result.skippedEntries += 1;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const markerPath = path.join(entryDir, CACHE_MARKER_FILE);
|
|
412
|
+
const sourceDir = path.join(entryDir, 'source');
|
|
413
|
+
if (!(await isReusableCacheEntry(entryDir, markerPath, sourceDir))) {
|
|
414
|
+
result.skippedEntries += 1;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
let markerText;
|
|
418
|
+
try {
|
|
419
|
+
markerText = await fsp.readFile(markerPath, 'utf8');
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
result.skippedEntries += 1;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const marker = parseCacheMarkerMetadata(markerText);
|
|
426
|
+
if (!marker) {
|
|
427
|
+
result.skippedEntries += 1;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (marker.createdAtMs < expiresBeforeMs) {
|
|
431
|
+
if (await removeCacheEntryWithinRoot(cacheRoot, entryDir)) {
|
|
432
|
+
result.prunedEntries += 1;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
result.skippedEntries += 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
284
442
|
/**
|
|
285
443
|
* Finds a reusable cache entry whose marker metadata includes the expected fields.
|
|
286
444
|
*
|
|
@@ -304,6 +462,7 @@ export async function findReusableExternalTemplateSourceCache(descriptor) {
|
|
|
304
462
|
!(await isPrivateCacheDirectory(namespaceDir))) {
|
|
305
463
|
return null;
|
|
306
464
|
}
|
|
465
|
+
await pruneExternalTemplateCache();
|
|
307
466
|
let entries;
|
|
308
467
|
try {
|
|
309
468
|
entries = await fsp.readdir(namespaceDir, { withFileTypes: true });
|
|
@@ -371,6 +530,7 @@ export async function resolveExternalTemplateSourceCache(descriptor, populateSou
|
|
|
371
530
|
!(await ensurePrivateCacheDirectory(namespaceDir))) {
|
|
372
531
|
return null;
|
|
373
532
|
}
|
|
533
|
+
await pruneExternalTemplateCache();
|
|
374
534
|
if (await isReusableCacheEntry(entryDir, markerPath, sourceDir)) {
|
|
375
535
|
return {
|
|
376
536
|
cacheHit: true,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Original-source range returned from a masked-source pattern match. */
|
|
2
|
+
export interface SourceRange {
|
|
3
|
+
end: number;
|
|
4
|
+
start: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Masks TypeScript comments with spaces while preserving newlines and offsets.
|
|
8
|
+
*/
|
|
9
|
+
export declare function maskTypeScriptComments(source: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Masks TypeScript comments and quoted/template literals with spaces while
|
|
12
|
+
* preserving source offsets. This is a lightweight lexer for runtime checks,
|
|
13
|
+
* not a full TypeScript parser, so template interpolation and regex/division
|
|
14
|
+
* ambiguity are intentionally left to callers that need deeper syntax analysis.
|
|
15
|
+
*/
|
|
16
|
+
export declare function maskTypeScriptCommentsAndLiterals(source: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Tests for a pattern after hiding comments and quoted/template literals.
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasExecutablePattern(source: string, pattern: RegExp): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Tests for a pattern after hiding comments while leaving literals intact.
|
|
23
|
+
*/
|
|
24
|
+
export declare function hasUncommentedPattern(source: string, pattern: RegExp): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Finds the first masked-source match that maps back to the original source.
|
|
27
|
+
*/
|
|
28
|
+
export declare function findExecutablePatternMatch(source: string, patterns: readonly RegExp[]): SourceRange | undefined;
|