Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of co-located templates RFC. #249

Merged
merged 34 commits into from
Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fbda2e0
Initial spike of co-located templates RFC.
chancancode May 9, 2019
764615e
Use globals for now
chancancode Aug 10, 2019
a640658
Fix addon compilation
chancancode Aug 12, 2019
4c6017b
Better error for missing default export
chancancode Aug 13, 2019
942c301
v3.2.0-alpha.0
chancancode Aug 22, 2019
66dff2f
Fix some Node 8 issues
chancancode Aug 24, 2019
b9295c5
Fix lint
chancancode Aug 24, 2019
6b5a4d6
Extract ColocatedTemplateProcessor to dedicated file.
rwjblue Sep 5, 2019
10e5f31
Merge remote-tracking branch 'origin/master' into colocation
rwjblue Sep 5, 2019
3aabb50
Update linting config to allow async functions.
rwjblue Sep 5, 2019
d7a579b
Add broccoli-debug logging.
rwjblue Sep 5, 2019
f7f3f47
Start fleshing out basic testing infrastructure.
rwjblue Sep 5, 2019
1dbfb87
Merge remote-tracking branch 'origin/master' into colocation
rwjblue Sep 5, 2019
6ba80f4
Merge remote-tracking branch 'origin/master' into colocation
rwjblue Sep 9, 2019
24e8ad6
yarn lint:js --fix
rwjblue Sep 9, 2019
fb2808a
Extract helper method to build merged component JS contents.
rwjblue Sep 5, 2019
f5a0b01
Add TODO comments for additional tasks.
rwjblue Sep 6, 2019
6de52e5
Flesh out more tests.
rwjblue Sep 6, 2019
5709c0f
Avoid adding the import for setComponentTemplate.
rwjblue Sep 6, 2019
b44e14b
yarn lint:js --fix
rwjblue Sep 9, 2019
bb4ad58
Fix colocated-broccoli-plugin tests
rwjblue Sep 9, 2019
9362a4c
Add babel plugin to add setComponentTemplate usage.
rwjblue Sep 9, 2019
952287b
Simplify implementation / reduce duplication.
rwjblue Sep 9, 2019
cfa93e3
Make requires more lazy.
rwjblue Sep 9, 2019
eba43a7
Add proper guard for using colocation.
rwjblue Sep 9, 2019
99ac97d
Refactor registry toTree setup.
rwjblue Sep 9, 2019
60b8ae9
Tweak comments in colocated-broccoli-plugin.
rwjblue Sep 9, 2019
2d58f11
Ensure debugTree's are not "merged" for different invocations.
rwjblue Sep 9, 2019
9972294
Add tests for scoped addons.
rwjblue Sep 9, 2019
db7e5f2
Only process colocated files from <app-or-addon-name>/components/
rwjblue Sep 9, 2019
1ef7d42
Merge pull request #280 from rwjblue/colocation-refactor
rwjblue Sep 14, 2019
791bde4
Fix setComponentTemplate argument ordering for native classes.
rwjblue Sep 14, 2019
253503f
Only opt-in to colocation when using octane edition.
rwjblue Sep 15, 2019
35b66bf
Merge remote-tracking branch 'origin/master' into colocation
rwjblue Sep 23, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2017,
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['ember', 'prettier'],
Expand All @@ -17,6 +17,7 @@ module.exports = {
'.eslintrc.js',
'.prettierrc.js',
'.template-lintrc.js',
'colocated-broccoli-plugin.js',
'ember-cli-build.js',
'lib/**/*.js',
'testem.js',
Expand All @@ -27,7 +28,7 @@ module.exports = {
excludedFiles: ['addon/**', 'addon-test-support/**', 'app/**', 'tests/dummy/app/**'],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015,
ecmaVersion: 2018,
},
env: {
browser: false,
Expand All @@ -44,7 +45,7 @@ module.exports = {
files: ['node-tests/**/*.js'],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015,
ecmaVersion: 2018,
},
env: {
browser: false,
Expand Down
61 changes: 61 additions & 0 deletions lib/colocated-babel-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// For ease of debuggin / tweaking:
// https://astexplorer.net/#/gist/bcca584efdab6c981a75618642c76a22/1e1d262eaeb47b7da66150e0781a02b96e597b25
module.exports = function(babel) {
let t = babel.types;

return {
name: 'ember-cli-htmlbars-colocation-template',

visitor: {
VariableDeclarator(path, state) {
if (path.node.id.name === '__COLOCATED_TEMPLATE__') {
state.colocatedTemplateFound = true;
}
},

ExportDefaultDeclaration(path, state) {
if (!state.colocatedTemplateFound) {
return;
}

let defaultExportDeclaration = path.node.declaration;
let setComponentTemplateMemberExpression = t.memberExpression(
t.identifier('Ember'),
t.identifier('_setComponentTemplate')
);
let colocatedTemplateIdentifier = t.identifier('__COLOCATED_TEMPLATE__');

if (defaultExportDeclaration.type === 'ClassDeclaration') {
// when the default export is a ClassDeclaration with an `id`,
// wrapping it in a CallExpression would remove that class from the
// local scope which would cause issues for folks using the declared
// name _after_ the export
if (defaultExportDeclaration.id !== null) {
path.parent.body.push(
t.expressionStatement(
t.callExpression(setComponentTemplateMemberExpression, [
colocatedTemplateIdentifier,
defaultExportDeclaration.id,
])
)
);
} else {
path.node.declaration = t.callExpression(setComponentTemplateMemberExpression, [
colocatedTemplateIdentifier,
t.classExpression(
null,
defaultExportDeclaration.superClass,
defaultExportDeclaration.body
),
]);
}
} else {
path.node.declaration = t.callExpression(setComponentTemplateMemberExpression, [
colocatedTemplateIdentifier,
defaultExportDeclaration,
]);
}
},
},
};
};
121 changes: 121 additions & 0 deletions lib/colocated-broccoli-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use strict';

const fs = require('fs');
const mkdirp = require('mkdirp');
const copyFileSync = require('fs-copy-file-sync');
const path = require('path');
const walkSync = require('walk-sync');
const Plugin = require('broccoli-plugin');

function detectRootName(files) {
let [first] = files;
let parts = first.split('/');

let root;
if (parts[0].startsWith('@')) {
root = parts.slice(0, 2).join('/');
} else {
root = parts[0];
}

if (!files.every(f => f.startsWith(root))) {
root = null;
}

return root;
}

module.exports = class ColocatedTemplateProcessor extends Plugin {
constructor(tree, options) {
super([tree], options);
}

build() {
let files = walkSync(this.inputPaths[0], { directories: false });

let root = detectRootName(files);

let filesToCopy = [];
files.forEach(filePath => {
if (root === null) {
// do nothing, we cannot detect the proper root path for the app/addon
// being processed
filesToCopy.push(filePath);
return;
}

let filePathParts = path.parse(filePath);
let inputPath = path.join(this.inputPaths[0], filePath);

// TODO: why are these different?
// Apps: my-app/components/foo.hbs, my-app/templates/components/foo.hbs
// Addons: components/foo.js, templates/components/foo.hbs
//
// will be fixed by https://github.com/ember-cli/ember-cli/pull/8834

let isInsideComponentsFolder = filePath.startsWith(`${root}/components/`);

// copy forward non-hbs files
// TODO: don't copy .js files that will ultimately be overridden
if (!isInsideComponentsFolder || filePathParts.ext !== '.hbs') {
filesToCopy.push(filePath);
return;
}

// TODO: deal with alternate extensions (e.g. ts)
let possibleJSPath = path.join(filePathParts.dir, filePathParts.name + '.js');
let hasJSFile = fs.existsSync(path.join(this.inputPaths[0], possibleJSPath));

if (filePathParts.name === 'template') {
// TODO: maybe warn?
return;
}

let templateContents = fs.readFileSync(inputPath, { encoding: 'utf8' });
let jsContents = null;

// TODO: deal with hygiene?
let prefix = `import { hbs } from 'ember-cli-htmlbars';\nconst __COLOCATED_TEMPLATE__ = hbs\`${templateContents}\`;\n`;

if (hasJSFile) {
// add the template, call setComponentTemplate

jsContents = fs.readFileSync(path.join(this.inputPaths[0], possibleJSPath), {
encoding: 'utf8',
});

if (!jsContents.includes('export default')) {
let message = `\`${filePath}\` does not contain a \`default export\`. Did you forget to export the component class?`;
jsContents = `${jsContents}\nthrow new Error(${JSON.stringify(message)});`;
prefix = '';
}
} else {
// create JS file, use null component pattern

jsContents = `import templateOnly from '@ember/component/template-only';\n\nexport default templateOnly();\n`;
}

jsContents = prefix + jsContents;

let outputPath = path.join(this.outputPath, possibleJSPath);

// TODO: don't speculatively mkdirSync (likely do in a try/catch with ENOENT)
mkdirp.sync(path.dirname(outputPath));
fs.writeFileSync(outputPath, jsContents, { encoding: 'utf8' });
});

filesToCopy.forEach(filePath => {
let inputPath = path.join(this.inputPaths[0], filePath);
let outputPath = path.join(this.outputPath, filePath);

// avoid copying file over top of a previously written one
if (fs.existsSync(outputPath)) {
return;
}

// TODO: don't speculatively mkdirSync (likely do in a try/catch with ENOENT)
mkdirp.sync(path.dirname(outputPath));
copyFileSync(inputPath, outputPath);
});
}
};
72 changes: 64 additions & 8 deletions lib/ember-addon-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,79 @@

const path = require('path');
const utils = require('./utils');
const debugGenerator = require('heimdalljs-logger');
const logger = debugGenerator('ember-cli-htmlbars');
const logger = require('heimdalljs-logger')('ember-cli-htmlbars');
const hasEdition = require('@ember/edition-utils').has;

let registryInvocationCounter = 0;

module.exports = {
name: require('../package').name,

parentRegistry: null,

_shouldColocateTemplates() {
if (this._cachedShouldColocateTemplates) {
return this._cachedShouldColocateTemplates;
}

const semver = require('semver');

let babel = this.parent.addons.find(a => a.name === 'ember-cli-babel');
let hasBabel = babel !== undefined;
let babelVersion = hasBabel && babel.pkg.version;

// using this.project.emberCLIVersion() allows us to avoid issues when `npm
// link` is used; if this addon were linked and we did something like
// `require('ember-cli/package').version` we would get our own ember-cli
// version **not** the one in use currently
let emberCLIVersion = this.project.emberCLIVersion();

let hasValidBabelVersion = hasBabel && semver.gte(babelVersion, '7.11.0');
let hasValidEmberCLIVersion = semver.gte(emberCLIVersion, '3.12.0-beta.2');
let hasOctane = hasEdition('octane');

this._cachedShouldColocateTemplates =
hasOctane && hasValidBabelVersion && hasValidEmberCLIVersion;

return this._cachedShouldColocateTemplates;
},

setupPreprocessorRegistry(type, registry) {
// ensure that broccoli-ember-hbs-template-compiler is not processing hbs files
registry.remove('template', 'broccoli-ember-hbs-template-compiler');

// when this.parent === this.project, `this.parent.name` is a function 😭
let parentName = typeof this.parent.name === 'function' ? this.parent.name() : this.parent.name;

registry.add('template', {
name: 'ember-cli-htmlbars',
ext: 'hbs',
_addon: this,
toTree(tree) {
let debugTree = require('broccoli-debug').buildDebugCallback(
`ember-cli-htmlbars:${parentName}:tree-${registryInvocationCounter++}`
);

let shouldColocateTemplates = this._addon._shouldColocateTemplates();
let htmlbarsOptions = this._addon.htmlbarsOptions();
let TemplateCompiler = require('./template-compiler-plugin');
return new TemplateCompiler(tree, htmlbarsOptions);

let inputTree = debugTree(tree, '01-input');

if (shouldColocateTemplates) {
const ColocatedTemplateProcessor = require('./colocated-broccoli-plugin');

inputTree = debugTree(new ColocatedTemplateProcessor(inputTree), '02-colocated-output');
}

const TemplateCompiler = require('./template-compiler-plugin');
return debugTree(new TemplateCompiler(inputTree, htmlbarsOptions), '03-output');
},

precompile(string) {
precompile(string, options) {
let htmlbarsOptions = this._addon.htmlbarsOptions();
let templateCompiler = htmlbarsOptions.templateCompiler;
return utils.template(templateCompiler, string);

return utils.template(templateCompiler, string, options);
},
});

Expand All @@ -46,7 +93,7 @@ module.exports = {

// add the babel-plugin-htmlbars-inline-precompile to the list of plugins
// used by `ember-cli-babel` addon
if (!this._isBabelPluginRegistered(babelPlugins)) {
if (!this._isInlinePrecompileBabelPluginRegistered(babelPlugins)) {
let pluginWrappers = this.astPlugins();
let templateCompilerPath = this.templateCompilerPath();
let pluginInfo = utils.setupPlugins(pluginWrappers);
Expand Down Expand Up @@ -110,6 +157,15 @@ module.exports = {
babelPlugins.push(htmlBarsPlugin);
}
}

if (this._shouldColocateTemplates()) {
const { hasPlugin, addPlugin } = require('ember-cli-babel-plugin-helpers');
let colocatedPluginPath = require.resolve('./colocated-babel-plugin');

if (!hasPlugin(babelPlugins, colocatedPluginPath)) {
addPlugin(babelPlugins, colocatedPluginPath);
}
}
},

/**
Expand All @@ -119,7 +175,7 @@ module.exports = {
* For non parallel api, check the 'modules' to see if it contains the babel plugin
* @param {*} plugins
*/
_isBabelPluginRegistered(plugins) {
_isInlinePrecompileBabelPluginRegistered(plugins) {
return plugins.some(plugin => {
if (Array.isArray(plugin)) {
return plugin[0] === require.resolve('babel-plugin-htmlbars-inline-precompile');
Expand Down
8 changes: 4 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

const fs = require('fs');
const path = require('path');
const HTMLBarsInlinePrecompilePlugin = require.resolve('babel-plugin-htmlbars-inline-precompile');
const hashForDep = require('hash-for-dep');
const debugGenerator = require('heimdalljs-logger');
const logger = debugGenerator('ember-cli-htmlbars');
Expand Down Expand Up @@ -136,12 +135,13 @@ function setup(pluginInfo, options) {
precompile.baseDir = () => path.resolve(__dirname, '..');
precompile.cacheKey = () => cacheKey;

let precompileInlineHTMLBarsPlugin = [
HTMLBarsInlinePrecompilePlugin,
let plugin = [
require.resolve('babel-plugin-htmlbars-inline-precompile'),
{ precompile, modules: options.modules },
'ember-cli-htmlbars:inline-precompile',
];

return precompileInlineHTMLBarsPlugin;
return plugin;
}

function makeCacheKey(templateCompilerPath, pluginInfo, extra) {
Expand Down
Loading