/
index.js
167 lines (139 loc) · 5.54 KB
/
index.js
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
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 = 'npm/', 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<typeof normalizeSpecifier>} */
let meta;
const importerMeta = importer && !isDiskPath(importer) && normalizeSpecifier(importer);
let isExternal = false;
let isEntry = 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);
} 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 = '';
}
// 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: 'absolute' };
}
// 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);
// 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);
}