-
-
Notifications
You must be signed in to change notification settings - Fork 106
Expand file tree
/
Copy pathindex.js
More file actions
185 lines (155 loc) · 6.3 KB
/
index.js
File metadata and controls
185 lines (155 loc) · 6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import { posix, sep } from 'path';
import * as kl from 'kolorist';
import { memo } from './utils.js';
import { resolvePackageVersion, loadPackageFile, getPackageVersionFromDeps } from './registry.js';
import { resolveModule } from './resolve.js';
import { debug, formatPath } from '../../lib/output-utils.js';
/**
* @param {Object} options
* @param {string} [options.publicPath] URL path prefix to use for npm module scripts
* @param {string} [options.prefix] Import prefix to use internally for representing npm modules
* @param {boolean} [options.external] If `false`, resolved npm dependencies will be inlined by Rollup.
* @returns {import('rollup').Plugin}
*/
export default function npmPlugin({ publicPath = '/@npm', prefix = '\bnpm/', external = true } = {}) {
const log = debug('npm:plugin');
return {
name: 'npm-plugin',
async resolveId(id, importer) {
if (id[0] === '\0' || /^[\w-]+:/.test(id)) return;
if (importer) {
if (importer[0] === '\0') importer = '';
// replace windows paths
importer = importer
.replace(/^[A-Z]:/, '')
.split(sep)
.join('/');
}
if (id.startsWith(publicPath)) return { id, external };
if (id.startsWith(prefix)) id = id.substring(prefix.length);
else if (/^(?:\0|[a-z]+:|\/)/.test(id)) return;
if (importer && importer.startsWith(prefix)) importer = importer.substring(prefix.length);
// let module, path, version;
/** @type {ReturnType <normalizeSpecifier>} */
let meta;
const importerMeta = importer && !isDiskPath(importer) && normalizeSpecifier(importer);
let isExternal = false;
let isEntry = false;
let wasRelative = false;
// A relative import from within a module (resolve based on importer):
if (isDiskPath(id)) {
// not an npm module
if (!importerMeta) return;
meta = Object.assign({}, importerMeta);
meta.path = posix.join(posix.dirname(meta.path || ''), id);
wasRelative = true;
} else {
// An absolute, self or bare import
meta = normalizeSpecifier(id);
// not imported by an npm module, or imported by a _different_ module
if (!importerMeta || meta.specifier !== importerMeta.specifier) {
isEntry = true;
}
if (external && importerMeta && meta.specifier !== importerMeta.specifier) {
isExternal = true;
}
}
// use package.json version range from importer:
if (!meta.version && importerMeta) {
try {
const importerPkg = JSON.parse(
await loadPackageFile({
module: importerMeta.module,
version: importerMeta.version,
path: 'package.json'
})
);
const contextualVersion = getPackageVersionFromDeps(importerPkg, meta.module);
if (contextualVersion) {
meta.version = contextualVersion;
}
} catch (e) {}
}
meta.version = meta.version || '';
// Resolve @latest --> @10.4.1
await resolvePackageVersion(meta);
// Versions that resolve to the root are removed
// (see "Option 3" in wmr-middleware.jsL247)
let emitVersion = true;
if ((await resolvePackageVersion({ module: meta.module, version: '' })).version === meta.version) {
emitVersion = false;
// meta.version = '';
}
// Compute the final path
const { module, version, path } = meta;
const readFile = (path = '') => loadPackageFile({ module, version, path });
const hasFile = path =>
readFile(path)
.then(() => true)
.catch(() => false);
// When referencing a file within a package containing many files that import eachother,
// it's unfortunately impossible to know whether those imports should be bundled or not.
// Unless a package provides an export map, any file within a package could be referenced directly.
// That means we can't bundle the imports without risking duplication.
if (wasRelative) {
const pkg = JSON.parse(await loadPackageFile({ ...meta, path: 'package.json' }));
if (!pkg.exports) {
isExternal = true;
// directory traversal resolved to mainfield - remove to avoid duplicated modules:
if ([pkg.module, pkg.main, pkg.browser].some(f => f && f.replace(/^\.\//, '') === path)) {
meta.path = '';
}
}
}
// Mark everything except self-imports as external: (eg: "preact/hooks" importing "preact")
// Note: if `external=false` here, we're building a combined bundle and want to merge npm deps.
if (isExternal) {
const versionTag = emitVersion && meta.version ? '@' + meta.version : '';
id = `${meta.module}${versionTag}${meta.path ? '/' + meta.path : ''}`;
return { id: `${publicPath}/${id}`, external: true };
}
// If external=true, we're bundling a single module, so "no importer" means an entry
// If external=false, we're bundling a whole app, so "different importer" means an entry
const isInternalImport = external ? !!importer : !isEntry;
let resolvedPath = await resolveModule(path, {
readFile,
hasFile,
module,
internal: isInternalImport
});
resolvedPath = resolvedPath.replace(/^\//, '');
let res;
// CSS files are not handled by this plugin.
if (/\.css$/.test(id) && (await hasFile(resolvedPath))) {
res = `./node_modules/${meta.module}/${resolvedPath}`;
log(`${kl.cyan(formatPath(id))} -> ${kl.dim(formatPath(res))}`);
return res;
}
res = `${prefix}${meta.module}${meta.version ? '@' + meta.version : ''}/${resolvedPath}`;
log(`${kl.cyan(formatPath(id))} -> ${kl.dim(formatPath(res))}`);
return res;
},
load(id) {
// only load modules this plugin resolved
if (!id.startsWith(prefix)) return;
id = id.substring(prefix.length);
const spec = normalizeSpecifier(id);
return loadPackageFile(spec);
}
};
}
const PACKAGE_SPECIFIER = /^((?:@[\w.-]{1,200}\/)?[\w.-]{1,200})(?:@([a-z0-9^.~>=<-]{1,50}))?(?:\/(.*))?$/i;
export const normalizeSpecifier = memo(spec => {
let [, module = '', version = '', path = ''] = spec.match(PACKAGE_SPECIFIER) || [];
if (!module) throw Error(`Invalid specifier: ${spec}`);
version = (version || '').toLowerCase();
module = module.toLowerCase();
const specifier = module + (path ? '/' + path : '');
return { module, version, path, specifier };
});
/** @param {string} filename */
function isDiskPath(filename) {
// only check for windows paths if we're on windows
if (sep === '\\' && /^(([A-Z]+:)?\\|\.\.?(\\|$))/.test(filename)) return true;
return /^(file:\/\/)?([A-Z]:)?(\/|\.\.?(\/|$))/.test(filename);
}