UNPKG

10.4 kBJavaScriptView Raw
1const {ext, parse, nodejsIds, resolveModuleId, relativeModuleId} = require('dumber-module-loader/dist/id-utils');
2const {stripSourceMappingUrl, getSourceMap} = require('./shared');
3const {info, error} = require('./log');
4const path = require('path');
5
6module.exports = class PackageReader {
7 constructor(fileReader) {
8 this.fileReader = fileReader;
9 this._readFile = this._readFile.bind(this);
10 }
11
12 banner() {
13 const {version, name} = this;
14 const config = this.fileReader.packageConfig;
15
16 let info = [];
17 if (config.location) {
18 const location = path.relative(process.cwd(), config.location);
19 info.push('location: ' + location);
20 }
21 if (config.main) {
22 info.push('main: ' + config.main);
23 }
24 let result = `${version.padEnd(10)} ${name}`;
25 if (info.length) result += ` { ${info.join(', ')} }`;
26 return result;
27 }
28
29 ensureMainPath() {
30 if (this.hasOwnProperty('mainPath')) return Promise.resolve();
31
32 return this.fileReader('package.json')
33 .then(file => {
34 let metadata = JSON.parse(file.contents);
35 this.name = metadata.name;
36 this.version = metadata.version || 'N/A';
37 this.browserReplacement = _browserReplacement(metadata.browser);
38
39 const main = _main(metadata);
40 // Need to resolve main to real main path.
41 // For example, main "./lib" could be resolved to
42 // "./lib.js", "./lib.json", "./lib/index.js"
43 return this._nodejsLoadAsFile(main)
44 .catch(() => this._nodejsLoadIndex(main))
45 // When main file is missing, fall back to index.js
46 .catch(() => 'index.js');
47 })
48 .then(mainPath => {
49 const replacement = this.browserReplacement['./' + mainPath];
50 if (replacement) {
51 // replacement is always local, remove leading ./
52 mainPath = replacement.slice(2);
53 }
54
55 this.mainPath = mainPath;
56 this.parsedMainId = parse(mainPath);
57 return this;
58 });
59 }
60
61 readMain() {
62 return this.ensureMainPath()
63 .then(() => this._readFile(this.mainPath));
64 }
65
66 readResource(resource) {
67 return this.ensureMainPath().then(() => {
68 let parts = this.parsedMainId.parts;
69 let len = parts.length;
70 let i = 0;
71
72 const findResource = () => {
73 if (i >= len) {
74 return Promise.reject(new Error(`could not find "${resource}" in package ${this.name}`));
75 }
76
77 let resParts = parts.slice(0, i);
78 resParts.push(resource);
79
80 let fullResource = resParts.join('/');
81
82 const replacement = this.browserReplacement['./' + fullResource] ||
83 this.browserReplacement['./' + fullResource + '.js'];
84 if (replacement) {
85 // replacement is always local, remove leading ./
86 fullResource = replacement.slice(2);
87 }
88
89 return this._nodejsLoad(fullResource).then(
90 filePath => this._readFile(filePath),
91 () => {
92 i += 1;
93 return findResource();
94 }
95 );
96 }
97
98 return findResource();
99 }).then(unit => {
100 const requested = this.name + '/' + resource;
101
102 if (nodejsIds(requested).indexOf(unit.moduleId) === -1) {
103 // The requested id could be different from real id.
104 // for example, some browser replacement.
105 // e.g. readable-stream/readable -> readable-stream/readable-browser
106 if (unit.alias) {
107 unit.alias = [unit.alias, requested];
108 } else {
109 unit.alias = requested;
110 }
111 }
112 return unit;
113 });
114 }
115
116 // readFile contents, cleanup dep, normalise browser replacement
117 _readFile(filePath) {
118 if (!this._logged) {
119 this._logged = true;
120 info(this.banner());
121 }
122
123 return this.fileReader(filePath).then(file => {
124 // Bypass traced cache
125 if (file.defined) return file;
126
127 const moduleId = this.name + '/' + parse(filePath).bareId;
128
129 let replacement;
130 if (normalizeExt(filePath) === '.js') {
131 Object.keys(this.browserReplacement).forEach(key => {
132 const target = this.browserReplacement[key];
133 const baseId = this.name + '/index';
134 const sourceModule = key.startsWith('.') ?
135 relativeModuleId(moduleId, resolveModuleId(baseId, key)) :
136 key;
137
138 let targetModule;
139 if (target) {
140 targetModule = relativeModuleId(moduleId, resolveModuleId(baseId, target));
141 } else {
142 // {"module-a": false}
143 // replace with special placeholder __ignore__
144 targetModule = '__ignore__';
145 }
146
147 if (!replacement) replacement = {};
148 replacement[sourceModule] = targetModule;
149 });
150 }
151
152 const unit = {
153 path: file.path.replace(/\\/g, '/'),
154 contents: stripSourceMappingUrl(file.contents),
155 moduleId,
156 packageName: this.name,
157 packageMainPath: this.mainPath,
158 sourceMap: getSourceMap(file.contents, file.path)
159 };
160
161 // the replacement will be picked up by transformers/replace.js
162 if (replacement) unit.replacement = replacement;
163
164 if (unit.moduleId === this.name + '/' + this.parsedMainId.bareId) {
165 // add alias from package name to main file module id.
166 // but don't add alias from "foo" to "foo/main.css".
167 const mExt = normalizeExt(unit.path);
168 const pExt = normalizeExt(this.name);
169 if (mExt === pExt || (mExt === '.js' && !pExt)) {
170 unit.alias = this.name;
171 }
172 }
173
174 return unit;
175 });
176 }
177
178 _whenFileExists(p) {
179 return this.fileReader.exists(p)
180 .then(exists => {
181 if (!exists) throw new Error('File does not exist: ' + p);
182 });
183 }
184
185 // https://nodejs.org/dist/latest-v10.x/docs/api/modules.html
186 // after "high-level algorithm in pseudocode of what require.resolve() does"
187 _nodejsLoadAsFile(filePath) {
188 return this._whenFileExists(filePath).then(
189 () => filePath,
190 () => {
191 const jsFilePath = filePath + '.js';
192 return this._whenFileExists(jsFilePath).then(
193 () => jsFilePath,
194 () => {
195 const jsonFilePath = filePath + '.json';
196 return this._whenFileExists(jsonFilePath).then(
197 () => jsonFilePath
198 );
199 }
200 );
201 }
202 ).then(
203 p => p.replace(/\\/g, '/'), // normalize path
204 () => {
205 throw new Error(`cannot load Nodejs file for ${filePath}`)
206 }
207 );
208 }
209
210 _nodejsLoadIndex(dirPath) {
211 const indexJsFilePath = path.join(dirPath, 'index.js');
212 return this._whenFileExists(indexJsFilePath).then(
213 () => indexJsFilePath,
214 () => {
215 const indexJsonFilePath = path.join(dirPath, 'index.json');
216 return this._whenFileExists(indexJsonFilePath).then(
217 () => indexJsonFilePath
218 );
219 }
220 ).then(
221 p => p.replace(/\\/g, '/'), // normalize path
222 () => {
223 throw new Error(`cannot load Nodejs index file for ${dirPath}`)
224 }
225 );
226 }
227
228 _nodejsLoadAsDirectory(dirPath) {
229 const packageJsonPath = path.join(dirPath, 'package.json');
230 return this.fileReader(packageJsonPath).then(
231 file => {
232 let metadata;
233 try {
234 metadata = JSON.parse(file.contents);
235 } catch (err) {
236 error('Failed to parse ' + packageJsonPath);
237 error(err);
238 throw err;
239 }
240
241 // path.join also cleans up leading './'.
242 const mainResourcePath = path.join(dirPath, _main(metadata));
243
244 return this._nodejsLoadAsFile(mainResourcePath)
245 .catch(() => this._nodejsLoadIndex(mainResourcePath));
246 },
247 () => this._nodejsLoadIndex(dirPath)
248 ).catch(() => {
249 throw new Error(`cannot load Nodejs directory for ${dirPath}`)
250 });
251 }
252
253 _nodejsLoad(filePath) {
254 return this._nodejsLoadAsFile(filePath)
255 .catch(() => this._nodejsLoadAsDirectory(filePath));
256 }
257};
258
259function _main(metadata) {
260 let main;
261 // try 1.browser > 2.module > 3.main
262 // the order is to target browser.
263 // it probably should use different order for electron app
264 // for electron 1.module > 2.browser > 3.main
265
266 // dumberForcedMain is not in package.json.
267 // it is the forced main override in dumber config,
268 // set by package-file-reader/default.js and
269 // package-file-reader/jsdelivr.js.
270 if (typeof metadata.dumberForcedMain === 'string') {
271 main = metadata.dumberForcedMain;
272 } else if (typeof metadata.browser === 'string') {
273 // use package.json browser field if possible.
274 main = metadata.browser;
275 } else if (typeof metadata.browser === 'object' &&
276 typeof metadata.browser['.'] === 'string') {
277 // use package.json browser mapping {".": "dist/index.js"} if possible.
278 main = metadata.browser['.'];
279 } else if (typeof metadata.module === 'string' &&
280 !(metadata.name && metadata.name.startsWith('aurelia-'))) {
281 // prefer es module format over cjs, just like webpack.
282 // this improves compatibility with TypeScript.
283 // ignores aurelia-* core npm packages as some module
284 // field might still point to es2015 folder.
285 main = metadata.module;
286 } else if (typeof metadata.main === 'string') {
287 main = metadata.main;
288 }
289
290 main = main || 'index';
291 if (main.indexOf('./') === 0) main = main.slice(2);
292 return main;
293}
294
295// https://github.com/defunctzombie/package-browser-field-spec
296function _browserReplacement(browser) {
297 // string browser field is alternative main
298 if (!browser || typeof browser === 'string') return {};
299
300 let replacement = {};
301
302 Object.keys(browser).forEach(key => {
303 // leave {".": "dist/index.js"} to the main field replacment
304 if (key === '.') return;
305 const target = browser[key];
306
307 let sourceModule = filePathToModuleId(key);
308
309 if (typeof target === 'string') {
310 let targetModule = filePathToModuleId(target);
311 if (!targetModule.startsWith('.')) {
312 // replacement is always local
313 targetModule = './' + targetModule;
314 }
315 replacement[sourceModule] = targetModule;
316 } else {
317 replacement[sourceModule] = false;
318 }
319 });
320
321 return replacement;
322}
323
324function filePathToModuleId(filePath) {
325 return parse(filePath.replace(/\\/g, '/')).bareId;
326}
327
328function normalizeExt(path) {
329 const e = ext(path);
330 if (e === '.mjs' || e === '.cjs') return '.js';
331 return e;
332}