Created
August 30, 2017 21:23
-
-
Save spectras/d39f36494b23ab43230897452b157fd3 to your computer and use it in GitHub Desktop.
Generate an entry point suitable for using Ember.js with rollup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
"use strict"; | |
/* Copyright (C) 2017 Julien Hartmann, [email protected] | |
* | |
* 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; | |
}); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; 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