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.
@@ -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;AAuCD,KAAK,uCAAuC,GAAG;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,wBAAsB,gCAAgC,CACpD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,uCAAuC,GAC5C,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED,wBAA8B,wBAAwB,CAAC,IAAI,GAAE,OAA8B,GAAG,OAAO,CAAC,MAAM,CAAC,CAgL5G"}
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 classRegex = /class(?:=|:)/;
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
- // init
73
- function shouldAdd(code) {
74
- return classRegex.test(code);
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 addPath(file) {
77
- if (outputFilePath &&
78
- file !== '' && // No nothing
79
- root) {
80
- const normalizedFilePath = await normalizeCollectedSourceFilePath(file, {
81
- outputFilePath,
82
- root,
83
- safePackages: opts.safePackages
84
- });
85
- if (!/\.svelte-kit/.test(normalizedFilePath) && // No svelte-kit files
86
- // No dep files unless marked as safe
87
- (!/(?:\.pnpm|\.vite)/.test(normalizedFilePath) ||
88
- opts.safePackages.some((p) => normalizedFilePath.includes(`node_modules/${p}`)))) {
89
- const relativeFilePath = toPosixPath(relative(dirname(outputFilePath), normalizedFilePath));
90
- if (normalizedFilePath === outputFilePath || relativeFilePath === outFileName)
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); // adjust delay as needed
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('Wrote', lines.length);
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
- root = config.root;
131
- outputFilePath = resolve(root, outFileName);
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
- if (await exists(outputFilePath)) {
141
- const fileLines = (await readFile(outputFilePath, 'utf8')).split('\n');
142
- for (const fileLine of fileLines) {
143
- await addPath(fileLine.replace(/@source\s+'(.*?)';/, '$1'));
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 { dirname, join } from 'path';
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 { normalizeCollectedSourceFilePath } from './vite-plugin-component-source-collector.js';
6
- const temporaryDirectories = [];
7
- async function createTemporaryDirectory() {
8
- const directory = await mkdtemp(join(tmpdir(), 'svelte-ag-component-source-collector-'));
9
- temporaryDirectories.push(directory);
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 createFile(filePath, contents = '') {
13
- await mkdir(dirname(filePath), { recursive: true });
14
- await writeFile(filePath, contents);
23
+ async function writeJson(filePath, value) {
24
+ await writeFile(filePath, JSON.stringify(value, null, 2));
15
25
  }
16
- afterEach(async () => {
17
- await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
18
- });
19
- describe('normalizeCollectedSourceFilePath', () => {
20
- it('canonicalizes pnpm store paths for safe packages', async () => {
21
- const baseDirectory = await createTemporaryDirectory();
22
- const appRoot = join(baseDirectory, 'app');
23
- const outputFilePath = join(appRoot, 'component-sources.css');
24
- const pnpmPackageRoot = join(appRoot, 'node_modules', '.pnpm', 'svelte-ag@1.0.56_hash', 'node_modules', 'svelte-ag');
25
- const sourceFilePath = join(pnpmPackageRoot, 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte');
26
- await createFile(join(pnpmPackageRoot, 'package.json'), JSON.stringify({ name: 'svelte-ag' }));
27
- await createFile(sourceFilePath);
28
- const normalizedPath = await normalizeCollectedSourceFilePath(sourceFilePath, {
29
- outputFilePath,
30
- root: appRoot,
31
- safePackages: ['svelte-ag']
32
- });
33
- expect(normalizedPath).toBe(join(appRoot, 'node_modules', 'svelte-ag', 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte'));
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('canonicalizes linked package paths outside the project root', async () => {
36
- const baseDirectory = await createTemporaryDirectory();
37
- const appRoot = join(baseDirectory, 'app');
38
- const outputFilePath = join(appRoot, 'component-sources.css');
39
- const linkedPackageRoot = join(baseDirectory, 'packages', 'svelte-ag');
40
- const sourceFilePath = join(linkedPackageRoot, 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte');
41
- await createFile(join(linkedPackageRoot, 'package.json'), JSON.stringify({ name: 'svelte-ag' }));
42
- await createFile(sourceFilePath);
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('leaves project files unchanged even when the project matches a safe package name', async () => {
51
- const baseDirectory = await createTemporaryDirectory();
52
- const appRoot = join(baseDirectory, 'svelte-ag');
53
- const outputFilePath = join(appRoot, 'component-sources.css');
54
- const sourceFilePath = join(appRoot, 'src', 'lib', 'components', 'Button.svelte');
55
- await createFile(join(appRoot, 'package.json'), JSON.stringify({ name: 'svelte-ag' }));
56
- await createFile(sourceFilePath);
57
- const normalizedPath = await normalizeCollectedSourceFilePath(sourceFilePath, {
58
- outputFilePath,
59
- root: appRoot,
60
- safePackages: ['svelte-ag']
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-ag",
3
- "version": "1.0.64",
3
+ "version": "1.0.66",
4
4
  "description": "Useful svelte components",
5
5
  "bugs": "https://github.com/ageorgeh/svelte-ag/issues",
6
6
  "repository": {
@@ -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('.')) return filePath;
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 classRegex = /class(?:=|:)/;
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
- // init
116
- function shouldAdd(code: string) {
117
- return classRegex.test(code);
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
- outputFilePath &&
123
- file !== '' && // No nothing
124
- root
121
+ /\.svelte-kit/.test(normalizedFilePath) ||
122
+ (/(?:\.pnpm|\.vite)/.test(normalizedFilePath) &&
123
+ !opts.safePackages?.some((packageName) => normalizedFilePath.includes(`node_modules/${packageName}`)))
125
124
  ) {
126
- const normalizedFilePath = await normalizeCollectedSourceFilePath(file, {
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); // adjust delay as needed
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('Wrote', lines.length);
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
- root = config.root;
185
- outputFilePath = resolve(root, outFileName);
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
- if (await exists(outputFilePath)) {
197
- const fileLines = (await readFile(outputFilePath, 'utf8')).split('\n');
198
- for (const fileLine of fileLines) {
199
- await addPath(fileLine.replace(/@source\s+'(.*?)';/, '$1'));
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 { dirname, join } from 'path';
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 { normalizeCollectedSourceFilePath } from './vite-plugin-component-source-collector.js';
5
+ import { build, normalizePath, type Plugin } from 'vite';
6
6
 
7
- const temporaryDirectories: string[] = [];
7
+ const tempDirectories: string[] = [];
8
8
 
9
- async function createTemporaryDirectory() {
10
- const directory = await mkdtemp(join(tmpdir(), 'svelte-ag-component-source-collector-'));
11
- temporaryDirectories.push(directory);
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 createFile(filePath: string, contents = '') {
16
- await mkdir(dirname(filePath), { recursive: true });
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
- afterEach(async () => {
21
- await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { force: true, recursive: true })));
22
- });
31
+ async function createProjectRoot(): Promise<string> {
32
+ const root = await createTempDirectory('vite-plugin-component-source-collector-');
23
33
 
24
- describe('normalizeCollectedSourceFilePath', () => {
25
- it('canonicalizes pnpm store paths for safe packages', async () => {
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
- await createFile(join(pnpmPackageRoot, 'package.json'), JSON.stringify({ name: 'svelte-ag' }));
40
- await createFile(sourceFilePath);
37
+ await writeJson(join(root, 'package.json'), {
38
+ name: 'collector-test-app',
39
+ private: true,
40
+ type: 'module'
41
+ });
41
42
 
42
- const normalizedPath = await normalizeCollectedSourceFilePath(sourceFilePath, {
43
- outputFilePath,
44
- root: appRoot,
45
- safePackages: ['svelte-ag']
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
- expect(normalizedPath).toBe(
49
- join(appRoot, 'node_modules', 'svelte-ag', 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte')
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
- it('canonicalizes linked package paths outside the project root', async () => {
54
- const baseDirectory = await createTemporaryDirectory();
55
- const appRoot = join(baseDirectory, 'app');
56
- const outputFilePath = join(appRoot, 'component-sources.css');
57
- const linkedPackageRoot = join(baseDirectory, 'packages', 'svelte-ag');
58
- const sourceFilePath = join(linkedPackageRoot, 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte');
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
- await createFile(join(linkedPackageRoot, 'package.json'), JSON.stringify({ name: 'svelte-ag' }));
61
- await createFile(sourceFilePath);
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
- const normalizedPath = await normalizeCollectedSourceFilePath(sourceFilePath, {
64
- outputFilePath,
65
- root: appRoot,
66
- safePackages: ['svelte-ag']
67
- });
92
+ describe('vite-plugin-component-source-collector', () => {
93
+ beforeEach(() => {
94
+ vi.restoreAllMocks();
95
+ vi.spyOn(console, 'log').mockImplementation(() => {});
96
+ });
68
97
 
69
- expect(normalizedPath).toBe(
70
- join(appRoot, 'node_modules', 'svelte-ag', 'dist', 'lib', 'components', 'dnd', 'DndDroppable.svelte')
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('leaves project files unchanged even when the project matches a safe package name', async () => {
75
- const baseDirectory = await createTemporaryDirectory();
76
- const appRoot = join(baseDirectory, 'svelte-ag');
77
- const outputFilePath = join(appRoot, 'component-sources.css');
78
- const sourceFilePath = join(appRoot, 'src', 'lib', 'components', 'Button.svelte');
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
- await createFile(join(appRoot, 'package.json'), JSON.stringify({ name: 'svelte-ag' }));
81
- await createFile(sourceFilePath);
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 normalizedPath = await normalizeCollectedSourceFilePath(sourceFilePath, {
84
- outputFilePath,
85
- root: appRoot,
86
- safePackages: ['svelte-ag']
87
- });
134
+ const lines = await runCollectorBuild(root);
88
135
 
89
- expect(normalizedPath).toBe(sourceFilePath);
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
  });