├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── common.js ├── common.min.js ├── csp.html ├── index.html ├── js-browser ├── converter.js ├── crypto.js ├── index.html ├── main.js ├── promise-pjs.js └── test.js ├── js-node ├── converter.js ├── main.js └── test.js ├── nonce.min.txt ├── nonce.txt ├── package.json ├── root.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .travis.yml 3 | csp.html 4 | index.html 5 | root.js 6 | test.js 7 | node_modules/* 8 | js-browser/* 9 | js-node/* 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 7 5 | git: 6 | depth: 1 7 | branches: 8 | only: 9 | - master 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 by Andrea Giammarchi - @WebReflection 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommonJS + module.import() [![build status](https://secure.travis-ci.org/WebReflection/common-js.svg)](http://travis-ci.org/WebReflection/common-js) 2 | 3 | This module aim is to bring both CommonJS like module behavior on Web browsers, 4 | and a promise based `module.import(path)` to both browsers and NodeJS. 5 | 6 | Yes, it resolves paths relatively to the current one! 7 | 8 | Yes, it is secure too, check the [CSP enabled page](https://webreflection.github.io/common-js/csp.html)! 9 | 10 | Don't miss [the introductory blog post](https://medium.com/@WebReflection/asynchronous-module-import-path-b9f56675e109#.8nsdv9571) about this idea! 11 | 12 | 13 | ### Browser Example 14 | ```html 15 | 16 | 17 | 22 | 23 | ``` 24 | 25 | Having a single script with `id="common-js"` is all it takes to be able to load asynchronously any other file or module. 26 | 27 | The main entry point `/js-browser/main.js` will resolve relative paths from `/js-browser/` folder. 28 | 29 | Its loaded modules will resolve their own imported paths from where they've been loaded, and so on. 30 | The same goes for NodeJS, it's indeed same logic behind `require`. 31 | 32 | ```js 33 | // /js-browser/main.js loading /js-browser/test.js 34 | module.import('./test').then(function (test) { 35 | test('Hello CommonJS!'); 36 | // will output: 37 | // Hello CommonJS! 38 | // from /js-browser/test.js 39 | }); 40 | 41 | // the /js-browser/test.js content 42 | module.exports = function (message) { 43 | alert(message + '\nfrom ' + module.filename); 44 | }; 45 | ``` 46 | 47 | 48 | 49 | ### Load multiple modules at once 50 | ```js 51 | Promise.all([ 52 | module.import('./a'), 53 | module.import('//cdn.something.com/cool.js'), 54 | module.import('../sw.js'), 55 | module.import('/root/too.js') 56 | ]).then(function (modules) { 57 | const [a, cool, sw, too] = modules; 58 | }); 59 | ``` 60 | 61 | 62 | ### Exporting modules asynchronously 63 | ```js 64 | // an async example of /js-browser/test.js content 65 | // for the /js-browser/main.js file nothing changes 66 | module.exports = new Promise(function (resolve) { 67 | setTimeout( 68 | resolve, 69 | 1000, 70 | function (message) { 71 | alert(message + '\nfrom ' + module.filename); 72 | } 73 | ); 74 | }); 75 | ``` 76 | 77 | 78 | 79 | ### Compatibility 80 | You can test your target directly through the [live test page](https://webreflection.github.io/common-js/). 81 | What I could test was the following: 82 | 83 | **Mobile** Android 2+, iOS5+, WP7+, BBOS7+, FFOS1+, WebOS2+, Kindle Paper & Fire 84 | 85 | **Desktop** Chrome, FF, Safari, Opera, IE9+ (theoretically IE8 too but it needs few polyfills upfront) 86 | 87 | 88 | 89 | 90 | ### What else? 91 | The synchronous `require` and both `__filename` and `__dirname` are also exposed, but nothing else from NodeJS core is available. 92 | You are responsible for loading all the modules you need, possibly only when you need them. 93 | 94 | 95 | 96 | ### F.A.Q 97 | 98 | * **Does it load every time?** 99 | It uses a cache, like NodeJS does. If you load same module twice, even from different relative paths, it'll use the cached one. 100 | * **Why on the module?** 101 | There are scripts, script type module, `importScripts`, a [dynamic import proposal](https://github.com/tc39/proposal-dynamic-import#import), you name it ... this one actually works and it's backward compatible with modules that don't care about this solution existing. 102 | * **Why not ES2015 modules?** 103 | Because those, so far, never truly solved anything. Actually, ES6 modules created more problems due inability to require modules at runtime and/or on the browser. 104 | * **Is there a CDN I can use to test?** 105 | There is always one for npm modules. [https://unpkg.com/common-js@latest](https://unpkg.com/common-js@latest/common.min.js) should be already OK. 106 | * **Is this using eval?** 107 | No. It's using a technique that is even compatible with highest security standards such [Content Security Policy](https://w3c.github.io/webappsec-csp/) 108 | 109 | 110 | 111 | ### License 112 | Copyright (C) 2017 by Andrea Giammarchi - @WebReflection -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | /*! (C) 2017 Andrea Giammarchi */ 2 | if (typeof module === 'object') { 3 | 'import' in module || (module.constructor.prototype.import = function (path) { 4 | var self = this; 5 | return Promise.resolve().then(function () { 6 | return self.require(path); 7 | }); 8 | }); 9 | } else { 10 | (function CommonJS(info, el) { 11 | var 12 | protoPath = /^(?:[a-z]+:)?\/\//, 13 | npmPath = /^[a-zA-Z_-]/, 14 | __filename = info._ || el.getAttribute('data-main').replace(npmPath, './$&'), 15 | normalize = function (url) { 16 | if (protoPath.test(url)) return url; 17 | if (npmPath.test(url)) return gModule._path(url); 18 | for (var 19 | path = __filename.slice(0, __filename.lastIndexOf('/')), 20 | length = url.length, 21 | c, i = 0, p = 0; i < length; p = i + 1 22 | ) { 23 | i = url.indexOf('/', p); 24 | if (i < 0) { 25 | i = length; 26 | path += '/' + url.slice(p); 27 | if (!/\.js$/i.test(path)) path += '.js'; 28 | } else if (i === 0) { 29 | path = ''; 30 | } else { 31 | c = p; p = i; 32 | while (p && url.charAt(p - 1) === '.') --p; 33 | switch (i - p) { 34 | case 0: path += '/' + url.slice(c, i); break; 35 | case 1: break; 36 | case 2: path = path.slice(0, path.lastIndexOf('/')); break; 37 | } 38 | } 39 | } 40 | return path; 41 | }, 42 | onload = function (xhr, path, resolve) { 43 | var 44 | html = document.documentElement, 45 | script = document.createElement('script') 46 | ; 47 | script.setAttribute('nonce', gModule._nonce); 48 | script.textContent = 'module.$(function(){' + 49 | 'var module=' + gModule._cjs + '(arguments[0]),' + 50 | '__filename=module.filename,' + 51 | '__dirname=__filename.slice(0,__filename.lastIndexOf("/")),' + 52 | 'require=module.require,' + 53 | 'exports=module.exports;(function(){"use strict";\n' + 54 | xhr.responseText + 55 | ';\n}.call(exports));return module.exports;' + 56 | '}(module));'; 57 | gModule._ = path; 58 | gModule.$ = function (exports) { 59 | resolve(gModule._cache[path] = exports); 60 | }; 61 | // cleanup after, no matter what 62 | setTimeout(function () { html.removeChild(script); },1); 63 | // execute the script (synchronously) 64 | html.appendChild(script); 65 | }, 66 | error = function (path, xhr) { 67 | throw (gModule._cache[path] = new Error(xhr.statusText)); 68 | }, 69 | load = function (path) { 70 | var 71 | remote = path, 72 | m = /^((?:[a-z]+?:\/\/)?[^/]+)\/([^@]+)@latest(\/.*)?$/.exec(path), 73 | resolve = function (exports) { module = exports; }, 74 | xhr = new XMLHttpRequest(), 75 | module 76 | ; 77 | if (m) { 78 | // hopefully soon ... 79 | // xhr.open('GET', 'https://registry.npmjs.org/' + m[2] + '/latest', false); 80 | // meanwhile ... 81 | xhr.open('GET', 'http://www.3site.eu/latest/?@=' + m[2], false); 82 | xhr.send(null); 83 | if (xhr.status < 400) module = JSON.parse(xhr.responseText); 84 | else return error(path, xhr); 85 | remote = m[1] + '/' + m[2] + '@' + module.version + (m[3] || ''); 86 | } 87 | xhr = new XMLHttpRequest(); 88 | xhr.open('GET', remote, false); 89 | xhr.send(module = null); 90 | if (xhr.status < 400) onload(xhr, path, resolve); 91 | else error(path, xhr); 92 | return module; 93 | }, 94 | exports = {}, 95 | module = { 96 | filename: __filename, 97 | exports: exports, 98 | require: function (url) { 99 | var path = normalize(url); 100 | return gModule._cache[path] || load(path); 101 | }, 102 | import: function (url) { 103 | var path = normalize(url); 104 | return Promise.resolve( 105 | gModule._cache[path] || 106 | (gModule._cache[path] = new Promise( 107 | function (resolve, reject) { 108 | var xhr = new XMLHttpRequest(); 109 | xhr.open('GET', path, true); 110 | xhr.onreadystatechange = function () { 111 | if (xhr.readyState == 4) { 112 | if (xhr.status < 400) onload(xhr, path, resolve); 113 | else reject(new Error(xhr.statusText)); 114 | } 115 | }; 116 | xhr.send(null); 117 | } 118 | )) 119 | ); 120 | } 121 | }, 122 | gModule = window.module || module 123 | ; 124 | if (gModule === module) { 125 | window.global = window; 126 | window.module = module; 127 | window.process = {browser: true}; 128 | module._cache = Object.create(null); 129 | module._nonce = el.getAttribute('nonce'); 130 | module._cjs = '' + CommonJS; 131 | module._path = function (url) { 132 | var i = url.indexOf('/'), length = url.length; 133 | return 'https://unpkg.com/' + url.slice(0, i < 0 ? length : i) + 134 | '@latest' + (i < 0 ? '' : url.slice(i)); 135 | }; 136 | module.import('./' + __filename.split('/').pop()); 137 | } 138 | return module; 139 | }({_:''}, document.getElementById('common-js'))); 140 | } -------------------------------------------------------------------------------- /common.min.js: -------------------------------------------------------------------------------- 1 | /*! (C) 2017 Andrea Giammarchi */ 2 | "object"==typeof module?"import"in module||(module.constructor.prototype["import"]=function(e){var t=this;return Promise.resolve().then(function(){return t.require(e)})}):!function e(t,n){var r=/^(?:[a-z]+:)?\/\//,o=/^[a-zA-Z_-]/,s=t._||n.getAttribute("data-main").replace(o,"./$&"),i=function(e){if(r.test(e))return e;if(o.test(e))return d._path(e);for(var t,n=s.slice(0,s.lastIndexOf("/")),i=e.length,u=0,c=0;u 2 | 3 | 4 | 5 | 6 | 12 | 15 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /js-browser/converter.js: -------------------------------------------------------------------------------- 1 | // asynchronous converter.js 2 | module.exports = module.import('./crypto') 3 | .then(function (crypto) { 4 | // return the module to export 5 | return { 6 | sha256: function (str, secret = '') { 7 | return crypto.createHmac('sha256', secret) 8 | .update(str) 9 | .digest('hex'); 10 | } 11 | }; 12 | }); -------------------------------------------------------------------------------- /js-browser/crypto.js: -------------------------------------------------------------------------------- 1 | // this is just a mock for demo purpose 2 | module.exports = { 3 | createHmac: function () { 4 | return { 5 | update: function () { return this }, 6 | digest: function () { 7 | return 'bbd1fba4a3848c751e24b7b6d7afc33da8a63973c14c59ca8f53b7a6230eba9a'; 8 | } 9 | }; 10 | } 11 | }; -------------------------------------------------------------------------------- /js-browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /js-browser/main.js: -------------------------------------------------------------------------------- 1 | module.import('./test').then(function (test) { 2 | document.documentElement.style.fontFamily = 'sans-serif'; 3 | test('Asynchronous CommonJS!\nfrom ' + __filename); 4 | require('./test')('... but Synchronous too ...'); 5 | }); 6 | 7 | // sync example 8 | // require('./test')('Hello CommonJS!\nfrom ' + __filename); 9 | 10 | // multiple imports 11 | Promise.all([ 12 | // local module 13 | module.import('./converter'), 14 | // looks automatically through unpkg.cdn directly 15 | // will be loaded remotely 16 | module.import('classtrophobic-es5') 17 | ]) 18 | .then(function (modules) { 19 | var 20 | converter = modules[0], 21 | Class = modules[1], 22 | result = [ 23 | converter.sha256('yolo'), 24 | typeof Class === 'function' 25 | ].join('\n') 26 | ; 27 | if (typeof console !== 'undefined') console.log(result); 28 | else alert(result); 29 | }); 30 | -------------------------------------------------------------------------------- /js-browser/promise-pjs.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2016 Justin Ridgewell - MIT Style License */ 2 | function Promise(a){if(!(this instanceof Promise))throw new TypeError("Constructor Promise requires `new`");if(!isFunction(a))throw new TypeError("Must pass resolver function");this._state=PendingPromise;this._value=[];this._isChainEnd=!0;doResolve(this,adopter(this,FulfilledPromise),adopter(this,RejectedPromise),{then:a})}Promise.prototype.then=function(a,b){a=isFunction(a)?a:void 0;b=isFunction(b)?b:void 0;if(a||b)this._isChainEnd=!1;return this._state(this._value,a,b)}; 3 | Promise.prototype["catch"]=function(a){return this.then(void 0,a)};Promise.resolve=function(a){return isObject(a)&&a instanceof this?a:new this(function(b){b(a)})};Promise.reject=function(a){return new this(function(b,d){d(a)})};Promise.all=function(a){var b=this;return new b(function(d,c){var f=a.length,e=Array(f);if(0===f)return d(e);each(a,function(a,g){b.resolve(a).then(function(a){e[g]=a;0===--f&&d(e)},c)})})}; 4 | Promise.race=function(a){var b=this;return new b(function(d,c){for(var f=0,e=a.length;f 4 | crypto.createHmac('sha256', secret) 5 | .update(str) 6 | .digest('hex') 7 | }; 8 | -------------------------------------------------------------------------------- /js-node/main.js: -------------------------------------------------------------------------------- 1 | module.import('./test').then(function (test) { 2 | test('Hello CommonJS!'); 3 | }); -------------------------------------------------------------------------------- /js-node/test.js: -------------------------------------------------------------------------------- 1 | module.exports = function (message) { 2 | console.log(message); 3 | }; -------------------------------------------------------------------------------- /nonce.min.txt: -------------------------------------------------------------------------------- 1 | eI26oJgDsppcOcjfcysbVOvOTFhz9afQG64Cf4KInwg= 2 | -------------------------------------------------------------------------------- /nonce.txt: -------------------------------------------------------------------------------- 1 | jA7XtV4Pehdob0C3lDoGSD1hM8KB+naQdoMNZxf7cMs= 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common-js", 3 | "version": "0.3.8", 4 | "description": "module.exports and module.import for browsers too", 5 | "main": "common.js", 6 | "scripts": { 7 | "test": "node test.js", 8 | "build": "npm run minify; npm run nonce; npm run size;", 9 | "size": "cat common.js | wc -c;cat common.min.js | wc -c;gzip -c common.min.js | wc -c", 10 | "minify": "uglifyjs common.js --support-ie8 --comments=/^!/ --compress --mangle -o common.min.js", 11 | "nonce": "cat common.js | openssl dgst -sha256 -binary | base64 > nonce.txt;cat common.min.js | openssl dgst -sha256 -binary | base64 > nonce.min.txt" 12 | }, 13 | "keywords": [ 14 | "require", 15 | "module", 16 | "exports", 17 | "import", 18 | "async", 19 | "browser" 20 | ], 21 | "author": "Andrea Giammarchi", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "uglify-js": "^2.7.5" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/WebReflection/common-js.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/WebReflection/common-js/issues" 32 | }, 33 | "homepage": "https://github.com/WebReflection/common-js#readme" 34 | } 35 | -------------------------------------------------------------------------------- /root.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require('./common'); 2 | var called = false; 3 | console.log = function (message) { 4 | called = true; 5 | console.assert('Hello CommonJS!' === message, 'right message'); 6 | }; 7 | module.import('./js-node/main').then(function () { 8 | setTimeout(function() { 9 | console.assert(called, 'executed'); 10 | }, 200); 11 | }); --------------------------------------------------------------------------------