@monoes/monodesign 1.2.20 → 1.2.22
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 +2 -2
- package/cli/engine/design-system-checks.mjs +381 -0
- package/cli/engine/design-system-parse.mjs +433 -0
- package/cli/engine/design-system.mjs +20 -736
- package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
- package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
- package/cli/engine/engines/regex/detect-text.mjs +11 -523
- package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
- package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
- package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
- package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
- package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
- package/cli/lib/monodesign-config-consent.mjs +93 -0
- package/cli/lib/monodesign-config-core.mjs +37 -0
- package/cli/lib/monodesign-config-detection.mjs +133 -0
- package/cli/lib/monodesign-config-filter.mjs +198 -0
- package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
- package/cli/lib/monodesign-config.mjs +21 -623
- package/package.json +2 -2
- package/skill/scripts/context-fields.mjs +65 -0
- package/skill/scripts/context-resolve.mjs +425 -0
- package/skill/scripts/context-update.mjs +151 -0
- package/skill/scripts/context-utils.mjs +68 -0
- package/skill/scripts/context-workspace.mjs +245 -0
- package/skill/scripts/context.mjs +17 -891
- package/skill/scripts/hook-admin-config.mjs +152 -0
- package/skill/scripts/hook-admin-ignore.mjs +131 -0
- package/skill/scripts/hook-admin-manifest.mjs +292 -0
- package/skill/scripts/hook-admin.mjs +8 -541
- package/skill/scripts/hook-lib-cache.mjs +145 -0
- package/skill/scripts/hook-lib-config.mjs +292 -0
- package/skill/scripts/hook-lib-detector.mjs +72 -0
- package/skill/scripts/hook-lib-events.mjs +182 -0
- package/skill/scripts/hook-lib-findings.mjs +201 -0
- package/skill/scripts/hook-lib-ignore.mjs +205 -0
- package/skill/scripts/hook-lib-render.mjs +273 -0
- package/skill/scripts/hook-lib-run.mjs +281 -0
- package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
- package/skill/scripts/hook-lib.mjs +71 -1747
- package/skill/scripts/lib/design-parser-colors.mjs +182 -0
- package/skill/scripts/lib/design-parser-extract.mjs +333 -0
- package/skill/scripts/lib/design-parser-sections.mjs +214 -0
- package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
- package/skill/scripts/lib/design-parser.mjs +10 -783
- package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
- package/skill/scripts/live/manual-apply-compact.mjs +122 -0
- package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
- package/skill/scripts/live/manual-apply-log.mjs +179 -0
- package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
- package/skill/scripts/live/manual-apply.mjs +65 -640
- package/skill/scripts/live/svelte-component-accept.mjs +248 -0
- package/skill/scripts/live/svelte-component-css.mjs +230 -0
- package/skill/scripts/live/svelte-component-markup.mjs +104 -0
- package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
- package/skill/scripts/live/svelte-component-session.mjs +158 -0
- package/skill/scripts/live/svelte-component.mjs +32 -792
- package/skill/scripts/live-accept-mutate.mjs +152 -0
- package/skill/scripts/live-accept-parse.mjs +370 -0
- package/skill/scripts/live-accept-scrub.mjs +59 -0
- package/skill/scripts/live-accept-search.mjs +58 -0
- package/skill/scripts/live-accept.mjs +15 -631
- package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
- package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
- package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
- package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
- package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
- package/skill/scripts/live-commit-manual-edits.mjs +24 -881
- package/skill/scripts/live-copy-edit-agent.mjs +16 -551
- package/skill/scripts/live-copy-edit-compact.mjs +137 -0
- package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
- package/skill/scripts/live-copy-edit-runners.mjs +297 -0
- package/skill/scripts/live-inject-csp.mjs +132 -0
- package/skill/scripts/live-inject-files.mjs +229 -0
- package/skill/scripts/live-inject-tag.mjs +114 -0
- package/skill/scripts/live-inject.mjs +13 -440
- package/skill/scripts/live-server-poll.mjs +232 -0
- package/skill/scripts/live-server-routes.mjs +449 -0
- package/skill/scripts/live-server-state.mjs +361 -0
- package/skill/scripts/live-server.mjs +10 -946
- package/skill/scripts/live-wrap-css.mjs +132 -0
- package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
- package/skill/scripts/live-wrap-search.mjs +247 -0
- package/skill/scripts/live-wrap.mjs +21 -484
- package/skill/scripts/palette-seeds.mjs +401 -0
- package/skill/scripts/palette.mjs +1 -395
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ALLOWED_EXTS, SENSITIVE_PATH, isGeneratedPath, EDIT_COUNT_THRESHOLD, truthy, depthIsSet, resolveCacheCwd, resolveProjectPlatform, isNativePlatform, readConfig, matchConfiguredExtension } from './hook-lib-config.mjs';
|
|
4
|
+
import { readCache, persistCache, ensureFile, bumpEditCount, suppressionNotice } from './hook-lib-cache.mjs';
|
|
5
|
+
import { matchesAnyGlob, filterFindings, dedupeAgainstCache, rememberFindings } from './hook-lib-findings.mjs';
|
|
6
|
+
import { renderGroupedTemplate, relativize, renderCleanAck, renderPendingAck, shouldEmitAckForFile, designSystemOptions, appendDesignSystemNote, payload } from './hook-lib-render.mjs';
|
|
7
|
+
import { resolveTargetFiles, resolveHarness, normalizeHookEvent } from './hook-lib-events.mjs';
|
|
8
|
+
import { hasPathTraversal, normalizeScanTargets, expandScanTargets } from './hook-lib-scan-targets.mjs';
|
|
9
|
+
import { loadDetector } from './hook-lib-detector.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run the hook with explicit dependencies. Returns a result object:
|
|
13
|
+
* { exitCode, stdout, audit, reason? }
|
|
14
|
+
*
|
|
15
|
+
* Never throws. All errors are converted to `exitCode: 0` + audit entry.
|
|
16
|
+
*/
|
|
17
|
+
export async function runHook({ stdinJson, env = {}, cwd = process.cwd(), now = Date.now, detector } = {}) {
|
|
18
|
+
const audit = { ts: new Date(now()).toISOString(), event: 'PostToolUse' };
|
|
19
|
+
const result = (extra) => ({ exitCode: 0, stdout: '', audit: { ...audit, ...extra } });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Re-entrancy guard.
|
|
23
|
+
if (depthIsSet(env.MONODESIGN_HOOK_DEPTH) || depthIsSet(env.CLAUDE_HOOK_DEPTH)) {
|
|
24
|
+
return result({ reentrant: true, durationMs: 0 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (truthy(env.MONODESIGN_HOOK_DISABLED)) {
|
|
28
|
+
return result({ skipped: 'env-disabled', durationMs: 0 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const started = Date.now();
|
|
32
|
+
|
|
33
|
+
let event;
|
|
34
|
+
try {
|
|
35
|
+
event = typeof stdinJson === 'string' ? JSON.parse(stdinJson) : stdinJson;
|
|
36
|
+
} catch {
|
|
37
|
+
return result({ skipped: 'stdin-malformed', durationMs: Date.now() - started });
|
|
38
|
+
}
|
|
39
|
+
if (!event || typeof event !== 'object') {
|
|
40
|
+
return result({ skipped: 'stdin-empty', durationMs: Date.now() - started });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const harness = resolveHarness(env, event);
|
|
44
|
+
event = normalizeHookEvent(event, cwd, harness);
|
|
45
|
+
audit.harness = harness;
|
|
46
|
+
|
|
47
|
+
const sessionCwd = event.cwd || cwd;
|
|
48
|
+
const primaryFiles = normalizeScanTargets(resolveTargetFiles(event, sessionCwd), sessionCwd);
|
|
49
|
+
const projectCwd = resolveCacheCwd(primaryFiles[0], sessionCwd);
|
|
50
|
+
audit.cwd = projectCwd;
|
|
51
|
+
const primaryFileSet = new Set(primaryFiles);
|
|
52
|
+
const targetFiles = expandScanTargets(primaryFiles, projectCwd);
|
|
53
|
+
audit.session = event.session_id || null;
|
|
54
|
+
if (event.tool_name) audit.tool = event.tool_name;
|
|
55
|
+
|
|
56
|
+
if (targetFiles.length === 0) {
|
|
57
|
+
return result({ skipped: 'no-file-path', durationMs: Date.now() - started });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const config = readConfig(projectCwd);
|
|
61
|
+
if (config.enabled === false) {
|
|
62
|
+
return result({ skipped: 'config-disabled', durationMs: Date.now() - started });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const platform = resolveProjectPlatform(projectCwd);
|
|
66
|
+
if (isNativePlatform(platform)) {
|
|
67
|
+
return result({ skipped: 'native-platform', platform, durationMs: Date.now() - started });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const cache = readCache(projectCwd);
|
|
71
|
+
const sessionId = event.session_id || 'unknown';
|
|
72
|
+
const det = detector || await loadDetector();
|
|
73
|
+
if (!det || typeof det.detectText !== 'function') {
|
|
74
|
+
// Cache is not mutated yet at this point; nothing to persist.
|
|
75
|
+
return result({ skipped: 'detector-missing', durationMs: Date.now() - started });
|
|
76
|
+
}
|
|
77
|
+
const scanOptions = designSystemOptions(config, det, projectCwd);
|
|
78
|
+
|
|
79
|
+
let pendingWinner = null;
|
|
80
|
+
let cleanWinner = null;
|
|
81
|
+
const freshGroups = [];
|
|
82
|
+
let suppressionWinner = null;
|
|
83
|
+
let detectorThrewAny = false;
|
|
84
|
+
let lastSkip = 'no-scannable-file';
|
|
85
|
+
let suppressedHit = false;
|
|
86
|
+
let cacheDirty = false;
|
|
87
|
+
|
|
88
|
+
for (const filePath of targetFiles) {
|
|
89
|
+
audit.file = filePath;
|
|
90
|
+
|
|
91
|
+
if (hasPathTraversal(filePath) || SENSITIVE_PATH.test(filePath)) {
|
|
92
|
+
lastSkip = 'sensitive';
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (isGeneratedPath(filePath, projectCwd)) {
|
|
96
|
+
lastSkip = 'generated';
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
101
|
+
const configuredExt = matchConfiguredExtension(filePath, config.extensions);
|
|
102
|
+
audit.ext = configuredExt ? configuredExt.ext : ext;
|
|
103
|
+
if (!ALLOWED_EXTS.has(ext) && !configuredExt) {
|
|
104
|
+
lastSkip = 'extension';
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const relForMatch = relativize(filePath, projectCwd);
|
|
109
|
+
if (matchesAnyGlob(relForMatch, config.ignoreFiles) || matchesAnyGlob(filePath, config.ignoreFiles)) {
|
|
110
|
+
lastSkip = 'config-ignore-file';
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!fs.existsSync(filePath)) {
|
|
114
|
+
lastSkip = 'file-missing';
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (primaryFileSet.has(filePath)) {
|
|
119
|
+
const editCount = bumpEditCount(cache, sessionId, filePath);
|
|
120
|
+
cacheDirty = true;
|
|
121
|
+
audit.editCount = editCount;
|
|
122
|
+
|
|
123
|
+
if (editCount > EDIT_COUNT_THRESHOLD) {
|
|
124
|
+
const wasJustCrossed = editCount === EDIT_COUNT_THRESHOLD + 1;
|
|
125
|
+
if (wasJustCrossed && !suppressionWinner) {
|
|
126
|
+
suppressionWinner = { filePath };
|
|
127
|
+
}
|
|
128
|
+
lastSkip = 'suppressed';
|
|
129
|
+
suppressedHit = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
135
|
+
let findings;
|
|
136
|
+
let detectorThrew = false;
|
|
137
|
+
const useHtmlEngine = configuredExt
|
|
138
|
+
? configuredExt.engine === 'html'
|
|
139
|
+
: (ext === '.html' || ext === '.htm');
|
|
140
|
+
if (useHtmlEngine && typeof det.detectHtml === 'function') {
|
|
141
|
+
try { findings = await det.detectHtml(filePath, scanOptions); } catch { findings = []; detectorThrew = true; }
|
|
142
|
+
} else {
|
|
143
|
+
try { findings = await det.detectText(content, filePath, scanOptions); } catch { findings = []; detectorThrew = true; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const filtered = filterFindings(findings || [], content, ext, config);
|
|
147
|
+
const fresh = dedupeAgainstCache(filtered, cache, sessionId, filePath);
|
|
148
|
+
audit.findings = (findings || []).length;
|
|
149
|
+
audit.freshFindings = fresh.length;
|
|
150
|
+
|
|
151
|
+
if (fresh.length > 0) {
|
|
152
|
+
rememberFindings(cache, sessionId, filePath, fresh);
|
|
153
|
+
cacheDirty = true;
|
|
154
|
+
freshGroups.push({ filePath, findings: fresh });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (detectorThrew) {
|
|
159
|
+
detectorThrewAny = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (filtered.length > 0 && !pendingWinner) {
|
|
164
|
+
const known = (ensureFile(cache, sessionId, filePath).findings || []).slice();
|
|
165
|
+
pendingWinner = { filePath, known };
|
|
166
|
+
} else if (filtered.length === 0 && !cleanWinner) {
|
|
167
|
+
cleanWinner = { filePath };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Persist only when the write is earned: fresh findings justify creating
|
|
172
|
+
// `.monodesign/` (dedup and suppression need it), and an already-present
|
|
173
|
+
// `.monodesign/` dir marks a project that opted in. A non-UI edit, or a
|
|
174
|
+
// clean UI edit in a project with no Monodesign footprint, must be a
|
|
175
|
+
// no-op on disk (issues #344, #305).
|
|
176
|
+
if (freshGroups.length > 0
|
|
177
|
+
|| (cacheDirty && fs.existsSync(path.join(projectCwd, '.monodesign')))) {
|
|
178
|
+
persistCache(projectCwd, cache);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (freshGroups.length > 0) {
|
|
182
|
+
const firstGroup = freshGroups[0];
|
|
183
|
+
const text = appendDesignSystemNote(renderGroupedTemplate(freshGroups, config, { cwd: projectCwd }), scanOptions);
|
|
184
|
+
const allFindings = freshGroups.flatMap((group) => group.findings);
|
|
185
|
+
return {
|
|
186
|
+
exitCode: 0,
|
|
187
|
+
stdout: payload(text, 'PostToolUse', harness),
|
|
188
|
+
emission: {
|
|
189
|
+
kind: 'fresh',
|
|
190
|
+
file: firstGroup.filePath,
|
|
191
|
+
findings: firstGroup.findings,
|
|
192
|
+
groups: freshGroups,
|
|
193
|
+
},
|
|
194
|
+
audit: {
|
|
195
|
+
...audit,
|
|
196
|
+
file: firstGroup.filePath,
|
|
197
|
+
emitted: true,
|
|
198
|
+
freshFiles: freshGroups.length,
|
|
199
|
+
freshFindings: allFindings.length,
|
|
200
|
+
chars: text.length,
|
|
201
|
+
durationMs: Date.now() - started,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (detectorThrewAny && !pendingWinner && !cleanWinner) {
|
|
207
|
+
return result({ emitted: false, error: 'detector-threw', durationMs: Date.now() - started });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (truthy(env.MONODESIGN_HOOK_QUIET) || config.quiet === true) {
|
|
211
|
+
return result({ emitted: false, quiet: true, durationMs: Date.now() - started });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (pendingWinner && shouldEmitAckForFile(pendingWinner.filePath, config)) {
|
|
215
|
+
const text = appendDesignSystemNote(renderPendingAck(pendingWinner.filePath, pendingWinner.known, { cwd: projectCwd }), scanOptions);
|
|
216
|
+
return {
|
|
217
|
+
exitCode: 0,
|
|
218
|
+
stdout: payload(text, 'PostToolUse', harness),
|
|
219
|
+
emission: { kind: 'pending', file: pendingWinner.filePath, known: pendingWinner.known },
|
|
220
|
+
audit: {
|
|
221
|
+
...audit,
|
|
222
|
+
file: pendingWinner.filePath,
|
|
223
|
+
emitted: true,
|
|
224
|
+
kind: 'pending',
|
|
225
|
+
pending: pendingWinner.known.length,
|
|
226
|
+
chars: text.length,
|
|
227
|
+
durationMs: Date.now() - started,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (suppressionWinner) {
|
|
233
|
+
const text = suppressionNotice(relativize(suppressionWinner.filePath, projectCwd));
|
|
234
|
+
return {
|
|
235
|
+
exitCode: 0,
|
|
236
|
+
stdout: payload(text, 'PostToolUse', harness),
|
|
237
|
+
emission: { kind: 'suppression', file: suppressionWinner.filePath },
|
|
238
|
+
audit: {
|
|
239
|
+
...audit,
|
|
240
|
+
file: suppressionWinner.filePath,
|
|
241
|
+
suppressed: true,
|
|
242
|
+
emitted: true,
|
|
243
|
+
durationMs: Date.now() - started,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (cleanWinner && shouldEmitAckForFile(cleanWinner.filePath, config)) {
|
|
249
|
+
const text = appendDesignSystemNote(renderCleanAck(cleanWinner.filePath, { cwd: projectCwd }), scanOptions);
|
|
250
|
+
return {
|
|
251
|
+
exitCode: 0,
|
|
252
|
+
stdout: payload(text, 'PostToolUse', harness),
|
|
253
|
+
emission: { kind: 'clean', file: cleanWinner.filePath },
|
|
254
|
+
audit: {
|
|
255
|
+
...audit,
|
|
256
|
+
file: cleanWinner.filePath,
|
|
257
|
+
emitted: true,
|
|
258
|
+
kind: 'clean',
|
|
259
|
+
chars: text.length,
|
|
260
|
+
durationMs: Date.now() - started,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (pendingWinner || cleanWinner) {
|
|
266
|
+
return result({ emitted: false, skipped: 'non-ui-ack', durationMs: Date.now() - started });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (suppressedHit) {
|
|
270
|
+
return result({ suppressed: true, emitted: false, durationMs: Date.now() - started });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return result({ skipped: lastSkip, durationMs: Date.now() - started });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return {
|
|
276
|
+
exitCode: 0,
|
|
277
|
+
stdout: '',
|
|
278
|
+
audit: { ...audit, error: String(err?.message ? err.message : err) },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// UI components often keep slop in a sibling/co-located stylesheet while the
|
|
5
|
+
// JSX edit is what triggered PostToolUse. Scan those styles too so an App.jsx
|
|
6
|
+
// patch doesn't report "clean" while styles.css still has Inter/bounce/etc.
|
|
7
|
+
const UI_CODE_EXTS = new Set(['.jsx', '.tsx', '.vue', '.svelte', '.astro']);
|
|
8
|
+
const STYLE_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
9
|
+
const CO_SCAN_STYLE_NAMES = [
|
|
10
|
+
'styles.css', 'styles.scss', 'styles.sass', 'styles.less',
|
|
11
|
+
'index.css', 'index.scss', 'index.sass', 'index.less',
|
|
12
|
+
'global.css', 'global.scss', 'global.sass', 'global.less',
|
|
13
|
+
'globals.css', 'globals.scss', 'globals.sass', 'globals.less',
|
|
14
|
+
];
|
|
15
|
+
const MAX_SCAN_TARGETS = 6;
|
|
16
|
+
|
|
17
|
+
const STATIC_STYLE_IMPORT_RE = /import\s+(?:[\w*{}\s,$]+\s+from\s+)?['"]([^'"]+\.(?:css|scss|sass|less))['"]/gi;
|
|
18
|
+
|
|
19
|
+
export function hasPathTraversal(filePath) {
|
|
20
|
+
return typeof filePath === 'string' && filePath.includes('..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isInsideProject(filePath, projectCwd) {
|
|
24
|
+
if (!filePath || !projectCwd || hasPathTraversal(filePath)) return false;
|
|
25
|
+
try {
|
|
26
|
+
const rel = path.relative(projectCwd, filePath).split(path.sep).join('/');
|
|
27
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseStaticStyleImports(content, fromFile, projectCwd) {
|
|
34
|
+
if (!content || typeof content !== 'string') return [];
|
|
35
|
+
const dir = path.dirname(fromFile);
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const m of content.matchAll(STATIC_STYLE_IMPORT_RE)) {
|
|
38
|
+
let p = (m[1] || '').trim();
|
|
39
|
+
if (!p) continue;
|
|
40
|
+
if (p.startsWith('.')) p = path.resolve(dir, p);
|
|
41
|
+
else if (!path.isAbsolute(p)) p = path.resolve(projectCwd, p);
|
|
42
|
+
if (!isInsideProject(p, projectCwd)) continue;
|
|
43
|
+
out.push(p);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function coLocatedStylesheets(filePath) {
|
|
49
|
+
const dir = path.dirname(filePath);
|
|
50
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
51
|
+
const candidates = new Set([
|
|
52
|
+
path.join(dir, `${base}.css`),
|
|
53
|
+
path.join(dir, `${base}.module.css`),
|
|
54
|
+
path.join(dir, `${base}.scss`),
|
|
55
|
+
path.join(dir, `${base}.module.scss`),
|
|
56
|
+
path.join(dir, `${base}.sass`),
|
|
57
|
+
path.join(dir, `${base}.module.sass`),
|
|
58
|
+
path.join(dir, `${base}.less`),
|
|
59
|
+
path.join(dir, `${base}.module.less`),
|
|
60
|
+
]);
|
|
61
|
+
for (const name of CO_SCAN_STYLE_NAMES) {
|
|
62
|
+
candidates.add(path.join(dir, name));
|
|
63
|
+
}
|
|
64
|
+
return [...candidates].filter((p) => fs.existsSync(p));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function normalizeScanTargets(primaryTargets, projectCwd) {
|
|
68
|
+
if (!Array.isArray(primaryTargets) || primaryTargets.length === 0) return [];
|
|
69
|
+
const ordered = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const baseCwd = projectCwd || process.cwd();
|
|
72
|
+
const normalizeTarget = (p) => {
|
|
73
|
+
// Preserve literal `..` segments so downstream sensitive-path checks
|
|
74
|
+
// still fire. path.resolve would collapse `/foo/../etc/passwd`.
|
|
75
|
+
if (hasPathTraversal(p)) return p;
|
|
76
|
+
return path.isAbsolute(p) ? p : path.resolve(baseCwd, p);
|
|
77
|
+
};
|
|
78
|
+
const add = (p) => {
|
|
79
|
+
if (ordered.length >= MAX_SCAN_TARGETS) return;
|
|
80
|
+
const abs = normalizeTarget(p);
|
|
81
|
+
if (seen.has(abs)) return;
|
|
82
|
+
seen.add(abs);
|
|
83
|
+
ordered.push(abs);
|
|
84
|
+
return abs;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const p of primaryTargets) add(p);
|
|
88
|
+
return ordered;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function expandScanTargets(primaryTargets, projectCwd) {
|
|
92
|
+
const ordered = normalizeScanTargets(primaryTargets, projectCwd);
|
|
93
|
+
if (ordered.length === 0) return [];
|
|
94
|
+
const seen = new Set(ordered);
|
|
95
|
+
const baseCwd = projectCwd || process.cwd();
|
|
96
|
+
const add = (p) => {
|
|
97
|
+
if (ordered.length >= MAX_SCAN_TARGETS) return;
|
|
98
|
+
const abs = hasPathTraversal(p) ? p : (path.isAbsolute(p) ? p : path.resolve(baseCwd, p));
|
|
99
|
+
if (seen.has(abs)) return;
|
|
100
|
+
seen.add(abs);
|
|
101
|
+
ordered.push(abs);
|
|
102
|
+
return abs;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const normalizedPrimaries = [];
|
|
106
|
+
for (const p of ordered) normalizedPrimaries.push(p);
|
|
107
|
+
|
|
108
|
+
for (const p of normalizedPrimaries) {
|
|
109
|
+
if (ordered.length >= MAX_SCAN_TARGETS) break;
|
|
110
|
+
if (!isInsideProject(p, baseCwd)) continue;
|
|
111
|
+
const ext = path.extname(p).toLowerCase();
|
|
112
|
+
if (STYLE_EXTS.has(ext) || !UI_CODE_EXTS.has(ext)) continue;
|
|
113
|
+
|
|
114
|
+
let content = '';
|
|
115
|
+
try { content = fs.readFileSync(p, 'utf-8'); } catch { /* unreadable primary */ }
|
|
116
|
+
|
|
117
|
+
for (const imp of parseStaticStyleImports(content, p, projectCwd)) {
|
|
118
|
+
add(imp);
|
|
119
|
+
if (ordered.length >= MAX_SCAN_TARGETS) break;
|
|
120
|
+
}
|
|
121
|
+
for (const col of coLocatedStylesheets(p)) {
|
|
122
|
+
add(col);
|
|
123
|
+
if (ordered.length >= MAX_SCAN_TARGETS) break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return ordered;
|
|
128
|
+
}
|