├── .eslintignore ├── .gitignore ├── .npmignore ├── package.json ├── .editorconfig ├── lib ├── sw.js ├── plugins │ ├── json.js │ ├── babel.js │ ├── text.js │ ├── jsx.js │ ├── env.js │ ├── common.js │ └── resolve.js ├── client.js └── core.js ├── gulpfile.js ├── LICENSE ├── .eslintrc.yml ├── README.md └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /dist 4 | npm-debug.log 5 | .npmrc 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /node_modules 3 | /coverage 4 | /test 5 | gulpfile.js 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unchained-js", 3 | "version": "0.2.0", 4 | "description": "ES6 modules in browsers without bundlers.", 5 | "main": "lib/core.js", 6 | "license": "MIT", 7 | "author": "Edoardo Cavazza ", 8 | "devDependencies": { 9 | "@babel/standalone": "^7.0.0-beta.34", 10 | "eslint": "^4.13.1", 11 | "gulp": "^3.9.1", 12 | "gulp-concat": "^2.6.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # RNA 2 | # EditorConfig is awesome: http://EditorConfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | insert_final_newline = true 12 | 13 | # Matches multiple files with brace expansion notation 14 | # Set default charset 15 | [*.{js,json,html,css,scss,sass}] 16 | charset = utf-8 17 | 18 | # 4 space indentation 19 | [*.{js,html,css,scss,sass}] 20 | indent_style = space 21 | indent_size = 4 22 | 23 | # 2 space indentation 24 | [*.{json,yml,yaml}] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [Makefile] 29 | indent_style = tab 30 | indent_size = 4 31 | 32 | # RNA -------------------------------------------------------------------------------- /lib/sw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unchained ServiceWorker sample. 3 | */ 4 | 5 | // promptly activate the ServiceWorker. 6 | self.addEventListener('install', (event) => { 7 | event.waitUntil(self.skipWaiting()); // Activate worker immediately 8 | }); 9 | 10 | self.addEventListener('activate', (event) => { 11 | event.waitUntil(self.clients.claim()); // Become available to all pages 12 | }); 13 | 14 | // intercept fetch events. 15 | self.addEventListener('fetch', (event) => { 16 | // check if requested resource is an import. 17 | if (self.Unchained.check(event)) { 18 | event.respondWith( 19 | // resolve the resource response 20 | self.Unchained.resolve(event) 21 | ); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const gulp = require('gulp'); 4 | const concat = require('gulp-concat'); 5 | 6 | gulp.task('build-sw', () => 7 | gulp.src( 8 | [ 9 | 'node_modules/@babel/standalone/babel.min.js', 10 | './lib/core.js', 11 | './lib/plugins/*.js', 12 | './lib/sw.js', 13 | ] 14 | ) 15 | .pipe(concat('unchained.sw.js')) 16 | .pipe(gulp.dest('./dist/')) 17 | ); 18 | 19 | gulp.task('build-client', () => 20 | gulp.src( 21 | [ 22 | './lib/client.js', 23 | ] 24 | ) 25 | .pipe(concat('unchained.client.js')) 26 | .pipe(gulp.dest('./dist/')) 27 | ); 28 | 29 | gulp.task('build', ['build-sw', 'build-client']); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Edoardo Cavazza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/plugins/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unchained JSON plugin. 3 | * Handle .json files import. 4 | */ 5 | ((Unchained) => { 6 | /** 7 | * @class JSONPlugin 8 | * @extends Unchained.Plugin 9 | */ 10 | class JSONPlugin extends Unchained.Plugin { 11 | /** 12 | * @inheritdoc 13 | */ 14 | get types() { 15 | return ['application/json', 'text/json']; 16 | } 17 | 18 | /** 19 | * Convert JSON files in ES6 module. 20 | * 21 | * @param {FileDefinition} file The input file. 22 | * @param {FileAnalysis} result The previous code analysis. 23 | * @return {Promise} The transformed code analysis. 24 | */ 25 | async transform(file, result) { 26 | if (!result.ast) { 27 | // export the json. 28 | return Unchained.transform(file, { 29 | code: `export default ${result.code}`, 30 | }); 31 | } 32 | // a plugin already handled this file. 33 | return result; 34 | } 35 | } 36 | 37 | // register the plugin with `json` name. 38 | Unchained.registerPlugin('json', JSONPlugin); 39 | })(self.Unchained); 40 | -------------------------------------------------------------------------------- /lib/plugins/babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unchained Babel plugin. 3 | * Transpiled code using babel. 4 | */ 5 | ((Unchained) => { 6 | /** 7 | * @class BabelPlugin 8 | * @extends Unchained.Plugin 9 | */ 10 | class BabelPlugin extends Unchained.Plugin { 11 | /** 12 | * @inheritdoc 13 | */ 14 | get types() { 15 | return ['application/javascript', 'text/javascript']; 16 | } 17 | 18 | /** 19 | * @inheritdoc 20 | */ 21 | test(file) { 22 | // check config. 23 | return (this.config.plugins || this.config.presets) && super.test(file); 24 | } 25 | 26 | /** 27 | * Transpile the code. 28 | * 29 | * @param {FileDefinition} file The input file. 30 | * @param {FileAnalysis} result The previous code analysis. 31 | * @return {Promise} The transformed code analysis. 32 | */ 33 | async transform(file, result) { 34 | // transform the code. 35 | return Unchained.transform(file, result, this.config); 36 | } 37 | } 38 | 39 | // register the plugin with `babel` name. 40 | Unchained.registerPlugin('babel', BabelPlugin); 41 | })(self.Unchained); 42 | -------------------------------------------------------------------------------- /lib/plugins/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unchained Text plugin. 3 | * Handle text files import. 4 | */ 5 | ((Unchained) => { 6 | /** 7 | * @class TextPlugin 8 | * @extends Unchained.Plugin 9 | */ 10 | class TextPlugin extends Unchained.Plugin { 11 | /** 12 | * @inheritdoc 13 | */ 14 | get types() { 15 | return ['text/']; 16 | } 17 | 18 | /** 19 | * Convert text files into ES6 module. 20 | * 21 | * @param {FileDefinition} file The input file. 22 | * @param {FileAnalysis} result The previous code analysis. 23 | * @return {Promise} The transformed code analysis. 24 | */ 25 | async transform(file, result) { 26 | if (!result.ast) { 27 | // escape ` character. 28 | let escaped = result.code.replace(/`/g, '\\`'); 29 | // export the file as a string. 30 | return Unchained.transform(file, { 31 | code: `export default \`${escaped}\``, 32 | }); 33 | } 34 | // a plugin already handled this file. 35 | return result; 36 | } 37 | } 38 | 39 | // register the plugin with `text` name. 40 | Unchained.registerPlugin('text', TextPlugin); 41 | })(self.Unchained); 42 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | # RNA 2 | extends: 3 | - eslint:recommended 4 | 5 | globals: 6 | process: true 7 | 8 | env: 9 | es6: true 10 | browser: true 11 | 12 | parserOptions: 13 | ecmaVersion: 6 14 | sourceType: module 15 | ecmaFeatures: 16 | jsx: true 17 | generators: false 18 | objectLiteralDuplicateProperties: false 19 | 20 | rules: 21 | quotes: 22 | - 1 23 | - single 24 | semi: 25 | - 1 26 | - always 27 | indent: 28 | - 1 29 | - 4 30 | - SwitchCase: 1 31 | func-names: 0 32 | prefer-const: 0 33 | space-before-function-paren: 34 | - 1 35 | - never 36 | no-proto: 0 37 | no-param-reassign: 0 38 | quote-props: 39 | - 1 40 | - consistent-as-needed 41 | radix: 0 42 | no-new-func: 0 43 | arrow-body-style: 44 | - 2 45 | - as-needed 46 | arrow-parens: 0 47 | arrow-spacing: 48 | - 2 49 | - before: true 50 | after: true 51 | comma-dangle: 52 | - 1 53 | - always-multiline 54 | constructor-super: 0 55 | generator-star-spacing: 0 56 | no-class-assign: 0 57 | no-confusing-arrow: 58 | - 2 59 | - allowParens: true 60 | no-const-assign: 2 61 | no-new-symbol: 2 62 | no-restricted-globals: 0 63 | no-restricted-imports: 0 64 | no-this-before-super: 0 65 | no-var: 2 66 | no-useless-constructor: 2 67 | object-shorthand: 68 | - 1 69 | - always 70 | prefer-arrow-callback: 2 71 | prefer-spread: 0 72 | prefer-reflect: 0 73 | prefer-rest-params: 2 74 | prefer-template: 1 75 | require-yield: 0 76 | sort-imports: 0 77 | template-curly-spacing: 2 78 | yield-star-spacing: 79 | - 2 80 | - after 81 | max-depth: 82 | - 0 83 | - 4 84 | max-params: 85 | - 0 86 | - 3 87 | max-statements: 88 | - 0 89 | - 10 90 | no-bitwise: 0 91 | no-plusplus: 0 92 | no-unused-vars: 93 | - 1 94 | - varsIgnorePattern: (IDOM|process) 95 | no-console: 96 | - 1 97 | 98 | # RNA 99 | -------------------------------------------------------------------------------- /lib/plugins/jsx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unchained JSX plugin. 3 | * Handle .jsx files import. 4 | */ 5 | ((Unchained) => { 6 | /** 7 | * custom babel plugin. 8 | * @param {Object} config Plugin configuration. 9 | * @param {Object} config.types The AST types factory. 10 | */ 11 | function wrapJSX({ types }) { 12 | return { 13 | visitor: { 14 | // intercept top-level statements. 15 | Program(path) { 16 | // force export default of the jsx. 17 | const decl = types.exportDefaultDeclaration( 18 | types.functionExpression(null, [types.identifier('jsx')], types.blockStatement([ 19 | types.returnStatement(path.node.body[0].expression), 20 | ])) 21 | ); 22 | path.node.body = [decl]; 23 | }, 24 | }, 25 | }; 26 | } 27 | 28 | /** 29 | * @class JSXPlugin 30 | * @extends Unchained.Plugin 31 | * 32 | * @param {String} config.pragma The vdom factory. 33 | */ 34 | class JSXPlugin extends Unchained.Plugin { 35 | /** 36 | * @inheritdoc 37 | */ 38 | get types() { 39 | return ['text/jsx']; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | test(file) { 46 | // ultra-base check for `` presence in the file content. 47 | return file.content.match(/<[\w-_]+[\s>]/) && super.test(file); 48 | } 49 | 50 | /** 51 | * Convert the JSX syntax. 52 | * 53 | * @param {FileDefinition} file The input file. 54 | * @param {FileAnalysis} result The previous code analysis. 55 | * @return {Promise} The transformed code analysis. 56 | */ 57 | async transform(file, result) { 58 | // transform the code. 59 | return Unchained.transform(file, result, { 60 | plugins: [ 61 | 'syntax-jsx', 62 | wrapJSX, 63 | ['transform-react-jsx', this.config], 64 | ], 65 | }); 66 | } 67 | } 68 | 69 | // register the plugin with `jsx` name. 70 | Unchained.registerPlugin('jsx', JSXPlugin); 71 | })(self.Unchained); 72 | -------------------------------------------------------------------------------- /lib/plugins/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unchained ENV plugin. 3 | * Replace process.env with the given value. 4 | */ 5 | ((Unchained) => { 6 | /** 7 | * @class ENVPlugin 8 | * @extends Unchained.Plugin 9 | * 10 | * @param {Object} config.env A set of ENV variables. 11 | */ 12 | class ENVPlugin extends Unchained.Plugin { 13 | /** 14 | * @inheritdoc 15 | */ 16 | test(file) { 17 | // check if the file contains `process.env`. 18 | return file.content.match(/process\.env/) && super.test(file); 19 | } 20 | 21 | /** 22 | * Replace `prcoess.env.{x}` variables. 23 | * 24 | * @param {FileDefinition} file The input file. 25 | * @param {FileAnalysis} result The previous code analysis. 26 | * @return {Promise} The transformed code analysis. 27 | */ 28 | async transform(file, result) { 29 | // store env variables. 30 | let env = this.config.env || {}; 31 | return Unchained.transform(file, result, { 32 | plugins: [ 33 | // babel plugin. 34 | ({ types }) => { 35 | return { 36 | visitor: { 37 | // intercept member expressions. 38 | MemberExpression(path) { 39 | // the member expression requires `process.env`. 40 | if (path.get('object').matchesPattern('process.env')) { 41 | // evaluate the key. 42 | const key = path.toComputedKey(); 43 | // check if the key is a string, so we can replace it during the transpiling. 44 | if (types.isStringLiteral(key)) { 45 | // replace the value. 46 | path.replaceWith(types.valueToNode(env[key])); 47 | } 48 | } 49 | }, 50 | }, 51 | }; 52 | }, 53 | ], 54 | }); 55 | } 56 | } 57 | 58 | // register the plugin with `env` name. 59 | Unchained.registerPlugin('env', ENVPlugin); 60 | })(self.Unchained); 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unchained 2 | 3 | [![npm](https://img.shields.io/npm/v/unchained-js.svg)](https://www.npmjs.com/package/unchained-js) 4 | 5 | Unchained takes advantage from browsers support for ES6 modules and Service Workers in order to load a full web application without using a bundler like Webpack or Rollup. 6 | 7 | ☢️ *This project is just a research about web technologies.* 8 | 9 | DO NOT use it in production. 10 | 11 | ## Why 12 | 13 | * Since Safari, Firefox and Chrome started to support ES6 modules syntax, I started to look for a good practise to load my applications. 14 | 15 | * Bundlers are great, and I will continue to use them for working/production environments, but I felt nostalgic about the times where I used to build application without installing ~1000 node modules just to start. 16 | 17 | Read more on Medium: https://medium.com/@edoardo.cavazza/a-study-about-how-to-improve-frontend-dev-experience-without-a-bundler-1b4c3a461a35 18 | 19 | ## How it works 20 | 21 | Native ES6 modules syntax accepts relative paths only (so, support for dependencies installed by NPM/Yarn is missing). Also, it doesn't work with other source formats rather than javascript (JSON, texts, styles...) or syntaxes (like JSX). 22 | 23 | Today, those issues are resolved on dev environment side by bundlers (Webpack, Rollup, Browserify) and transpilers (Babel, Traceur). 24 | 25 | The idea is to intercept import calls and transform the source in a ServiceWorker context, using the magninificent Babel standalone distribution to manipulate sources and resolve NPM dependencies. 26 | 27 | ![Unchained concept](https://docs.google.com/drawings/d/e/2PACX-1vQdqQI38CpJUSRT7diAH9dQOb-N8fGmp8LpOIdmJ6WbebEeDuzenx5wuZNtD0sPCpkYQ3INe3LsRHqM/pub?w=1362&h=1437) 28 | 29 | 30 | ## Usage 31 | 32 | Install from NPM: 33 | ```sh 34 | $ npm install unchained-js 35 | # OR 36 | $ yarn add unchained-js 37 | ``` 38 | 39 | Use the Unchained client helper to register a ServiceWorker and to import the main application file. 40 | 41 | **index.html** 42 | ```html 43 | 44 | 50 | ``` 51 | 52 | **sw.js** 53 | ```js 54 | // import Unchained core and plugins 55 | self.importScripts('node_modules/unchained-js/dist/unchained.sw.js'); 56 | ``` 57 | 58 | **index.js** 59 | ```js 60 | import { Component, h, render } from 'preact'; 61 | 62 | class App extends Component { 63 | render() { 64 | return

Hello world!

; 65 | } 66 | } 67 | 68 | render(document.body, ); 69 | ``` 70 | 71 | ## Configuration 72 | 73 | The Unchained object can be configured with a set of plugins, through the `Unchained.resolve` method. 74 | 75 | ### Plugins 76 | 77 | An array of Plugin constructors *or* Plugin instances *or* Plugin names. 78 | 79 | ```js 80 | { 81 | plugins: [ 82 | // constrcutor 83 | Unchained.TextPlugin, 84 | // instance 85 | new Unchained.ResolvePlugin(), 86 | // name 87 | 'env', 88 | // constructor|instance|name with options 89 | ['jsx', { pragram: 'h' }] 90 | ] 91 | } 92 | ``` 93 | 94 | > The Plugin name may be registered via the `Unchained.registerPlugin(name, constructor)` method. 95 | 96 | A list of available plugins can be found [here](https://github.com/edoardocavazza/unchained/wiki/Plugins). 97 | 98 | ### Via querystring 99 | 100 | You may also configure Unchained via querystring in the service worker registration url: 101 | 102 | ```js 103 | navigator.serviceWorker.register(`sw.js?unchained={"plugins":["env", "text"]}`); 104 | ``` 105 | 106 | The equivalent can be written using the third parameter of the `Unchained.register` helper method: 107 | ```js 108 | Unchained.register('sw.js', { scope: '/' }, { 109 | plugins: ['env', 'text'], 110 | }); 111 | ``` 112 | 113 | ## Browsers support ☢️ 114 | 115 | Support for [Service Workers](https://caniuse.com/#feat=serviceworkers), [ES6 syntax](https://kangax.github.io/compat-table/es6) and [ES6 modules](https://caniuse.com/#feat=es6-module) is required. 116 | 117 | Manually tested on Chrome v63.0.3239.84. 118 | 119 | ## Resources 120 | 121 | **ES6 modules** 122 | - [`import`](https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Statements/import) | [`export`](https://developer.mozilla.org/it/docs/Web/JavaScript/Reference/Statements/export) 123 | - [Dynamic `import()`](https://developers.google.com/web/updates/2017/11/dynamic-import) 124 | - [`