Skip to content

Commit 52bf8f8

Browse files
author
Ives van Hoorne
committed
Make dynamic fetching robust & provide dynamic polyfills for node libraries
1 parent 1d078ea commit 52bf8f8

File tree

8 files changed

+263
-91
lines changed

8 files changed

+263
-91
lines changed

src/sandbox/eval/loaders/eval.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
// @flow
2+
import buildProcess from './utils/process';
23

34
/* eslint-disable no-unused-vars */
45
export default function(code: string, require: Function, exports) {
56
const module = { exports: {} };
67
const global = window;
7-
const process = { env: { NODE_ENV: 'development' } };
8+
const process = buildProcess();
89

910
try {
1011
eval(code); // eslint-disable-line no-eval
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const process = {};
2+
process.title = 'browser';
3+
process.browser = true;
4+
process.env = { NODE_ENV: 'development' };
5+
process.argv = [];
6+
process.version = ''; // empty string to avoid regexp issues
7+
process.versions = {};
8+
9+
function noop() {}
10+
11+
process.on = noop;
12+
process.addListener = noop;
13+
process.once = noop;
14+
process.off = noop;
15+
process.removeListener = noop;
16+
process.removeAllListeners = noop;
17+
process.emit = noop;
18+
process.prependListener = noop;
19+
process.prependOnceListener = noop;
20+
21+
process.listeners = function(name) {
22+
return [];
23+
};
24+
25+
process.binding = function(name) {
26+
throw new Error('process.binding is not supported');
27+
};
28+
29+
process.cwd = function() {
30+
return '/';
31+
};
32+
process.chdir = function(dir) {
33+
throw new Error('process.chdir is not supported');
34+
};
35+
process.umask = function() {
36+
return 0;
37+
};
38+
39+
export default function build() {
40+
return process;
41+
}

src/sandbox/eval/manager.js

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import * as pathUtils from 'common/utils/path';
77
import type { Module } from './entities/module';
88
import TranspiledModule from './transpiled-module';
99
import Preset from './presets';
10-
import fetchModule from './npm/fetch-npm-module';
10+
import fetchModule, { getCombinedMetas } from './npm/fetch-npm-module';
11+
import coreLibraries from './npm/get-core-libraries';
1112
import getDependencyName from './utils/get-dependency-name';
1213
import DependencyNotFoundError from '../errors/dependency-not-found-error';
1314
import ModuleNotFoundError from '../errors/module-not-found-error';
@@ -26,7 +27,11 @@ export type Manifest = {
2627
},
2728
dependencies: Array<{ name: string, version: string }>,
2829
dependencyDependencies: {
29-
[name: string]: string,
30+
[name: string]: {
31+
semver: string,
32+
resolved: string,
33+
parents: string[],
34+
},
3035
},
3136
dependencyAliases: {
3237
[name: string]: {
@@ -35,7 +40,7 @@ export type Manifest = {
3540
},
3641
};
3742

38-
const NODE_LIBS = ['dgram', 'fs', 'path', 'net', 'tls', 'child_process'];
43+
const NODE_LIBS = ['dgram', 'fs', 'net', 'tls', 'child_process'];
3944

4045
export default class Manager {
4146
id: string;
@@ -237,15 +242,13 @@ export default class Manager {
237242
currentPath: string,
238243
defaultExtensions: Array<string> = ['js', 'jsx', 'json']
239244
): Module {
240-
const isDependency = /^(\w|@\w)/.test(path);
241-
242245
const aliasedPath = this.getAliasedDependencyPath(path, currentPath);
243-
246+
const shimmedPath = coreLibraries[aliasedPath] || aliasedPath;
244247
try {
245-
const resolvedPath = resolve.sync(aliasedPath, {
248+
const resolvedPath = resolve.sync(shimmedPath, {
246249
filename: currentPath,
247250
extensions: defaultExtensions.map(ext => '.' + ext),
248-
isFile: p => !!this.transpiledModules[p],
251+
isFile: p => !!this.transpiledModules[p] || !!getCombinedMetas()[p],
249252
readFileSync: p => {
250253
if (this.transpiledModules[p]) {
251254
return this.transpiledModules[p].module.code;
@@ -268,35 +271,41 @@ export default class Manager {
268271

269272
return this.transpiledModules[resolvedPath].module;
270273
} catch (e) {
274+
let connectedPath = /^(\w|@\w)/.test(shimmedPath)
275+
? pathUtils.join('/node_modules', shimmedPath)
276+
: pathUtils.join(pathUtils.dirname(currentPath), shimmedPath);
277+
278+
const isDependency = connectedPath.includes('/node_modules/');
279+
280+
connectedPath = connectedPath.replace('/node_modules/', '');
281+
271282
if (!isDependency) {
272-
throw new ModuleNotFoundError(aliasedPath, false);
283+
throw new ModuleNotFoundError(shimmedPath, false);
273284
}
274285

275-
const dependencyName = getDependencyName(path);
286+
const dependencyName = getDependencyName(connectedPath);
276287

277288
if (
278289
this.manifest.dependencies.find(d => d.name === dependencyName) ||
279290
this.manifest.dependencyDependencies[dependencyName]
280291
) {
281-
throw new ModuleNotFoundError(aliasedPath, true);
292+
throw new ModuleNotFoundError(connectedPath, true);
282293
} else {
283-
throw new DependencyNotFoundError(path);
294+
throw new DependencyNotFoundError(connectedPath);
284295
}
285296
}
286297
}
287298

288-
downloadPromises = {};
289-
290-
async downloadDependency(path: string): Promise<TranspiledModule> {
291-
this.downloadPromises[path] =
292-
this.downloadPromises[path] ||
293-
fetchModule(
294-
path,
295-
this.manifest,
296-
this.preset.ignoredExtensions
297-
).then(module => this.getTranspiledModule(module));
298-
299-
return this.downloadPromises[path];
299+
async downloadDependency(
300+
path: string,
301+
currentPath: string
302+
): Promise<TranspiledModule> {
303+
return fetchModule(
304+
path,
305+
currentPath,
306+
this,
307+
this.preset.ignoredExtensions
308+
).then(module => this.getTranspiledModule(module));
300309
}
301310

302311
/**
Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// @flow
22
import * as pathUtils from 'common/utils/path';
3+
import resolve from 'browser-resolve';
34

45
import type { Module } from '../entities/module';
5-
import type { Manifest } from '../manager';
6+
import Manager from '../manager';
67

78
import DependencyNotFoundError from '../../errors/dependency-not-found-error';
8-
import nodeResolvePath from '../utils/node-resolve-path';
9+
import getDependencyName from '../utils/get-dependency-name';
910

1011
type Meta = {
1112
[path: string]: any,
@@ -21,88 +22,151 @@ type Packages = {
2122
type MetaFiles = Array<{ path: string, files?: MetaFiles }>;
2223

2324
const metas: Metas = {};
25+
let combinedMetas: Meta = {};
2426
const packages: Packages = {};
2527

26-
function normalize(files: MetaFiles, fileObject: Meta = {}) {
28+
export function getCombinedMetas() {
29+
return combinedMetas;
30+
}
31+
32+
function normalize(depName: string, files: MetaFiles, fileObject: Meta = {}) {
2733
for (let i = 0; i < files.length; i += 1) {
28-
fileObject[files[i].path] = true; // eslint-disable-line no-param-reassign
34+
const absolutePath = pathUtils.join(
35+
'/node_modules',
36+
depName,
37+
files[i].path
38+
);
39+
fileObject[absolutePath] = true; // eslint-disable-line no-param-reassign
2940

3041
if (files[i].files) {
31-
normalize(files[i].files, fileObject);
42+
normalize(depName, files[i].files, fileObject);
3243
}
3344
}
3445

3546
return fileObject;
3647
}
3748

3849
function getMeta(name: string, version: string) {
39-
if (metas[`${name}@${version}`]) {
40-
return metas[`${name}@${version}`];
50+
const id = `${name}@${version}`;
51+
if (metas[id]) {
52+
return metas[id];
4153
}
4254

43-
return window
55+
metas[id] = window
4456
.fetch(`https://unpkg.com/${name}@${version}/?meta`)
4557
.then(x => x.json())
46-
.then(metaInfo => {
47-
const normalizedMetaInfo = normalize(metaInfo.files);
48-
// rewrite to path: any object
49-
metas[`${name}@${version}`] = normalizedMetaInfo;
50-
51-
return normalizedMetaInfo;
58+
.then(metaInfo => normalize(name, metaInfo.files))
59+
.then(normalizedMetas => {
60+
combinedMetas = { ...combinedMetas, ...normalizedMetas };
61+
return normalizedMetas;
5262
});
63+
64+
return metas[id];
5365
}
5466

55-
export default async function fetchModule(
56-
path: string,
57-
manifest: Manifest,
58-
defaultExtensions: Array<string> = ['js', 'jsx', 'json']
59-
): Promise<Module> {
67+
function downloadDependency(depName: string, depVersion: string, path: string) {
6068
if (packages[path]) {
6169
return packages[path];
6270
}
6371

72+
const relativePath = path.replace(
73+
pathUtils.join('/node_modules', depName),
74+
''
75+
);
76+
77+
packages[path] = window
78+
.fetch(`https://unpkg.com/${depName}@${depVersion}${relativePath}`)
79+
.then(x => {
80+
if (x.ok) {
81+
return x.text();
82+
}
83+
84+
return `throw new Error("Could not find module ${path}`;
85+
})
86+
.then(x => ({
87+
path,
88+
code: x,
89+
}));
90+
91+
return packages[path];
92+
}
93+
94+
export default async function fetchModule(
95+
path: string,
96+
currentPath: string,
97+
manager: Manager,
98+
defaultExtensions: Array<string> = ['js', 'jsx', 'json']
99+
): Promise<Module> {
64100
const installedDependencies = {
65-
...manifest.dependencies.reduce(
101+
...manager.manifest.dependencies.reduce(
66102
(t, n) => ({ ...t, [n.name]: n.version }),
67103
{}
68104
),
69-
...manifest.dependencyDependencies,
105+
...manager.manifest.dependencyDependencies,
70106
};
71107

72-
const dependencyParts = path.split('/');
73-
const dependencyName = path.startsWith('@')
74-
? `${dependencyParts[0]}/${dependencyParts[1]}`
75-
: dependencyParts[0];
108+
const dependencyName = getDependencyName(path);
76109

77110
const version = installedDependencies[dependencyName];
78111

79112
if (!version) {
80113
throw new DependencyNotFoundError(path);
81114
}
82115

83-
const meta = await getMeta(dependencyName, version);
84-
85-
const resolvedPath = nodeResolvePath(
86-
path.replace(dependencyName, ''),
87-
meta,
88-
defaultExtensions
89-
);
90-
91-
return window
92-
.fetch(`https://unpkg.com/${dependencyName}@${version}${resolvedPath}`)
93-
.then(x => {
94-
if (x.ok) {
95-
return x.text();
116+
const meta = await getMeta(dependencyName, version.resolved);
117+
118+
return new Promise((res, reject) => {
119+
resolve(
120+
path,
121+
{
122+
filename: currentPath,
123+
extensions: defaultExtensions.map(ext => '.' + ext),
124+
isFile: (p, c) => c(null, !!manager.transpiledModules[p] || !!meta[p]),
125+
readFile: async (p, c, cb) => {
126+
const callback = cb || c;
127+
if (manager.transpiledModules[p]) {
128+
return callback(null, manager.transpiledModules[p].module.code);
129+
}
130+
131+
const depName = getDependencyName(p);
132+
const depInfo = manager.manifest.dependencyDependencies[depName];
133+
134+
if (depInfo) {
135+
try {
136+
const module = await downloadDependency(
137+
depName,
138+
depInfo.resolved,
139+
p
140+
);
141+
142+
if (module) {
143+
manager.addModule(module);
144+
145+
callback(null, module.code);
146+
return null;
147+
}
148+
} catch (e) {
149+
// Let it throw the error
150+
}
151+
}
152+
153+
const err = new Error('Could not find ' + p);
154+
err.code = 'ENOENT';
155+
156+
callback(err);
157+
return null;
158+
},
159+
},
160+
(err, resolvedPath) => {
161+
if (err) {
162+
console.error(err);
163+
return reject(err);
164+
}
165+
166+
return res(
167+
downloadDependency(dependencyName, version.resolved, resolvedPath)
168+
);
96169
}
97-
98-
return `throw new Error("Could not find module ${path}`;
99-
})
100-
.then(x => ({
101-
path: pathUtils.join('/node_modules', dependencyName, resolvedPath),
102-
code: x,
103-
}))
104-
.then(module => {
105-
packages[path] = module;
106-
return module;
107-
});
170+
);
171+
});
108172
}

0 commit comments

Comments
 (0)