sprockets-commoner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7be0d75114fb75a73b272d1493290b864cce8455
4
+ data.tar.gz: 3c98183d4ecfc9ab3824ece0acf091255f9df219
5
+ SHA512:
6
+ metadata.gz: 6985592443fef03121a57c8a8e1f3c37e684a2fedb0561014f8c242f2629fccf6dc2f3c30789494b4a41eececaccde49424a40b858b7a7e11ad3c204093da44e
7
+ data.tar.gz: 16d3679cb215ca29fd2c8b1513cfb09704096ab31cf368ad42e5a7c8b6a41bbd13e3344ddfc0bbd89ee9f38119428f257ba3b4ab9749dec58dd896c2d3cf9307
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ node_modules
11
+ npm-debug.log
data/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v5.8.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at bouke@shopify.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # Sprockets::Commoner
2
+
3
+ `Sprockets::Commoner` is a gem that enables JavaScript package and Babel transformation in Sprockets.
4
+
5
+ ## Features
6
+
7
+ * Compile JavaScript modules into a single bundle.
8
+ * Run Babel transforms.
9
+ * Easy setup. We adhere to the Rails Way™ and don't require any additional configuration for the simplest and most common set ups.
10
+ * Integrate tightly into Sprockets/Rails. You can use any of Sprockets' features like ERB inside your files without having to jump through crazy hoops.
11
+ * Designed to emit code that compresses incredibly well. The code generated will be smaller than webpack or browserify.
12
+ * Use `process.env` inside your JavaScript. This will also run on anything in `node_modules` (e.g. Babel), to ensure dependencies are also compressed optimally.
13
+ * Automatic deduplication of Babel helpers. No need to use `babel-runtime`, as commoner will automatically detect which helpers are used and share them between modules in a way that compressed very well.
14
+
15
+ ## Setup
16
+
17
+ ### Requirements
18
+
19
+ 1. Ruby v2+.
20
+ 2. Rails/Any other application that uses Sprockets.
21
+ 2. NPM v3+. We only support version 3 because commoner doesn't do any sort of deduplication of dependencies, so you could end up with a huge bundle if you don't want out. We only test against version 3, so you will definitely run into issues when running version 2.
22
+ 3. We recommend and support version 4+ of Node.js.
23
+
24
+ ### 10-second simple set up
25
+
26
+ To get started, let's begin with the simplest possible set up: just enabling resolving of `require`.
27
+
28
+ 1. Add `sprockets-commoner` to Gemfile, run `bundle install`, and restart your Rails server.
29
+ 1. Add package.json with `babel-core` version 6 and any packages you want. For the example, we'll use the excellent [lodash](https://lodash.com) library. `npm install -S babel-core@6 lodash`
30
+ 1. `require` your client-side JavaScript packages from `application.js`!
31
+ ```
32
+ var _ = require('lodash');
33
+
34
+ console.log(_.map([1, 2, 3], function(n) { return n * 3; }));
35
+ ```
36
+
37
+ ### Enabling Babel transforms
38
+
39
+ 1. Install any Babel plugins or presets you want to use. We'll use the default ES2015 preset; `npm install babel-preset-es2015`.
40
+ 1. Add a `.babelrc` with you required configuration. We just need to do `echo '{presets: ["es2015"]}' > .babelrc`.
41
+ 1. Use any feature you want! For example, let's use `import` and arrow functions in our `application.js`:
42
+
43
+ ```
44
+ import {map} from 'lodash';
45
+
46
+ console.log(map([1, 2, 3], (n) => n * 3));
47
+ ```
48
+
49
+ ### Advanced configuration
50
+
51
+ #### Fine-tuned selection of files to process
52
+
53
+ By default, commoner will process any file under the application root directory. If you want more fine-tuned control over which files to process, you can specify which paths to include or exclude. To do so, you will need to re-register the Sprockets processor. For example:
54
+
55
+ ```
56
+ # In config/initializers/sprockets_commoner.rb
57
+ Rails.application.config.assets.configure do |env|
58
+ env.unregister_postprocessor('application/javascript', Sprockets::Commoner::Processor)
59
+ env.register_postprocessor('application/javascript', Sprockets::Commoner::Processor.new(
60
+ env.root,
61
+ # include, exclude, and babel_exclude patterns can be path prefixes or regexes.
62
+ # Explicitely list paths to include. The default is `[env.root]`
63
+ include: ['app/assets/javascripts/subdirectory'],
64
+ # List files to ignore and not process require calls or apply any Babel transforms to. Default is empty.
65
+ exclude: [/ignored/],
66
+ # Anything listed in babel_exclude has its require calls resolved, but no transforms listed in .babelrcs applied.
67
+ # Default is [/node_modules/]
68
+ babel_exclude: [/node_modules/]
69
+ ))
70
+ end
71
+ ```
72
+
73
+ ## Testing with Teaspoon
74
+
75
+ Teaspoon is not compatible with commoner by default. Under `test/demo` you can find a demo Rails application, containing a setup with Teaspoon. The key change is a custom boot partial and JavaScript file, found under `spec/javascripts/fixtures/teaspoon/suite`. Simply copy those two files under `fixtures/teaspoon/suite` in your JavaScript tests, make necessary changes to `boot.js.erb` to point to your test files, and you should be good to go.
76
+
77
+ ## CoffeeScript interoperability
78
+
79
+ Commoner is designed from the start to facilitate a transition from CoffeeScript to ES2015.
80
+
81
+ ### Importing CoffeeScript files
82
+
83
+ Any JavaScript file can `require` a CoffeeScript file, which will cause that CoffeeScript file to be scanned for a global variable reference and the `require` call to be replaced with a reference.
84
+ If we have the following two files:
85
+
86
+ ```
87
+ # file.coffee
88
+ class window.ImportantClass
89
+ ```
90
+
91
+ ```
92
+ // main.js
93
+ var klass = require('./file');
94
+
95
+ new klass();
96
+ ```
97
+
98
+ Then the second file will just be compiled down to `new window.ImportantClass()`. Importing global references works for global assignments and class definitions.
99
+
100
+ ### Expose
101
+
102
+ We have added a custom directive that makes it very easy to expose an ES2015 module to the global namespace so it can be used by CoffeeScript files or any other code. For example:
103
+
104
+ ```
105
+ 'expose window.MyClass`;
106
+
107
+ export default class MyClass {}
108
+ ```
109
+
110
+ `expose` will use the default export if available, otherwise the whole module namespace will be assigned to the global variable. For example:
111
+
112
+ ```
113
+ // constants.js
114
+ 'expose window.Constants';
115
+
116
+ export const A = 1;
117
+ export const B = 2;
118
+ export const C = 3;
119
+ ```
120
+
121
+ This will make `window.Constants` equal `{A: 1, B: 2, C: 3}`.
122
+
123
+ ## Methodology
124
+
125
+ Commoner registers a postprocessor that takes any `application/javascript` file and passes it through Babel.
126
+
127
+ ### `require` support
128
+
129
+ `Sprockets::Commoner` enables support for `require()` by replacing `require` function calls with variable references. It does this by assigning every file a unique identifier based on their filename. For example, if the file you're referencing is located at `<root>/node_modules/package/index.js` it will get the name `__commoner_module__node_modules$package$index_js`. Any file that then `require`s this specific file will have that `require` call replaced with the identifier.
130
+ reference.
131
+
132
+ The Babel plugin also communicates back any files that were required to make sure they are included by Sprockets. `Sprockets::Commoner` depends on the topological ordering that Sprockets does to make sure any module that is needed is instantiated before it is used.
133
+
134
+ ### Example
135
+
136
+ After the regular Babel plugins are done doing their thing, `babel-plugin-commoner-internal` kicks in, which is commoner's plugin that does the actual resolving. This plugin does the following things:
137
+
138
+ * It finds any `require` calls and rewires them to variable references (as detailed in [`require` support](#require-support))
139
+ * It wraps the module in a function and supplies it with `module` and `exports`. The end value of `module.exports` gets assigned to the module identifier, which is referenced by other files (as specified in '`require` support') Example:
140
+
141
+ ```
142
+ var __commoner_module__node_modules$package$index_js = __commoner_initialize_module__(function (module, exports) {
143
+ exports.default = 123;
144
+ });
145
+ ```
146
+ * If these is an expose directive at the top of the file, it assigns `module.exports` to the specified variable. For example, if the top of the file contains `'expose window.Whatever';` it will assign `exports.default` if there is a default, otherwise it just assigns `exports`. Therefore if our file has `expose window.Whatever` and no default, it will get `window.Whatever = exports;` appended to it.
147
+
148
+ #### Bundling
149
+
150
+ After all the files have been transformed, there is a bundle step which combines all of the processed JavaScript modules together. It then prepends an initializer function and all the Babel helpers (which are shared between all modules).
151
+
152
+ For example, if we have the following two files:
153
+
154
+ ```
155
+ // module.js
156
+ export default function a() {
157
+ return 1;
158
+ };
159
+ ```
160
+
161
+ ```
162
+ // application.js
163
+ import a from './module';
164
+
165
+ a();
166
+ ```
167
+
168
+ We will end up with the following (browser-runnable) file:
169
+
170
+ ```
171
+ !function() {
172
+ var __commoner_initialize_module__ = function(f) {
173
+ var module = {exports: {}};
174
+ f.call(module.exports, module, module.exports);
175
+ return module.exports;
176
+ };
177
+ var global = window;
178
+ var __commoner_helper__interopRequireDefault = function (obj) {
179
+ return obj && obj.__esModule ? obj : {
180
+ default: obj
181
+ };
182
+ };
183
+ var __commoner_module__app$assets$javascripts$module_js = __commoner_initialize_module__(function (module, exports) {
184
+ "use strict";
185
+
186
+ Object.defineProperty(exports, "__esModule", {
187
+ value: true
188
+ });
189
+ exports.default = a;
190
+ function a() {
191
+ return 1;
192
+ };
193
+ });
194
+ var __commoner_module__app$assets$javascripts$application_js = __commoner_initialize_module__(function (module, exports) {
195
+ 'use strict';
196
+
197
+ var _module2 = __commoner_helper__interopRequireDefault(__commoner_module__app$assets$javascripts$module_js);
198
+
199
+ (0, _module2.default)();
200
+ });
201
+ }();
202
+ ```
203
+
204
+ This file is meant to be compressed, and does incredibly well when processed by UglifyJS.
205
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :spec
data/circle.yml ADDED
@@ -0,0 +1,13 @@
1
+ machine:
2
+ node:
3
+ version: v5.7.0
4
+ ruby:
5
+ version: 2.2.2
6
+
7
+ dependencies:
8
+ override:
9
+ - script/setup
10
+
11
+ test:
12
+ override:
13
+ - script/test
@@ -0,0 +1,2 @@
1
+ /node_modules
2
+ npm-debug.log
@@ -0,0 +1,24 @@
1
+ # babel-plugin-commoner
2
+
3
+ ## Usage
4
+
5
+ `babel-plugin-commoner` should be included in your `.babelrc` by adding `commoner`. You can't install it through npm, but this directory is automatically added to the path by the Sprockets plugin.
6
+
7
+ ## Options
8
+
9
+ * `globals`: A mapping of module name to global variable name, which will ovveride any import of that package with a global variable reference.
10
+
11
+ ## Example
12
+
13
+ ```json
14
+ {
15
+ presets: ["es2015"],
16
+ plugins: [
17
+ ["commoner", {
18
+ globals: {
19
+ "jquery": "$"
20
+ }
21
+ }]
22
+ ]
23
+ }
24
+ ```
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+ if (typeof Object.assign != 'function') {
3
+ (function () {
4
+ Object.assign = function (target) {
5
+ 'use strict';
6
+ if (target === undefined || target === null) {
7
+ throw new TypeError('Cannot convert undefined or null to object');
8
+ }
9
+
10
+ var output = Object(target);
11
+ for (var index = 1; index < arguments.length; index++) {
12
+ var source = arguments[index];
13
+ if (source !== undefined && source !== null) {
14
+ for (var nextKey in source) {
15
+ if (source.hasOwnProperty(nextKey)) {
16
+ output[nextKey] = source[nextKey];
17
+ }
18
+ }
19
+ }
20
+ }
21
+ return output;
22
+ };
23
+ })();
24
+ }
25
+
26
+ var fs = require('fs');
27
+ var dirname = require('path').dirname;
28
+ var resolve = require('browser-resolve').sync;
29
+
30
+ module.exports = function (context) {
31
+ var t = context.types;
32
+ var exposeTemplate = context.template("$0 = exports['default'] != null ? exports['default'] : exports;");
33
+
34
+ var opts = null;
35
+ var rootRegex = null;
36
+ var identifierRegex = null;
37
+
38
+ function createIdentifierRegex() {
39
+ var globals = ['window'].concat(opts.globalNamespaces).map(function(namespace) {
40
+ return namespace + '\\.';
41
+ });
42
+
43
+ // Construct regex with prefixes which denote globals (like window.Something, @Something, or anything else in opts.globalNamespaces)
44
+ var globalObject = '(?:@|' + globals.join('|') + ')';
45
+ var validIdentifier = '[a-zA-Z][_a-zA-Z0-9]*';
46
+ // Look for identifiers that look like 'window.Something' or 'Shopify.Something'
47
+ var validAssignment = '(' + globalObject + validIdentifier + '(?:\\.' + validIdentifier + ')*)';
48
+ // Look for 'window.Something =' or 'class window.Something'
49
+ return '^(?:(?:' + validAssignment + '\\s*=)|(?:class ' + validAssignment + '))';
50
+ }
51
+
52
+ /*
53
+ * BIGGEST HACK OF __ALL_TIME__
54
+ * If we're requiring a CoffeeScript file, we're going to be assuming that we're assigning to the global namespace in that file.
55
+ * so, use a RegExp to find out that variable definition. (yep)
56
+ */
57
+ function findDeclarationInCoffeeFile(path) {
58
+ var contents = fs.readFileSync(path);
59
+ var identifiers = [];
60
+
61
+ for (var regexp = new RegExp(identifierRegex, 'gm'), find = regexp.exec(contents); find != null; find = regexp.exec(contents)) {
62
+ identifiers.push(find[1] || find[2]);
63
+ }
64
+
65
+ if (identifiers.length === 0) {
66
+ throw new Error('No identifiers found in ' + path);
67
+ } else if (identifiers.length > 1) {
68
+ throw new Error('Multiple identifiers found in ' + path);
69
+ }
70
+
71
+ return identifiers[0].replace(/^@/, 'window.');
72
+ }
73
+
74
+ function isRequire(path) {
75
+ return path.isCallExpression() && path.get('callee').isIdentifier({ name: 'require' }) && !path.scope.hasBinding('require');
76
+ }
77
+
78
+ // Get the target path from a require call
79
+ function requireTarget(path) {
80
+ var evaluate = path.get('arguments')[0].evaluate();
81
+ if (!evaluate.confident || path.node.arguments.length !== 1) {
82
+ return null;
83
+ }
84
+
85
+ var target = evaluate.value;
86
+ if (typeof target !== 'string') {
87
+ throw new Error('Invalid require call, string expected');
88
+ }
89
+ return target;
90
+ }
91
+
92
+ // Find any 'expose <name>' directive and get back the value of '<name>'
93
+ function findExpose(directives) {
94
+ var result = void 0;
95
+ for (var i = 0; i < directives.length; i++) {
96
+ if (result = /^expose ([A-Za-z\.]+)$/.exec(directives[i].value.value)) {
97
+ directives.splice(i, 1);
98
+ return result[1];
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+
105
+ // Transform a path into a variable name
106
+ function pathToIdentifier(path) {
107
+ var escapedPath = path.replace(rootRegex, '').replace(/[^a-zA-Z0-9_]/g, function (match) {
108
+ if (match === '/') {
109
+ return '$';
110
+ } else {
111
+ return '_';
112
+ }
113
+ });
114
+ return '__commoner_module__' + escapedPath;
115
+ }
116
+
117
+ function resolveTarget(file, path) {
118
+ var name = void 0;
119
+ if (opts.globals != null && (name = opts.globals[path]) != null) {
120
+ return name;
121
+ } else {
122
+ var resolvedPath = resolve(path, opts);
123
+ file.metadata.required.push(resolvedPath);
124
+
125
+ // Check if the path is under sourceRoot
126
+ var root = file.opts.sourceRoot;
127
+ if (!rootRegex.test(resolvedPath)) {
128
+ throw new Error("Cannot find module '" + path + "' from '" + dirname(file.opts.filename) + "' under '" + root + "'");
129
+ }
130
+
131
+ if (/\.coffee$/.test(resolvedPath)) {
132
+ // If it's a coffee script file, look for global variable assignments
133
+ return findDeclarationInCoffeeFile(resolvedPath);
134
+ } else {
135
+ // Otherwise we just look for the module by referencing its Special Identifier™
136
+ return pathToIdentifier(resolvedPath);
137
+ }
138
+ }
139
+ }
140
+
141
+ var callRewriter = {
142
+ VariableDeclarator: function VariableDeclarator(path, state) {
143
+ var init = path.get('init');
144
+ if (!isRequire(init)) {
145
+ return;
146
+ }
147
+ var binding = path.scope.getBinding(path.node.id.name);
148
+ if (!binding.constant) {
149
+ return;
150
+ }
151
+
152
+ var target = requireTarget(init);
153
+ if (target == null) {
154
+ return;
155
+ }
156
+
157
+ var name = resolveTarget(state.file, target);
158
+ path.scope.rename(name);
159
+ path.scope.rename(path.node.id.name, name);
160
+ path.remove();
161
+ },
162
+ CallExpression: function CallExpression(path, state) {
163
+ if (!isRequire(path)) {
164
+ return;
165
+ }
166
+
167
+ var target = requireTarget(path);
168
+ if (target == null) {
169
+ return;
170
+ }
171
+
172
+ var replacement = resolveTarget(state.file, target);
173
+ switch (path.parent.type) {
174
+ case "ExpressionStatement":
175
+ // We just need to know there's a dependency, we can remove it then
176
+ path.remove();
177
+ break;
178
+ default:
179
+ // Otherwise we just look for the module by referencing its Special Identifier™
180
+ path.replaceWith(t.identifier(replacement));
181
+ break;
182
+ }
183
+ }
184
+ };
185
+
186
+ return {
187
+ pre: function pre(file) {
188
+ if (file.metadata.required == null) {
189
+ file.metadata.required = [];
190
+ }
191
+ },
192
+
193
+ visitor: {
194
+ MemberExpression: function MemberExpression(path) {
195
+ if (path.get("object").matchesPattern("process.env")) {
196
+ var key = path.toComputedKey();
197
+ if (t.isStringLiteral(key)) {
198
+ path.replaceWith(t.valueToNode(process.env[key.value]));
199
+ }
200
+ }
201
+ },
202
+ UnaryExpression: function UnaryExpression(path) {
203
+ if (!path.node.operator === 'typeof') {
204
+ return;
205
+ }
206
+
207
+ var argument = path.get('argument');
208
+ if (!path.get('argument').isIdentifier()) {
209
+ return;
210
+ }
211
+
212
+ var name = path.node.argument.name;
213
+ if (name !== 'module' && name !== 'exports') {
214
+ return;
215
+ }
216
+
217
+ if (path.scope.hasBinding(name)) {
218
+ return;
219
+ }
220
+
221
+ path.replaceWith(t.stringLiteral('object'));
222
+ },
223
+ Program: {
224
+ exit: function exit(path, state) {
225
+ // Get options from commoner-options and merge them with the options
226
+ // that were passed to this plugin in .babelrc
227
+ opts = {
228
+ globalNamespaces: [],
229
+ // We can get these from Sprockets
230
+ extensions: ['.js', '.json', '.coffee', '.js.erb', '.coffee.erb']
231
+ };
232
+
233
+ // Look for the sprockets-commoner plugin for extra options
234
+ state.file.opts.plugins.map(function (plugin) {
235
+ return plugin[1];
236
+ }).filter(function (opts) {
237
+ return opts != null && opts.__commoner_options;
238
+ }).forEach(function (plugin) {
239
+ return Object.assign(opts, plugin);
240
+ });
241
+
242
+ Object.assign(opts, state.opts, { basedir: dirname(state.file.opts.filename) });
243
+ rootRegex = new RegExp('^' + state.file.opts.sourceRoot + '/');
244
+ identifierRegex = createIdentifierRegex();
245
+
246
+ // Signal back to Sprockets that we're rewiring
247
+ state.file.metadata.commonerEnabled = true;
248
+
249
+ var node = path.node;
250
+ var identifier = pathToIdentifier(state.file.opts.filename);
251
+ var expose = findExpose(node.directives);
252
+ if (expose != null) {
253
+ node.body.push(exposeTemplate(t.identifier(expose)));
254
+ }
255
+
256
+ // Transform module to a variable assignment.
257
+ // This variable is then referenced by any dependant children.
258
+ node.body = [t.variableDeclaration('var', [t.variableDeclarator(t.identifier(identifier), t.callExpression(t.identifier('__commoner_initialize_module__'), [t.functionExpression(null, [t.identifier('module'), t.identifier('exports')], t.blockStatement(node.body, node.directives))]))])];
259
+ node.directives = [];
260
+ path.traverse(callRewriter, state);
261
+ }
262
+ }
263
+ }
264
+ };
265
+ };
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "babel-plugin-sprockets-commoner-internal",
3
+ "private": true,
4
+ "description": "Find require calls and resolve the library they're calling.",
5
+ "scripts": {
6
+ "test": "NODE_ENV=test mocha"
7
+ },
8
+ "devDependencies": {
9
+ "babel-core": "^6.2.1",
10
+ "babel-plugin-transform-es2015-arrow-functions": "^6.5.2",
11
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.2.1",
12
+ "mocha": "^2.3.4"
13
+ }
14
+ }
@@ -0,0 +1,3 @@
1
+ # commoner-options
2
+
3
+ This is a dummy Babel plugin that's just used to receive options from Sprockets.
@@ -0,0 +1,7 @@
1
+ module.exports = function () {
2
+ return {
3
+ pre: function pre() {
4
+ this.opts.__commoner_options = true;
5
+ }
6
+ };
7
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "babel-plugin-commoner-options",
3
+ "description": "A bogus plugin used for receiving options from Sprockets",
4
+ "private": true
5
+ }
@@ -0,0 +1,15 @@
1
+ require 'sprockets'
2
+ require 'sprockets/commoner/processor'
3
+ require 'sprockets/commoner/bundle'
4
+
5
+ module Sprockets
6
+ module Commoner
7
+ end
8
+
9
+ register_postprocessor 'application/javascript', ::Sprockets::Commoner::Processor
10
+ register_bundle_metadata_reducer 'application/javascript', :commoner_enabled, false, :|
11
+ register_bundle_metadata_reducer 'application/javascript', :commoner_used_helpers, Set.new, :+
12
+ register_bundle_processor 'application/javascript', ::Sprockets::Commoner::Bundle
13
+ end
14
+
15
+ require 'sprockets/commoner/railtie' if defined?(Rails)
@@ -0,0 +1,57 @@
1
+ require 'schmooze'
2
+
3
+ module Sprockets
4
+ module Commoner
5
+ class Bundle < Schmooze::Base
6
+ dependencies generator: 'babel-generator.default',
7
+ babelHelpers: 'babel-helpers',
8
+ t: 'babel-types'
9
+
10
+ method :generate_helpers, <<-JS
11
+ function(helpers) {
12
+ if (helpers.length === 0) {
13
+ return '';
14
+ }
15
+ var declaration = t.variableDeclaration('var',
16
+ helpers.map(function(helper) {
17
+ return t.variableDeclarator(t.identifier('__commoner_helper__' + helper), babelHelpers.get(helper));
18
+ })
19
+ );
20
+ return generator(declaration).code;
21
+ }
22
+ JS
23
+
24
+ PRELUDE = <<-JS.freeze
25
+ !function() {
26
+ var __commoner_initialize_module__ = function(f) {
27
+ var module = {exports: {}};
28
+ f.call(module.exports, module, module.exports);
29
+ return module.exports;
30
+ };
31
+ var global = window;
32
+ JS
33
+
34
+ OUTRO = <<-JS.freeze
35
+
36
+ }();
37
+ JS
38
+ def self.instance(env)
39
+ @instance ||= new(env.root)
40
+ end
41
+
42
+ def self.call(input)
43
+ instance(input[:environment]).call(input)
44
+ end
45
+
46
+ def call(input)
47
+ return unless input[:metadata][:commoner_enabled]
48
+
49
+ used_helpers = input[:metadata][:commoner_used_helpers]
50
+ helpers = generate_helpers(used_helpers.to_a)
51
+ {
52
+ data: "#{PRELUDE}#{helpers}\n#{input[:data]}#{OUTRO}"
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,174 @@
1
+ require 'schmooze'
2
+ require 'open3'
3
+
4
+ module Sprockets
5
+ module Commoner
6
+ class Processor < Schmooze::Base
7
+ BABELRC_FILE = '.babelrc'.freeze
8
+ PACKAGE_JSON = 'package.json'.freeze
9
+ JS_PACKAGE_PATH = File.expand_path('../../../js', __dir__)
10
+ ALLOWED_EXTENSIONS = /\.js(?:\.erb)?\z/
11
+
12
+ dependencies babel: 'babel-core', commoner: 'babel-plugin-sprockets-commoner-internal'
13
+
14
+ method :version, 'function() { return [process.version, babel.version]; }'
15
+ method :transform, %q{function(code, opts, commonerOpts) {
16
+ try {
17
+ var file = new babel.File(opts);
18
+
19
+ // The actual helpers are generated in bundle.rb
20
+ file.set("helperGenerator", function(name) { return babel.types.identifier('__commoner_helper__' + name); });
21
+
22
+ var commonerPlugin = babel.OptionManager.normalisePlugin(commoner);
23
+ file.buildPluginsForOptions({plugins: [[commonerPlugin, commonerOpts]]});
24
+
25
+ return file.wrap(code, function () {
26
+ file.addCode(code);
27
+ file.parseCode(code);
28
+ return file.transform();
29
+ });
30
+ } catch (err) {
31
+ if (err.codeFrame != null) {
32
+ err.message += "\n";
33
+ err.message += err.codeFrame;
34
+ }
35
+ throw err;
36
+ }
37
+ }}
38
+ def self.instance(env)
39
+ @instance ||= new(env.root)
40
+ end
41
+
42
+ def self.call(input)
43
+ instance(input[:environment]).call(input)
44
+ end
45
+
46
+ attr_reader :include, :exclude, :babel_exclude
47
+ def initialize(root, include: [root], exclude: [], babel_exclude: [/node_modules/])
48
+ @include = include.map {|path| expand_to_root(path, root) }
49
+ @exclude = exclude.map {|path| expand_to_root(path, root) }
50
+ @babel_exclude = babel_exclude.map {|path| expand_to_root(path, root) }
51
+ super(root, 'NODE_PATH' => JS_PACKAGE_PATH)
52
+ end
53
+
54
+ def call(input)
55
+ @cache_key ||= [
56
+ self.class.name,
57
+ version,
58
+ VERSION,
59
+ ].freeze
60
+
61
+ filename = input[:filename]
62
+
63
+ return unless should_process?(filename)
64
+
65
+ @env = input[:environment]
66
+ @required = input[:metadata][:required].to_a
67
+ insertion_index = @required.index(input[:uri]) || -1
68
+ @dependencies = Set.new(input[:metadata][:dependencies])
69
+
70
+
71
+ babel_config = babelrc_data(filename)
72
+
73
+ result = input[:cache].fetch([filename, @cache_key, input[:data], babel_config]) do
74
+ transform(input[:data], options(input), paths: @env.paths)
75
+ end
76
+
77
+ if result['metadata'].has_key?('required')
78
+ result['metadata']['required'].each do |r|
79
+ asset = resolve(r, accept: input[:content_type], pipeline: :self)
80
+ @required.insert(insertion_index, asset)
81
+ end
82
+ end
83
+
84
+ {
85
+ data: result['code'],
86
+ dependencies: @dependencies,
87
+ required: Set.new(@required),
88
+
89
+ commoner_used_helpers: Set.new(input[:metadata][:commoner_used_helpers]) + result['metadata']['usedHelpers'],
90
+ commoner_enabled: input[:metadata][:commoner_enabled] | result['metadata']['commonerEnabled'],
91
+ }
92
+ end
93
+
94
+ private
95
+ def expand_to_root(path, root)
96
+ if path.is_a?(String)
97
+ File.expand_path(path, root)
98
+ else
99
+ path
100
+ end
101
+ end
102
+
103
+ def should_process?(filename)
104
+ return false unless ALLOWED_EXTENSIONS =~ filename
105
+ return false unless self.include.empty? || match_any?(self.include, filename)
106
+ return false if match_any?(self.exclude, filename)
107
+ true
108
+ end
109
+
110
+ def match_any?(patterns, filename)
111
+ patterns.any? { |pattern| pattern_match(pattern, filename) }
112
+ end
113
+
114
+ def pattern_match(pattern, filename)
115
+ if pattern.is_a?(String)
116
+ filename.start_with?(pattern)
117
+ else
118
+ pattern === filename
119
+ end
120
+ end
121
+
122
+ def babelrc_data(filename)
123
+ while filename != (filename = File.dirname(filename))
124
+ begin
125
+ name = File.join(filename, BABELRC_FILE)
126
+ data = File.read(name)
127
+ depend_on_file(name)
128
+ return data
129
+ rescue Errno::ENOENT
130
+ name = File.join(filename, PACKAGE_JSON)
131
+ data = package_babel_data(name)
132
+ if data
133
+ depend_on_file(name)
134
+ return JSON.dump(data)
135
+ else
136
+ nil
137
+ end
138
+ end
139
+ end
140
+ return nil
141
+ end
142
+
143
+ def package_babel_data(filename)
144
+ return JSON.parse(File.read(filename))['babel']
145
+ rescue Errno::ENOENT
146
+ return nil
147
+ end
148
+
149
+ def options(input)
150
+ # TODO(bouk): Fix sourcemaps. Sourcemaps are only available in Sprockets v4
151
+ {
152
+ 'ast' => false,
153
+ 'babelrc' => !match_any?(self.babel_exclude, input[:filename]),
154
+ 'filename' => input[:filename],
155
+ 'filenameRelative' => PathUtils.split_subpath(input[:load_path], input[:filename]),
156
+ 'moduleRoot' => nil,
157
+ 'sourceRoot' => @env.root,
158
+ }
159
+ end
160
+
161
+ def resolve(path, **kargs)
162
+ uri, deps = @env.resolve!(path, load_paths: [@env.root], **kargs)
163
+ @dependencies.merge(deps)
164
+ uri
165
+ end
166
+
167
+ def depend_on_file(path)
168
+ uri, deps = @env.resolve!(path, load_paths: [@env.root])
169
+ @dependencies.merge(deps)
170
+ uri
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,11 @@
1
+ module Sprockets
2
+ module Commoner
3
+ class Railtie < ::Rails::Railtie
4
+ initializer 'sprockets-commoner' do
5
+ # We need to disable debugging because otherwise Rails will include each file individually, while we need everything to be bundled up together into a single file.
6
+ config.assets.debug = false
7
+ config.assets.paths << 'node_modules'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Sprockets
2
+ module Commoner
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sprockets/commoner/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sprockets-commoner"
8
+ spec.version = Sprockets::Commoner::VERSION
9
+ spec.authors = ["Bouke van der Bijl"]
10
+ spec.homepage = 'https://github.com/Shopify/sprockets-commoner'
11
+ spec.email = ["bouke@shopify.com"]
12
+ spec.license = 'MIT'
13
+
14
+ spec.summary = %q{Use Babel in Sprockets to compile modules for the browser}
15
+ spec.description = %q{Sprockets::Commoner uses Node.JS to compile ES2015+ files to ES5 using Babel directly from NPM, without vendoring it.}
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|script|js\/babel-plugin-sprockets-commoner-internal\/test)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+ spec.add_dependency "sprockets", ">= 3", "< 4"
30
+ spec.add_dependency "schmooze", "~> 0.1.5"
31
+
32
+ spec.add_development_dependency "rake", "~> 10.0"
33
+ spec.add_development_dependency "minitest", "~> 5.0"
34
+ spec.add_development_dependency "coffee-script", "~> 2.4"
35
+ spec.add_development_dependency "uglifier", "~> 2.7"
36
+ spec.add_development_dependency "pry", "~> 0.10"
37
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sprockets-commoner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bouke van der Bijl
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sprockets
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '4'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '4'
33
+ - !ruby/object:Gem::Dependency
34
+ name: schmooze
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.5
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.5
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: minitest
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: coffee-script
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.4'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.4'
89
+ - !ruby/object:Gem::Dependency
90
+ name: uglifier
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.7'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.7'
103
+ - !ruby/object:Gem::Dependency
104
+ name: pry
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.10'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.10'
117
+ description: Sprockets::Commoner uses Node.JS to compile ES2015+ files to ES5 using
118
+ Babel directly from NPM, without vendoring it.
119
+ email:
120
+ - bouke@shopify.com
121
+ executables: []
122
+ extensions: []
123
+ extra_rdoc_files: []
124
+ files:
125
+ - ".gitignore"
126
+ - ".nvmrc"
127
+ - CODE_OF_CONDUCT.md
128
+ - Gemfile
129
+ - README.md
130
+ - Rakefile
131
+ - circle.yml
132
+ - js/babel-plugin-sprockets-commoner-internal/.gitignore
133
+ - js/babel-plugin-sprockets-commoner-internal/README.md
134
+ - js/babel-plugin-sprockets-commoner-internal/index.js
135
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/browser-resolve/LICENSE
136
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/browser-resolve/README.md
137
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/browser-resolve/empty.js
138
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/browser-resolve/index.js
139
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/browser-resolve/package.json
140
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/LICENSE
141
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/README.md
142
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/index.js
143
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/lib/async.js
144
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/lib/caller.js
145
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/lib/core.js
146
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/lib/core.json
147
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/lib/node-modules-paths.js
148
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/lib/sync.js
149
+ - js/babel-plugin-sprockets-commoner-internal/node_modules/resolve/package.json
150
+ - js/babel-plugin-sprockets-commoner-internal/package.json
151
+ - js/babel-plugin-sprockets-commoner/README.md
152
+ - js/babel-plugin-sprockets-commoner/index.js
153
+ - js/babel-plugin-sprockets-commoner/package.json
154
+ - lib/sprockets/commoner.rb
155
+ - lib/sprockets/commoner/bundle.rb
156
+ - lib/sprockets/commoner/processor.rb
157
+ - lib/sprockets/commoner/railtie.rb
158
+ - lib/sprockets/commoner/version.rb
159
+ - sprockets-commoner.gemspec
160
+ homepage: https://github.com/Shopify/sprockets-commoner
161
+ licenses:
162
+ - MIT
163
+ metadata:
164
+ allowed_push_host: https://rubygems.org
165
+ post_install_message:
166
+ rdoc_options: []
167
+ require_paths:
168
+ - lib
169
+ required_ruby_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ requirements: []
180
+ rubyforge_project:
181
+ rubygems_version: 2.5.1
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: Use Babel in Sprockets to compile modules for the browser
185
+ test_files: []