svelte-ag 1.2.8 → 1.3.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/dist/api/form.svelte.d.ts +7 -10
- package/dist/api/form.svelte.d.ts.map +1 -1
- package/dist/api/form.svelte.js +6 -9
- package/dist/api/query/entrypoint.svelte.d.ts +14 -6
- package/dist/api/query/entrypoint.svelte.d.ts.map +1 -1
- package/dist/api/query/entrypoint.svelte.js +11 -5
- package/dist/api/query/entrypoint.unit.test.js +23 -25
- package/dist/api/query/query.svelte.d.ts +4 -5
- package/dist/api/query/query.svelte.d.ts.map +1 -1
- package/dist/api/query/query.svelte.js +8 -12
- package/dist/api/query/query.unit.test.js +21 -30
- package/dist/api/query/utils.svelte.d.ts +8 -2
- package/dist/api/query/utils.svelte.d.ts.map +1 -1
- package/dist/api/query/utils.svelte.js +5 -4
- package/dist/bin/build-tailwind-manifest/build-tailwind-manifest.unit.test.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/build-tailwind-manifest.unit.test.js +386 -0
- package/dist/bin/build-tailwind-manifest/cli.d.ts +2 -0
- package/dist/bin/build-tailwind-manifest/cli.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/cli.js +122 -0
- package/dist/bin/build-tailwind-manifest/graph.d.ts +9 -0
- package/dist/bin/build-tailwind-manifest/graph.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/graph.js +405 -0
- package/dist/bin/build-tailwind-manifest/index.d.ts +1 -0
- package/dist/bin/build-tailwind-manifest/index.js +6 -0
- package/dist/bin/build-tailwind-manifest/manifest-generator.d.ts +7 -0
- package/dist/bin/build-tailwind-manifest/manifest-generator.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/manifest-generator.js +85 -0
- package/dist/bin/build-tailwind-manifest/path-utils.d.ts +6 -0
- package/dist/bin/build-tailwind-manifest/path-utils.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/path-utils.js +41 -0
- package/dist/bin/build-tailwind-manifest/types.d.ts +15 -0
- package/dist/bin/build-tailwind-manifest/types.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/types.js +1 -0
- package/dist/index.d.ts +29 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/tailwind-sources.manifest.jsonc +193 -3
- package/dist/vite/vite-plugin-component-source-collector.d.ts.map +1 -1
- package/dist/vite/vite-plugin-component-source-collector.js +1 -6
- package/package.json +7 -5
- package/src/lib/api/form.svelte.ts +27 -34
- package/src/lib/api/query/entrypoint.svelte.ts +28 -11
- package/src/lib/api/query/entrypoint.unit.test.ts +25 -27
- package/src/lib/api/query/query.svelte.ts +18 -17
- package/src/lib/api/query/query.unit.test.ts +25 -35
- package/src/lib/api/query/utils.svelte.ts +5 -4
- package/src/lib/bin/build-tailwind-manifest/build-tailwind-manifest.unit.test.ts +591 -0
- package/src/lib/bin/build-tailwind-manifest/cli.ts +147 -0
- package/src/lib/bin/build-tailwind-manifest/graph.ts +521 -0
- package/src/lib/bin/build-tailwind-manifest/index.ts +8 -0
- package/src/lib/bin/build-tailwind-manifest/manifest-generator.ts +119 -0
- package/src/lib/bin/build-tailwind-manifest/path-utils.ts +53 -0
- package/src/lib/bin/build-tailwind-manifest/types.ts +18 -0
- package/src/lib/vite/vite-plugin-component-source-collector.ts +1 -6
- package/dist/bin/build-tailwind-manifest.d.ts +0 -15
- package/dist/bin/build-tailwind-manifest.d.ts.map +0 -1
- package/dist/bin/build-tailwind-manifest.js +0 -581
- package/dist/bin/build-tailwind-manifest.unit.test.d.ts.map +0 -1
- package/dist/bin/build-tailwind-manifest.unit.test.js +0 -208
- package/src/lib/bin/build-tailwind-manifest.ts +0 -757
- package/src/lib/bin/build-tailwind-manifest.unit.test.ts +0 -309
- /package/dist/bin/{build-tailwind-manifest.unit.test.d.ts → build-tailwind-manifest/build-tailwind-manifest.unit.test.d.ts} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { glob, readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { relative, join, resolve, basename, dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { normalizeManifestExportFilter } from '../../vite/tailwind-sources-manifest.js';
|
|
6
|
+
import { generateTailwindManifestForPackage } from './manifest-generator.js';
|
|
7
|
+
import { toPosixPath } from './path-utils.js';
|
|
8
|
+
import type { CliOptions } from './types.js';
|
|
9
|
+
|
|
10
|
+
const IGNORED_DIRECTORIES = new Set(['.git', '.svelte-kit', 'node_modules']);
|
|
11
|
+
const WATCH_POLL_INTERVAL_MS = 700;
|
|
12
|
+
|
|
13
|
+
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
14
|
+
const options = parseCliArgs(argv);
|
|
15
|
+
const rootDir = process.cwd();
|
|
16
|
+
|
|
17
|
+
if (options.watch) {
|
|
18
|
+
await runWatch(rootDir, options);
|
|
19
|
+
} else {
|
|
20
|
+
await runBuild(rootDir, options);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runBuild(rootDir: string, options: CliOptions): Promise<string[]> {
|
|
25
|
+
const packageDirs = await discoverPackageDirectories(rootDir, options.packagePatterns);
|
|
26
|
+
if (packageDirs.length === 0) {
|
|
27
|
+
throw new Error('[tailwind-manifest] No matching package.json files found.');
|
|
28
|
+
}
|
|
29
|
+
await Promise.all(packageDirs.map((packageDir) => buildPackage(rootDir, packageDir, options.exportFilters)));
|
|
30
|
+
return packageDirs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runWatch(rootDir: string, options: CliOptions): Promise<void> {
|
|
34
|
+
const packageDirs = await runBuild(rootDir, options);
|
|
35
|
+
const snapshots = new Map(
|
|
36
|
+
await Promise.all(
|
|
37
|
+
packageDirs.map(async (packageDir) => [packageDir, await createDirectorySnapshot(packageDir)] as const)
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
console.log(`[tailwind-manifest] watching ${packageDirs.length} package${packageDirs.length === 1 ? '' : 's'}`);
|
|
42
|
+
|
|
43
|
+
const interval = setInterval(async () => {
|
|
44
|
+
for (const packageDir of packageDirs) {
|
|
45
|
+
const nextSnapshot = await createDirectorySnapshot(packageDir);
|
|
46
|
+
if (snapshots.get(packageDir) === nextSnapshot) continue;
|
|
47
|
+
|
|
48
|
+
snapshots.set(packageDir, nextSnapshot);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await buildPackage(rootDir, packageDir, options.exportFilters);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}, WATCH_POLL_INTERVAL_MS);
|
|
57
|
+
|
|
58
|
+
process.on('SIGINT', () => {
|
|
59
|
+
clearInterval(interval);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await new Promise(() => {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseCliArgs(argv: string[]): CliOptions {
|
|
67
|
+
const packagePatterns: string[] = [];
|
|
68
|
+
const exportFilters: string[] = [];
|
|
69
|
+
let watch = false;
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
72
|
+
const arg = argv[i];
|
|
73
|
+
const value = argv[i + 1];
|
|
74
|
+
|
|
75
|
+
if (arg === '--watch') {
|
|
76
|
+
watch = true;
|
|
77
|
+
} else if (arg === '--packages') {
|
|
78
|
+
if (!value) throw new Error('--packages requires a value');
|
|
79
|
+
packagePatterns.push(
|
|
80
|
+
...value
|
|
81
|
+
.split(',')
|
|
82
|
+
.map((part) => part.trim())
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
);
|
|
85
|
+
i += 1;
|
|
86
|
+
} else if (arg === '--exports') {
|
|
87
|
+
if (!value) throw new Error('--exports requires a value');
|
|
88
|
+
exportFilters.push(...value.split(',').map(normalizeManifestExportFilter).filter(Boolean));
|
|
89
|
+
i += 1;
|
|
90
|
+
} else {
|
|
91
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { exportFilters, packagePatterns, watch };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function discoverPackageDirectories(rootDir: string, packagePatterns: string[]): Promise<string[]> {
|
|
99
|
+
if (packagePatterns.length === 0 && existsSync(join(rootDir, 'package.json'))) {
|
|
100
|
+
return [rootDir];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const packageJsonPaths = new Set<string>();
|
|
104
|
+
for (const pattern of packagePatterns) {
|
|
105
|
+
for await (const matchedPath of glob(pattern, { cwd: rootDir })) {
|
|
106
|
+
const absolutePath = resolve(rootDir, matchedPath);
|
|
107
|
+
if (basename(absolutePath) === 'package.json') {
|
|
108
|
+
packageJsonPaths.add(absolutePath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return [...packageJsonPaths].map((packageJsonPath) => dirname(packageJsonPath)).sort();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function buildPackage(rootDir: string, packageDir: string, exportFilters: string[]): Promise<void> {
|
|
117
|
+
const result = await generateTailwindManifestForPackage(packageDir, { exportFilters });
|
|
118
|
+
const relativeOutputPath = relative(rootDir, result.outputFile) || result.outputFile;
|
|
119
|
+
console.log(
|
|
120
|
+
`[tailwind-manifest] ${result.didWrite ? 'wrote' : 'unchanged'} ${relativeOutputPath} (${result.exportCount} exports)`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function createDirectorySnapshot(packageDir: string): Promise<string> {
|
|
125
|
+
const files: string[] = [];
|
|
126
|
+
const queue = [packageDir];
|
|
127
|
+
|
|
128
|
+
while (queue.length > 0) {
|
|
129
|
+
const currentDir = queue.shift();
|
|
130
|
+
if (!currentDir) break;
|
|
131
|
+
|
|
132
|
+
for (const entry of await readdir(currentDir, { withFileTypes: true })) {
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
135
|
+
queue.push(join(currentDir, entry.name));
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const absolutePath = join(currentDir, entry.name);
|
|
141
|
+
const fileStat = await stat(absolutePath);
|
|
142
|
+
files.push(`${toPosixPath(relative(packageDir, absolutePath))}:${fileStat.size}:${fileStat.mtimeMs}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return files.sort().join('|');
|
|
147
|
+
}
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Visitor,
|
|
6
|
+
parseSync,
|
|
7
|
+
type CallExpression,
|
|
8
|
+
type Expression,
|
|
9
|
+
type ExpressionStatement,
|
|
10
|
+
type Node,
|
|
11
|
+
type ObjectProperty,
|
|
12
|
+
type ParserOptions,
|
|
13
|
+
type Program,
|
|
14
|
+
type StringLiteral,
|
|
15
|
+
type TemplateLiteral
|
|
16
|
+
} from 'oxc-parser';
|
|
17
|
+
import { parse as parseSvelte } from 'svelte/compiler';
|
|
18
|
+
|
|
19
|
+
import { ensureRelativeManifestSourcePath } from '../../vite/tailwind-sources-manifest.js';
|
|
20
|
+
import { isPathInside, isRelativeSpecifier, resolveLocalImportPath, toPosixPath } from './path-utils.js';
|
|
21
|
+
import type { GraphScan } from './types.js';
|
|
22
|
+
|
|
23
|
+
const CLASS_COLLECTOR_CALLS = new Set(['cn', 'clsx', 'cva', 'tv']);
|
|
24
|
+
|
|
25
|
+
type ModuleStatement = Extract<
|
|
26
|
+
Program['body'][number],
|
|
27
|
+
{ type: 'ImportDeclaration' | 'ExportNamedDeclaration' | 'ExportAllDeclaration' }
|
|
28
|
+
>;
|
|
29
|
+
type ImportDeclarationNode = Extract<ModuleStatement, { type: 'ImportDeclaration' }>;
|
|
30
|
+
type ExportNamedDeclarationNode = Extract<ModuleStatement, { type: 'ExportNamedDeclaration' }>;
|
|
31
|
+
type ExportAllDeclarationNode = Extract<ModuleStatement, { type: 'ExportAllDeclaration' }>;
|
|
32
|
+
|
|
33
|
+
type IndirectExport =
|
|
34
|
+
| { kind: 'all'; specifier: string }
|
|
35
|
+
| { kind: 'namespace'; exportName: string; specifier: string }
|
|
36
|
+
| { kind: 'named'; exportName: string; specifier: string };
|
|
37
|
+
|
|
38
|
+
type FileAnalysis = {
|
|
39
|
+
classes: Set<string>;
|
|
40
|
+
manifestSourcePath: string | null;
|
|
41
|
+
localSpecifiers: string[];
|
|
42
|
+
directLocalExports: Set<string>;
|
|
43
|
+
importBindings: Map<string, string>;
|
|
44
|
+
indirectExports: IndirectExport[];
|
|
45
|
+
importedLocalExports: Map<string, string>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type GraphScanner = {
|
|
49
|
+
scanFileGraph(entryFiles: string[]): Promise<GraphScan>;
|
|
50
|
+
readEntrySymbolTargets(entryFile: string): Promise<Map<string, string>>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function createGraphScanner(packageDir: string): GraphScanner {
|
|
54
|
+
const analysisCache = new Map<string, Promise<FileAnalysis>>();
|
|
55
|
+
const graphScanCache = new Map<string, GraphScan>();
|
|
56
|
+
const symbolTargetCache = new Map<string, Map<string, string>>();
|
|
57
|
+
const resolvedImportCache = new Map<string, string | null>();
|
|
58
|
+
|
|
59
|
+
const resolveCachedImportPath = (specifier: string, importerPath: string): string | null => {
|
|
60
|
+
const cacheKey = `${importerPath}\0${specifier}`;
|
|
61
|
+
const cached = resolvedImportCache.get(cacheKey);
|
|
62
|
+
if (cached !== undefined) return cached;
|
|
63
|
+
|
|
64
|
+
const resolved = resolveLocalImportPath(specifier, importerPath);
|
|
65
|
+
resolvedImportCache.set(cacheKey, resolved);
|
|
66
|
+
return resolved;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const analyzeFile = (filePath: string): Promise<FileAnalysis> => {
|
|
70
|
+
let cached = analysisCache.get(filePath);
|
|
71
|
+
if (!cached) {
|
|
72
|
+
cached = (async (): Promise<FileAnalysis> => {
|
|
73
|
+
const source = await readFile(filePath, 'utf8');
|
|
74
|
+
const classes = new Set<string>();
|
|
75
|
+
const localSpecifiers = new Set<string>();
|
|
76
|
+
const directLocalExports = new Set<string>();
|
|
77
|
+
const importBindings = new Map<string, string>();
|
|
78
|
+
const indirectExports: IndirectExport[] = [];
|
|
79
|
+
const importedLocalExports = new Map<string, string>();
|
|
80
|
+
|
|
81
|
+
collectClassNamesFromSource(filePath, source, classes);
|
|
82
|
+
|
|
83
|
+
for (const statement of getModuleStatements(filePath, source)) {
|
|
84
|
+
const specifier = getModuleRequest(statement);
|
|
85
|
+
|
|
86
|
+
if (statement.type === 'ImportDeclaration') {
|
|
87
|
+
if (specifier && isRelativeSpecifier(specifier) && statement.importKind !== 'type') {
|
|
88
|
+
localSpecifiers.add(specifier);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
collectImportBindings(statement, importBindings);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (specifier && isRelativeSpecifier(specifier) && statement.exportKind !== 'type') {
|
|
96
|
+
localSpecifiers.add(specifier);
|
|
97
|
+
|
|
98
|
+
if (statement.type === 'ExportAllDeclaration' && statement.exported === null) {
|
|
99
|
+
indirectExports.push({ kind: 'all', specifier });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (statement.type === 'ExportAllDeclaration') {
|
|
104
|
+
const exportName = getNodeName(statement.exported);
|
|
105
|
+
if (exportName) {
|
|
106
|
+
indirectExports.push({ kind: 'namespace', exportName, specifier });
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const specifierNode of statement.specifiers) {
|
|
112
|
+
if ('exportKind' in specifierNode && specifierNode.exportKind === 'type') continue;
|
|
113
|
+
|
|
114
|
+
const exportName = getNodeName(specifierNode.exported) ?? getNodeName(specifierNode.local);
|
|
115
|
+
if (exportName) {
|
|
116
|
+
indirectExports.push({ kind: 'named', exportName, specifier });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (statement.type !== 'ExportNamedDeclaration' || statement.exportKind === 'type') {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
collectExportedDeclarationNames(statement.declaration, directLocalExports);
|
|
127
|
+
|
|
128
|
+
for (const specifierNode of statement.specifiers) {
|
|
129
|
+
if ('exportKind' in specifierNode && specifierNode.exportKind === 'type') continue;
|
|
130
|
+
|
|
131
|
+
const exportName = getNodeName(specifierNode.exported) ?? getNodeName(specifierNode.local);
|
|
132
|
+
const localName = getNodeName(specifierNode.local);
|
|
133
|
+
if (exportName && localName) {
|
|
134
|
+
importedLocalExports.set(exportName, localName);
|
|
135
|
+
directLocalExports.add(exportName);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const match of source.matchAll(/@import\s+['"]([^'"]+)['"]/g)) {
|
|
141
|
+
if (match[1] && isRelativeSpecifier(match[1])) {
|
|
142
|
+
localSpecifiers.add(match[1]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
classes,
|
|
148
|
+
manifestSourcePath: filePath.endsWith('.css')
|
|
149
|
+
? ensureRelativeManifestSourcePath(toPosixPath(path.relative(packageDir, filePath)))
|
|
150
|
+
: null,
|
|
151
|
+
localSpecifiers: [...localSpecifiers],
|
|
152
|
+
directLocalExports,
|
|
153
|
+
importBindings,
|
|
154
|
+
indirectExports,
|
|
155
|
+
importedLocalExports
|
|
156
|
+
};
|
|
157
|
+
})();
|
|
158
|
+
|
|
159
|
+
analysisCache.set(filePath, cached);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return cached;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const scanFromRoot = async (entryFile: string, visiting: Set<string>): Promise<GraphScan> => {
|
|
166
|
+
if (visiting.has(entryFile) || !isPathInside(packageDir, entryFile)) {
|
|
167
|
+
return createEmptyScan();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cached = graphScanCache.get(entryFile);
|
|
171
|
+
if (cached) {
|
|
172
|
+
return cloneGraphScan(cached);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const nextVisiting = new Set(visiting);
|
|
176
|
+
nextVisiting.add(entryFile);
|
|
177
|
+
|
|
178
|
+
const analysis = await analyzeFile(entryFile);
|
|
179
|
+
const scan: GraphScan = {
|
|
180
|
+
classes: new Set(analysis.classes),
|
|
181
|
+
sources: analysis.manifestSourcePath ? new Set([analysis.manifestSourcePath]) : new Set<string>()
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
for (const specifier of analysis.localSpecifiers) {
|
|
185
|
+
const resolvedPath = resolveCachedImportPath(specifier, entryFile);
|
|
186
|
+
if (!resolvedPath) continue;
|
|
187
|
+
|
|
188
|
+
mergeGraphScan(scan, await scanFromRoot(resolvedPath, nextVisiting));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
graphScanCache.set(entryFile, cloneGraphScan(scan));
|
|
192
|
+
return scan;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const readTargets = async (entryFile: string, visiting: Set<string>): Promise<Map<string, string>> => {
|
|
196
|
+
if (visiting.has(entryFile)) return new Map();
|
|
197
|
+
|
|
198
|
+
const cached = symbolTargetCache.get(entryFile);
|
|
199
|
+
if (cached) {
|
|
200
|
+
return new Map(cached);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const nextVisiting = new Set(visiting);
|
|
204
|
+
nextVisiting.add(entryFile);
|
|
205
|
+
|
|
206
|
+
const analysis = await analyzeFile(entryFile);
|
|
207
|
+
const symbolTargets = new Map<string, string>();
|
|
208
|
+
|
|
209
|
+
for (const exportNode of analysis.indirectExports) {
|
|
210
|
+
const targetFile = resolveCachedImportPath(exportNode.specifier, entryFile);
|
|
211
|
+
if (!targetFile) continue;
|
|
212
|
+
|
|
213
|
+
if (exportNode.kind === 'all') {
|
|
214
|
+
for (const [symbolName, nestedTargetFile] of await readTargets(targetFile, nextVisiting)) {
|
|
215
|
+
if (!symbolTargets.has(symbolName)) {
|
|
216
|
+
symbolTargets.set(symbolName, nestedTargetFile);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!symbolTargets.has(exportNode.exportName)) {
|
|
223
|
+
symbolTargets.set(exportNode.exportName, targetFile);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const [exportName, localName] of analysis.importedLocalExports) {
|
|
228
|
+
const specifier = analysis.importBindings.get(localName);
|
|
229
|
+
if (!specifier || symbolTargets.has(exportName)) continue;
|
|
230
|
+
|
|
231
|
+
const targetFile = resolveCachedImportPath(specifier, entryFile);
|
|
232
|
+
if (targetFile) {
|
|
233
|
+
symbolTargets.set(exportName, targetFile);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const exportName of analysis.directLocalExports) {
|
|
238
|
+
if (!symbolTargets.has(exportName)) {
|
|
239
|
+
symbolTargets.set(exportName, entryFile);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
symbolTargetCache.set(entryFile, new Map(symbolTargets));
|
|
244
|
+
return symbolTargets;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
async scanFileGraph(entryFiles: string[]): Promise<GraphScan> {
|
|
249
|
+
const scan = createEmptyScan();
|
|
250
|
+
|
|
251
|
+
for (const entryFile of entryFiles) {
|
|
252
|
+
mergeGraphScan(scan, await scanFromRoot(entryFile, new Set()));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return scan;
|
|
256
|
+
},
|
|
257
|
+
async readEntrySymbolTargets(entryFile: string): Promise<Map<string, string>> {
|
|
258
|
+
return readTargets(entryFile, new Set());
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function readEntrySymbolTargets(
|
|
264
|
+
entryFile: string,
|
|
265
|
+
visited = new Set<string>()
|
|
266
|
+
): Promise<Map<string, string>> {
|
|
267
|
+
if (visited.has(entryFile)) return new Map();
|
|
268
|
+
void visited;
|
|
269
|
+
|
|
270
|
+
const scanner = createGraphScanner(path.dirname(entryFile));
|
|
271
|
+
return scanner.readEntrySymbolTargets(entryFile);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function scanFileGraph(entryFiles: string[], packageDir: string): Promise<GraphScan> {
|
|
275
|
+
return createGraphScanner(packageDir).scanFileGraph(entryFiles);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function createEmptyScan(): GraphScan {
|
|
279
|
+
return { classes: new Set<string>(), sources: new Set<string>() };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function cloneGraphScan(scan: GraphScan): GraphScan {
|
|
283
|
+
return { classes: new Set(scan.classes), sources: new Set(scan.sources) };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function mergeGraphScan(target: GraphScan, source: GraphScan): void {
|
|
287
|
+
for (const className of source.classes) target.classes.add(className);
|
|
288
|
+
for (const sourcePath of source.sources) target.sources.add(sourcePath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function collectClassNamesFromSource(filePath: string, source: string, out: Set<string>): void {
|
|
292
|
+
const collectTailwindStrings = (snippet: string) => {
|
|
293
|
+
const program = parseSync(filePath, snippet, getOxcParserOptions(filePath)).program;
|
|
294
|
+
new Visitor({
|
|
295
|
+
CallExpression(node) {
|
|
296
|
+
const calleeName = getCallExpressionName(node);
|
|
297
|
+
if (calleeName !== '' && CLASS_COLLECTOR_CALLS.has(calleeName)) {
|
|
298
|
+
collectStringLiteralsFromExpression(node, out);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
Property(node) {
|
|
302
|
+
if (!isObjectPropertyNode(node)) return;
|
|
303
|
+
|
|
304
|
+
const keyName = getObjectPropertyKeyName(node);
|
|
305
|
+
if (keyName && /class/i.test(keyName)) {
|
|
306
|
+
collectStringLiteralsFromExpression(node.value, out);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}).visit(program);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const markupSource = filePath.endsWith('.svelte') ? source.replace(/<!--[\s\S]*?-->/g, '') : source;
|
|
313
|
+
|
|
314
|
+
for (const match of markupSource.matchAll(/(?:class|className)\s*=\s*(['"`])([\s\S]*?)\1/g)) {
|
|
315
|
+
for (const token of (match[2] ?? '').split(/\s+/)) {
|
|
316
|
+
if (token !== '' && !token.includes('${')) out.add(token);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const match of markupSource.matchAll(/(?:class|className)\s*=\s*\{([\s\S]*?)\}/g)) {
|
|
321
|
+
const expression = match[1]?.trim();
|
|
322
|
+
if (expression) {
|
|
323
|
+
collectTailwindStrings(`(${expression})`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const scriptBlock of getModuleSnippets(filePath, source)) {
|
|
328
|
+
collectTailwindStrings(scriptBlock);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getModuleSnippets(filePath: string, source: string): string[] {
|
|
333
|
+
if (!filePath.endsWith('.svelte')) {
|
|
334
|
+
return [source];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const ast = parseSvelte(source, { filename: filePath, modern: true });
|
|
339
|
+
return [ast.module, ast.instance].filter(Boolean).map((script) => {
|
|
340
|
+
const content = script!.content as unknown as { start: number; end: number };
|
|
341
|
+
return source.slice(content.start, content.end);
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
return [source];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getModuleStatements(filePath: string, source: string): ModuleStatement[] {
|
|
349
|
+
return getModuleSnippets(filePath, source).flatMap((snippet) =>
|
|
350
|
+
parseSync(filePath, snippet, getOxcParserOptions(filePath)).program.body.filter(isModuleStatement)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getOxcParserOptions(filePath: string): ParserOptions {
|
|
355
|
+
return {
|
|
356
|
+
lang:
|
|
357
|
+
filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')
|
|
358
|
+
? 'js'
|
|
359
|
+
: filePath.endsWith('.jsx')
|
|
360
|
+
? 'jsx'
|
|
361
|
+
: filePath.endsWith('.tsx')
|
|
362
|
+
? 'tsx'
|
|
363
|
+
: filePath.endsWith('.d.ts')
|
|
364
|
+
? 'dts'
|
|
365
|
+
: 'ts',
|
|
366
|
+
sourceType: 'module'
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function isModuleStatement(statement: Program['body'][number]): statement is ModuleStatement {
|
|
371
|
+
return (
|
|
372
|
+
statement.type === 'ImportDeclaration' ||
|
|
373
|
+
statement.type === 'ExportNamedDeclaration' ||
|
|
374
|
+
statement.type === 'ExportAllDeclaration'
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function collectExportedDeclarationNames(
|
|
379
|
+
declaration: ExportNamedDeclarationNode['declaration'],
|
|
380
|
+
exportNames: Set<string>
|
|
381
|
+
): void {
|
|
382
|
+
if (!declaration) return;
|
|
383
|
+
|
|
384
|
+
if ('id' in declaration && declaration.id) {
|
|
385
|
+
const declarationName = getNodeName(declaration.id);
|
|
386
|
+
if (declarationName) exportNames.add(declarationName);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (declaration.type !== 'VariableDeclaration') return;
|
|
391
|
+
|
|
392
|
+
for (const declarator of declaration.declarations) {
|
|
393
|
+
collectBindingNames(declarator.id, exportNames);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function collectBindingNames(node: Node, names: Set<string>): void {
|
|
398
|
+
const nodeName = getNodeName(node);
|
|
399
|
+
if (nodeName) {
|
|
400
|
+
names.add(nodeName);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if ('properties' in node && Array.isArray(node.properties)) {
|
|
405
|
+
for (const property of node.properties) {
|
|
406
|
+
if (!property || typeof property !== 'object') continue;
|
|
407
|
+
if ('value' in property && property.value && typeof property.value === 'object') {
|
|
408
|
+
collectBindingNames(property.value as Node, names);
|
|
409
|
+
} else if ('key' in property && property.key && typeof property.key === 'object') {
|
|
410
|
+
collectBindingNames(property.key as Node, names);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if ('elements' in node && Array.isArray(node.elements)) {
|
|
416
|
+
for (const element of node.elements) {
|
|
417
|
+
if (element && typeof element === 'object') {
|
|
418
|
+
collectBindingNames(element as Node, names);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function collectImportBindings(statement: ImportDeclarationNode, importBindings: Map<string, string>): void {
|
|
425
|
+
const specifier = getModuleRequest(statement);
|
|
426
|
+
if (!specifier || !isRelativeSpecifier(specifier) || statement.importKind === 'type') return;
|
|
427
|
+
|
|
428
|
+
for (const specifierNode of statement.specifiers ?? []) {
|
|
429
|
+
if (specifierNode.type === 'ImportSpecifier' && specifierNode.importKind === 'type') {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
importBindings.set(specifierNode.local.name, specifier);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getModuleRequest(
|
|
438
|
+
statement: ImportDeclarationNode | ExportNamedDeclarationNode | ExportAllDeclarationNode
|
|
439
|
+
): string | null {
|
|
440
|
+
if (statement.source === null) return null;
|
|
441
|
+
return typeof statement.source.value === 'string' ? statement.source.value : null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function getNodeName(node: { type: string } | null): string | null {
|
|
445
|
+
if (node === null) return null;
|
|
446
|
+
if ('name' in node && typeof node.name === 'string') return node.name;
|
|
447
|
+
if ('value' in node && typeof node.value === 'string') return node.value;
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getCallExpressionName(node: CallExpression): string {
|
|
452
|
+
if (node.callee.type === 'Identifier') {
|
|
453
|
+
return node.callee.name;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.property.type === 'Identifier') {
|
|
457
|
+
return node.callee.property.name;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return '';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getObjectPropertyKeyName(node: ObjectProperty): string | null {
|
|
464
|
+
if (node.key.type === 'Identifier') {
|
|
465
|
+
return node.key.name;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
|
|
469
|
+
return node.key.value;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isObjectPropertyNode(node: Node): node is ObjectProperty {
|
|
476
|
+
return node.type === 'Property' && 'value' in node && 'method' in node;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function collectStringLiteralsFromExpression(expression: Expression, out: Set<string>): void {
|
|
480
|
+
new Visitor({
|
|
481
|
+
Literal(literal) {
|
|
482
|
+
if (literal.type === 'Literal' && typeof literal.value === 'string') {
|
|
483
|
+
addClassTokens(literal, out);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
TemplateLiteral(template) {
|
|
487
|
+
if (template.expressions.length === 0) {
|
|
488
|
+
addClassTokens(template, out);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}).visit(createProgramFromExpression(expression));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function addClassTokens(node: StringLiteral | TemplateLiteral, out: Set<string>): void {
|
|
495
|
+
const rawValue =
|
|
496
|
+
node.type === 'Literal' ? node.value : node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join('');
|
|
497
|
+
if (typeof rawValue !== 'string') return;
|
|
498
|
+
|
|
499
|
+
for (const token of rawValue.split(/\s+/)) {
|
|
500
|
+
if (token !== '' && !token.includes('${')) out.add(token);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function createProgramFromExpression(expression: Expression): Program {
|
|
505
|
+
const statement: ExpressionStatement = {
|
|
506
|
+
type: 'ExpressionStatement',
|
|
507
|
+
expression,
|
|
508
|
+
directive: null,
|
|
509
|
+
start: expression.start,
|
|
510
|
+
end: expression.end
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
type: 'Program',
|
|
515
|
+
body: [statement],
|
|
516
|
+
sourceType: 'module',
|
|
517
|
+
hashbang: null,
|
|
518
|
+
start: expression.start,
|
|
519
|
+
end: expression.end
|
|
520
|
+
};
|
|
521
|
+
}
|