@ripple-ts/vite-plugin 0.2.175 → 0.2.176
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 +3 -2
- package/src/index.js +307 -5
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.
|
|
6
|
+
"version": "0.2.176",
|
|
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.
|
|
29
|
+
"ripple": "0.2.176"
|
|
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
|
+
/** @typedef {{excludeRippleExternalModules?: boolean}} RipplePluginOptions */
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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,91 @@ export function ripple(inlineOptions) {
|
|
|
41
260
|
enforce: 'pre',
|
|
42
261
|
api,
|
|
43
262
|
|
|
44
|
-
async
|
|
45
|
-
|
|
263
|
+
async config(userConfig) {
|
|
264
|
+
if (excludeRippleExternalModules) {
|
|
265
|
+
console.log(
|
|
266
|
+
'[@ripple-ts/vite-plugin] Skipping node_modules scan (excludeRippleExternalModules is set to true)',
|
|
267
|
+
);
|
|
268
|
+
return {
|
|
269
|
+
optimizeDeps: {
|
|
270
|
+
exclude: userConfig.optimizeDeps?.exclude || [],
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Scan node_modules for Ripple packages early
|
|
276
|
+
console.log('[@ripple-ts/vite-plugin] Scanning for Ripple packages...');
|
|
277
|
+
const detectedPackages = scanForRipplePackages(userConfig.root || process.cwd());
|
|
278
|
+
detectedPackages.forEach((pkg) => {
|
|
279
|
+
ripplePackages.add(pkg);
|
|
280
|
+
});
|
|
281
|
+
const existingExclude = userConfig.optimizeDeps?.exclude || [];
|
|
282
|
+
console.log('[@ripple-ts/vite-plugin] Scan complete. Found:', detectedPackages);
|
|
283
|
+
console.log(
|
|
284
|
+
`[@ripple-ts/vite-plugin] Original vite.config 'optimizeDeps.exclude':`,
|
|
285
|
+
existingExclude,
|
|
286
|
+
);
|
|
287
|
+
// Merge with existing exclude list
|
|
288
|
+
const allExclude = [...new Set([...existingExclude, ...ripplePackages])];
|
|
289
|
+
|
|
290
|
+
console.log(`[@ripple-ts/vite-plugin] Merged 'optimizeDeps.exclude':`, allExclude);
|
|
291
|
+
console.log(
|
|
292
|
+
'[@ripple-ts/vite-plugin] Pass',
|
|
293
|
+
{ excludeRippleExternalModules: true },
|
|
294
|
+
`option to the 'ripple' plugin to skip this scan.`,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Return a config hook that will merge with user's config
|
|
298
|
+
return {
|
|
299
|
+
optimizeDeps: {
|
|
300
|
+
exclude: allExclude,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async configResolved(resolvedConfig) {
|
|
306
|
+
root = resolvedConfig.root;
|
|
307
|
+
config = resolvedConfig;
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
async resolveId(id, importer, options) {
|
|
311
|
+
// Skip non-package imports (relative/absolute paths)
|
|
312
|
+
if (id.startsWith('.') || id.startsWith('/') || id.includes(':')) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Extract package name and subpath (handle scoped packages)
|
|
317
|
+
let packageName;
|
|
318
|
+
let subpath = '.';
|
|
319
|
+
|
|
320
|
+
if (id.startsWith('@')) {
|
|
321
|
+
const parts = id.split('/');
|
|
322
|
+
packageName = parts.slice(0, 2).join('/');
|
|
323
|
+
subpath = parts.length > 2 ? './' + parts.slice(2).join('/') : '.';
|
|
324
|
+
} else {
|
|
325
|
+
const parts = id.split('/');
|
|
326
|
+
packageName = parts[0];
|
|
327
|
+
subpath = parts.length > 1 ? './' + parts.slice(1).join('/') : '.';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Skip if already detected
|
|
331
|
+
if (ripplePackages.has(packageName)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Try to find package.json
|
|
336
|
+
const pkgJsonPath = resolvePackageJson(packageName, root || process.cwd());
|
|
337
|
+
|
|
338
|
+
if (pkgJsonPath && hasRippleSource(pkgJsonPath, subpath)) {
|
|
339
|
+
ripplePackages.add(packageName);
|
|
340
|
+
|
|
341
|
+
// If we're in dev mode and config is available, update optimizeDeps
|
|
342
|
+
if (config?.command === 'serve') {
|
|
343
|
+
console.log(`[@ripple-ts/vite-plugin] Detected Ripple source package: ${packageName}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return null; // Let Vite handle the actual resolution
|
|
46
348
|
},
|
|
47
349
|
|
|
48
350
|
async load(id, opts) {
|