Skip to content

Commit 9a7d0e8

Browse files
merge: bring main into wt/error-iframe
Signed-off-by: Sun-sunshine06 <Sun-sunshine06@users.noreply.github.com>
2 parents 5025f01 + 9120dec commit 9a7d0e8

31 files changed

Lines changed: 2038 additions & 154 deletions
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import type { Dirent } from 'node:fs';
2+
import { readFile, readdir } from 'node:fs/promises';
3+
import { basename, extname, join, relative } from 'node:path';
4+
import {
5+
STORED_DESIGN_SYSTEM_SCHEMA_VERSION,
6+
type StoredDesignSystem,
7+
} from '@open-codesign/shared';
8+
9+
const IGNORED_DIRS = new Set([
10+
'.git',
11+
'.idea',
12+
'.next',
13+
'.turbo',
14+
'.vscode',
15+
'build',
16+
'coverage',
17+
'dist',
18+
'node_modules',
19+
'out',
20+
]);
21+
22+
const CANDIDATE_EXTS = new Set([
23+
'.css',
24+
'.scss',
25+
'.sass',
26+
'.less',
27+
'.js',
28+
'.jsx',
29+
'.ts',
30+
'.tsx',
31+
'.json',
32+
'.md',
33+
]);
34+
35+
const PRIORITY_PATTERNS = [
36+
/tailwind\.config/i,
37+
/tokens?/i,
38+
/theme/i,
39+
/brand/i,
40+
/palette/i,
41+
/typography/i,
42+
/global\.(css|scss|sass|less)$/i,
43+
/variables?\.(css|scss|sass|less)$/i,
44+
];
45+
46+
const MAX_FILES = 160;
47+
const MAX_SELECTED_FILES = 12;
48+
const MAX_FILE_CHARS = 32_000;
49+
50+
interface CandidateFile {
51+
fullPath: string;
52+
relativePath: string;
53+
score: number;
54+
}
55+
56+
function pushUnique(target: string[], value: string, max: number): void {
57+
if (!value || target.includes(value) || target.length >= max) return;
58+
target.push(value);
59+
}
60+
61+
function cleanValue(value: string): string {
62+
return value
63+
.replace(/\s+/g, ' ')
64+
.replace(/["'`,]/g, '')
65+
.trim();
66+
}
67+
68+
function scoreCandidate(relativePath: string): number {
69+
const fileName = basename(relativePath);
70+
let score = 1;
71+
for (const pattern of PRIORITY_PATTERNS) {
72+
if (pattern.test(relativePath) || pattern.test(fileName)) score += 20;
73+
}
74+
if (/\.(css|scss|sass|less)$/i.test(fileName)) score += 8;
75+
if (/tailwind/i.test(relativePath)) score += 8;
76+
if (/src\//i.test(relativePath)) score += 4;
77+
return score;
78+
}
79+
80+
async function collectCandidateFiles(
81+
rootPath: string,
82+
dirPath: string,
83+
files: CandidateFile[],
84+
): Promise<void> {
85+
if (files.length >= MAX_FILES) return;
86+
87+
let entries: Dirent<string>[];
88+
try {
89+
entries = await readdir(dirPath, { withFileTypes: true, encoding: 'utf8' });
90+
} catch {
91+
return;
92+
}
93+
94+
for (const entry of entries) {
95+
if (files.length >= MAX_FILES) return;
96+
const fullPath = join(dirPath, entry.name);
97+
if (entry.isDirectory()) {
98+
if (!IGNORED_DIRS.has(entry.name)) {
99+
await collectCandidateFiles(rootPath, fullPath, files);
100+
}
101+
continue;
102+
}
103+
if (!entry.isFile()) continue;
104+
const extension = extname(entry.name).toLowerCase();
105+
if (!CANDIDATE_EXTS.has(extension) && !/tailwind\.config/i.test(entry.name)) continue;
106+
const relativePath = relative(rootPath, fullPath).replace(/\\/g, '/');
107+
files.push({ fullPath, relativePath, score: scoreCandidate(relativePath) });
108+
}
109+
}
110+
111+
function collectCssVarValues(
112+
raw: string,
113+
colors: string[],
114+
spacing: string[],
115+
radius: string[],
116+
shadows: string[],
117+
): void {
118+
for (const match of raw.matchAll(/--([a-z0-9-]+)\s*:\s*([^;}{\n]+)/gi)) {
119+
const name = match[1]?.toLowerCase() ?? '';
120+
const value = cleanValue(match[2] ?? '');
121+
if (!value) continue;
122+
if (/color|accent|surface|text|brand|primary|secondary/.test(name))
123+
pushUnique(colors, value, 24);
124+
if (/space|spacing|gap|padding|margin/.test(name)) pushUnique(spacing, value, 16);
125+
if (/radius|rounded/.test(name)) pushUnique(radius, value, 16);
126+
if (/shadow/.test(name)) pushUnique(shadows, value, 16);
127+
}
128+
}
129+
130+
function collectLooseValues(
131+
raw: string,
132+
colors: string[],
133+
fonts: string[],
134+
spacing: string[],
135+
radius: string[],
136+
shadows: string[],
137+
): void {
138+
for (const match of raw.matchAll(/#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi)) {
139+
pushUnique(colors, cleanValue(match[0] ?? ''), 24);
140+
}
141+
142+
for (const match of raw.matchAll(/(?:font-family|fontFamily)[^:=]*[:=]\s*([^\n;]+)/gi)) {
143+
const value = cleanValue(match[1] ?? '');
144+
for (const part of value.split(',')) pushUnique(fonts, cleanValue(part), 16);
145+
}
146+
147+
for (const match of raw.matchAll(
148+
/(?:spacing|space|gap|padding|margin)[^:=\n]*[:=]\s*([^\n,;]+)/gi,
149+
)) {
150+
pushUnique(spacing, cleanValue(match[1] ?? ''), 16);
151+
}
152+
153+
for (const match of raw.matchAll(/(?:radius|rounded|borderRadius)[^:=\n]*[:=]\s*([^\n,;]+)/gi)) {
154+
pushUnique(radius, cleanValue(match[1] ?? ''), 16);
155+
}
156+
157+
for (const match of raw.matchAll(/(?:shadow|boxShadow)[^:=\n]*[:=]\s*([^\n,;]+)/gi)) {
158+
pushUnique(shadows, cleanValue(match[1] ?? ''), 16);
159+
}
160+
}
161+
162+
function buildSummary(
163+
snapshot: Omit<StoredDesignSystem, 'schemaVersion' | 'summary' | 'extractedAt'>,
164+
): string {
165+
const repoLabel = basename(snapshot.rootPath);
166+
const parts = [
167+
`Scanned ${snapshot.sourceFiles.length} likely design-system files under ${repoLabel}.`,
168+
];
169+
if (snapshot.colors.length > 0)
170+
parts.push(`Color language: ${snapshot.colors.slice(0, 5).join(', ')}.`);
171+
if (snapshot.fonts.length > 0)
172+
parts.push(`Typography: ${snapshot.fonts.slice(0, 4).join(', ')}.`);
173+
if (snapshot.spacing.length > 0)
174+
parts.push(`Spacing cues: ${snapshot.spacing.slice(0, 4).join(', ')}.`);
175+
if (snapshot.radius.length > 0)
176+
parts.push(`Corner radius cues: ${snapshot.radius.slice(0, 4).join(', ')}.`);
177+
if (snapshot.shadows.length > 0)
178+
parts.push(`Shadow cues: ${snapshot.shadows.slice(0, 4).join(', ')}.`);
179+
if (parts.length === 1) {
180+
parts.push(
181+
'No strong structured tokens were extracted, so lean on the referenced styling files and keep the output conservative and cohesive.',
182+
);
183+
}
184+
return parts.join(' ');
185+
}
186+
187+
export async function scanDesignSystem(rootPath: string): Promise<StoredDesignSystem> {
188+
const candidates: CandidateFile[] = [];
189+
await collectCandidateFiles(rootPath, rootPath, candidates);
190+
191+
const selected = candidates
192+
.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath))
193+
.slice(0, MAX_SELECTED_FILES);
194+
195+
const colors: string[] = [];
196+
const fonts: string[] = [];
197+
const spacing: string[] = [];
198+
const radius: string[] = [];
199+
const shadows: string[] = [];
200+
201+
for (const file of selected) {
202+
let raw = '';
203+
try {
204+
raw = await readFile(file.fullPath, 'utf8');
205+
} catch {
206+
continue;
207+
}
208+
const snippet = raw.slice(0, MAX_FILE_CHARS);
209+
collectCssVarValues(snippet, colors, spacing, radius, shadows);
210+
collectLooseValues(snippet, colors, fonts, spacing, radius, shadows);
211+
}
212+
213+
const baseSnapshot = {
214+
rootPath,
215+
sourceFiles: selected.map((file) => file.relativePath),
216+
colors,
217+
fonts,
218+
spacing,
219+
radius,
220+
shadows,
221+
};
222+
223+
return {
224+
schemaVersion: STORED_DESIGN_SYSTEM_SCHEMA_VERSION,
225+
...baseSnapshot,
226+
summary: buildSummary(baseSnapshot),
227+
extractedAt: new Date().toISOString(),
228+
};
229+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createRequire } from 'node:module';
2+
3+
const require = createRequire(import.meta.url);
4+
5+
export const electron = require('electron') as typeof import('electron');
6+
7+
export const { app, BrowserWindow, dialog, ipcMain, safeStorage, shell } = electron;

‎apps/desktop/src/main/exporter-ipc.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type ExporterFormat, exportArtifact } from '@open-codesign/exporters';
22
import { CodesignError } from '@open-codesign/shared';
3-
import { type BrowserWindow, dialog, ipcMain } from 'electron';
3+
import type { BrowserWindow } from 'electron';
4+
import { dialog, ipcMain } from './electron-runtime';
45

56
const FORMAT_FILTERS: Record<ExporterFormat, Electron.FileFilter[]> = {
67
html: [{ name: 'HTML', extensions: ['html'] }],

0 commit comments

Comments
 (0)