From 3dc5b23b1e076d5a60fa1904e8ebc96f1734a9d8 Mon Sep 17 00:00:00 2001 From: Dan Gebhardt Date: Fri, 3 Feb 2017 10:31:18 -0500 Subject: [PATCH] Initial commit --- .gitignore | 7 + .npmignore | 10 + CODE_OF_CONDUCT.md | 9 + LICENSE | 19 ++ README.md | 10 + lib/broccoli/get-module-config.js | 24 ++ lib/broccoli/glimmer-app.js | 272 +++++++++++++++++++ lib/broccoli/glimmer-template-precompiler.js | 38 +++ lib/broccoli/module-map-creator.js | 102 +++++++ lib/broccoli/rollup-with-dependencies.js | 59 ++++ package.json | 37 +++ 11 files changed, 587 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/broccoli/get-module-config.js create mode 100644 lib/broccoli/glimmer-app.js create mode 100644 lib/broccoli/glimmer-template-precompiler.js create mode 100644 lib/broccoli/module-map-creator.js create mode 100644 lib/broccoli/rollup-with-dependencies.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..593d13c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.DS_Store +.vscode +**/*.log +dist +tests +tmp diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..759c5c5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,10 @@ +node_modules +test +tests +tmp +testem.json +tsconfig.* +.travis.yml +.gitignore +.DS_Store +*.log diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..98fe2cd --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +The Ember team and community are committed to everyone having a safe and inclusive experience. + +**Our Community Guidelines / Code of Conduct can be found here**: + +http://emberjs.com/guidelines/ + +For a history of updates, see the page history here: + +https://github.com/emberjs/website/commits/master/source/guidelines.html.erb diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c159e63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Dan Gebhardt, Tom Dale, and contributors. + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6268a4d --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# glimmer-application-pipeline [![Build Status](https://secure.travis-ci.org/glimmerjs/glimmer-application-pipeline.svg?branch=master)](http://travis-ci.org/glimmerjs/glimmer-application-pipeline) + +## Acknowledgements + +Thanks to [Monegraph](http://monegraph.com) for funding the initial development +of this library. + +## License + +MIT License. diff --git a/lib/broccoli/get-module-config.js b/lib/broccoli/get-module-config.js new file mode 100644 index 0000000..5715e66 --- /dev/null +++ b/lib/broccoli/get-module-config.js @@ -0,0 +1,24 @@ +module.exports = function(config) { + let moduleConfig = config.moduleConfiguration; + + let collectionMap = {}; + let collectionPaths = []; + let collections = moduleConfig.collections; + let collectionNames = Object.keys(collections); + collectionNames.forEach(function(collectionName) { + var collection = collections[collectionName]; + var fullPath = collectionName; + if (collection.group) { + fullPath = collection.group + '/' + fullPath; + } + collectionPaths.push(fullPath); + collectionMap[fullPath] = collectionName; + }); + + moduleConfig.collectionMap = collectionMap; + moduleConfig.collectionPaths = collectionPaths; + + // console.log('moduleConfig', moduleConfig); + + return moduleConfig; +}; diff --git a/lib/broccoli/glimmer-app.js b/lib/broccoli/glimmer-app.js new file mode 100644 index 0000000..7a989c6 --- /dev/null +++ b/lib/broccoli/glimmer-app.js @@ -0,0 +1,272 @@ +"use strict"; + +var defaultsDeep = require('ember-cli-lodash-subset').defaultsDeep; +var EmberApp = require('ember-cli/lib/broccoli/ember-app'); +var ConfigLoader = require('broccoli-config-loader'); +var ConfigReplace = require('broccoli-config-replace'); +var Funnel = require('broccoli-funnel'); + +var fs = require('fs'); +var path = require('path'); +var rimraf = require('rimraf'); +var typescript = require('broccoli-typescript-compiler'); +var concat = require('broccoli-concat'); +var merge = require('broccoli-merge-trees'); +var stew = require('broccoli-stew'); +var compileSass = require('broccoli-sass'); +var replace = require('broccoli-string-replace'); +var mv = stew.mv; +var find = stew.find; +var map = stew.map; +var babel = require('broccoli-babel-transpiler'); +var assetRev = require('broccoli-asset-rev'); +var uglify = require('broccoli-uglify-sourcemap'); + +var RollupWithDependencies = require('./rollup-with-dependencies'); +var GlimmerTemplatePrecompiler = require('./glimmer-template-precompiler'); +const ModuleMapCreator = require('./module-map-creator'); + +var DEFAULT_CONFIG = { + configPath: './config/environment', + trees: { + app: 'src', + styles: 'src/ui/styles' + }, + jshintrc: { + tests: 'tests', + app: 'src' + } +}; + +module.exports = GlimmerApp; + +/** + * GlimmerApp provides an interface to a package (app, engine, or addon) + * compatible with the module unification layout. + * + * @class GlimmerApp + * @extends EmberApp + * @constructor + * @param {Object} [defaults] + * @param {Object} [options={}] Configuration options + */ +function GlimmerApp(defaults, options) { + if (arguments.length === 0) { + options = {}; + } else if (arguments.length === 1) { + options = defaults; + } else { + defaultsDeep(options, defaults); + } + + this.appConstructor(defaultsDeep(options, DEFAULT_CONFIG)); +} + +GlimmerApp.__proto__ = EmberApp; + +GlimmerApp.prototype = Object.create(EmberApp.prototype); +GlimmerApp.prototype.constructor = GlimmerApp; +GlimmerApp.prototype.appConstructor = EmberApp.prototype.constructor; + +GlimmerApp.prototype.toTree = function(options) { + var projectDir = process.cwd(); + var projectFolder = this.projectFolder = (options && options.projectFolder || 'src'); + var app = projectDir + '/' + projectFolder; + + console.log('Building app tree:', app); + + if (process.env.EMBER_ENV !== 'production') { + rimraf.sync('dist'); + } + + var tsOptions = { + tsconfig: { + compilerOptions: { + target: "es5", + module: "es2015", + inlineSourceMap: true, + inlineSources: true, + moduleResolution: "node", + rootDir: '.', + mapRoot : '/' + } + } + }; + + // Select all app src, except config file that needs to be rewritten + var tsTree = find(app, { + include: ['**/*.ts'], + exclude: ['**/*.d.ts', 'config/**/*.ts'] + }); + + var resolvableTree = find(app, { + include: ['**/*.ts', '**/*.hbs'], + exclude: ['**/*.d.ts', 'config/**/*.ts'] + }); + + var configEnvironment = find(app, { + include: ['config/environment.ts'] + }); + +var moduleMap = this.buildModuleMap(resolvableTree, configEnvironment); + + configEnvironment = this.rewriteConfigEnvironment(configEnvironment); + + tsTree = merge([ + tsTree, + moduleMap, + configEnvironment + ]); + + var jsTree = typescript(tsTree, tsOptions); + + var hbsTree = find([ app ], { + include: [ + '**/*.hbs' + ] + }); + + hbsTree = new GlimmerTemplatePrecompiler(hbsTree, { + rootName: this.project.pkg.name + }); + + jsTree = merge([jsTree, + hbsTree]); + + jsTree = new RollupWithDependencies(jsTree, { + inputFiles: ['**/*.js'], + rollup: { + onwarn(message) { + // Suppress known error message caused by TypeScript compiled code with Rollup + // https://github.com/rollup/rollup/wiki/Troubleshooting#this-is-undefined + if (/The \`this\` keyword is equivalent to \`undefined\` at the top level of an ES module, and has been rewritten/.test(message)) { + return; + } + console.error(message); + }, + entry: 'index.js', + dest: 'app.js', + sourceMap: 'inline' + } + }); + + if (process.env.EMBER_ENV === 'production') { + jsTree = uglify(jsTree, { + compress: { + screw_ie8: true, + }, + sourceMapConfig: { + enabled: false + } + }); + } + + var htmlTree = this.index(); + + var cssTree = compileSass([app + '/ui/styles'], + 'app.scss', 'app.css'); + + var appTree = merge([ + jsTree, + htmlTree, + cssTree, + 'public/' + ]); + + if (process.env.EMBER_ENV === 'production') { + var extensions = ['js']; + var replaceExtensions = ['html', 'js']; + + if (!options.preventCssFingerPrinting) { + extensions.push('css'); + replaceExtensions.push('css'); + } + appTree = assetRev(appTree, { + extensions: extensions, + replaceExtensions: replaceExtensions + }); + + if (!options.disableCache) { + var manifestTree = appcache(appTree, { + cache: ['/'], + network: ['*'], + treeCacheEntryPathPrefix: options && options.prefix || null + }); + + appTree = merge([appTree, manifestTree]); + } + } + + return appTree; +} + +GlimmerApp.prototype._configPath = function() { + return path.join(this.name, 'config', 'environments', this.env + '.json'); +} + +GlimmerApp.prototype.rewriteConfigEnvironment = function(src) { + return new ConfigReplace(src, this._configTree(), { + configPath: this._configPath(), + files: [ 'config/environment.ts' ], + patterns: this._configReplacePatterns() + }); +} + +GlimmerApp.prototype.buildModuleMap = function(src, config) { + return new ModuleMapCreator(src, this._configTree(), { + configPath: this._configPath() + }); +} + +GlimmerApp.prototype.index = function() { + var htmlName = this.options.outputPaths.app.html; + var files = [ + 'index.html' + ]; + + var index = new Funnel(this.trees.app + '/ui', { + files: files, + getDestinationPath: function(relativePath) { + if (relativePath === 'index.html') { + relativePath = htmlName; + } + return relativePath; + }, + annotation: 'Funnel: index.html' + }); + + return new ConfigReplace(index, this._configTree(), { + configPath: path.join(this.name, 'config', 'environments', this.env + '.json'), + files: [ htmlName ], + patterns: this._configReplacePatterns() + }); +}; + +GlimmerApp.prototype._configTree = function() { + if (this._cachedConfigTree) { + return this._cachedConfigTree; + } + + var configPath = this.project.configPath(); + var configTree = new ConfigLoader(path.dirname(configPath), { + env: this.env, + tests: this.tests, + project: this.project + }); + + this._cachedConfigTree = new Funnel(configTree, { + srcDir: '/', + destDir: this.name + '/config', + annotation: 'Funnel (config)' + }); + + return this._cachedConfigTree; +} + +function getNpmDependencies(projectPath) { + return JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'))).dependencies; +} + +function getBowerDependencies(projectPath) { + return JSON.parse(fs.readFileSync(path.join(projectPath, 'bower.json'))).dependencies; +} diff --git a/lib/broccoli/glimmer-template-precompiler.js b/lib/broccoli/glimmer-template-precompiler.js new file mode 100644 index 0000000..95d0490 --- /dev/null +++ b/lib/broccoli/glimmer-template-precompiler.js @@ -0,0 +1,38 @@ +var Filter = require('broccoli-persistent-filter'); +var compiler = require('@glimmer/compiler'); + +GlimmerTemplatePrecompiler.prototype = Object.create(Filter.prototype); +GlimmerTemplatePrecompiler.prototype.constructor = GlimmerTemplatePrecompiler; + +function GlimmerTemplatePrecompiler(inputNode, options) { + this.options = options || {}; + Filter.call(this, inputNode, { + annotation: options.annotation + }); +} + +GlimmerTemplatePrecompiler.prototype.extensions = ['hbs']; +GlimmerTemplatePrecompiler.prototype.targetExtension = 'js'; + +function getTemplateSpecifier(rootName, relativePath) { + let path = relativePath.split('/'); + + // TODO - should use module map config to be rigorous + if (path[path.length - 1] === 'template.hbs') { + path.pop(); + } + if (path[0] === 'ui') { + path.shift(); + } + + return 'template:/' + rootName + '/' + path.join('/'); +} + +GlimmerTemplatePrecompiler.prototype.processString = function(content, relativePath) { + console.log('GlimmerTemplatePrecompiler.processString', relativePath, this.options); + + let specifier = getTemplateSpecifier(this.options.rootName, relativePath); + return 'export default ' + compiler.precompile(content, { meta: { specifier } }) + ';'; +}; + +module.exports = GlimmerTemplatePrecompiler; diff --git a/lib/broccoli/module-map-creator.js b/lib/broccoli/module-map-creator.js new file mode 100644 index 0000000..777aeaf --- /dev/null +++ b/lib/broccoli/module-map-creator.js @@ -0,0 +1,102 @@ +const Plugin = require('broccoli-plugin'); +const fs = require('fs'); +const path = require('path'); +const walkSync = require('walk-sync'); +const getModuleConfig = require('./get-module-config'); + +function ModuleMapCreator(src, config, options) { + options = options || {}; + Plugin.call(this, [src, config], { + annotation: options.annotation + }); + this.options = options; +} + +ModuleMapCreator.prototype = Object.create(Plugin.prototype); +ModuleMapCreator.prototype.constructor = ModuleMapCreator; +ModuleMapCreator.prototype.build = function() { + function specifierFromModule(modulePrefix, moduleConfig, modulePath) { + let path; + let collectionPath; + + for (let i = 0, l = moduleConfig.collectionPaths.length; i < l; i++) { + path = moduleConfig.collectionPaths[i]; + if (modulePath.indexOf(path) === 0) { + collectionPath = path; + break; + } + } + + if (collectionPath) { + // trim group/collection from module path + modulePath = modulePath.substr(collectionPath.length); + } else { + collectionPath = 'main'; + } + let parts = modulePath.split('/'); + + let collectionName = moduleConfig.collectionMap[collectionPath]; + + let name, type, namespace; + if (parts.length > 1) { + type = parts.pop(); + } + name = parts.pop(); + if (parts.length > 0) { + namespace = parts.join('/'); + } + + let specifierPath = [modulePrefix, collectionName]; + if (namespace) { + specifierPath.push(namespace); + } + specifierPath.push(name); + + let specifier = type + ':/' + specifierPath.join('/'); + + console.log('specifier:', specifier); + + return specifier; + } + + let configPath = path.join(this.inputPaths[1], this.options.configPath); + let configContents = fs.readFileSync(configPath, { encoding: 'utf8' }); + let config = JSON.parse(configContents); + + let modulePrefix = config.modulePrefix; + var moduleConfig = getModuleConfig(config); + var paths = walkSync(this.inputPaths[0]); + var modules = []; + var moduleImports = []; + var mapContents = []; + + paths.forEach(function(entry) { + if (entry.indexOf('.') > -1) { + var module = entry.substring(0, entry.lastIndexOf('.')); + + // filter out index module + if (module !== 'index' && module !== 'main') { + modules.push(module); + } + } + }); + modules.forEach(function(module) { + var specifier = specifierFromModule(modulePrefix, moduleConfig, module); + var moduleImportPath = '../' + module; + var moduleVar = '__' + module.replace(/\//g, '__').replace(/-/g, '_') + '__'; + var moduleImport = "import { default as " + moduleVar + " } from '" + moduleImportPath + "';"; + moduleImports.push(moduleImport); + mapContents.push("'" + specifier + "': " + moduleVar); + }); + var destPath = path.join(this.outputPath, 'config'); + if (!fs.existsSync(destPath)) { + fs.mkdirSync(destPath); + } + + var contents = moduleImports.join('\n') + '\n' + + "export default moduleMap = {" + mapContents.join(',') + "};" + '\n'; + + fs.writeFileSync(path.join(this.outputPath, 'config', 'module-map.ts'), contents, { encoding: 'utf8' }); +}; + +module.exports = ModuleMapCreator; diff --git a/lib/broccoli/rollup-with-dependencies.js b/lib/broccoli/rollup-with-dependencies.js new file mode 100644 index 0000000..03b4192 --- /dev/null +++ b/lib/broccoli/rollup-with-dependencies.js @@ -0,0 +1,59 @@ +"use strict"; + +const Rollup = require('broccoli-rollup'); +const nodeResolve = require('rollup-plugin-node-resolve'); +const babel = require('rollup-plugin-babel'); +const sourcemaps = require('rollup-plugin-sourcemaps'); + +const fs = require('fs'); +const path = require('path'); + +RollupWithDependencies.prototype = Object.create(Rollup.prototype); +RollupWithDependencies.prototype.constructor = RollupWithDependencies; + +function RollupWithDependencies(inputNode, options) { + if (!(this instanceof RollupWithDependencies)) { + return new RollupWithDependencies(inputNode, options); + } + + Rollup.call(this, inputNode, options); +} + +RollupWithDependencies.prototype.build = function() { + let plugins = this.rollupOptions.plugins || []; + let inputPath = this.inputPaths[0]; + + plugins.push(sourcemaps()); + + plugins.push(babel({ + presets: 'es2015-rollup', + // TODO sourceMaps: 'inline', + retainLines: true + })); + + plugins.push({ + resolveId(importee, importer) { + let modulePath = path.join(inputPath, importee, 'index.js'); + + if (fs.existsSync(modulePath)) { + return modulePath; + } + + modulePath = path.join(inputPath, importee + '.js'); + if (fs.existsSync(modulePath)) { + return modulePath; + } + } + }); + + plugins.push(nodeResolve({ + jsnext: true, + main: true + })); + + this.rollupOptions.plugins = plugins; + + return Rollup.prototype.build.apply(this, arguments); +}; + +module.exports = RollupWithDependencies; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3bf25af --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "@glimmer/application-pipeline", + "version": "0.0.1", + "description": "Tooling for developing Glimmer standalone apps with ember-cli", + "contributors": [ + "Dan Gebhardt ", + "Tom Dale " + ], + "repository": "https://github.com/glimmerjs/glimmer-application-pipeline", + "license": "MIT", + "dependencies": { + "@glimmer/compiler": "^0.21.1", + "@glimmer/di": "^0.1.8", + "babel-preset-es2015-rollup": "^1.2.0", + "broccoli-asset-rev": "^2.4.3", + "broccoli-babel-transpiler": "next", + "broccoli-caching-writer": "^2.2.1", + "broccoli-concat": "^2.2.0", + "broccoli-merge-trees": "^1.1.1", + "broccoli-persistent-filter": "^1.2.2", + "broccoli-rollup": "^1.0.3", + "broccoli-sass": "^0.7.0", + "broccoli-stew": "^1.3.1", + "broccoli-string-replace": "^0.1.1", + "broccoli-typescript-compiler": "^1.0.1", + "broccoli-uglify-sourcemap": "^1.4.0", + "ember-cli": "2.10.0", + "handlebars": "^4.0.5", + "rimraf": "^2.5.4", + "rollup": "^0.36.4", + "rollup-plugin-babel": "^2.6.1", + "rollup-plugin-node-resolve": "^2.0.0", + "rollup-plugin-sourcemaps": "^0.4.1", + "typescript": "^2.1.5", + "walk-sync": "^0.3.1" + } +}