svelte-ag 1.0.64 → 1.0.65

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,CAsM5G"}
@@ -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,53 +39,46 @@ 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
45
  const classRegex = /class(?:=|:)/;
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
53
  function shouldAdd(code) {
74
54
  return classRegex.test(code);
75
55
  }
56
+ async function normalizeCollectedSourceFilePath(file) {
57
+ const cleanedFileName = file.replace(/[?#].*$/, '');
58
+ const resolvedFilePath = isAbsolute(cleanedFileName)
59
+ ? resolve(cleanedFileName)
60
+ : resolve(dirname(outputFilePath), cleanedFileName);
61
+ let currentDirectory = dirname(resolvedFilePath);
62
+ while (true) {
63
+ const packageName = await readPackageNameAt(currentDirectory);
64
+ if (packageName !== null) {
65
+ const currentDirectoryPosix = toPosixPath(currentDirectory);
66
+ const isExternalPackage = !isPathInside(dirname(nodeModulesPath), currentDirectory) || currentDirectoryPosix.includes('/node_modules/');
67
+ if (isExternalPackage && opts.safePackages.includes(packageName)) {
68
+ return resolve(nodeModulesPath, packageName, relative(currentDirectory, resolvedFilePath));
69
+ }
70
+ return resolvedFilePath;
71
+ }
72
+ const parentDirectory = dirname(currentDirectory);
73
+ if (parentDirectory === currentDirectory) {
74
+ return resolvedFilePath;
75
+ }
76
+ currentDirectory = parentDirectory;
77
+ }
78
+ }
76
79
  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
- });
80
+ if (outputFilePath && file !== '') {
81
+ const normalizedFilePath = await normalizeCollectedSourceFilePath(file);
85
82
  if (!/\.svelte-kit/.test(normalizedFilePath) && // No svelte-kit files
86
83
  // No dep files unless marked as safe
87
84
  (!/(?:\.pnpm|\.vite)/.test(normalizedFilePath) ||
@@ -104,10 +101,6 @@ export default async function componentSourceCollector(opts = { safePackages: []
104
101
  }
105
102
  }, 1000); // adjust delay as needed
106
103
  }
107
- async function touch(path) {
108
- const handle = await open(path, 'a');
109
- await handle.close();
110
- }
111
104
  const writeOutFile = async () => {
112
105
  const lines = Array.from(componentFiles)
113
106
  .map((d) => `@source '${d}';`)
@@ -127,8 +120,16 @@ export default async function componentSourceCollector(opts = { safePackages: []
127
120
  */
128
121
  async configResolved(resolved) {
129
122
  config = resolved;
130
- root = config.root;
131
- outputFilePath = resolve(root, outFileName);
123
+ outputFilePath = resolve(config.root, outFileName);
124
+ let current = config.root;
125
+ while (true) {
126
+ if (await exists(join(current, 'package.json'))) {
127
+ nodeModulesPath = join(current, 'node_modules');
128
+ break;
129
+ }
130
+ else
131
+ current = dirname(current);
132
+ }
132
133
  console.log('tailwind-sources:configResolved: Command is', config.command);
133
134
  await touch(outputFilePath);
134
135
  if (config.command === 'build' && firstRound) {
@@ -159,7 +160,7 @@ export default async function componentSourceCollector(opts = { safePackages: []
159
160
  'npm-shrinkwrap.json',
160
161
  // pnpm install-state changes:
161
162
  'node_modules/.modules.yaml'
162
- ].map((p) => join(root, p));
163
+ ].map((p) => join(config.root, p));
163
164
  server.watcher.add(lockFiles);
164
165
  const onChange = async (file) => {
165
166
  if (!lockFiles.includes(file))
@@ -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.65",
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,76 +62,56 @@ 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
68
  const classRegex = /class(?:=|:)/;
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
78
  function shouldAdd(code: string) {
117
79
  return classRegex.test(code);
118
80
  }
119
81
 
82
+ async function normalizeCollectedSourceFilePath(file: string): Promise<string> {
83
+ const cleanedFileName = file.replace(/[?#].*$/, '');
84
+ const resolvedFilePath = isAbsolute(cleanedFileName)
85
+ ? resolve(cleanedFileName)
86
+ : resolve(dirname(outputFilePath), cleanedFileName);
87
+
88
+ let currentDirectory = dirname(resolvedFilePath);
89
+
90
+ while (true) {
91
+ const packageName = await readPackageNameAt(currentDirectory);
92
+ if (packageName !== null) {
93
+ const currentDirectoryPosix = toPosixPath(currentDirectory);
94
+ const isExternalPackage =
95
+ !isPathInside(dirname(nodeModulesPath), currentDirectory) || currentDirectoryPosix.includes('/node_modules/');
96
+
97
+ if (isExternalPackage && opts.safePackages.includes(packageName)) {
98
+ return resolve(nodeModulesPath, packageName, relative(currentDirectory, resolvedFilePath));
99
+ }
100
+
101
+ return resolvedFilePath;
102
+ }
103
+
104
+ const parentDirectory = dirname(currentDirectory);
105
+ if (parentDirectory === currentDirectory) {
106
+ return resolvedFilePath;
107
+ }
108
+
109
+ currentDirectory = parentDirectory;
110
+ }
111
+ }
120
112
  async function addPath(file: string) {
121
- if (
122
- outputFilePath &&
123
- file !== '' && // No nothing
124
- root
125
- ) {
126
- const normalizedFilePath = await normalizeCollectedSourceFilePath(file, {
127
- outputFilePath,
128
- root,
129
- safePackages: opts.safePackages
130
- });
113
+ if (outputFilePath && file !== '') {
114
+ const normalizedFilePath = await normalizeCollectedSourceFilePath(file);
131
115
 
132
116
  if (
133
117
  !/\.svelte-kit/.test(normalizedFilePath) && // No svelte-kit files
@@ -154,11 +138,6 @@ export default async function componentSourceCollector(opts: Options = { safePac
154
138
  }, 1000); // adjust delay as needed
155
139
  }
156
140
 
157
- async function touch(path: string) {
158
- const handle = await open(path, 'a');
159
- await handle.close();
160
- }
161
-
162
141
  const writeOutFile = async () => {
163
142
  const lines = Array.from(componentFiles)
164
143
  .map((d) => `@source '${d}';`)
@@ -181,8 +160,15 @@ export default async function componentSourceCollector(opts: Options = { safePac
181
160
  */
182
161
  async configResolved(resolved) {
183
162
  config = resolved;
184
- root = config.root;
185
- outputFilePath = resolve(root, outFileName);
163
+ outputFilePath = resolve(config.root, outFileName);
164
+
165
+ let current = config.root;
166
+ while (true) {
167
+ if (await exists(join(current, 'package.json'))) {
168
+ nodeModulesPath = join(current, 'node_modules');
169
+ break;
170
+ } else current = dirname(current);
171
+ }
186
172
 
187
173
  console.log('tailwind-sources:configResolved: Command is', config.command);
188
174
 
@@ -216,7 +202,7 @@ export default async function componentSourceCollector(opts: Options = { safePac
216
202
  'npm-shrinkwrap.json',
217
203
  // pnpm install-state changes:
218
204
  'node_modules/.modules.yaml'
219
- ].map((p) => join(root!, p));
205
+ ].map((p) => join(config.root!, p));
220
206
  server.watcher.add(lockFiles);
221
207
  const onChange = async (file: string) => {
222
208
  if (!lockFiles.includes(file)) return;
@@ -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
  });