Skip to content

Instantly share code, notes, and snippets.

@spectras
Created August 30, 2017 21:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spectras/d39f36494b23ab43230897452b157fd3 to your computer and use it in GitHub Desktop.
Save spectras/d39f36494b23ab43230897452b157fd3 to your computer and use it in GitHub Desktop.
Generate an entry point suitable for using Ember.js with rollup
#!/usr/bin/env node
"use strict";
/* Copyright (C) 2017 Julien Hartmann, juli1.hartmann@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* The program requires the template compiler from Ember to be available
* in a "deps" subdirectory. Or just edit the path below.
*
* Run as ./ember-register.js config-file.ini myproject.main.js
*
* Sample configuration file attached in the gist.
*/
const assert = require('assert');
const compiler = require('./deps/ember-template-compiler.js');
const fs = require('fs');
const path = require('path');
const process = require('process');
/****************************************************************************/
function parseArgs(argv) {
var config = {
input: null,
output: null,
includes: []
};
var opt = null;
function addOption(option, value) {
switch (option) {
case 'I':
config.includes.push(value);
break;
case null:
if (!config.input) { config.input = value; break; }
if (!config.output) { config.output = value; break; }
throw new Error("Syntax: ember-register [options] input [output]");
}
}
for (var idx = 2; idx < argv.length; ++idx) {
const token = argv[idx];
if (token[0] === '-' && token.length >= 2) {
if (token.length > 2) {
addOption(token[1], token.substr(2));
} else {
opt = token[1];
}
} else if (opt) {
addOption(opt, token);
opt = null;
} else {
addOption(null, token);
}
}
if (!config.input) { throw new Error("Requires an input argument"); }
if (!config.output) { config.output = '-'; }
config.includes.unshift(path.dirname(config.input));
return config;
}
function parseConfig(filePath) {
const ini = require('ini');
const conf = ini.parse(fs.readFileSync(filePath, 'utf-8'));
const registrations = conf.registrations || {};
var register = {};
conf.register.split(',').forEach(function (name) {
name = name.trim();
var dirname = name + 's';
var options = {};
if (registrations.hasOwnProperty(name)) {
registrations[name].split(" ").forEach(function (str) {
var option = str.split(':');
options[option[0]] = option[1];
});
}
if (options.dirname) {
dirname = options.dirname;
delete options.dirname;
}
register[name] = {
name: name,
dirname: dirname,
options: options
};
});
return {
globals: conf.globals || {},
register: register,
roots: conf.roots || [],
imports: conf.imports || []
};
}
/****************************************************************************/
function findPath(filePath, locations) {
if (path.isAbsolute(filePath)) {
if (fs.existsSync(path)) { return attempt; }
throw new Error(`File not found: ${filePath}`);
}
for (var idx = 0; idx < locations.length; ++idx) {
const attempt = path.join(locations[idx], filePath);
if (fs.existsSync(attempt)) { return attempt; }
}
throw new Error(`File not found: ${filePath} -- searched in ${locations}`);
}
function readdir(dirPath) {
return new Promise((resolve, reject) => {
fs.readdir(dirPath, (err, files) => {
if (err) { reject(err); } else { resolve(files); }
});
});
}
function walkPath(dirPath, callback) {
return readdir(dirPath).then(files => {
return Promise.all(files.map(function (fileName) {
return walkPath(path.join(dirPath, fileName), callback);
}));
}, error => {
if (error.code === "ENOTDIR") { callback(dirPath); return; }
throw error;
});
}
function camelize(str) {
return str.replace(/(?!^)[_-]([a-zA-Z])/, (match, p1) => { return p1.toUpperCase(); });
}
/****************************************************************************/
class Generator {
constructor (config) {
this.tplExtensions = ['.tpl', '.hbs'];
this.typesByDirname = {};
this.typesByName = {};
this.suffixes = ['.js'].concat(this.tplExtensions);
for (var key in config.register) { if (config.register.hasOwnProperty(key)) {
const entry = config.register[key];
this.typesByName[entry.name] = entry;
this.typesByDirname[entry.dirname] = entry;
}}
this.initializers = [];
this.registrations = [];
this.templates = [];
}
// Drop one suffix from the list, if present
removeSuffix(str, suffixes) {
for (var idx = 0; idx < suffixes.length; ++idx) {
if (str.endsWith(suffixes[idx])) {
return str.substr(0, str.length - suffixes[idx].length);
}
}
return str;
}
// Convert a file path into a token list, handling extensions
tokenizePath(path) {
var tokens = path.split('/');
tokens = tokens.filter(token => { return token; });
tokens[tokens.length - 1] = this.removeSuffix(
tokens[tokens.length - 1], this.suffixes
);
return tokens;
}
makeRegistrationName(tokens) {
return tokens.map(camelize).join('.');
}
// Compile a template file. Returns a promise that resolves to template code.
compileTemplate(filePath) {
var data = fs.readFileSync(filePath, 'utf-8');
return compiler.precompile(data.toString(), false);
}
// Handle a single file
handleFile(filePath, root) {
assert(!root || filePath.startsWith(root));
const tokens = this.tokenizePath(root ? filePath.substr(root.length) : filePath);
const types = this.typesByDirname;
for (var bit = 0; bit < tokens.length; ++bit) {
const token = tokens[bit];
const prefix = bit > 0 ? [tokens[bit - 1]] : [];
if (token === 'templates') {
this.templates.push({
name: prefix.concat(tokens.slice(bit + 1)).join('/'),
code: this.compileTemplate(filePath)
});
break;
}
if (token === 'initializers') {
this.initializers.push(filePath);
break;
}
if (types.hasOwnProperty(token)) {
this.registrations.push({
type: types[token].name,
name: this.makeRegistrationName(prefix.concat(tokens.slice(bit + 1))),
fileName: filePath
});
break;
}
}
if (bit === tokens.length) {
switch (tokens[bit - 1]) {
case 'component':
this.registrations.push({
type: 'component',
name: tokens[bit - 2],
fileName: filePath
});
break;
case 'template':
this.templates.push({
name: 'components/' + tokens[bit - 2],
code: this.compileTemplate(filePath)
});
break;
}
}
}
output(stream) {
var imports = this.registrations.map((registration, idx) => {
var fileName = path.resolve(registration.fileName);
if (stream.path) { fileName = path.relative(path.dirname(stream.path), fileName); }
return `import r${idx} from '${fileName}';`;
}).concat(this.initializers.map(fileName => {
fileName = path.resolve(fileName);
if (stream.path) { fileName = path.relative(path.dirname(stream.path), fileName); }
return `import {} from '${fileName}';`;
})).join("\n");
var options = [];
Object.keys(this.typesByName).forEach(key => {
const items = this.typesByName[key].options;
if (Object.keys(items).length > 0) {
const opts = Object.keys(items).map(optKey => {
const optValue = items[optKey];
return `${optKey}: ${optValue}`;
}).join(', ');
options.push(` app.registerOptionsForType('${key}', {${opts}});`);
}
});
var registrations = this.registrations.map((registration, idx) => {
return ` app.register("${registration.type}:${registration.name}", r${idx});`
}).join("\n");
var templates = this.templates.map(template => {
return ` t["${template.name}"] = Ember.HTMLBars.template(${template.code});`;
}).join("\n");
stream.write(
`import Ember from 'ember';
${imports}
Ember.Application.initializer({
name: 'registrations',
initialize: function (app) {
var t = Ember.TEMPLATES;
${options}
${registrations}
${templates}
}
});
`
, 'utf-8');
}
}
/****************************************************************************/
(function main() {
const args = parseArgs(process.argv);
const config = parseConfig(args.input);
var generator = new Generator(config);
var rootPromises = config.roots.map(function (path) {
path = findPath(path, args.includes);
return walkPath(path, function (fileName) {
generator.handleFile(fileName, path);
});
});
var importPromises = config.imports.map(function (path) {
path = findPath(path, args.includes);
return walkPath(path, function (fileName) {
generator.handleFile(fileName, null);
});
});
Promise.all(rootPromises.concat(importPromises)).then(function () {
generator.output(args.output === '-'
? process.stdout
: fs.WriteStream(args.output));
}, error => {
console.error("Path enumeration failed: " + error.toString());
throw error;
});
})();
; Uses INI syntax
; paths are relative to this file
roots[] = myproject
roots[] = path/to/mylibrary
register = adapter, controller, helper, model, route, service, view
[registrations]
model = singleton:false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment