@ripple-ts/vite-plugin 0.2.175 → 0.2.177

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Vite plugin for Ripple",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.175",
6
+ "version": "0.2.177",
7
7
  "type": "module",
8
8
  "module": "src/index.js",
9
9
  "main": "src/index.js",
@@ -24,7 +24,8 @@
24
24
  "url": "https://github.com/Ripple-TS/ripple/issues"
25
25
  },
26
26
  "devDependencies": {
27
+ "type-fest": "^5.1.0",
27
28
  "vite": "^7.1.9",
28
- "ripple": "0.2.175"
29
+ "ripple": "0.2.177"
29
30
  }
30
31
  }
package/src/index.js CHANGED
@@ -1,9 +1,19 @@
1
+ /** @import {PackageJson} from 'type-fest' */
2
+ /** @import {Plugin, ResolvedConfig} from 'vite' */
3
+ /** @import {RipplePluginOptions} from '@ripple-ts/vite-plugin' */
4
+
1
5
  import { compile } from 'ripple/compiler';
2
6
  import fs from 'node:fs';
7
+ import { createRequire } from 'node:module';
3
8
 
4
9
  const VITE_FS_PREFIX = '/@fs/';
5
10
  const IS_WINDOWS = process.platform === 'win32';
6
11
 
12
+ /**
13
+ * @param {string} filename
14
+ * @param {ResolvedConfig['root']} root
15
+ * @returns {boolean}
16
+ */
7
17
  function existsInRoot(filename, root) {
8
18
  if (filename.startsWith(VITE_FS_PREFIX)) {
9
19
  return false; // vite already tagged it as out of root
@@ -11,6 +21,12 @@ function existsInRoot(filename, root) {
11
21
  return fs.existsSync(root + filename);
12
22
  }
13
23
 
24
+ /**
25
+ * @param {string} filename
26
+ * @param {ResolvedConfig['root']} root
27
+ * @param {'style'} type
28
+ * @returns {string}
29
+ */
14
30
  function createVirtualImportId(filename, root, type) {
15
31
  const parts = ['ripple', `type=${type}`];
16
32
  if (type === 'style') {
@@ -27,13 +43,216 @@ function createVirtualImportId(filename, root, type) {
27
43
  return `${filename}?${parts.join('&')}`;
28
44
  }
29
45
 
30
- export function ripple(inlineOptions) {
31
- const api = {};
46
+ /**
47
+ * Check if a package contains Ripple source files by examining its package.json
48
+ * @param {string} packageJsonPath
49
+ * @param {string} subpath - The subpath being imported (e.g., '.' or './foo')
50
+ * @returns {boolean}
51
+ */
52
+ function hasRippleSource(packageJsonPath, subpath = '.') {
53
+ try {
54
+ /** @type {PackageJson} */
55
+ const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
32
56
 
33
- let root;
57
+ // Check if main/module/exports point to .ripple files
58
+ /** @param {string | undefined} p */
59
+ const checkPath = (p) => p && typeof p === 'string' && p.endsWith('.ripple');
60
+
61
+ // Handle exports field (modern)
62
+ if (pkgJson.exports) {
63
+ /**
64
+ * @param {PackageJson.Exports} exports
65
+ * @returns {string | null}
66
+ */
67
+ const resolveExport = (exports) => {
68
+ if (typeof exports === 'string') {
69
+ return exports;
70
+ }
71
+ if (typeof exports === 'object' && exports !== null) {
72
+ // Try import condition first, then default
73
+ const exp = /** @type {Record<string, PackageJson.Exports>} */ (exports);
74
+ if (typeof exp.import === 'string') {
75
+ return exp.import;
76
+ }
77
+ if (typeof exp.default === 'string') {
78
+ return exp.default;
79
+ }
80
+ // Recursively check nested conditions
81
+ for (const value of Object.values(exp)) {
82
+ const resolved = resolveExport(value);
83
+ if (resolved) return resolved;
84
+ }
85
+ }
86
+ return null;
87
+ };
88
+
89
+ // Get the exports value for the subpath
90
+ /** @type {PackageJson.Exports | undefined} */
91
+ const exportsValue =
92
+ typeof pkgJson.exports === 'string'
93
+ ? pkgJson.exports
94
+ : typeof pkgJson.exports === 'object' && pkgJson.exports !== null
95
+ ? /** @type {Record<string, PackageJson.Exports>} */ (pkgJson.exports)[subpath]
96
+ : undefined;
97
+
98
+ if (exportsValue) {
99
+ const resolved = resolveExport(exportsValue);
100
+ if (resolved && checkPath(resolved)) {
101
+ return true;
102
+ }
103
+ }
104
+ }
105
+
106
+ // Fallback to main/module for root imports
107
+ if (subpath === '.') {
108
+ if (checkPath(pkgJson.main) || checkPath(pkgJson.module)) {
109
+ return true;
110
+ }
111
+ }
112
+
113
+ // Last resort: scan the package directory for .ripple files
114
+ const packageDir = packageJsonPath.replace('/package.json', '');
115
+ return hasRippleFilesInDirectory(packageDir);
116
+ } catch (e) {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Recursively check if a directory contains any .ripple files
123
+ * @param {string} dir
124
+ * @param {number} [maxDepth=3]
125
+ * @returns {boolean}
126
+ */
127
+ function hasRippleFilesInDirectory(dir, maxDepth = 3) {
128
+ if (maxDepth <= 0) return false;
129
+
130
+ try {
131
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
132
+
133
+ for (const entry of entries) {
134
+ // Skip node_modules and hidden directories
135
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
136
+ continue;
137
+ }
138
+
139
+ if (entry.isFile() && entry.name.endsWith('.ripple')) {
140
+ return true;
141
+ }
142
+
143
+ if (entry.isDirectory()) {
144
+ const subDir = dir + '/' + entry.name;
145
+ if (hasRippleFilesInDirectory(subDir, maxDepth - 1)) {
146
+ return true;
147
+ }
148
+ }
149
+ }
150
+ } catch (e) {
151
+ // Ignore errors
152
+ }
153
+
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Try to resolve a package's package.json from node_modules
159
+ * @param {string} packageName
160
+ * @param {string} fromDir
161
+ * @returns {string | null}
162
+ */
163
+ function resolvePackageJson(packageName, fromDir) {
164
+ try {
165
+ const require = createRequire(fromDir + '/package.json');
166
+ const packagePath = require.resolve(packageName + '/package.json');
167
+ return packagePath;
168
+ } catch (e) {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Scan node_modules for packages containing Ripple source files
175
+ * @param {string} rootDir
176
+ * @returns {string[]}
177
+ */
178
+ function scanForRipplePackages(rootDir) {
179
+ /** @type {string[]} */
180
+ const ripplePackages = [];
181
+ const nodeModulesPath = rootDir + '/node_modules';
182
+
183
+ if (!fs.existsSync(nodeModulesPath)) {
184
+ return ripplePackages;
185
+ }
186
+
187
+ try {
188
+ // Read all directories in node_modules
189
+ const entries = fs.readdirSync(nodeModulesPath, { withFileTypes: true });
190
+
191
+ for (const entry of entries) {
192
+ // Skip .pnpm and other hidden directories
193
+ if (entry.name.startsWith('.')) continue;
194
+
195
+ // Handle scoped packages (@org/package)
196
+ if (entry.name.startsWith('@')) {
197
+ const scopePath = nodeModulesPath + '/' + entry.name;
198
+ try {
199
+ const scopedEntries = fs.readdirSync(scopePath, { withFileTypes: true });
34
200
 
201
+ for (const scopedEntry of scopedEntries) {
202
+ if (scopedEntry.name.startsWith('.')) continue;
203
+ const packageName = entry.name + '/' + scopedEntry.name;
204
+ const pkgPath = scopePath + '/' + scopedEntry.name;
205
+
206
+ // Follow symlinks to get the real path
207
+ const realPath = fs.realpathSync(pkgPath);
208
+ const pkgJsonPath = realPath + '/package.json';
209
+
210
+ if (fs.existsSync(pkgJsonPath) && hasRippleSource(pkgJsonPath, '.')) {
211
+ ripplePackages.push(packageName);
212
+ }
213
+ }
214
+ } catch (e) {
215
+ // Skip if can't read scoped directory
216
+ }
217
+ } else {
218
+ // Regular package
219
+ const pkgPath = nodeModulesPath + '/' + entry.name;
220
+
221
+ try {
222
+ // Follow symlinks to get the real path
223
+ const realPath = fs.realpathSync(pkgPath);
224
+ const pkgJsonPath = realPath + '/package.json';
225
+
226
+ if (fs.existsSync(pkgJsonPath) && hasRippleSource(pkgJsonPath, '.')) {
227
+ ripplePackages.push(entry.name);
228
+ }
229
+ } catch (e) {
230
+ // Skip if can't resolve symlink
231
+ }
232
+ }
233
+ }
234
+ } catch (e) {
235
+ // Ignore errors during scanning
236
+ }
237
+
238
+ return ripplePackages;
239
+ }
240
+
241
+ /**
242
+ * @param {RipplePluginOptions} [inlineOptions]
243
+ * @returns {Plugin[]}
244
+ */
245
+ export function ripple(inlineOptions = {}) {
246
+ const { excludeRippleExternalModules = false } = inlineOptions;
247
+ const api = {};
248
+ /** @type {ResolvedConfig['root']} */
249
+ let root;
250
+ /** @type {ResolvedConfig} */
251
+ let config;
252
+ const ripplePackages = new Set();
35
253
  const cssCache = new Map();
36
254
 
255
+ /** @type {Plugin[]} */
37
256
  const plugins = [
38
257
  {
39
258
  name: 'vite-plugin',
@@ -41,8 +260,88 @@ export function ripple(inlineOptions) {
41
260
  enforce: 'pre',
42
261
  api,
43
262
 
44
- async configResolved(config) {
45
- root = config.root;
263
+ async config(userConfig) {
264
+ if (excludeRippleExternalModules) {
265
+ return {
266
+ optimizeDeps: {
267
+ exclude: userConfig.optimizeDeps?.exclude || [],
268
+ },
269
+ };
270
+ }
271
+
272
+ // Scan node_modules for Ripple packages early
273
+ console.log('[@ripple-ts/vite-plugin] Scanning for Ripple packages...');
274
+ const detectedPackages = scanForRipplePackages(userConfig.root || process.cwd());
275
+ detectedPackages.forEach((pkg) => {
276
+ ripplePackages.add(pkg);
277
+ });
278
+ const existingExclude = userConfig.optimizeDeps?.exclude || [];
279
+ console.log('[@ripple-ts/vite-plugin] Scan complete. Found:', detectedPackages);
280
+ console.log(
281
+ `[@ripple-ts/vite-plugin] Original vite.config 'optimizeDeps.exclude':`,
282
+ existingExclude,
283
+ );
284
+ // Merge with existing exclude list
285
+ const allExclude = [...new Set([...existingExclude, ...ripplePackages])];
286
+
287
+ console.log(`[@ripple-ts/vite-plugin] Merged 'optimizeDeps.exclude':`, allExclude);
288
+ console.log(
289
+ '[@ripple-ts/vite-plugin] Pass',
290
+ { excludeRippleExternalModules: true },
291
+ `option to the 'ripple' plugin to skip this scan.`,
292
+ );
293
+
294
+ // Return a config hook that will merge with user's config
295
+ return {
296
+ optimizeDeps: {
297
+ exclude: allExclude,
298
+ },
299
+ };
300
+ },
301
+
302
+ async configResolved(resolvedConfig) {
303
+ root = resolvedConfig.root;
304
+ config = resolvedConfig;
305
+ },
306
+
307
+ async resolveId(id, importer, options) {
308
+ // Skip non-package imports (relative/absolute paths)
309
+ if (id.startsWith('.') || id.startsWith('/') || id.includes(':')) {
310
+ return null;
311
+ }
312
+
313
+ // Extract package name and subpath (handle scoped packages)
314
+ let packageName;
315
+ let subpath = '.';
316
+
317
+ if (id.startsWith('@')) {
318
+ const parts = id.split('/');
319
+ packageName = parts.slice(0, 2).join('/');
320
+ subpath = parts.length > 2 ? './' + parts.slice(2).join('/') : '.';
321
+ } else {
322
+ const parts = id.split('/');
323
+ packageName = parts[0];
324
+ subpath = parts.length > 1 ? './' + parts.slice(1).join('/') : '.';
325
+ }
326
+
327
+ // Skip if already detected
328
+ if (ripplePackages.has(packageName)) {
329
+ return null;
330
+ }
331
+
332
+ // Try to find package.json
333
+ const pkgJsonPath = resolvePackageJson(packageName, root || process.cwd());
334
+
335
+ if (pkgJsonPath && hasRippleSource(pkgJsonPath, subpath)) {
336
+ ripplePackages.add(packageName);
337
+
338
+ // If we're in dev mode and config is available, update optimizeDeps
339
+ if (config?.command === 'serve') {
340
+ console.log(`[@ripple-ts/vite-plugin] Detected Ripple source package: ${packageName}`);
341
+ }
342
+ }
343
+
344
+ return null; // Let Vite handle the actual resolution
46
345
  },
47
346
 
48
347
  async load(id, opts) {
package/types/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
- declare module 'vite-plugin' {
2
- export function ripple(): any
1
+ import type { Plugin } from 'vite';
2
+
3
+ declare module '@ripple-ts/vite-plugin' {
4
+ export function ripple(options?: RipplePluginOptions): Plugin[];
5
+
6
+ export interface RipplePluginOptions {
7
+ excludeRippleExternalModules?: boolean;
8
+ }
3
9
  }