svelte-ag 1.0.64 → 1.0.66
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/vite/vite-plugin-component-source-collector.d.ts +0 -6
- package/dist/vite/vite-plugin-component-source-collector.d.ts.map +1 -1
- package/dist/vite/vite-plugin-component-source-collector.js +83 -64
- package/dist/vite/vite-plugin-component-source-collector.unit.test.js +100 -56
- package/package.json +1 -1
- package/src/lib/vite/vite-plugin-component-source-collector.ts +89 -86
- package/src/lib/vite/vite-plugin-component-source-collector.unit.test.ts +117 -66
|
@@ -15,12 +15,6 @@ interface Options {
|
|
|
15
15
|
*/
|
|
16
16
|
safePackages: string[];
|
|
17
17
|
}
|
|
18
|
-
type NormalizeCollectedSourceFilePathOptions = {
|
|
19
|
-
outputFilePath: string;
|
|
20
|
-
root: string;
|
|
21
|
-
safePackages: string[];
|
|
22
|
-
};
|
|
23
|
-
export declare function normalizeCollectedSourceFilePath(file: string, opts: NormalizeCollectedSourceFilePathOptions): Promise<string>;
|
|
24
18
|
export default function componentSourceCollector(opts?: Options): Promise<Plugin>;
|
|
25
19
|
export {};
|
|
26
20
|
//# sourceMappingURL=vite-plugin-component-source-collector.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin-component-source-collector.d.ts","sourceRoot":"","sources":["../../src/lib/vite/vite-plugin-component-source-collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,MAAM,CAAC;AAMnD,UAAU,OAAO;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B;;OAEG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"vite-plugin-component-source-collector.d.ts","sourceRoot":"","sources":["../../src/lib/vite/vite-plugin-component-source-collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,MAAM,CAAC;AAMnD,UAAU,OAAO;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B;;OAEG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AA2CD,wBAA8B,wBAAwB,CAAC,IAAI,GAAE,OAA8B,GAAG,OAAO,CAAC,MAAM,CAAC,CAuN5G"}
|
|
@@ -7,10 +7,14 @@ const componentFiles = new Set();
|
|
|
7
7
|
let firstRound = true;
|
|
8
8
|
const packageJsonCache = new Map();
|
|
9
9
|
function ensureDotRelative(filePath) {
|
|
10
|
-
if (filePath.startsWith('
|
|
10
|
+
if (filePath.startsWith('./'))
|
|
11
11
|
return filePath;
|
|
12
12
|
return `./${filePath}`;
|
|
13
13
|
}
|
|
14
|
+
async function touch(path) {
|
|
15
|
+
const handle = await open(path, 'a');
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
14
18
|
function isPathInside(parentPath, childPath) {
|
|
15
19
|
const relativePath = relative(parentPath, childPath);
|
|
16
20
|
return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
|
|
@@ -35,65 +39,63 @@ async function readPackageNameAt(directory) {
|
|
|
35
39
|
packageJsonCache.set(directory, packageNamePromise);
|
|
36
40
|
return packageNamePromise;
|
|
37
41
|
}
|
|
38
|
-
export async function normalizeCollectedSourceFilePath(file, opts) {
|
|
39
|
-
const cleanedFileName = file.replace(/[?#].*$/, '');
|
|
40
|
-
const resolvedFilePath = isAbsolute(cleanedFileName)
|
|
41
|
-
? resolve(cleanedFileName)
|
|
42
|
-
: resolve(dirname(opts.outputFilePath), cleanedFileName);
|
|
43
|
-
let currentDirectory = dirname(resolvedFilePath);
|
|
44
|
-
while (true) {
|
|
45
|
-
const packageName = await readPackageNameAt(currentDirectory);
|
|
46
|
-
if (packageName !== null) {
|
|
47
|
-
const currentDirectoryPosix = toPosixPath(currentDirectory);
|
|
48
|
-
const isExternalPackage = !isPathInside(opts.root, currentDirectory) || currentDirectoryPosix.includes('/node_modules/');
|
|
49
|
-
if (isExternalPackage && opts.safePackages.includes(packageName)) {
|
|
50
|
-
return resolve(opts.root, 'node_modules', packageName, relative(currentDirectory, resolvedFilePath));
|
|
51
|
-
}
|
|
52
|
-
return resolvedFilePath;
|
|
53
|
-
}
|
|
54
|
-
const parentDirectory = dirname(currentDirectory);
|
|
55
|
-
if (parentDirectory === currentDirectory) {
|
|
56
|
-
return resolvedFilePath;
|
|
57
|
-
}
|
|
58
|
-
currentDirectory = parentDirectory;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
42
|
export default async function componentSourceCollector(opts = { safePackages: [] }) {
|
|
62
43
|
// constants
|
|
63
44
|
const outFileName = opts.outputFile ?? 'component-sources.css';
|
|
64
|
-
const
|
|
45
|
+
const classAttributeRegex = /\bclass\s*=/;
|
|
65
46
|
const importRegex = /@import\s+['"]([^'"]+)['"]/g;
|
|
66
|
-
let outputFilePath = undefined;
|
|
67
|
-
let root = undefined;
|
|
68
47
|
// state
|
|
48
|
+
let outputFilePath;
|
|
49
|
+
let nodeModulesPath;
|
|
69
50
|
let config;
|
|
70
51
|
let initialTransformDone = false;
|
|
71
52
|
let initialTransformTimer = null;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
function shouldAdd(code, id) {
|
|
54
|
+
// Svelte's `class:` directive toggles local classes and should not be treated as
|
|
55
|
+
// a Tailwind source signal. Including those files can pull in component-local
|
|
56
|
+
// style modules that Tailwind should never parse directly.
|
|
57
|
+
if (id.includes('?svelte&type=style'))
|
|
58
|
+
return false;
|
|
59
|
+
return classAttributeRegex.test(code);
|
|
75
60
|
}
|
|
76
|
-
async function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return;
|
|
92
|
-
// Dont add itself
|
|
93
|
-
componentFiles.add(ensureDotRelative(relativeFilePath));
|
|
61
|
+
async function normalizeCollectedSourceFilePath(file) {
|
|
62
|
+
const cleanedFileName = file.replace(/[?#].*$/, '');
|
|
63
|
+
const resolvedFilePath = isAbsolute(cleanedFileName)
|
|
64
|
+
? resolve(cleanedFileName)
|
|
65
|
+
: resolve(dirname(outputFilePath), cleanedFileName);
|
|
66
|
+
let currentDirectory = dirname(resolvedFilePath);
|
|
67
|
+
while (true) {
|
|
68
|
+
const packageName = await readPackageNameAt(currentDirectory);
|
|
69
|
+
if (packageName !== null) {
|
|
70
|
+
const currentDirectoryPosix = toPosixPath(currentDirectory);
|
|
71
|
+
const isExternalPackage = !isPathInside(dirname(nodeModulesPath), currentDirectory) || currentDirectoryPosix.includes('/node_modules/');
|
|
72
|
+
if (isExternalPackage && opts.safePackages.includes(packageName)) {
|
|
73
|
+
return resolve(nodeModulesPath, packageName, relative(currentDirectory, resolvedFilePath));
|
|
74
|
+
}
|
|
75
|
+
return resolvedFilePath;
|
|
94
76
|
}
|
|
77
|
+
const parentDirectory = dirname(currentDirectory);
|
|
78
|
+
if (parentDirectory === currentDirectory) {
|
|
79
|
+
return resolvedFilePath;
|
|
80
|
+
}
|
|
81
|
+
currentDirectory = parentDirectory;
|
|
95
82
|
}
|
|
96
83
|
}
|
|
84
|
+
async function addPath(file) {
|
|
85
|
+
if (!outputFilePath || file === '')
|
|
86
|
+
return;
|
|
87
|
+
const normalizedFilePath = await normalizeCollectedSourceFilePath(file);
|
|
88
|
+
if (/\.svelte-kit/.test(normalizedFilePath) ||
|
|
89
|
+
(/(?:\.pnpm|\.vite)/.test(normalizedFilePath) &&
|
|
90
|
+
!opts.safePackages?.some((packageName) => normalizedFilePath.includes(`node_modules/${packageName}`)))) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const relativeFilePath = toPosixPath(relative(dirname(outputFilePath), normalizedFilePath));
|
|
94
|
+
if (normalizedFilePath === outputFilePath || relativeFilePath === outFileName)
|
|
95
|
+
return;
|
|
96
|
+
// Dont add itself
|
|
97
|
+
componentFiles.add(ensureDotRelative(relativeFilePath));
|
|
98
|
+
}
|
|
97
99
|
function scheduleInitialWrite() {
|
|
98
100
|
if (initialTransformTimer)
|
|
99
101
|
clearTimeout(initialTransformTimer);
|
|
@@ -102,11 +104,7 @@ export default async function componentSourceCollector(opts = { safePackages: []
|
|
|
102
104
|
writeOutFile();
|
|
103
105
|
initialTransformDone = true;
|
|
104
106
|
}
|
|
105
|
-
}, 1000);
|
|
106
|
-
}
|
|
107
|
-
async function touch(path) {
|
|
108
|
-
const handle = await open(path, 'a');
|
|
109
|
-
await handle.close();
|
|
107
|
+
}, 1000);
|
|
110
108
|
}
|
|
111
109
|
const writeOutFile = async () => {
|
|
112
110
|
const lines = Array.from(componentFiles)
|
|
@@ -115,7 +113,7 @@ export default async function componentSourceCollector(opts = { safePackages: []
|
|
|
115
113
|
if (outputFilePath) {
|
|
116
114
|
const didWrite = await writeIfDifferent(outputFilePath, lines.join('\n'));
|
|
117
115
|
if (didWrite)
|
|
118
|
-
console.log('
|
|
116
|
+
console.log('tailwind-sources:wrote', lines.length);
|
|
119
117
|
}
|
|
120
118
|
};
|
|
121
119
|
// ---- plugin ---- //
|
|
@@ -127,8 +125,16 @@ export default async function componentSourceCollector(opts = { safePackages: []
|
|
|
127
125
|
*/
|
|
128
126
|
async configResolved(resolved) {
|
|
129
127
|
config = resolved;
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
outputFilePath = resolve(config.root, outFileName);
|
|
129
|
+
let current = config.root;
|
|
130
|
+
while (true) {
|
|
131
|
+
if (await exists(join(current, 'package.json'))) {
|
|
132
|
+
nodeModulesPath = join(current, 'node_modules');
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
current = dirname(current);
|
|
137
|
+
}
|
|
132
138
|
console.log('tailwind-sources:configResolved: Command is', config.command);
|
|
133
139
|
await touch(outputFilePath);
|
|
134
140
|
if (config.command === 'build' && firstRound) {
|
|
@@ -136,13 +142,26 @@ export default async function componentSourceCollector(opts = { safePackages: []
|
|
|
136
142
|
componentFiles.clear();
|
|
137
143
|
firstRound = false;
|
|
138
144
|
}
|
|
139
|
-
else if (config.command === 'serve') {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
else if (config.command === 'serve' && (await exists(outputFilePath))) {
|
|
146
|
+
const fileLines = (await readFile(outputFilePath, 'utf8')).split('\n');
|
|
147
|
+
for (const fileLine of fileLines) {
|
|
148
|
+
const sourcePath = fileLine.replace(/@source\s+'(.*?)';/, '$1');
|
|
149
|
+
if (sourcePath === fileLine)
|
|
150
|
+
continue;
|
|
151
|
+
const resolvedSourcePath = resolve(dirname(outputFilePath), sourcePath);
|
|
152
|
+
if (resolvedSourcePath.endsWith('.css')) {
|
|
153
|
+
await addPath(sourcePath);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const code = await readFile(resolvedSourcePath, 'utf8');
|
|
158
|
+
if (shouldAdd(code, resolvedSourcePath)) {
|
|
159
|
+
await addPath(sourcePath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Ignore stale source entries that no longer resolve on disk.
|
|
144
164
|
}
|
|
145
|
-
// console.log('config resolved', componentFiles);
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
},
|
|
@@ -159,7 +178,7 @@ export default async function componentSourceCollector(opts = { safePackages: []
|
|
|
159
178
|
'npm-shrinkwrap.json',
|
|
160
179
|
// pnpm install-state changes:
|
|
161
180
|
'node_modules/.modules.yaml'
|
|
162
|
-
].map((p) => join(root, p));
|
|
181
|
+
].map((p) => join(config.root, p));
|
|
163
182
|
server.watcher.add(lockFiles);
|
|
164
183
|
const onChange = async (file) => {
|
|
165
184
|
if (!lockFiles.includes(file))
|
|
@@ -192,7 +211,7 @@ export default async function componentSourceCollector(opts = { safePackages: []
|
|
|
192
211
|
}
|
|
193
212
|
}
|
|
194
213
|
// Adds all other files with the classRegex
|
|
195
|
-
if (shouldAdd(code)) {
|
|
214
|
+
if (shouldAdd(code, id)) {
|
|
196
215
|
await addPath(id);
|
|
197
216
|
}
|
|
198
217
|
if (!initialTransformDone) {
|
|
@@ -1,64 +1,108 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
|
3
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
-
import {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { build, normalizePath } from 'vite';
|
|
6
|
+
const tempDirectories = [];
|
|
7
|
+
function svelteFixtureLoader() {
|
|
8
|
+
return {
|
|
9
|
+
name: 'svelte-fixture-loader',
|
|
10
|
+
async load(id) {
|
|
11
|
+
if (!id.endsWith('.svelte'))
|
|
12
|
+
return null;
|
|
13
|
+
const source = await readFile(id, 'utf8');
|
|
14
|
+
return `export default ${JSON.stringify(source)};`;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function createTempDirectory(prefix) {
|
|
19
|
+
const directory = await mkdtemp(join(tmpdir(), prefix));
|
|
20
|
+
tempDirectories.push(directory);
|
|
10
21
|
return directory;
|
|
11
22
|
}
|
|
12
|
-
async function
|
|
13
|
-
await
|
|
14
|
-
await writeFile(filePath, contents);
|
|
23
|
+
async function writeJson(filePath, value) {
|
|
24
|
+
await writeFile(filePath, JSON.stringify(value, null, 2));
|
|
15
25
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
async function createProjectRoot() {
|
|
27
|
+
const root = await createTempDirectory('vite-plugin-component-source-collector-');
|
|
28
|
+
await mkdir(join(root, 'src'), { recursive: true });
|
|
29
|
+
await mkdir(join(root, 'node_modules'), { recursive: true });
|
|
30
|
+
await writeJson(join(root, 'package.json'), {
|
|
31
|
+
name: 'collector-test-app',
|
|
32
|
+
private: true,
|
|
33
|
+
type: 'module'
|
|
34
|
+
});
|
|
35
|
+
await writeFile(join(root, 'src', 'main.js'), ["import 'safe-pkg/Button.svelte';", "import './app.css';", ''].join('\n'));
|
|
36
|
+
await writeFile(join(root, 'src', 'app.css'), ['@import "safe-pkg/theme.css";', ''].join('\n'));
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
async function createSafePackage(packageRoot) {
|
|
40
|
+
await mkdir(packageRoot, { recursive: true });
|
|
41
|
+
await writeJson(join(packageRoot, 'package.json'), {
|
|
42
|
+
name: 'safe-pkg',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
type: 'module'
|
|
45
|
+
});
|
|
46
|
+
await writeFile(join(packageRoot, 'Button.svelte'), '<button class="pkg-button">Click</button>\n');
|
|
47
|
+
await writeFile(join(packageRoot, 'theme.css'), '.pkg-theme { color: red; }\n');
|
|
48
|
+
}
|
|
49
|
+
async function runCollectorBuild(root) {
|
|
50
|
+
vi.resetModules();
|
|
51
|
+
const { default: componentSourceCollector } = await import('./vite-plugin-component-source-collector.js');
|
|
52
|
+
const collector = await componentSourceCollector({ safePackages: ['safe-pkg'] });
|
|
53
|
+
await build({
|
|
54
|
+
configFile: false,
|
|
55
|
+
logLevel: 'silent',
|
|
56
|
+
publicDir: false,
|
|
57
|
+
resolve: {
|
|
58
|
+
preserveSymlinks: false
|
|
59
|
+
},
|
|
60
|
+
root,
|
|
61
|
+
plugins: [collector, svelteFixtureLoader()],
|
|
62
|
+
build: {
|
|
63
|
+
write: false,
|
|
64
|
+
rollupOptions: {
|
|
65
|
+
input: join(root, 'src', 'main.js')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
const contents = await readFile(join(root, 'component-sources.css'), 'utf8');
|
|
70
|
+
return contents
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
}
|
|
75
|
+
describe('vite-plugin-component-source-collector', () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
vi.restoreAllMocks();
|
|
78
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
vi.restoreAllMocks();
|
|
82
|
+
await Promise.all(tempDirectories.splice(0).map((directory) => rm(directory, {
|
|
83
|
+
recursive: true,
|
|
84
|
+
force: true
|
|
85
|
+
})));
|
|
34
86
|
});
|
|
35
|
-
it('
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const normalizedPath = await normalizeCollectedSourceFilePath(sourceFilePath, {
|
|
44
|
-
outputFilePath,
|
|
45
|
-
root: appRoot,
|
|
46
|
-
safePackages: ['svelte-ag']
|
|
47
|
-
});
|
|
48
|
-
expect(normalizedPath).toBe(join(appRoot, 'node_modules', 'svelte-ag', 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte'));
|
|
87
|
+
it('collects safe package component and css sources from installed node_modules packages', async () => {
|
|
88
|
+
const root = await createProjectRoot();
|
|
89
|
+
await createSafePackage(join(root, 'node_modules', 'safe-pkg'));
|
|
90
|
+
const lines = await runCollectorBuild(root);
|
|
91
|
+
expect(lines).toEqual([
|
|
92
|
+
"@source './node_modules/safe-pkg/Button.svelte';",
|
|
93
|
+
"@source './node_modules/safe-pkg/theme.css';"
|
|
94
|
+
]);
|
|
49
95
|
});
|
|
50
|
-
it('
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
expect(normalizedPath).toBe(sourceFilePath);
|
|
96
|
+
it('normalizes symlinked pnpm-style package sources back to node_modules paths', async () => {
|
|
97
|
+
const root = await createProjectRoot();
|
|
98
|
+
const linkedPackageRoot = await createTempDirectory('vite-plugin-component-source-linked-package-');
|
|
99
|
+
await createSafePackage(linkedPackageRoot);
|
|
100
|
+
await symlink(linkedPackageRoot, join(root, 'node_modules', 'safe-pkg'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
101
|
+
const lines = await runCollectorBuild(root);
|
|
102
|
+
expect(lines).toEqual([
|
|
103
|
+
"@source './node_modules/safe-pkg/Button.svelte';",
|
|
104
|
+
"@source './node_modules/safe-pkg/theme.css';"
|
|
105
|
+
]);
|
|
106
|
+
expect(lines.join('\n')).not.toContain(normalizePath(linkedPackageRoot));
|
|
63
107
|
});
|
|
64
108
|
});
|
package/package.json
CHANGED
|
@@ -27,9 +27,13 @@ let firstRound = true;
|
|
|
27
27
|
const packageJsonCache = new Map<string, Promise<string | null>>();
|
|
28
28
|
|
|
29
29
|
function ensureDotRelative(filePath: string): string {
|
|
30
|
-
if (filePath.startsWith('
|
|
30
|
+
if (filePath.startsWith('./')) return filePath;
|
|
31
31
|
return `./${filePath}`;
|
|
32
32
|
}
|
|
33
|
+
async function touch(path: string) {
|
|
34
|
+
const handle = await open(path, 'a');
|
|
35
|
+
await handle.close();
|
|
36
|
+
}
|
|
33
37
|
|
|
34
38
|
function isPathInside(parentPath: string, childPath: string): boolean {
|
|
35
39
|
const relativePath = relative(parentPath, childPath);
|
|
@@ -58,90 +62,74 @@ async function readPackageNameAt(directory: string): Promise<string | null> {
|
|
|
58
62
|
return packageNamePromise;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
type NormalizeCollectedSourceFilePathOptions = {
|
|
62
|
-
outputFilePath: string;
|
|
63
|
-
root: string;
|
|
64
|
-
safePackages: string[];
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export async function normalizeCollectedSourceFilePath(
|
|
68
|
-
file: string,
|
|
69
|
-
opts: NormalizeCollectedSourceFilePathOptions
|
|
70
|
-
): Promise<string> {
|
|
71
|
-
const cleanedFileName = file.replace(/[?#].*$/, '');
|
|
72
|
-
const resolvedFilePath = isAbsolute(cleanedFileName)
|
|
73
|
-
? resolve(cleanedFileName)
|
|
74
|
-
: resolve(dirname(opts.outputFilePath), cleanedFileName);
|
|
75
|
-
|
|
76
|
-
let currentDirectory = dirname(resolvedFilePath);
|
|
77
|
-
|
|
78
|
-
while (true) {
|
|
79
|
-
const packageName = await readPackageNameAt(currentDirectory);
|
|
80
|
-
if (packageName !== null) {
|
|
81
|
-
const currentDirectoryPosix = toPosixPath(currentDirectory);
|
|
82
|
-
const isExternalPackage =
|
|
83
|
-
!isPathInside(opts.root, currentDirectory) || currentDirectoryPosix.includes('/node_modules/');
|
|
84
|
-
|
|
85
|
-
if (isExternalPackage && opts.safePackages.includes(packageName)) {
|
|
86
|
-
return resolve(opts.root, 'node_modules', packageName, relative(currentDirectory, resolvedFilePath));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return resolvedFilePath;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const parentDirectory = dirname(currentDirectory);
|
|
93
|
-
if (parentDirectory === currentDirectory) {
|
|
94
|
-
return resolvedFilePath;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
currentDirectory = parentDirectory;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
65
|
export default async function componentSourceCollector(opts: Options = { safePackages: [] }): Promise<Plugin> {
|
|
102
66
|
// constants
|
|
103
67
|
const outFileName = opts.outputFile ?? 'component-sources.css';
|
|
104
|
-
const
|
|
68
|
+
const classAttributeRegex = /\bclass\s*=/;
|
|
105
69
|
const importRegex = /@import\s+['"]([^'"]+)['"]/g;
|
|
106
70
|
|
|
107
|
-
let outputFilePath: string | undefined = undefined;
|
|
108
|
-
let root: string | undefined = undefined;
|
|
109
|
-
|
|
110
71
|
// state
|
|
72
|
+
let outputFilePath: string;
|
|
73
|
+
let nodeModulesPath: string;
|
|
111
74
|
let config: ResolvedConfig;
|
|
112
75
|
let initialTransformDone = false;
|
|
113
76
|
let initialTransformTimer: NodeJS.Timeout | null = null;
|
|
114
77
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
78
|
+
function shouldAdd(code: string, id: string) {
|
|
79
|
+
// Svelte's `class:` directive toggles local classes and should not be treated as
|
|
80
|
+
// a Tailwind source signal. Including those files can pull in component-local
|
|
81
|
+
// style modules that Tailwind should never parse directly.
|
|
82
|
+
if (id.includes('?svelte&type=style')) return false;
|
|
83
|
+
return classAttributeRegex.test(code);
|
|
118
84
|
}
|
|
119
85
|
|
|
86
|
+
async function normalizeCollectedSourceFilePath(file: string): Promise<string> {
|
|
87
|
+
const cleanedFileName = file.replace(/[?#].*$/, '');
|
|
88
|
+
const resolvedFilePath = isAbsolute(cleanedFileName)
|
|
89
|
+
? resolve(cleanedFileName)
|
|
90
|
+
: resolve(dirname(outputFilePath), cleanedFileName);
|
|
91
|
+
|
|
92
|
+
let currentDirectory = dirname(resolvedFilePath);
|
|
93
|
+
|
|
94
|
+
while (true) {
|
|
95
|
+
const packageName = await readPackageNameAt(currentDirectory);
|
|
96
|
+
if (packageName !== null) {
|
|
97
|
+
const currentDirectoryPosix = toPosixPath(currentDirectory);
|
|
98
|
+
const isExternalPackage =
|
|
99
|
+
!isPathInside(dirname(nodeModulesPath), currentDirectory) || currentDirectoryPosix.includes('/node_modules/');
|
|
100
|
+
|
|
101
|
+
if (isExternalPackage && opts.safePackages.includes(packageName)) {
|
|
102
|
+
return resolve(nodeModulesPath, packageName, relative(currentDirectory, resolvedFilePath));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return resolvedFilePath;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parentDirectory = dirname(currentDirectory);
|
|
109
|
+
if (parentDirectory === currentDirectory) {
|
|
110
|
+
return resolvedFilePath;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
currentDirectory = parentDirectory;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
120
116
|
async function addPath(file: string) {
|
|
117
|
+
if (!outputFilePath || file === '') return;
|
|
118
|
+
|
|
119
|
+
const normalizedFilePath = await normalizeCollectedSourceFilePath(file);
|
|
121
120
|
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
/\.svelte-kit/.test(normalizedFilePath) ||
|
|
122
|
+
(/(?:\.pnpm|\.vite)/.test(normalizedFilePath) &&
|
|
123
|
+
!opts.safePackages?.some((packageName) => normalizedFilePath.includes(`node_modules/${packageName}`)))
|
|
125
124
|
) {
|
|
126
|
-
|
|
127
|
-
outputFilePath,
|
|
128
|
-
root,
|
|
129
|
-
safePackages: opts.safePackages
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
!/\.svelte-kit/.test(normalizedFilePath) && // No svelte-kit files
|
|
134
|
-
// No dep files unless marked as safe
|
|
135
|
-
(!/(?:\.pnpm|\.vite)/.test(normalizedFilePath) ||
|
|
136
|
-
opts.safePackages.some((p) => normalizedFilePath.includes(`node_modules/${p}`)))
|
|
137
|
-
) {
|
|
138
|
-
const relativeFilePath = toPosixPath(relative(dirname(outputFilePath), normalizedFilePath));
|
|
139
|
-
|
|
140
|
-
if (normalizedFilePath === outputFilePath || relativeFilePath === outFileName) return;
|
|
141
|
-
// Dont add itself
|
|
142
|
-
componentFiles.add(ensureDotRelative(relativeFilePath));
|
|
143
|
-
}
|
|
125
|
+
return;
|
|
144
126
|
}
|
|
127
|
+
|
|
128
|
+
const relativeFilePath = toPosixPath(relative(dirname(outputFilePath), normalizedFilePath));
|
|
129
|
+
if (normalizedFilePath === outputFilePath || relativeFilePath === outFileName) return;
|
|
130
|
+
|
|
131
|
+
// Dont add itself
|
|
132
|
+
componentFiles.add(ensureDotRelative(relativeFilePath));
|
|
145
133
|
}
|
|
146
134
|
|
|
147
135
|
function scheduleInitialWrite() {
|
|
@@ -151,12 +139,7 @@ export default async function componentSourceCollector(opts: Options = { safePac
|
|
|
151
139
|
writeOutFile();
|
|
152
140
|
initialTransformDone = true;
|
|
153
141
|
}
|
|
154
|
-
}, 1000);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function touch(path: string) {
|
|
158
|
-
const handle = await open(path, 'a');
|
|
159
|
-
await handle.close();
|
|
142
|
+
}, 1000);
|
|
160
143
|
}
|
|
161
144
|
|
|
162
145
|
const writeOutFile = async () => {
|
|
@@ -166,7 +149,7 @@ export default async function componentSourceCollector(opts: Options = { safePac
|
|
|
166
149
|
|
|
167
150
|
if (outputFilePath) {
|
|
168
151
|
const didWrite = await writeIfDifferent(outputFilePath, lines.join('\n'));
|
|
169
|
-
if (didWrite) console.log('
|
|
152
|
+
if (didWrite) console.log('tailwind-sources:wrote', lines.length);
|
|
170
153
|
}
|
|
171
154
|
};
|
|
172
155
|
|
|
@@ -181,8 +164,15 @@ export default async function componentSourceCollector(opts: Options = { safePac
|
|
|
181
164
|
*/
|
|
182
165
|
async configResolved(resolved) {
|
|
183
166
|
config = resolved;
|
|
184
|
-
|
|
185
|
-
|
|
167
|
+
outputFilePath = resolve(config.root, outFileName);
|
|
168
|
+
|
|
169
|
+
let current = config.root;
|
|
170
|
+
while (true) {
|
|
171
|
+
if (await exists(join(current, 'package.json'))) {
|
|
172
|
+
nodeModulesPath = join(current, 'node_modules');
|
|
173
|
+
break;
|
|
174
|
+
} else current = dirname(current);
|
|
175
|
+
}
|
|
186
176
|
|
|
187
177
|
console.log('tailwind-sources:configResolved: Command is', config.command);
|
|
188
178
|
|
|
@@ -192,13 +182,26 @@ export default async function componentSourceCollector(opts: Options = { safePac
|
|
|
192
182
|
console.log('tailwind-sources: Clearing files list');
|
|
193
183
|
componentFiles.clear();
|
|
194
184
|
firstRound = false;
|
|
195
|
-
} else if (config.command === 'serve') {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
185
|
+
} else if (config.command === 'serve' && (await exists(outputFilePath))) {
|
|
186
|
+
const fileLines = (await readFile(outputFilePath, 'utf8')).split('\n');
|
|
187
|
+
for (const fileLine of fileLines) {
|
|
188
|
+
const sourcePath = fileLine.replace(/@source\s+'(.*?)';/, '$1');
|
|
189
|
+
if (sourcePath === fileLine) continue;
|
|
190
|
+
|
|
191
|
+
const resolvedSourcePath = resolve(dirname(outputFilePath), sourcePath);
|
|
192
|
+
if (resolvedSourcePath.endsWith('.css')) {
|
|
193
|
+
await addPath(sourcePath);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const code = await readFile(resolvedSourcePath, 'utf8');
|
|
199
|
+
if (shouldAdd(code, resolvedSourcePath)) {
|
|
200
|
+
await addPath(sourcePath);
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Ignore stale source entries that no longer resolve on disk.
|
|
200
204
|
}
|
|
201
|
-
// console.log('config resolved', componentFiles);
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
207
|
},
|
|
@@ -216,7 +219,7 @@ export default async function componentSourceCollector(opts: Options = { safePac
|
|
|
216
219
|
'npm-shrinkwrap.json',
|
|
217
220
|
// pnpm install-state changes:
|
|
218
221
|
'node_modules/.modules.yaml'
|
|
219
|
-
].map((p) => join(root!, p));
|
|
222
|
+
].map((p) => join(config.root!, p));
|
|
220
223
|
server.watcher.add(lockFiles);
|
|
221
224
|
const onChange = async (file: string) => {
|
|
222
225
|
if (!lockFiles.includes(file)) return;
|
|
@@ -251,7 +254,7 @@ export default async function componentSourceCollector(opts: Options = { safePac
|
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
// Adds all other files with the classRegex
|
|
254
|
-
if (shouldAdd(code)) {
|
|
257
|
+
if (shouldAdd(code, id)) {
|
|
255
258
|
await addPath(id);
|
|
256
259
|
}
|
|
257
260
|
|
|
@@ -1,91 +1,142 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
|
3
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { build, normalizePath, type Plugin } from 'vite';
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const tempDirectories: string[] = [];
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
function svelteFixtureLoader(): Plugin {
|
|
10
|
+
return {
|
|
11
|
+
name: 'svelte-fixture-loader',
|
|
12
|
+
async load(id) {
|
|
13
|
+
if (!id.endsWith('.svelte')) return null;
|
|
14
|
+
|
|
15
|
+
const source = await readFile(id, 'utf8');
|
|
16
|
+
return `export default ${JSON.stringify(source)};`;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function createTempDirectory(prefix: string): Promise<string> {
|
|
22
|
+
const directory = await mkdtemp(join(tmpdir(), prefix));
|
|
23
|
+
tempDirectories.push(directory);
|
|
12
24
|
return directory;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
|
-
async function
|
|
16
|
-
await
|
|
17
|
-
await writeFile(filePath, contents);
|
|
27
|
+
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
|
28
|
+
await writeFile(filePath, JSON.stringify(value, null, 2));
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
31
|
+
async function createProjectRoot(): Promise<string> {
|
|
32
|
+
const root = await createTempDirectory('vite-plugin-component-source-collector-');
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const baseDirectory = await createTemporaryDirectory();
|
|
27
|
-
const appRoot = join(baseDirectory, 'app');
|
|
28
|
-
const outputFilePath = join(appRoot, 'component-sources.css');
|
|
29
|
-
const pnpmPackageRoot = join(
|
|
30
|
-
appRoot,
|
|
31
|
-
'node_modules',
|
|
32
|
-
'.pnpm',
|
|
33
|
-
'svelte-ag@1.0.56_hash',
|
|
34
|
-
'node_modules',
|
|
35
|
-
'svelte-ag'
|
|
36
|
-
);
|
|
37
|
-
const sourceFilePath = join(pnpmPackageRoot, 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte');
|
|
34
|
+
await mkdir(join(root, 'src'), { recursive: true });
|
|
35
|
+
await mkdir(join(root, 'node_modules'), { recursive: true });
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
await writeJson(join(root, 'package.json'), {
|
|
38
|
+
name: 'collector-test-app',
|
|
39
|
+
private: true,
|
|
40
|
+
type: 'module'
|
|
41
|
+
});
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
await writeFile(
|
|
44
|
+
join(root, 'src', 'main.js'),
|
|
45
|
+
["import 'safe-pkg/Button.svelte';", "import './app.css';", ''].join('\n')
|
|
46
|
+
);
|
|
47
|
+
await writeFile(join(root, 'src', 'app.css'), ['@import "safe-pkg/theme.css";', ''].join('\n'));
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function createSafePackage(packageRoot: string): Promise<void> {
|
|
53
|
+
await mkdir(packageRoot, { recursive: true });
|
|
54
|
+
await writeJson(join(packageRoot, 'package.json'), {
|
|
55
|
+
name: 'safe-pkg',
|
|
56
|
+
version: '1.0.0',
|
|
57
|
+
type: 'module'
|
|
51
58
|
});
|
|
59
|
+
await writeFile(join(packageRoot, 'Button.svelte'), '<button class="pkg-button">Click</button>\n');
|
|
60
|
+
await writeFile(join(packageRoot, 'theme.css'), '.pkg-theme { color: red; }\n');
|
|
61
|
+
}
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
async function runCollectorBuild(root: string): Promise<string[]> {
|
|
64
|
+
vi.resetModules();
|
|
65
|
+
const { default: componentSourceCollector } = await import('./vite-plugin-component-source-collector.js');
|
|
66
|
+
const collector = await componentSourceCollector({ safePackages: ['safe-pkg'] });
|
|
67
|
+
|
|
68
|
+
await build({
|
|
69
|
+
configFile: false,
|
|
70
|
+
logLevel: 'silent',
|
|
71
|
+
publicDir: false,
|
|
72
|
+
resolve: {
|
|
73
|
+
preserveSymlinks: false
|
|
74
|
+
},
|
|
75
|
+
root,
|
|
76
|
+
plugins: [collector, svelteFixtureLoader()],
|
|
77
|
+
build: {
|
|
78
|
+
write: false,
|
|
79
|
+
rollupOptions: {
|
|
80
|
+
input: join(root, 'src', 'main.js')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
const contents = await readFile(join(root, 'component-sources.css'), 'utf8');
|
|
86
|
+
return contents
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
describe('vite-plugin-component-source-collector', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.restoreAllMocks();
|
|
95
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
96
|
+
});
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
afterEach(async () => {
|
|
99
|
+
vi.restoreAllMocks();
|
|
100
|
+
|
|
101
|
+
await Promise.all(
|
|
102
|
+
tempDirectories.splice(0).map((directory) =>
|
|
103
|
+
rm(directory, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
force: true
|
|
106
|
+
})
|
|
107
|
+
)
|
|
71
108
|
);
|
|
72
109
|
});
|
|
73
110
|
|
|
74
|
-
it('
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
111
|
+
it('collects safe package component and css sources from installed node_modules packages', async () => {
|
|
112
|
+
const root = await createProjectRoot();
|
|
113
|
+
await createSafePackage(join(root, 'node_modules', 'safe-pkg'));
|
|
114
|
+
|
|
115
|
+
const lines = await runCollectorBuild(root);
|
|
79
116
|
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
expect(lines).toEqual([
|
|
118
|
+
"@source './node_modules/safe-pkg/Button.svelte';",
|
|
119
|
+
"@source './node_modules/safe-pkg/theme.css';"
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('normalizes symlinked pnpm-style package sources back to node_modules paths', async () => {
|
|
124
|
+
const root = await createProjectRoot();
|
|
125
|
+
const linkedPackageRoot = await createTempDirectory('vite-plugin-component-source-linked-package-');
|
|
126
|
+
|
|
127
|
+
await createSafePackage(linkedPackageRoot);
|
|
128
|
+
await symlink(
|
|
129
|
+
linkedPackageRoot,
|
|
130
|
+
join(root, 'node_modules', 'safe-pkg'),
|
|
131
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
132
|
+
);
|
|
82
133
|
|
|
83
|
-
const
|
|
84
|
-
outputFilePath,
|
|
85
|
-
root: appRoot,
|
|
86
|
-
safePackages: ['svelte-ag']
|
|
87
|
-
});
|
|
134
|
+
const lines = await runCollectorBuild(root);
|
|
88
135
|
|
|
89
|
-
expect(
|
|
136
|
+
expect(lines).toEqual([
|
|
137
|
+
"@source './node_modules/safe-pkg/Button.svelte';",
|
|
138
|
+
"@source './node_modules/safe-pkg/theme.css';"
|
|
139
|
+
]);
|
|
140
|
+
expect(lines.join('\n')).not.toContain(normalizePath(linkedPackageRoot));
|
|
90
141
|
});
|
|
91
142
|
});
|