1 | // <img src=https://reun.solsort.com/icon.png width=96 height=96 align=right>
|
2 | //
|
3 | // [![website](https://img.shields.io/badge/website-reun.solsort.com-blue.svg)](https://reun.solsort.com/)
|
4 | // [![github](https://img.shields.io/badge/github-solsort/reun-blue.svg)](https://github.com/solsort/reun)
|
5 | // [![travis](https://img.shields.io/travis/solsort/reun.svg)](https://travis-ci.org/solsort/reun)
|
6 | // [![npm](https://img.shields.io/npm/v/reun.svg)](https://www.npmjs.com/package/reun)
|
7 | //
|
8 | // TODO: unit testing
|
9 | // TODO: documentaiton, - merge into source
|
10 | //
|
11 | // # REUN - require(unpkg)
|
12 | //
|
13 | // Reun is:
|
14 | //
|
15 | // - 100% client-side nodejs-like `require` for the browser.
|
16 | // - using https://unpkg.com/.
|
17 | // - dynamic, just `require(...)` whatever module you want from your source file. No need for `package.json`, - versions can be passed to require, i.e. `require('module@1.2.3')`.
|
18 | // - pretending to be a synchronous, even though it is asynchrounous. Tries to work in the typical cases, and will always fail in certain documented edge cases. Pragmatic, and not standard compliant.
|
19 | // - able to directly load many nodejs modules, that has not been packaged for the browser.
|
20 | // - adding custom functionality when desired, i.e. `module.meta`
|
21 | //
|
22 | // ## API
|
23 | //
|
24 | // - `reun.eval(code, [opt])` execute `code`, where `code` is either a function, or the string source of a module. `require()` is available and is pretending to be synchronous, and done relative to the `opt.uri`. Returns a promise of the function result or module-exports.
|
25 | // - `reun.require(module, [opt])` loads a module, path is relative to the `location.href` if available. Returns a promise.
|
26 | //
|
27 | // ## Usage example
|
28 | //
|
29 | // `index.html`:
|
30 | // ```html
|
31 | // <!DOCTYPE html>
|
32 | // <html>
|
33 | // <body>
|
34 | // <script src=https://unpkg.com/reun></script>
|
35 | // <script>reun.require('./example.js');</script>
|
36 | // </body>
|
37 | // </html>
|
38 | // ```
|
39 | //
|
40 | // `example.js`:
|
41 | // ```javascript
|
42 | // var uniq = require('uniq');
|
43 | // console.log(uniq([1,4,2,8,4,2,1,3,2]));
|
44 | // ```
|
45 | //
|
46 | // ## Extensions
|
47 | //
|
48 | // - `require('module@0.2.3')` allows you to require a specific version
|
49 | // - `module.meta` allows you to set meta information about your module, - this may later be used to automatically package the module for npm, cordova, ...
|
50 | //
|
51 | // ## Incompatibilities
|
52 | //
|
53 | // The implementation is a hack. We want to _pretend_ to be synchronous, but we also do not want to block the main thread. Instead `require` throws an exception when a module is not loaded yet. When we run a file, we catch this exception, load the module asynchrounously, and then rerun the file. Later on we might also search the source for `require("...")`, or `require('...')` and try to preload these modules, but this is not implemented yet.
|
54 | //
|
55 | // Also we just resolve the module name as `'https://unpkg.com/' + module_name`. To be more compatible with node modules, we may check the `package.json` in the future to make sure that the relative paths in the require works.
|
56 | //
|
57 | // - Custom exceptions from `require` should not caught.
|
58 | // - Code before a require, may be executed multiple times, - should be side-effect free.
|
59 | // - `require` may fail within callbacks, if the module has not been loaded before.
|
60 | // - If the source lives in a subdirectory, and the module is not packaged for the web, and contains relative paths, - the paths are wrongly resolved. A workaround is to `require('module/lib/index.js')` instead of `require('module')`.
|
61 | // - It does obviously not work with every module.
|
62 | //
|
63 | // In spite of these limitations, it is still possible to `require` many nodejs module directly to the web.
|
64 | //
|
65 | //
|
66 | // ## Project setup
|
67 |
|
68 | (function() { 'use strict';
|
69 | var da = typeof direape !== 'undefined' ? direape : require('direape');
|
70 | da.testSuite('reun');
|
71 | var reun = self.reun || {};
|
72 | var modules = {
|
73 | reun: reun,
|
74 | direape: da
|
75 | };
|
76 |
|
77 | // ## `reun.eval(src|fn, opt);`
|
78 | //
|
79 | // Functions will be called as a module with `require`, `exports`, and `module` as parameters, - similar to <http://requirejs.org/docs/commonjs.html>
|
80 |
|
81 | var runQueue = new Promise((resolve) => da.ready(() => resolve()));
|
82 |
|
83 | reun.eval = (fn, opt) => {
|
84 | runQueue = runQueue.then(() => do_eval(fn, opt))
|
85 | .catch((e) => da.nextTick(() => { throw e; }));
|
86 | return runQueue;
|
87 | };
|
88 |
|
89 | da.handle('reun:eval', (fn, opt) =>
|
90 | reun.eval(fn, opt).then(da.jsonify));
|
91 |
|
92 | // ## `reun.require(module-name, opt);`
|
93 |
|
94 | reun.require = (name, opt) =>
|
95 | reun.eval('module.exports = require("' + name + '",' +
|
96 | JSON.stringify(opt || {}) + ');',
|
97 | Object.assign({uri: self.location && self.location.href || './'}, opt));
|
98 |
|
99 | da.handle('reun:require', reun.require);
|
100 |
|
101 | // ## Implementation details
|
102 | //
|
103 | // ### moduleUrl
|
104 | //
|
105 | // Convert a require-address to a url.
|
106 | // path is baseurl used for mapping relative file paths (`./hello.js`) to url.
|
107 |
|
108 | function moduleUrl(module, opt) {
|
109 | var path = opt.uri || '';
|
110 |
|
111 | if(module.slice(0,4) === 'reun') {
|
112 | return 'reun';
|
113 | }
|
114 |
|
115 | if(module.startsWith('https:') ||
|
116 | module.startsWith('http:')) {
|
117 | return module;
|
118 | }
|
119 | path = path.replace(/[?#].*/, '');
|
120 | path = (module.startsWith('.')
|
121 | ? path.replace(/[/][^/]*$/, '/')
|
122 | : 'https://unpkg.com/');
|
123 | path = path + module;
|
124 | while(path.indexOf('/./') !== -1) {
|
125 | path = path.replace('/./', '/');
|
126 | }
|
127 | var prevPath;
|
128 | do {
|
129 | prevPath = path;
|
130 | path = path.replace(/[/][^/]*[/][.][.][/]/g, '/');
|
131 | } while(path !== prevPath);
|
132 | return path;
|
133 | }
|
134 |
|
135 | // ### do_eval
|
136 |
|
137 | function do_eval(fn, opt) {
|
138 | opt = opt || {};
|
139 | if(typeof fn === 'string') {
|
140 | fn = stringToFunction(fn, opt);
|
141 | }
|
142 | return executeModule(fn, opt);
|
143 | }
|
144 |
|
145 | // ### executeModule
|
146 |
|
147 | function executeModule(fn, opt) {
|
148 | opt.uri = opt.uri || '';
|
149 | var require = (name, opt) => reun_require(name, opt, module);
|
150 | var module = {
|
151 | require: require,
|
152 | uri: opt.uri,
|
153 | id: opt.uri.replace('https://unpkg.com/', '').replace(/@[^/]*/, ''),
|
154 | exports: {}
|
155 | };
|
156 | if(opt.main) {
|
157 | require.main = module;
|
158 | }
|
159 |
|
160 | return rerunModule(fn, module);
|
161 | }
|
162 |
|
163 | // ### rerunModule
|
164 | //
|
165 | function rerunModule(fn, module) {
|
166 | var result;
|
167 | try {
|
168 | fn(module.require, module.exports, module);
|
169 | result = module.exports;
|
170 | } catch (e) {
|
171 | if(e.constructor !== RequireError) {
|
172 | throw e;
|
173 | }
|
174 | return da.call(da.nid, 'da:GET', e.url)
|
175 | .catch(() => {
|
176 | throw new Error('require could not load "' + e.url + '" ' +
|
177 | 'Possibly module incompatible with http://reun.solsort.com/'); })
|
178 | .then((moduleSrc) => executeModule(stringToFunction(moduleSrc, e.opt),
|
179 | e.opt))
|
180 | .then((exports) => assignModule(e.url, exports))
|
181 | .then(() => rerunModule(fn, module));
|
182 | }
|
183 | return Promise.resolve(result);
|
184 | }
|
185 |
|
186 | // ### `shortName(uri)`
|
187 |
|
188 | function assignModule(uri, exports) {
|
189 |
|
190 | modules[uri] = exports;
|
191 | //
|
192 | // Find the short name of the module, and remember it by that alias,
|
193 | // to make sure that later requires for the module without version/url
|
194 | // returns the already loaded module.
|
195 | //
|
196 |
|
197 | if(exports.meta && exports.meta.id) {
|
198 | modules[exports.meta.id] = exports;
|
199 | }
|
200 |
|
201 | var name = uri
|
202 | .replace('https://unpkg.com/', '')
|
203 | .replace(/[@/].*/, '');
|
204 | if(!modules[name]) {
|
205 | modules[name] = exports;
|
206 | }
|
207 | }
|
208 |
|
209 | // ### reun_require
|
210 |
|
211 | function reun_require(name, opt, parentModule) {
|
212 | if(modules[name]) {
|
213 | return modules[name];
|
214 | }
|
215 | var url = moduleUrl(name, parentModule);
|
216 | if(!modules[url]) {
|
217 | throw new RequireError(name, url, opt);
|
218 | }
|
219 | return modules[url];
|
220 | }
|
221 |
|
222 | // ### stringToFunction
|
223 |
|
224 | function stringToFunction(src, opt) {
|
225 | var wrappedSrc = '(function(require,exports,module){' +
|
226 | src + '})//# sourceURL=' + opt.uri;
|
227 | return eval(wrappedSrc);
|
228 | }
|
229 |
|
230 | // ### RequireError
|
231 | //
|
232 | // When trying to load at module, that is not loaded yet, we throw this error:
|
233 |
|
234 | function RequireError(module, url, opt) {
|
235 | this.module = module;
|
236 | this.url = url;
|
237 | opt = opt || {};
|
238 | opt.uri = url;
|
239 | this.opt = opt;
|
240 | }
|
241 | RequireError.prototype.toString = function() {
|
242 | return 'RequireError:' + this.module +
|
243 | ' url:' + this.url;
|
244 | };
|
245 |
|
246 | // ## Main / test runner
|
247 |
|
248 | da.ready(() => {
|
249 | if((da.isNodeJs() && require.main === module && process.argv[2] === 'test') ||
|
250 | (self.REUN_RUN_TESTS)) {
|
251 | da.runTests('reun')
|
252 | .then(() => da.isNodeJs() && process.exit(0))
|
253 | .catch(() => da.isNodeJs() && process.exit(1));
|
254 | }
|
255 | });
|
256 | if(typeof module === 'object') {
|
257 | module.exports = reun;
|
258 | } else {
|
259 | self.reun = reun;
|
260 | }
|
261 |
|
262 | // ## end
|
263 | })();
|
264 |
|
265 | // # License
|
266 | //
|
267 | // This software is copyrighted solsort.com ApS, and available under GPLv3, as well as proprietary license upon request.
|
268 | //
|
269 | // Versions older than 10 years also fall into the public domain.
|
270 | //
|
271 |
|