@typed/virtual-modules 1.0.0-beta.1
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/README.md +135 -0
- package/dist/CompilerHostAdapter.d.ts +3 -0
- package/dist/CompilerHostAdapter.d.ts.map +1 -0
- package/dist/CompilerHostAdapter.js +160 -0
- package/dist/LanguageServiceAdapter.d.ts +3 -0
- package/dist/LanguageServiceAdapter.d.ts.map +1 -0
- package/dist/LanguageServiceAdapter.js +488 -0
- package/dist/LanguageServiceSession.d.ts +16 -0
- package/dist/LanguageServiceSession.d.ts.map +1 -0
- package/dist/LanguageServiceSession.js +122 -0
- package/dist/NodeModulePluginLoader.d.ts +8 -0
- package/dist/NodeModulePluginLoader.d.ts.map +1 -0
- package/dist/NodeModulePluginLoader.js +175 -0
- package/dist/PluginManager.d.ts +10 -0
- package/dist/PluginManager.d.ts.map +1 -0
- package/dist/PluginManager.js +151 -0
- package/dist/TypeInfoApi.d.ts +71 -0
- package/dist/TypeInfoApi.d.ts.map +1 -0
- package/dist/TypeInfoApi.js +855 -0
- package/dist/VmcConfigLoader.d.ts +31 -0
- package/dist/VmcConfigLoader.d.ts.map +1 -0
- package/dist/VmcConfigLoader.js +231 -0
- package/dist/VmcResolverLoader.d.ts +30 -0
- package/dist/VmcResolverLoader.d.ts.map +1 -0
- package/dist/VmcResolverLoader.js +71 -0
- package/dist/collectTypeTargetSpecs.d.ts +6 -0
- package/dist/collectTypeTargetSpecs.d.ts.map +1 -0
- package/dist/collectTypeTargetSpecs.js +19 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/internal/VirtualRecordStore.d.ts +60 -0
- package/dist/internal/VirtualRecordStore.d.ts.map +1 -0
- package/dist/internal/VirtualRecordStore.js +199 -0
- package/dist/internal/materializeVirtualFile.d.ts +12 -0
- package/dist/internal/materializeVirtualFile.d.ts.map +1 -0
- package/dist/internal/materializeVirtualFile.js +28 -0
- package/dist/internal/path.d.ts +40 -0
- package/dist/internal/path.d.ts.map +1 -0
- package/dist/internal/path.js +71 -0
- package/dist/internal/sanitize.d.ts +6 -0
- package/dist/internal/sanitize.d.ts.map +1 -0
- package/dist/internal/sanitize.js +15 -0
- package/dist/internal/tsInternal.d.ts +65 -0
- package/dist/internal/tsInternal.d.ts.map +1 -0
- package/dist/internal/tsInternal.js +99 -0
- package/dist/internal/validation.d.ts +28 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +66 -0
- package/dist/typeTargetBootstrap.d.ts +33 -0
- package/dist/typeTargetBootstrap.d.ts.map +1 -0
- package/dist/typeTargetBootstrap.js +57 -0
- package/dist/types.d.ts +405 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/package.json +38 -0
- package/src/CompilerHostAdapter.test.ts +180 -0
- package/src/CompilerHostAdapter.ts +316 -0
- package/src/LanguageServiceAdapter.test.ts +521 -0
- package/src/LanguageServiceAdapter.ts +631 -0
- package/src/LanguageServiceSession.ts +160 -0
- package/src/LanguageServiceTester.integration.test.ts +268 -0
- package/src/NodeModulePluginLoader.test.ts +170 -0
- package/src/NodeModulePluginLoader.ts +268 -0
- package/src/PluginManager.test.ts +178 -0
- package/src/PluginManager.ts +218 -0
- package/src/TypeInfoApi.test.ts +1211 -0
- package/src/TypeInfoApi.ts +1228 -0
- package/src/VmcConfigLoader.test.ts +108 -0
- package/src/VmcConfigLoader.ts +297 -0
- package/src/VmcResolverLoader.test.ts +181 -0
- package/src/VmcResolverLoader.ts +116 -0
- package/src/collectTypeTargetSpecs.ts +22 -0
- package/src/index.ts +35 -0
- package/src/internal/VirtualRecordStore.ts +304 -0
- package/src/internal/materializeVirtualFile.ts +38 -0
- package/src/internal/path.ts +106 -0
- package/src/internal/sanitize.ts +16 -0
- package/src/internal/tsInternal.ts +127 -0
- package/src/internal/validation.ts +85 -0
- package/src/typeTargetBootstrap.ts +75 -0
- package/src/types.ts +535 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createVirtualFileName, createVirtualKey, createWatchDescriptorKey } from "./path.js";
|
|
2
|
+
export function toResolvedModule(tsMod, fileName) {
|
|
3
|
+
return {
|
|
4
|
+
resolvedFileName: fileName,
|
|
5
|
+
extension: tsMod.Extension.Ts,
|
|
6
|
+
isExternalLibraryImport: false,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function createVirtualRecordStore(options) {
|
|
10
|
+
const recordsByKey = new Map();
|
|
11
|
+
const recordsByVirtualFile = new Map();
|
|
12
|
+
const descriptorToVirtualKeys = new Map();
|
|
13
|
+
const watcherByDescriptor = new Map();
|
|
14
|
+
let debounceTimer;
|
|
15
|
+
const pendingStaleKeys = new Set();
|
|
16
|
+
const { debounceMs, watchHost, shouldEvictRecord, onFlushStale, onMarkStale, onBeforeResolve, onAfterResolve, onRecordResolved, onEvictRecord, } = options;
|
|
17
|
+
const evictRecord = (record) => {
|
|
18
|
+
onEvictRecord?.(record);
|
|
19
|
+
recordsByKey.delete(record.key);
|
|
20
|
+
recordsByVirtualFile.delete(record.virtualFileName);
|
|
21
|
+
for (const descriptor of record.dependencies) {
|
|
22
|
+
const descriptorKey = createWatchDescriptorKey(descriptor);
|
|
23
|
+
const dependents = descriptorToVirtualKeys.get(descriptorKey);
|
|
24
|
+
if (dependents) {
|
|
25
|
+
dependents.delete(record.key);
|
|
26
|
+
if (dependents.size === 0) {
|
|
27
|
+
descriptorToVirtualKeys.delete(descriptorKey);
|
|
28
|
+
const watcher = watcherByDescriptor.get(descriptorKey);
|
|
29
|
+
if (watcher) {
|
|
30
|
+
watcher.close();
|
|
31
|
+
watcherByDescriptor.delete(descriptorKey);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const evictStaleImporters = () => {
|
|
38
|
+
const toEvict = [];
|
|
39
|
+
for (const record of recordsByKey.values()) {
|
|
40
|
+
if (shouldEvictRecord(record)) {
|
|
41
|
+
toEvict.push(record);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const record of toEvict) {
|
|
45
|
+
evictRecord(record);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const registerWatchers = (record) => {
|
|
49
|
+
for (const descriptor of record.dependencies) {
|
|
50
|
+
const descriptorKey = createWatchDescriptorKey(descriptor);
|
|
51
|
+
const dependents = descriptorToVirtualKeys.get(descriptorKey) ?? new Set();
|
|
52
|
+
dependents.add(record.key);
|
|
53
|
+
descriptorToVirtualKeys.set(descriptorKey, dependents);
|
|
54
|
+
if (watcherByDescriptor.has(descriptorKey)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (descriptor.type === "file" && watchHost?.watchFile) {
|
|
58
|
+
const watcher = watchHost.watchFile(descriptor.path, () => {
|
|
59
|
+
markStale(descriptorKey);
|
|
60
|
+
});
|
|
61
|
+
watcherByDescriptor.set(descriptorKey, watcher);
|
|
62
|
+
}
|
|
63
|
+
else if (descriptor.type === "glob" && watchHost?.watchDirectory) {
|
|
64
|
+
const watcher = watchHost.watchDirectory(descriptor.baseDir, () => {
|
|
65
|
+
markStale(descriptorKey);
|
|
66
|
+
}, descriptor.recursive);
|
|
67
|
+
watcherByDescriptor.set(descriptorKey, watcher);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const flushPendingStale = () => {
|
|
72
|
+
if (pendingStaleKeys.size === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
onFlushStale?.();
|
|
76
|
+
for (const descriptorKey of pendingStaleKeys) {
|
|
77
|
+
const keys = descriptorToVirtualKeys.get(descriptorKey);
|
|
78
|
+
if (!keys || keys.size === 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const record = recordsByKey.get(key);
|
|
83
|
+
if (record) {
|
|
84
|
+
record.stale = true;
|
|
85
|
+
onMarkStale?.(record);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
pendingStaleKeys.clear();
|
|
90
|
+
debounceTimer = undefined;
|
|
91
|
+
};
|
|
92
|
+
const markStale = (descriptorKey) => {
|
|
93
|
+
if (debounceMs !== undefined && debounceMs > 0) {
|
|
94
|
+
pendingStaleKeys.add(descriptorKey);
|
|
95
|
+
if (debounceTimer === undefined) {
|
|
96
|
+
debounceTimer = setTimeout(() => {
|
|
97
|
+
flushPendingStale();
|
|
98
|
+
}, debounceMs);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const keys = descriptorToVirtualKeys.get(descriptorKey);
|
|
103
|
+
if (!keys || keys.size === 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const key of keys) {
|
|
107
|
+
const record = recordsByKey.get(key);
|
|
108
|
+
if (record) {
|
|
109
|
+
record.stale = true;
|
|
110
|
+
onMarkStale?.(record);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const resolveRecord = (id, importer, previous) => {
|
|
115
|
+
onBeforeResolve?.();
|
|
116
|
+
try {
|
|
117
|
+
const resolveOptions = {
|
|
118
|
+
id,
|
|
119
|
+
importer,
|
|
120
|
+
createTypeInfoApiSession: options.createTypeInfoApiSession,
|
|
121
|
+
};
|
|
122
|
+
const resolution = options.resolver.resolveModule(resolveOptions);
|
|
123
|
+
if (resolution.status === "unresolved") {
|
|
124
|
+
return { status: "unresolved" };
|
|
125
|
+
}
|
|
126
|
+
if (resolution.status === "error") {
|
|
127
|
+
return {
|
|
128
|
+
status: "error",
|
|
129
|
+
diagnostic: resolution.diagnostic,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const key = createVirtualKey(id, importer);
|
|
133
|
+
const virtualFileName = createVirtualFileName(resolution.pluginName, key, { id, importer }, { projectRoot: options.projectRoot });
|
|
134
|
+
const record = {
|
|
135
|
+
key,
|
|
136
|
+
id,
|
|
137
|
+
importer,
|
|
138
|
+
pluginName: resolution.pluginName,
|
|
139
|
+
virtualFileName,
|
|
140
|
+
sourceText: resolution.sourceText,
|
|
141
|
+
dependencies: resolution.dependencies,
|
|
142
|
+
...(resolution.warnings?.length ? { warnings: resolution.warnings } : {}),
|
|
143
|
+
version: previous ? previous.version + 1 : 1,
|
|
144
|
+
stale: false,
|
|
145
|
+
};
|
|
146
|
+
recordsByKey.set(key, record);
|
|
147
|
+
recordsByVirtualFile.set(virtualFileName, record);
|
|
148
|
+
registerWatchers(record);
|
|
149
|
+
onRecordResolved?.(record);
|
|
150
|
+
return {
|
|
151
|
+
status: "resolved",
|
|
152
|
+
record,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
onAfterResolve?.();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const getOrBuildRecord = (id, importer) => {
|
|
160
|
+
evictStaleImporters();
|
|
161
|
+
const key = createVirtualKey(id, importer);
|
|
162
|
+
const existing = recordsByKey.get(key);
|
|
163
|
+
if (existing && !existing.stale) {
|
|
164
|
+
return {
|
|
165
|
+
status: "resolved",
|
|
166
|
+
record: existing,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return resolveRecord(id, importer, existing);
|
|
170
|
+
};
|
|
171
|
+
const dispose = () => {
|
|
172
|
+
if (debounceTimer !== undefined) {
|
|
173
|
+
clearTimeout(debounceTimer);
|
|
174
|
+
debounceTimer = undefined;
|
|
175
|
+
}
|
|
176
|
+
pendingStaleKeys.clear();
|
|
177
|
+
for (const watcher of watcherByDescriptor.values()) {
|
|
178
|
+
watcher.close();
|
|
179
|
+
}
|
|
180
|
+
watcherByDescriptor.clear();
|
|
181
|
+
descriptorToVirtualKeys.clear();
|
|
182
|
+
recordsByKey.clear();
|
|
183
|
+
recordsByVirtualFile.clear();
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
recordsByKey,
|
|
187
|
+
recordsByVirtualFile,
|
|
188
|
+
descriptorToVirtualKeys,
|
|
189
|
+
watcherByDescriptor,
|
|
190
|
+
evictRecord,
|
|
191
|
+
evictStaleImporters,
|
|
192
|
+
registerWatchers,
|
|
193
|
+
markStale,
|
|
194
|
+
flushPendingStale,
|
|
195
|
+
resolveRecord,
|
|
196
|
+
getOrBuildRecord,
|
|
197
|
+
dispose,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite relative import specifiers in sourceText so they resolve correctly when
|
|
3
|
+
* the file is placed in previewDir instead of importerDir.
|
|
4
|
+
*/
|
|
5
|
+
export declare function rewriteSourceForPreviewLocation(sourceText: string, importer: string, virtualFilePath: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Materialize virtual module content to disk at virtualFilePath. Rewrites relative
|
|
8
|
+
* imports so they resolve from the virtual file's location. Used so go-to-definition
|
|
9
|
+
* can open the file (path must exist on disk).
|
|
10
|
+
*/
|
|
11
|
+
export declare function materializeVirtualFile(virtualFilePath: string, importer: string, sourceText: string): void;
|
|
12
|
+
//# sourceMappingURL=materializeVirtualFile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materializeVirtualFile.d.ts","sourceRoot":"","sources":["../../src/internal/materializeVirtualFile.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,MAAM,CAUR;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,IAAI,CAIN"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { toPosixPath } from "./path.js";
|
|
4
|
+
/**
|
|
5
|
+
* Rewrite relative import specifiers in sourceText so they resolve correctly when
|
|
6
|
+
* the file is placed in previewDir instead of importerDir.
|
|
7
|
+
*/
|
|
8
|
+
export function rewriteSourceForPreviewLocation(sourceText, importer, virtualFilePath) {
|
|
9
|
+
const importerDir = dirname(resolve(importer));
|
|
10
|
+
const previewDir = dirname(resolve(virtualFilePath));
|
|
11
|
+
return sourceText.replace(/from\s+['"](\.\.?\/[^'"]+)['"]/g, (match, spec) => {
|
|
12
|
+
const absoluteTarget = resolve(importerDir, spec);
|
|
13
|
+
const newRel = toPosixPath(relative(previewDir, absoluteTarget));
|
|
14
|
+
const newSpec = newRel.startsWith(".") ? newRel : `./${newRel}`;
|
|
15
|
+
const quote = match.includes('"') ? '"' : "'";
|
|
16
|
+
return `from ${quote}${newSpec}${quote}`;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Materialize virtual module content to disk at virtualFilePath. Rewrites relative
|
|
21
|
+
* imports so they resolve from the virtual file's location. Used so go-to-definition
|
|
22
|
+
* can open the file (path must exist on disk).
|
|
23
|
+
*/
|
|
24
|
+
export function materializeVirtualFile(virtualFilePath, importer, sourceText) {
|
|
25
|
+
const rewritten = rewriteSourceForPreviewLocation(sourceText, importer, virtualFilePath);
|
|
26
|
+
mkdirSync(dirname(resolve(virtualFilePath)), { recursive: true });
|
|
27
|
+
writeFileSync(virtualFilePath, rewritten, "utf8");
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { WatchDependencyDescriptor } from "../types.js";
|
|
2
|
+
/** Base directory for virtual files under node_modules (enables go-to-definition to resolve correctly). */
|
|
3
|
+
export declare const VIRTUAL_NODE_MODULES_RELATIVE = "node_modules/.typed/virtual";
|
|
4
|
+
export declare const toPosixPath: (path: string) => string;
|
|
5
|
+
export declare const resolveRelativePath: (baseDir: string, relativePath: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Resolves relativePath against baseDir and ensures the result stays under baseDir.
|
|
8
|
+
* Use for plugin/caller-controlled paths to prevent path traversal.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolvePathUnderBase(baseDir: string, relativePath: string): {
|
|
11
|
+
ok: true;
|
|
12
|
+
path: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: "path-escapes-base";
|
|
16
|
+
};
|
|
17
|
+
/** Returns true if absolutePath is under baseDir (or equal). Canonicalizes with realpath so symlinks (e.g. /tmp) match. */
|
|
18
|
+
export declare function pathIsUnderBase(baseDir: string, absolutePath: string): boolean;
|
|
19
|
+
export declare const stableHash: (input: string) => string;
|
|
20
|
+
/** URI scheme for virtual module identifiers (e.g. `typed-virtual://0/...`). Single source of truth for all consumers. */
|
|
21
|
+
export declare const VIRTUAL_MODULE_URI_SCHEME: "typed-virtual";
|
|
22
|
+
export declare const createVirtualKey: (id: string, importer: string) => string;
|
|
23
|
+
export interface CreateVirtualFileNameParams {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly importer: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CreateVirtualFileNameOptions {
|
|
28
|
+
/** When provided, virtual files use projectRoot/node_modules/.typed/virtual/ so go-to-definition resolves correctly. */
|
|
29
|
+
readonly projectRoot?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Virtual file name for the TS program. When projectRoot is provided, uses
|
|
33
|
+
* node_modules/.typed/virtual/ so the path exists on disk (after materialization)
|
|
34
|
+
* and go-to-definition works like node_modules packages. Falls back to
|
|
35
|
+
* importer-adjacent path or typed-virtual:// URI when projectRoot is not set.
|
|
36
|
+
*/
|
|
37
|
+
export declare const createVirtualFileName: (pluginName: string, virtualKey: string, params?: CreateVirtualFileNameParams, options?: CreateVirtualFileNameOptions) => string;
|
|
38
|
+
export declare const createWatchDescriptorKey: (descriptor: WatchDependencyDescriptor) => string;
|
|
39
|
+
export declare const dedupeSorted: (values: readonly string[]) => readonly string[];
|
|
40
|
+
//# sourceMappingURL=path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/internal/path.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D,2GAA2G;AAC3G,eAAO,MAAM,6BAA6B,gCAAgC,CAAC;AAE3E,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAAoC,CAAC;AAEhF,eAAO,MAAM,mBAAmB,GAAI,SAAS,MAAM,EAAE,cAAc,MAAM,KAAG,MAC/B,CAAC;AAE9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,mBAAmB,CAAA;CAAE,CAQxE;AAED,2HAA2H;AAC3H,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAe9E;AAED,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MACkB,CAAC;AAI9D,0HAA0H;AAC1H,eAAO,MAAM,yBAAyB,EAAG,eAAwB,CAAC;AAElE,eAAO,MAAM,gBAAgB,GAAI,IAAI,MAAM,EAAE,UAAU,MAAM,KAAG,MAA8B,CAAC;AAE/F,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,4BAA4B;IAC3C,wHAAwH;IACxH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,SAAS,2BAA2B,EACpC,UAAU,4BAA4B,KACrC,MAaF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,YAAY,yBAAyB,KAAG,MAOhF,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,QAAQ,SAAS,MAAM,EAAE,KAAG,SAAS,MAAM,EAC1B,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
/** Base directory for virtual files under node_modules (enables go-to-definition to resolve correctly). */
|
|
5
|
+
export const VIRTUAL_NODE_MODULES_RELATIVE = "node_modules/.typed/virtual";
|
|
6
|
+
export const toPosixPath = (path) => path.replaceAll("\\", "/");
|
|
7
|
+
export const resolveRelativePath = (baseDir, relativePath) => toPosixPath(resolve(baseDir, relativePath));
|
|
8
|
+
/**
|
|
9
|
+
* Resolves relativePath against baseDir and ensures the result stays under baseDir.
|
|
10
|
+
* Use for plugin/caller-controlled paths to prevent path traversal.
|
|
11
|
+
*/
|
|
12
|
+
export function resolvePathUnderBase(baseDir, relativePath) {
|
|
13
|
+
const baseAbs = resolve(baseDir);
|
|
14
|
+
const resolved = resolve(baseDir, relativePath);
|
|
15
|
+
const rel = relative(baseAbs, resolved);
|
|
16
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
17
|
+
return { ok: false, error: "path-escapes-base" };
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, path: toPosixPath(resolved) };
|
|
20
|
+
}
|
|
21
|
+
/** Returns true if absolutePath is under baseDir (or equal). Canonicalizes with realpath so symlinks (e.g. /tmp) match. */
|
|
22
|
+
export function pathIsUnderBase(baseDir, absolutePath) {
|
|
23
|
+
let baseAbs;
|
|
24
|
+
let resolvedAbs;
|
|
25
|
+
try {
|
|
26
|
+
baseAbs = realpathSync(resolve(baseDir));
|
|
27
|
+
resolvedAbs = realpathSync(resolve(absolutePath));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
baseAbs = resolve(baseDir);
|
|
31
|
+
resolvedAbs = resolve(absolutePath);
|
|
32
|
+
}
|
|
33
|
+
const rel = relative(baseAbs, resolvedAbs);
|
|
34
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export const stableHash = (input) => createHash("sha1").update(input).digest("hex").slice(0, 16);
|
|
40
|
+
const sanitizeSegment = (value) => value.replaceAll(/[^a-zA-Z0-9._-]/g, "-");
|
|
41
|
+
/** URI scheme for virtual module identifiers (e.g. `typed-virtual://0/...`). Single source of truth for all consumers. */
|
|
42
|
+
export const VIRTUAL_MODULE_URI_SCHEME = "typed-virtual";
|
|
43
|
+
export const createVirtualKey = (id, importer) => `${importer}::${id}`;
|
|
44
|
+
/**
|
|
45
|
+
* Virtual file name for the TS program. When projectRoot is provided, uses
|
|
46
|
+
* node_modules/.typed/virtual/ so the path exists on disk (after materialization)
|
|
47
|
+
* and go-to-definition works like node_modules packages. Falls back to
|
|
48
|
+
* importer-adjacent path or typed-virtual:// URI when projectRoot is not set.
|
|
49
|
+
*/
|
|
50
|
+
export const createVirtualFileName = (pluginName, virtualKey, params, options) => {
|
|
51
|
+
const safePluginName = sanitizeSegment(pluginName);
|
|
52
|
+
const hash = stableHash(virtualKey);
|
|
53
|
+
const basename = `__virtual_${safePluginName}_${hash}.ts`;
|
|
54
|
+
if (params) {
|
|
55
|
+
const projectRoot = options?.projectRoot;
|
|
56
|
+
if (typeof projectRoot === "string" && projectRoot.trim().length > 0) {
|
|
57
|
+
return toPosixPath(join(resolve(projectRoot), VIRTUAL_NODE_MODULES_RELATIVE, basename));
|
|
58
|
+
}
|
|
59
|
+
const importerDir = dirname(toPosixPath(params.importer));
|
|
60
|
+
return `${importerDir}/${basename}`;
|
|
61
|
+
}
|
|
62
|
+
return `${VIRTUAL_MODULE_URI_SCHEME}://0/${safePluginName}/${hash}.ts`;
|
|
63
|
+
};
|
|
64
|
+
export const createWatchDescriptorKey = (descriptor) => {
|
|
65
|
+
if (descriptor.type === "file") {
|
|
66
|
+
return `file:${toPosixPath(descriptor.path)}`;
|
|
67
|
+
}
|
|
68
|
+
const globs = [...descriptor.relativeGlobs].sort().join("|");
|
|
69
|
+
return `glob:${toPosixPath(descriptor.baseDir)}:${descriptor.recursive ? "r" : "nr"}:${globs}`;
|
|
70
|
+
};
|
|
71
|
+
export const dedupeSorted = (values) => [...new Set(values.map(toPosixPath))].sort();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/internal/sanitize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAW5D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes error messages for structured diagnostics: strips stack traces and
|
|
3
|
+
* redacts absolute paths to avoid leaking filesystem layout.
|
|
4
|
+
*/
|
|
5
|
+
export function sanitizeErrorMessage(message) {
|
|
6
|
+
const withoutStack = message
|
|
7
|
+
.split(/\r?\n/)
|
|
8
|
+
.filter((line) => !/^\s*at\s+/.test(line.trim()))
|
|
9
|
+
.join("\n")
|
|
10
|
+
.trim();
|
|
11
|
+
const unixPath = /\/[\w./-]+\/[\w./-]+(?:\/[\w./-]*)*/g;
|
|
12
|
+
const winPath = /[A-Za-z]:[\\/][\w.-]+(?:[\\/][\w.-]+)*/g;
|
|
13
|
+
let out = withoutStack.replace(unixPath, "[path]").replace(winPath, "[path]");
|
|
14
|
+
return out.replace(/\s{2,}/g, " ").trim();
|
|
15
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal TypeScript API access.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript Compiler API does not expose all capabilities we need for
|
|
5
|
+
* type-target resolution, assignability fallbacks, and serialization. This
|
|
6
|
+
* module centralizes use of non-public APIs (via type casts) so that:
|
|
7
|
+
* - All such use is in one place and documented.
|
|
8
|
+
* - Callers can guard with try/catch and optional chaining.
|
|
9
|
+
* - Upgrades to new TS versions can be handled here.
|
|
10
|
+
*
|
|
11
|
+
* These APIs are not part of the public TypeScript contract and may change
|
|
12
|
+
* across versions. Tested with TypeScript 5.x.
|
|
13
|
+
*/
|
|
14
|
+
import type * as ts from "typescript";
|
|
15
|
+
/** Index signature entry shape from internal getIndexInfosOfType (not in public .d.ts). */
|
|
16
|
+
export interface IndexInfo {
|
|
17
|
+
readonly keyType: ts.Type;
|
|
18
|
+
readonly type: ts.Type;
|
|
19
|
+
readonly isReadonly?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get index signature key/value types for an object type when available.
|
|
23
|
+
* Uses internal TypeChecker.getIndexInfosOfType; may change in future TS versions.
|
|
24
|
+
* Returns undefined when the API is missing or throws.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getIndexInfosOfType(type: ts.Type, checker: ts.TypeChecker): readonly IndexInfo[] | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve an alias symbol to the symbol it refers to (re-exports, import-then-export).
|
|
29
|
+
* Only call when symbol.flags includes SymbolFlags.Alias.
|
|
30
|
+
* Uses internal TypeChecker.getAliasedSymbol; may change in future TS versions.
|
|
31
|
+
* Returns the original symbol when the API is missing or throws.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getAliasedSymbol(symbol: ts.Symbol, checker: ts.TypeChecker, tsMod: typeof import("typescript")): ts.Symbol;
|
|
34
|
+
/**
|
|
35
|
+
* Get the base (generic declaration) type from a TypeReference.
|
|
36
|
+
* Uses internal (type as TypeReference).target; may change in future TS versions.
|
|
37
|
+
* Returns undefined when not a TypeReference with target or when access throws.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getTypeReferenceTarget(type: ts.Type, checker: ts.TypeChecker): (ts.Type & {
|
|
40
|
+
symbol?: ts.Symbol;
|
|
41
|
+
}) | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Get the symbol attached to a type when available (e.g. interface, class, type alias).
|
|
44
|
+
* Uses internal (type as Type).symbol; may change in future TS versions.
|
|
45
|
+
*/
|
|
46
|
+
export declare function getTypeSymbol(type: ts.Type): ts.Symbol | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Get namespace/merged declaration exports from a symbol when available.
|
|
49
|
+
* Uses internal (symbol as Symbol).exports; may change in future TS versions.
|
|
50
|
+
* Prefer checker.getExportsOfModule(moduleSymbol) for module symbols when applicable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getSymbolExports(symbol: ts.Symbol): Map<unknown, ts.Symbol> | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Get base types of an interface/class for inheritance checks.
|
|
55
|
+
* Uses internal TypeChecker.getBaseTypes; may change in future TS versions.
|
|
56
|
+
* Returns undefined when the API is missing or throws (e.g. type is not interface or class).
|
|
57
|
+
*/
|
|
58
|
+
export declare function getBaseTypes(type: ts.InterfaceType, checker: ts.TypeChecker): ts.Type[] | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* ObjectFlags.Mapped value when the public ts.ObjectFlags is missing (e.g. older or different TS build).
|
|
61
|
+
* Used only as a fallback for mapped-type detection in serialization.
|
|
62
|
+
* Value 32 corresponds to ObjectFlags.Mapped in TypeScript 5.x; may need adjustment for other versions.
|
|
63
|
+
*/
|
|
64
|
+
export declare const FALLBACK_OBJECT_FLAGS_MAPPED = 32;
|
|
65
|
+
//# sourceMappingURL=tsInternal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsInternal.d.ts","sourceRoot":"","sources":["../../src/internal/tsInternal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAEtC,2FAA2F;AAC3F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,OAAO,EAAE,EAAE,CAAC,WAAW,GACtB,SAAS,SAAS,EAAE,GAAG,SAAS,CAQlC;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,EAAE,CAAC,MAAM,EACjB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,KAAK,EAAE,cAAc,YAAY,CAAC,GACjC,EAAE,CAAC,MAAM,CAUX;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,OAAO,EAAE,EAAE,CAAC,WAAW,GACtB,CAAC,EAAE,CAAC,IAAI,GAAG;IAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAWhD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,MAAM,GAAG,SAAS,CAElE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,EAAE,CAAC,MAAM,GAChB,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,SAAS,CAErC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,EAAE,CAAC,aAAa,EACtB,OAAO,EAAE,EAAE,CAAC,WAAW,GACtB,EAAE,CAAC,IAAI,EAAE,GAAG,SAAS,CAQvB;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal TypeScript API access.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript Compiler API does not expose all capabilities we need for
|
|
5
|
+
* type-target resolution, assignability fallbacks, and serialization. This
|
|
6
|
+
* module centralizes use of non-public APIs (via type casts) so that:
|
|
7
|
+
* - All such use is in one place and documented.
|
|
8
|
+
* - Callers can guard with try/catch and optional chaining.
|
|
9
|
+
* - Upgrades to new TS versions can be handled here.
|
|
10
|
+
*
|
|
11
|
+
* These APIs are not part of the public TypeScript contract and may change
|
|
12
|
+
* across versions. Tested with TypeScript 5.x.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Get index signature key/value types for an object type when available.
|
|
16
|
+
* Uses internal TypeChecker.getIndexInfosOfType; may change in future TS versions.
|
|
17
|
+
* Returns undefined when the API is missing or throws.
|
|
18
|
+
*/
|
|
19
|
+
export function getIndexInfosOfType(type, checker) {
|
|
20
|
+
try {
|
|
21
|
+
const fn = checker
|
|
22
|
+
.getIndexInfosOfType;
|
|
23
|
+
return fn?.(type);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve an alias symbol to the symbol it refers to (re-exports, import-then-export).
|
|
31
|
+
* Only call when symbol.flags includes SymbolFlags.Alias.
|
|
32
|
+
* Uses internal TypeChecker.getAliasedSymbol; may change in future TS versions.
|
|
33
|
+
* Returns the original symbol when the API is missing or throws.
|
|
34
|
+
*/
|
|
35
|
+
export function getAliasedSymbol(symbol, checker, tsMod) {
|
|
36
|
+
if ((symbol.flags & tsMod.SymbolFlags.Alias) === 0)
|
|
37
|
+
return symbol;
|
|
38
|
+
try {
|
|
39
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
40
|
+
return aliased ?? symbol;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return symbol;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get the base (generic declaration) type from a TypeReference.
|
|
48
|
+
* Uses internal (type as TypeReference).target; may change in future TS versions.
|
|
49
|
+
* Returns undefined when not a TypeReference with target or when access throws.
|
|
50
|
+
*/
|
|
51
|
+
export function getTypeReferenceTarget(type, checker) {
|
|
52
|
+
try {
|
|
53
|
+
const args = checker.getTypeArguments(type);
|
|
54
|
+
if (args.length > 0) {
|
|
55
|
+
const target = type.target;
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* getTypeArguments can throw for some type shapes */
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the symbol attached to a type when available (e.g. interface, class, type alias).
|
|
66
|
+
* Uses internal (type as Type).symbol; may change in future TS versions.
|
|
67
|
+
*/
|
|
68
|
+
export function getTypeSymbol(type) {
|
|
69
|
+
return type.symbol;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get namespace/merged declaration exports from a symbol when available.
|
|
73
|
+
* Uses internal (symbol as Symbol).exports; may change in future TS versions.
|
|
74
|
+
* Prefer checker.getExportsOfModule(moduleSymbol) for module symbols when applicable.
|
|
75
|
+
*/
|
|
76
|
+
export function getSymbolExports(symbol) {
|
|
77
|
+
return symbol.exports;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get base types of an interface/class for inheritance checks.
|
|
81
|
+
* Uses internal TypeChecker.getBaseTypes; may change in future TS versions.
|
|
82
|
+
* Returns undefined when the API is missing or throws (e.g. type is not interface or class).
|
|
83
|
+
*/
|
|
84
|
+
export function getBaseTypes(type, checker) {
|
|
85
|
+
try {
|
|
86
|
+
const fn = checker
|
|
87
|
+
.getBaseTypes;
|
|
88
|
+
return fn?.(type);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* ObjectFlags.Mapped value when the public ts.ObjectFlags is missing (e.g. older or different TS build).
|
|
96
|
+
* Used only as a fallback for mapped-type detection in serialization.
|
|
97
|
+
* Value 32 corresponds to ObjectFlags.Mapped in TypeScript 5.x; may need adjustment for other versions.
|
|
98
|
+
*/
|
|
99
|
+
export const FALLBACK_OBJECT_FLAGS_MAPPED = 32;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validators for plugin and TypeInfoApi boundaries. No I/O.
|
|
3
|
+
*/
|
|
4
|
+
export type ValidationOk<T> = {
|
|
5
|
+
ok: true;
|
|
6
|
+
value: T;
|
|
7
|
+
};
|
|
8
|
+
export type ValidationFail = {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
export type ValidationResult<T> = ValidationOk<T> | ValidationFail;
|
|
13
|
+
/**
|
|
14
|
+
* Validates a non-empty string (trimmed). Rejects null bytes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateNonEmptyString(value: unknown, name: string, maxLength?: number): ValidationResult<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Validates a path-like segment: string, non-empty, no null byte, optional max length.
|
|
19
|
+
* Does not trim so path semantics are preserved.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validatePathSegment(value: unknown, name: string, maxLength?: number): ValidationResult<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Validates relativeGlobs: array or single string; each element non-empty, no null byte,
|
|
24
|
+
* no parent-segment (..) or absolute paths, optional max length.
|
|
25
|
+
* Rejects patterns that could escape the intended base directory.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateRelativeGlobs(value: unknown, name: string, maxLength?: number): ValidationResult<string[]>;
|
|
28
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/internal/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AACrD,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3D,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC;AAEnE;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAA2B,GACrC,gBAAgB,CAAC,MAAM,CAAC,CAe1B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAA2B,GACrC,gBAAgB,CAAC,MAAM,CAAC,CAc1B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAA2B,GACrC,gBAAgB,CAAC,MAAM,EAAE,CAAC,CAiB5B"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validators for plugin and TypeInfoApi boundaries. No I/O.
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_MAX_LENGTH = 4096;
|
|
5
|
+
/**
|
|
6
|
+
* Validates a non-empty string (trimmed). Rejects null bytes.
|
|
7
|
+
*/
|
|
8
|
+
export function validateNonEmptyString(value, name, maxLength = DEFAULT_MAX_LENGTH) {
|
|
9
|
+
if (typeof value !== "string") {
|
|
10
|
+
return { ok: false, reason: `${name} must be a string` };
|
|
11
|
+
}
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (trimmed === "") {
|
|
14
|
+
return { ok: false, reason: `${name} must be non-empty` };
|
|
15
|
+
}
|
|
16
|
+
if (value.includes("\0")) {
|
|
17
|
+
return { ok: false, reason: `${name} must not contain null bytes` };
|
|
18
|
+
}
|
|
19
|
+
if (value.length > maxLength) {
|
|
20
|
+
return { ok: false, reason: `${name} exceeds max length ${maxLength}` };
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, value: trimmed };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validates a path-like segment: string, non-empty, no null byte, optional max length.
|
|
26
|
+
* Does not trim so path semantics are preserved.
|
|
27
|
+
*/
|
|
28
|
+
export function validatePathSegment(value, name, maxLength = DEFAULT_MAX_LENGTH) {
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
return { ok: false, reason: `${name} must be a string` };
|
|
31
|
+
}
|
|
32
|
+
if (value.length === 0 || value.trim() === "") {
|
|
33
|
+
return { ok: false, reason: `${name} must be non-empty` };
|
|
34
|
+
}
|
|
35
|
+
if (value.includes("\0")) {
|
|
36
|
+
return { ok: false, reason: `${name} must not contain null bytes` };
|
|
37
|
+
}
|
|
38
|
+
if (value.length > maxLength) {
|
|
39
|
+
return { ok: false, reason: `${name} exceeds max length ${maxLength}` };
|
|
40
|
+
}
|
|
41
|
+
return { ok: true, value };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates relativeGlobs: array or single string; each element non-empty, no null byte,
|
|
45
|
+
* no parent-segment (..) or absolute paths, optional max length.
|
|
46
|
+
* Rejects patterns that could escape the intended base directory.
|
|
47
|
+
*/
|
|
48
|
+
export function validateRelativeGlobs(value, name, maxLength = DEFAULT_MAX_LENGTH) {
|
|
49
|
+
const arr = Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
|
|
50
|
+
const result = [];
|
|
51
|
+
for (let i = 0; i < arr.length; i++) {
|
|
52
|
+
const v = arr[i];
|
|
53
|
+
const r = validatePathSegment(v, `${name}[${i}]`, maxLength);
|
|
54
|
+
if (!r.ok)
|
|
55
|
+
return r;
|
|
56
|
+
const normalized = r.value.replaceAll("\\", "/");
|
|
57
|
+
if (normalized.includes("..")) {
|
|
58
|
+
return { ok: false, reason: `${name}[${i}] must not contain parent path segments (..)` };
|
|
59
|
+
}
|
|
60
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) {
|
|
61
|
+
return { ok: false, reason: `${name}[${i}] must be a relative glob, not absolute` };
|
|
62
|
+
}
|
|
63
|
+
result.push(r.value);
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, value: result };
|
|
66
|
+
}
|