logicstamp-context 0.5.5 → 0.7.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/LLM_CONTEXT.md +67 -20
- package/README.md +50 -36
- package/dist/cli/commands/clean.d.ts +2 -2
- package/dist/cli/commands/clean.js +2 -2
- package/dist/cli/commands/compare.js +1 -1
- package/dist/cli/commands/compare.js.map +1 -1
- package/dist/cli/commands/context/configManager.js +2 -2
- package/dist/cli/commands/context/configManager.js.map +1 -1
- package/dist/cli/commands/context/contractBuilder.d.ts +1 -0
- package/dist/cli/commands/context/contractBuilder.d.ts.map +1 -1
- package/dist/cli/commands/context/contractBuilder.js +1 -1
- package/dist/cli/commands/context/contractBuilder.js.map +1 -1
- package/dist/cli/commands/context/incrementalWatch.d.ts +2 -2
- package/dist/cli/commands/context/incrementalWatch.d.ts.map +1 -1
- package/dist/cli/commands/context/incrementalWatch.js +100 -38
- package/dist/cli/commands/context/incrementalWatch.js.map +1 -1
- package/dist/cli/commands/context/index.d.ts +22 -2
- package/dist/cli/commands/context/index.d.ts.map +1 -1
- package/dist/cli/commands/context/index.js +24 -2
- package/dist/cli/commands/context/index.js.map +1 -1
- package/dist/cli/commands/context/statsCalculator.d.ts +1 -1
- package/dist/cli/commands/context/statsCalculator.js +4 -4
- package/dist/cli/commands/context/statsCalculator.js.map +1 -1
- package/dist/cli/commands/context/tokenEstimator.d.ts.map +1 -1
- package/dist/cli/commands/context/tokenEstimator.js +10 -7
- package/dist/cli/commands/context/tokenEstimator.js.map +1 -1
- package/dist/cli/commands/context/watchMode.d.ts +2 -2
- package/dist/cli/commands/context/watchMode.d.ts.map +1 -1
- package/dist/cli/commands/context/watchMode.js +8 -6
- package/dist/cli/commands/context/watchMode.js.map +1 -1
- package/dist/cli/commands/context.d.ts +2 -1
- package/dist/cli/commands/context.d.ts.map +1 -1
- package/dist/cli/commands/context.js +12 -7
- package/dist/cli/commands/context.js.map +1 -1
- package/dist/cli/commands/init.js +7 -7
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/security.d.ts +1 -1
- package/dist/cli/commands/security.js +1 -1
- package/dist/cli/commands/style.d.ts +2 -2
- package/dist/cli/commands/style.js +2 -2
- package/dist/cli/commands/validate.d.ts +1 -1
- package/dist/cli/commands/validate.js +2 -2
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/handlers/compareHandler.d.ts +1 -1
- package/dist/cli/handlers/compareHandler.js +5 -5
- package/dist/cli/handlers/compareHandler.js.map +1 -1
- package/dist/cli/handlers/contextHandler.d.ts +1 -1
- package/dist/cli/handlers/contextHandler.js +2 -2
- package/dist/cli/handlers/contextHandler.js.map +1 -1
- package/dist/cli/handlers/styleHandler.js +1 -1
- package/dist/cli/handlers/styleHandler.js.map +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +10 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/parser/argumentParser.d.ts +1 -1
- package/dist/cli/parser/argumentParser.d.ts.map +1 -1
- package/dist/cli/parser/argumentParser.js +6 -1
- package/dist/cli/parser/argumentParser.js.map +1 -1
- package/dist/cli/parser/helpText.d.ts.map +1 -1
- package/dist/cli/parser/helpText.js +37 -30
- package/dist/cli/parser/helpText.js.map +1 -1
- package/dist/cli/stamp.d.ts +2 -2
- package/dist/cli/stamp.js +3 -3
- package/dist/cli/stamp.js.map +1 -1
- package/dist/core/pack/loader.d.ts +2 -0
- package/dist/core/pack/loader.d.ts.map +1 -1
- package/dist/core/pack/loader.js +106 -5
- package/dist/core/pack/loader.js.map +1 -1
- package/dist/extractors/react/propExtractor.d.ts.map +1 -1
- package/dist/extractors/react/propExtractor.js +164 -217
- package/dist/extractors/react/propExtractor.js.map +1 -1
- package/dist/extractors/styling/styleExtractor.d.ts +5 -2
- package/dist/extractors/styling/styleExtractor.d.ts.map +1 -1
- package/dist/extractors/styling/styleExtractor.js +177 -5
- package/dist/extractors/styling/styleExtractor.js.map +1 -1
- package/dist/types/UIFContract.d.ts +27 -4
- package/dist/types/UIFContract.d.ts.map +1 -1
- package/dist/types/UIFContract.js.map +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/fileLock.d.ts.map +1 -1
- package/dist/utils/fileLock.js +23 -4
- package/dist/utils/fileLock.js.map +1 -1
- package/dist/utils/schemaValidator.d.ts +24 -0
- package/dist/utils/schemaValidator.d.ts.map +1 -0
- package/dist/utils/schemaValidator.js +137 -0
- package/dist/utils/schemaValidator.js.map +1 -0
- package/package.json +6 -5
- package/schema/logicstamp.context.schema.json +70 -9
package/dist/core/pack/loader.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Loader module - Load contracts, manifests, and source code
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
|
-
import { join, resolve, isAbsolute } from 'node:path';
|
|
5
|
+
import { join, resolve, isAbsolute, relative } from 'node:path';
|
|
6
6
|
import { debugError } from '../../utils/debug.js';
|
|
7
|
+
import { validateUIFContract } from '../../utils/schemaValidator.js';
|
|
7
8
|
import { loadSecurityReport, sanitizeCode } from '../../utils/codeSanitizer.js';
|
|
8
9
|
// Cache expiration time (5 minutes) - balances performance vs memory for long-running processes
|
|
9
10
|
const CACHE_MAX_AGE_MS = 5 * 60 * 1000;
|
|
@@ -37,7 +38,10 @@ let sanitizeStats = {
|
|
|
37
38
|
filesWithSecrets: 0,
|
|
38
39
|
totalSecretsReplaced: 0,
|
|
39
40
|
filesProcessed: [],
|
|
41
|
+
securityReportLoaded: false,
|
|
40
42
|
};
|
|
43
|
+
// Track whether security report was loaded during this context generation
|
|
44
|
+
let securityReportWasLoaded = false;
|
|
41
45
|
/**
|
|
42
46
|
* Record sanitization info into the module-level accumulator
|
|
43
47
|
* Thread-safe: callers aggregate results and call this once after processing
|
|
@@ -66,12 +70,17 @@ export function recordSanitizationBatch(infos) {
|
|
|
66
70
|
* Get and reset sanitization statistics
|
|
67
71
|
*/
|
|
68
72
|
export function getAndResetSanitizeStats() {
|
|
69
|
-
const stats = {
|
|
73
|
+
const stats = {
|
|
74
|
+
...sanitizeStats,
|
|
75
|
+
securityReportLoaded: securityReportWasLoaded,
|
|
76
|
+
};
|
|
70
77
|
sanitizeStats = {
|
|
71
78
|
filesWithSecrets: 0,
|
|
72
79
|
totalSecretsReplaced: 0,
|
|
73
80
|
filesProcessed: [],
|
|
81
|
+
securityReportLoaded: false,
|
|
74
82
|
};
|
|
83
|
+
securityReportWasLoaded = false;
|
|
75
84
|
return stats;
|
|
76
85
|
}
|
|
77
86
|
/**
|
|
@@ -111,17 +120,83 @@ export async function loadManifest(basePath) {
|
|
|
111
120
|
* Sidecar path is computed from the manifest key (project-relative): resolved from projectRoot + key + '.uif.json'
|
|
112
121
|
*/
|
|
113
122
|
export async function loadContract(entryId, projectRoot) {
|
|
123
|
+
// Validate path stays within project root (prevents path traversal attacks)
|
|
124
|
+
if (!isPathWithinRoot(entryId, projectRoot)) {
|
|
125
|
+
debugError('loader', 'loadContract', {
|
|
126
|
+
entryId,
|
|
127
|
+
projectRoot,
|
|
128
|
+
message: 'Path traversal attempt detected - path outside project root',
|
|
129
|
+
});
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
114
132
|
// Resolve relative path from project root
|
|
115
133
|
const absolutePath = isAbsolute(entryId) ? entryId : resolve(projectRoot, entryId);
|
|
116
134
|
const sidecarPath = `${absolutePath}.uif.json`;
|
|
135
|
+
// 1. Read file
|
|
136
|
+
let content;
|
|
117
137
|
try {
|
|
118
|
-
|
|
119
|
-
return JSON.parse(content);
|
|
138
|
+
content = await readFile(sidecarPath, 'utf8');
|
|
120
139
|
}
|
|
121
140
|
catch (error) {
|
|
122
|
-
|
|
141
|
+
const err = error;
|
|
142
|
+
// File not found is normal (sidecar doesn't exist yet) - silent return
|
|
143
|
+
if (err.code === 'ENOENT') {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
// Other read errors (permissions, etc.) - log and return
|
|
147
|
+
debugError('loader', 'loadContract', {
|
|
148
|
+
sidecarPath,
|
|
149
|
+
entryId,
|
|
150
|
+
message: 'Failed to read sidecar file',
|
|
151
|
+
errorCode: err.code,
|
|
152
|
+
errorMessage: err.message,
|
|
153
|
+
});
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
// 2. Parse JSON
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(content);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
const err = error;
|
|
163
|
+
debugError('loader', 'loadContract', {
|
|
164
|
+
sidecarPath,
|
|
165
|
+
entryId,
|
|
166
|
+
message: 'Invalid JSON in sidecar file',
|
|
167
|
+
parseError: err.message,
|
|
168
|
+
});
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// 3. Validate against UIFContract schema
|
|
172
|
+
const { valid, errors, data } = validateUIFContract(parsed);
|
|
173
|
+
if (!valid) {
|
|
174
|
+
// Cap errors to prevent log spam (AJV can output dozens of lines)
|
|
175
|
+
const MAX_ERRORS = 20;
|
|
176
|
+
const shownErrors = errors.slice(0, MAX_ERRORS);
|
|
177
|
+
const extraCount = errors.length - shownErrors.length;
|
|
178
|
+
debugError('loader', 'loadContract', {
|
|
179
|
+
sidecarPath,
|
|
180
|
+
entryId,
|
|
181
|
+
message: 'Invalid contract schema',
|
|
182
|
+
validationErrors: shownErrors,
|
|
183
|
+
...(extraCount > 0 && { additionalErrors: `+${extraCount} more` }),
|
|
184
|
+
hint: 'This file may have been generated by an older LogicStamp version; rerun `stamp context`',
|
|
185
|
+
});
|
|
123
186
|
return null;
|
|
124
187
|
}
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Check if a file path is within the project root directory
|
|
192
|
+
* Prevents path traversal attacks (e.g., `../../../sensitive.json`)
|
|
193
|
+
*/
|
|
194
|
+
function isPathWithinRoot(filePath, rootPath) {
|
|
195
|
+
const resolvedPath = resolve(rootPath, filePath);
|
|
196
|
+
const relativePath = relative(rootPath, resolvedPath);
|
|
197
|
+
// Path is outside root if relative path starts with '..' or is absolute
|
|
198
|
+
// On Windows, also check for drive letter changes (e.g., C: to D:)
|
|
199
|
+
return !relativePath.startsWith('..') && !isAbsolute(relativePath);
|
|
125
200
|
}
|
|
126
201
|
/**
|
|
127
202
|
* Normalize project root for comparison (handles Windows case-insensitivity and path variations)
|
|
@@ -141,6 +216,10 @@ function normalizeProjectRoot(path) {
|
|
|
141
216
|
async function getSecurityReport(projectRoot) {
|
|
142
217
|
// Check if we have a valid cached report
|
|
143
218
|
if (isCacheValid(securityReportCache, projectRoot)) {
|
|
219
|
+
// Track that we have a security report available
|
|
220
|
+
if (securityReportCache.report !== null) {
|
|
221
|
+
securityReportWasLoaded = true;
|
|
222
|
+
}
|
|
144
223
|
return securityReportCache.report;
|
|
145
224
|
}
|
|
146
225
|
// Load and cache the report with timestamp
|
|
@@ -150,6 +229,10 @@ async function getSecurityReport(projectRoot) {
|
|
|
150
229
|
projectRoot,
|
|
151
230
|
timestamp: Date.now(),
|
|
152
231
|
};
|
|
232
|
+
// Track that we have a security report available
|
|
233
|
+
if (report !== null) {
|
|
234
|
+
securityReportWasLoaded = true;
|
|
235
|
+
}
|
|
153
236
|
return report;
|
|
154
237
|
}
|
|
155
238
|
/**
|
|
@@ -161,6 +244,15 @@ async function getSecurityReport(projectRoot) {
|
|
|
161
244
|
* Callers should aggregate sanitization info and record it once after batch processing.
|
|
162
245
|
*/
|
|
163
246
|
export async function extractCodeHeader(entryId, projectRoot) {
|
|
247
|
+
// Validate path stays within project root (prevents path traversal attacks)
|
|
248
|
+
if (!isPathWithinRoot(entryId, projectRoot)) {
|
|
249
|
+
debugError('loader', 'extractCodeHeader', {
|
|
250
|
+
entryId,
|
|
251
|
+
projectRoot,
|
|
252
|
+
message: 'Path traversal attempt detected - path outside project root',
|
|
253
|
+
});
|
|
254
|
+
return { header: null };
|
|
255
|
+
}
|
|
164
256
|
try {
|
|
165
257
|
const absolutePath = isAbsolute(entryId) ? entryId : resolve(projectRoot, entryId);
|
|
166
258
|
// Read file content (source file is never modified)
|
|
@@ -201,6 +293,15 @@ export async function extractCodeHeader(entryId, projectRoot) {
|
|
|
201
293
|
* Callers should aggregate sanitization info and record it once after batch processing.
|
|
202
294
|
*/
|
|
203
295
|
export async function readSourceCode(entryId, projectRoot) {
|
|
296
|
+
// Validate path stays within project root (prevents path traversal attacks)
|
|
297
|
+
if (!isPathWithinRoot(entryId, projectRoot)) {
|
|
298
|
+
debugError('loader', 'readSourceCode', {
|
|
299
|
+
entryId,
|
|
300
|
+
projectRoot,
|
|
301
|
+
message: 'Path traversal attempt detected - path outside project root',
|
|
302
|
+
});
|
|
303
|
+
return { code: null };
|
|
304
|
+
}
|
|
204
305
|
try {
|
|
205
306
|
const absolutePath = isAbsolute(entryId) ? entryId : resolve(projectRoot, entryId);
|
|
206
307
|
// Read file content (source file is never modified)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/pack/loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/core/pack/loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"propExtractor.d.ts","sourceRoot":"","sources":["../../../src/extractors/react/propExtractor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"propExtractor.d.ts","sourceRoot":"","sources":["../../../src/extractors/react/propExtractor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAsG,MAAM,UAAU,CAAC;AAC1I,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAiN3D;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAiDzE;AAID,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC"}
|
|
@@ -8,22 +8,157 @@ import { hasExportedHooks, extractHookParameters } from './hookParameterExtracto
|
|
|
8
8
|
// TypeScript TypeFlags.Undefined constant (0x4000 = 16384)
|
|
9
9
|
// Used for checking if a union type includes undefined
|
|
10
10
|
const TYPEFLAG_UNDEFINED = 16384; // ts.TypeFlags.Undefined
|
|
11
|
+
/**
|
|
12
|
+
* Wraps a function call in a try-catch, returning a default value on error.
|
|
13
|
+
* Use this to simplify error handling for operations that may fail but shouldn't
|
|
14
|
+
* stop processing of other items.
|
|
15
|
+
*/
|
|
16
|
+
function safeExtract(fn, defaultValue, errorContext) {
|
|
17
|
+
try {
|
|
18
|
+
return fn();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (errorContext) {
|
|
22
|
+
debugError('propExtractor', 'extractProps', {
|
|
23
|
+
filePath: errorContext.filePath,
|
|
24
|
+
error: error instanceof Error ? error.message : String(error),
|
|
25
|
+
context: errorContext.context,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return defaultValue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
11
31
|
/**
|
|
12
32
|
* Safely get TypeScript type flags from a ts-morph Type
|
|
13
33
|
* Accesses the underlying TypeScript compiler type to get flags
|
|
14
34
|
*/
|
|
15
35
|
function getTypeFlags(type) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
const compilerType = type.compilerType;
|
|
37
|
+
if (compilerType && typeof compilerType.flags === 'number') {
|
|
38
|
+
return compilerType.flags;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if a union type includes undefined
|
|
44
|
+
*/
|
|
45
|
+
function isUndefinedType(type) {
|
|
46
|
+
const flags = getTypeFlags(type);
|
|
47
|
+
if ((flags & TYPEFLAG_UNDEFINED) !== 0) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return type.getText() === 'undefined';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Check if a union type contains undefined and extract the non-undefined type text
|
|
54
|
+
* Returns { hasUndefined, typeText } where typeText excludes undefined
|
|
55
|
+
*/
|
|
56
|
+
function analyzeUnionForUndefined(propType) {
|
|
57
|
+
if (!propType.isUnion()) {
|
|
58
|
+
return { hasUndefined: false, typeText: propType.getText() };
|
|
59
|
+
}
|
|
60
|
+
const unionTypes = propType.getUnionTypes();
|
|
61
|
+
const hasUndefined = unionTypes.some(isUndefinedType);
|
|
62
|
+
if (!hasUndefined) {
|
|
63
|
+
return { hasUndefined: false, typeText: propType.getText() };
|
|
64
|
+
}
|
|
65
|
+
const typeText = unionTypes
|
|
66
|
+
.filter(ut => !isUndefinedType(ut))
|
|
67
|
+
.map(ut => ut.getText())
|
|
68
|
+
.join(' | ');
|
|
69
|
+
return { hasUndefined: true, typeText };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extract a single prop from an interface property
|
|
73
|
+
*/
|
|
74
|
+
function extractInterfaceProp(prop) {
|
|
75
|
+
const name = prop.getName();
|
|
76
|
+
let isOptional = prop.hasQuestionToken();
|
|
77
|
+
let type = prop.getType().getText();
|
|
78
|
+
let didRebuildFromUnion = false;
|
|
79
|
+
// Check for union-with-undefined (some people write foo: string | undefined without ?)
|
|
80
|
+
if (!isOptional) {
|
|
81
|
+
const analysis = analyzeUnionForUndefined(prop.getType());
|
|
82
|
+
if (analysis.hasUndefined) {
|
|
83
|
+
isOptional = true;
|
|
84
|
+
type = analysis.typeText;
|
|
85
|
+
didRebuildFromUnion = true;
|
|
21
86
|
}
|
|
22
87
|
}
|
|
23
|
-
|
|
24
|
-
|
|
88
|
+
// If optional but we didn't rebuild from union types, strip undefined from type text
|
|
89
|
+
if (isOptional && !didRebuildFromUnion) {
|
|
90
|
+
type = stripUndefinedFromUnionText(type);
|
|
25
91
|
}
|
|
26
|
-
return
|
|
92
|
+
return { name, propType: normalizePropType(type, isOptional) };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract props from a single interface declaration
|
|
96
|
+
*/
|
|
97
|
+
function extractPropsFromInterface(iface, filePath) {
|
|
98
|
+
const props = {};
|
|
99
|
+
if (!/Props$/i.test(iface.getName())) {
|
|
100
|
+
return props;
|
|
101
|
+
}
|
|
102
|
+
for (const prop of iface.getProperties()) {
|
|
103
|
+
const result = safeExtract(() => extractInterfaceProp(prop), null, { filePath, context: 'props-interface-property' });
|
|
104
|
+
if (result) {
|
|
105
|
+
props[result.name] = result.propType;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return props;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Extract a single prop from a type alias property symbol
|
|
112
|
+
*/
|
|
113
|
+
function extractTypeAliasProp(prop, typeAlias) {
|
|
114
|
+
const name = prop.getName();
|
|
115
|
+
let isOptional = false;
|
|
116
|
+
let propType = prop.getTypeAtLocation(typeAlias).getText();
|
|
117
|
+
let didRebuildFromUnion = false;
|
|
118
|
+
// Method 1: Check if type is a union that includes undefined
|
|
119
|
+
const propTypeObj = prop.getTypeAtLocation(typeAlias);
|
|
120
|
+
const analysis = analyzeUnionForUndefined(propTypeObj);
|
|
121
|
+
if (analysis.hasUndefined) {
|
|
122
|
+
isOptional = true;
|
|
123
|
+
propType = analysis.typeText;
|
|
124
|
+
didRebuildFromUnion = true;
|
|
125
|
+
}
|
|
126
|
+
// Method 2: Check declarations for question token (AST method)
|
|
127
|
+
if (!isOptional) {
|
|
128
|
+
const declarations = prop.getDeclarations();
|
|
129
|
+
isOptional = declarations.some((decl) => {
|
|
130
|
+
if (Node.isPropertySignature(decl)) {
|
|
131
|
+
return decl.hasQuestionToken();
|
|
132
|
+
}
|
|
133
|
+
if (Node.isPropertyDeclaration(decl)) {
|
|
134
|
+
return decl.hasQuestionToken();
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// If optional but we didn't rebuild from union types, strip undefined from type text
|
|
140
|
+
if (isOptional && !didRebuildFromUnion) {
|
|
141
|
+
propType = stripUndefinedFromUnionText(propType);
|
|
142
|
+
}
|
|
143
|
+
return { name, propType: normalizePropType(propType, isOptional) };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract props from a single type alias declaration
|
|
147
|
+
*/
|
|
148
|
+
function extractPropsFromTypeAlias(typeAlias, filePath) {
|
|
149
|
+
const props = {};
|
|
150
|
+
if (!/Props$/i.test(typeAlias.getName())) {
|
|
151
|
+
return props;
|
|
152
|
+
}
|
|
153
|
+
const type = typeAlias.getType();
|
|
154
|
+
const properties = type.getProperties();
|
|
155
|
+
for (const prop of properties) {
|
|
156
|
+
const result = safeExtract(() => extractTypeAliasProp(prop, typeAlias), null, { filePath, context: 'props-typealias-property' });
|
|
157
|
+
if (result) {
|
|
158
|
+
props[result.name] = result.propType;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return props;
|
|
27
162
|
}
|
|
28
163
|
/**
|
|
29
164
|
* Extract component props from TypeScript interfaces/types
|
|
@@ -32,216 +167,28 @@ function getTypeFlags(type) {
|
|
|
32
167
|
export function extractProps(source) {
|
|
33
168
|
const props = {};
|
|
34
169
|
const filePath = source.getFilePath?.() ?? 'unknown';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (/Props$/i.test(iface.getName())) {
|
|
41
|
-
iface.getProperties().forEach((prop) => {
|
|
42
|
-
try {
|
|
43
|
-
const name = prop.getName();
|
|
44
|
-
let isOptional = prop.hasQuestionToken();
|
|
45
|
-
let type = prop.getType().getText();
|
|
46
|
-
let didRebuildFromUnion = false;
|
|
47
|
-
// Also check for union-with-undefined (some people write foo: string | undefined without ?)
|
|
48
|
-
if (!isOptional) {
|
|
49
|
-
try {
|
|
50
|
-
const propType = prop.getType();
|
|
51
|
-
if (propType.isUnion()) {
|
|
52
|
-
const unionTypes = propType.getUnionTypes();
|
|
53
|
-
const hasUndefined = unionTypes.some(ut => {
|
|
54
|
-
try {
|
|
55
|
-
const flags = getTypeFlags(ut);
|
|
56
|
-
if ((flags & TYPEFLAG_UNDEFINED) !== 0) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// Fallback to string check if flags not available
|
|
62
|
-
}
|
|
63
|
-
const text = ut.getText();
|
|
64
|
-
return text === 'undefined';
|
|
65
|
-
});
|
|
66
|
-
if (hasUndefined) {
|
|
67
|
-
isOptional = true;
|
|
68
|
-
// Normalize type text by removing undefined from union (handles any position)
|
|
69
|
-
type = unionTypes
|
|
70
|
-
.filter(ut => {
|
|
71
|
-
const flags = getTypeFlags(ut);
|
|
72
|
-
if ((flags & TYPEFLAG_UNDEFINED) !== 0) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
return ut.getText() !== 'undefined';
|
|
76
|
-
})
|
|
77
|
-
.map(ut => ut.getText())
|
|
78
|
-
.join(' | ');
|
|
79
|
-
didRebuildFromUnion = true;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Fallback if union check fails
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// If optional but we didn't rebuild from union types, strip undefined from type text
|
|
88
|
-
// This handles cases like "foo?: undefined | string" where ? token made us skip union logic
|
|
89
|
-
if (isOptional && !didRebuildFromUnion) {
|
|
90
|
-
type = stripUndefinedFromUnionText(type);
|
|
91
|
-
}
|
|
92
|
-
props[name] = normalizePropType(type, isOptional);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
debugError('propExtractor', 'extractProps', {
|
|
96
|
-
filePath,
|
|
97
|
-
error: error instanceof Error ? error.message : String(error),
|
|
98
|
-
context: 'props-interface-property',
|
|
99
|
-
});
|
|
100
|
-
// Continue with next property
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
debugError('propExtractor', 'extractProps', {
|
|
107
|
-
filePath,
|
|
108
|
-
error: error instanceof Error ? error.message : String(error),
|
|
109
|
-
context: 'props-interface',
|
|
110
|
-
});
|
|
111
|
-
// Continue with next interface
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
catch (error) {
|
|
116
|
-
debugError('propExtractor', 'extractProps', {
|
|
117
|
-
filePath,
|
|
118
|
-
error: error instanceof Error ? error.message : String(error),
|
|
119
|
-
context: 'props-interfaces-batch',
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
// Look for type aliases ending with Props
|
|
123
|
-
try {
|
|
124
|
-
source.getTypeAliases().forEach((typeAlias) => {
|
|
125
|
-
try {
|
|
126
|
-
if (/Props$/i.test(typeAlias.getName())) {
|
|
127
|
-
const type = typeAlias.getType();
|
|
128
|
-
const properties = type.getProperties();
|
|
129
|
-
properties.forEach((prop) => {
|
|
130
|
-
try {
|
|
131
|
-
const name = prop.getName();
|
|
132
|
-
// Check if optional using TypeScript's type system
|
|
133
|
-
let isOptional = false;
|
|
134
|
-
let propType = prop.getTypeAtLocation(typeAlias).getText();
|
|
135
|
-
let didRebuildFromUnion = false;
|
|
136
|
-
// Method 1: Check if type is a union that includes undefined
|
|
137
|
-
// Use type flags instead of string matching to avoid false positives
|
|
138
|
-
try {
|
|
139
|
-
const propTypeObj = prop.getTypeAtLocation(typeAlias);
|
|
140
|
-
if (propTypeObj.isUnion()) {
|
|
141
|
-
const unionTypes = propTypeObj.getUnionTypes();
|
|
142
|
-
const hasUndefined = unionTypes.some(ut => {
|
|
143
|
-
// Check type flags for undefined (more robust than string matching)
|
|
144
|
-
const flags = getTypeFlags(ut);
|
|
145
|
-
if ((flags & TYPEFLAG_UNDEFINED) !== 0) {
|
|
146
|
-
return true;
|
|
147
|
-
}
|
|
148
|
-
// Fallback: exact string match only (avoid false positives like SomeUndefinedType)
|
|
149
|
-
const text = ut.getText();
|
|
150
|
-
return text === 'undefined';
|
|
151
|
-
});
|
|
152
|
-
if (hasUndefined) {
|
|
153
|
-
isOptional = true;
|
|
154
|
-
// Normalize type text by removing undefined from union (handles any position)
|
|
155
|
-
// This ensures consistent output: undefined | string and string | undefined both become string
|
|
156
|
-
propType = unionTypes
|
|
157
|
-
.filter(ut => {
|
|
158
|
-
const flags = getTypeFlags(ut);
|
|
159
|
-
if ((flags & TYPEFLAG_UNDEFINED) !== 0) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
return ut.getText() !== 'undefined';
|
|
163
|
-
})
|
|
164
|
-
.map(ut => ut.getText())
|
|
165
|
-
.join(' | ');
|
|
166
|
-
didRebuildFromUnion = true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Fallback if union check fails
|
|
172
|
-
}
|
|
173
|
-
// Method 2: Check declarations for question token (AST method)
|
|
174
|
-
if (!isOptional) {
|
|
175
|
-
const declarations = prop.getDeclarations();
|
|
176
|
-
isOptional = declarations.some((decl) => {
|
|
177
|
-
// Use AST method to check for question token
|
|
178
|
-
// PropertySignature and PropertyDeclaration both have hasQuestionToken()
|
|
179
|
-
if (Node.isPropertySignature(decl)) {
|
|
180
|
-
return decl.hasQuestionToken();
|
|
181
|
-
}
|
|
182
|
-
if (Node.isPropertyDeclaration(decl)) {
|
|
183
|
-
return decl.hasQuestionToken();
|
|
184
|
-
}
|
|
185
|
-
return false;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
// If optional but we didn't rebuild from union types, strip undefined from type text
|
|
189
|
-
// This handles cases like "foo?: undefined | string" where ? token made us skip union logic
|
|
190
|
-
// Also handles "foo?: string | number" where union exists but doesn't contain undefined
|
|
191
|
-
if (isOptional && !didRebuildFromUnion) {
|
|
192
|
-
propType = stripUndefinedFromUnionText(propType);
|
|
193
|
-
}
|
|
194
|
-
props[name] = normalizePropType(propType, isOptional);
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
debugError('propExtractor', 'extractProps', {
|
|
198
|
-
filePath,
|
|
199
|
-
error: error instanceof Error ? error.message : String(error),
|
|
200
|
-
context: 'props-typealias-property',
|
|
201
|
-
});
|
|
202
|
-
// Continue with next property
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (error) {
|
|
208
|
-
debugError('propExtractor', 'extractProps', {
|
|
209
|
-
filePath,
|
|
210
|
-
error: error instanceof Error ? error.message : String(error),
|
|
211
|
-
context: 'props-typealias',
|
|
212
|
-
});
|
|
213
|
-
// Continue with next type alias
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
debugError('propExtractor', 'extractProps', {
|
|
219
|
-
filePath,
|
|
220
|
-
error: error instanceof Error ? error.message : String(error),
|
|
221
|
-
context: 'props-typealiases-batch',
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
// Always try to extract hook parameters if there are exported hooks
|
|
225
|
-
// This ensures hook parameters are captured even if there's a Props interface
|
|
226
|
-
// Props take priority on conflicts (if a prop exists in both, Props value is kept)
|
|
227
|
-
// Quick check: only extract if there might be exported hooks (performance optimization)
|
|
228
|
-
if (hasExportedHooks(source)) {
|
|
229
|
-
const hookParams = extractHookParameters(source);
|
|
230
|
-
if (Object.keys(hookParams).length > 0) {
|
|
231
|
-
// Merge hook parameters with Props, with Props taking priority on conflicts
|
|
232
|
-
// Save original props to preserve Props values, then merge hookParams, then restore Props
|
|
233
|
-
const originalProps = { ...props };
|
|
234
|
-
Object.assign(props, hookParams);
|
|
235
|
-
Object.assign(props, originalProps); // Props override any conflicting hook parameters
|
|
236
|
-
}
|
|
237
|
-
}
|
|
170
|
+
// Extract props from interfaces ending with Props
|
|
171
|
+
const interfaces = safeExtract(() => source.getInterfaces(), [], { filePath, context: 'props-interfaces-batch' });
|
|
172
|
+
for (const iface of interfaces) {
|
|
173
|
+
const ifaceProps = safeExtract(() => extractPropsFromInterface(iface, filePath), {}, { filePath, context: 'props-interface' });
|
|
174
|
+
Object.assign(props, ifaceProps);
|
|
238
175
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
176
|
+
// Extract props from type aliases ending with Props
|
|
177
|
+
const typeAliases = safeExtract(() => source.getTypeAliases(), [], { filePath, context: 'props-typealiases-batch' });
|
|
178
|
+
for (const typeAlias of typeAliases) {
|
|
179
|
+
const typeAliasProps = safeExtract(() => extractPropsFromTypeAlias(typeAlias, filePath), {}, { filePath, context: 'props-typealias' });
|
|
180
|
+
Object.assign(props, typeAliasProps);
|
|
181
|
+
}
|
|
182
|
+
// Extract hook parameters if there are exported hooks
|
|
183
|
+
// Props take priority on conflicts (if a prop exists in both, Props value is kept)
|
|
184
|
+
if (hasExportedHooks(source)) {
|
|
185
|
+
const hookParams = extractHookParameters(source);
|
|
186
|
+
if (Object.keys(hookParams).length > 0) {
|
|
187
|
+
// Merge hook parameters with Props, with Props taking priority on conflicts
|
|
188
|
+
const originalProps = { ...props };
|
|
189
|
+
Object.assign(props, hookParams);
|
|
190
|
+
Object.assign(props, originalProps); // Props override any conflicting hook parameters
|
|
191
|
+
}
|
|
245
192
|
}
|
|
246
193
|
return props;
|
|
247
194
|
}
|