-
-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of co-located templates RFC. (#249)
Initial implementation of co-located templates RFC.
- Loading branch information
Showing
12 changed files
with
611 additions
and
24 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
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, | ||
]); | ||
} | ||
}, | ||
}, | ||
}; | ||
}; |
This file contains 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
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); | ||
}); | ||
} | ||
}; |
This file contains 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
This file contains 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
Oops, something went wrong.