ember-cli-htmlbars
Advanced tools
Comparing version 4.0.9 to 4.1.0
@@ -0,1 +1,9 @@ | ||
## v4.1.0 (2019-12-10) | ||
#### :rocket: Enhancement | ||
* [#380](https://github.com/ember-cli/ember-cli-htmlbars/pull/380) Implement basic patching strategy for colocated components. ([@rwjblue](https://github.com/rwjblue)) | ||
#### Committers: 1 | ||
- Robert Jackson ([@rwjblue](https://github.com/rwjblue)) | ||
## v4.0.9 (2019-12-04) | ||
@@ -2,0 +10,0 @@ |
'use strict'; | ||
const fs = require('fs'); | ||
const mkdirp = require('mkdirp'); | ||
const copyFileSync = require('fs-copy-file-sync'); | ||
const path = require('path'); | ||
@@ -10,78 +8,106 @@ const walkSync = require('walk-sync'); | ||
const logger = require('heimdalljs-logger')('ember-cli-htmlbars:colocated-broccoli-plugin'); | ||
const FSTree = require('fs-tree-diff'); | ||
function detectRootName(files) { | ||
let [first] = files; | ||
let parts = first.split('/'); | ||
module.exports = class ColocatedTemplateProcessor extends Plugin { | ||
constructor(tree) { | ||
super([tree], { | ||
persistentOutput: true, | ||
}); | ||
let root; | ||
if (parts[0].startsWith('@')) { | ||
root = parts.slice(0, 2).join('/'); | ||
} else { | ||
root = parts[0]; | ||
this._lastTree = FSTree.fromEntries([]); | ||
} | ||
if (!files.every(f => f.startsWith(root))) { | ||
root = null; | ||
calculatePatch() { | ||
let updatedEntries = walkSync.entries(this.inputPaths[0]); | ||
let currentTree = FSTree.fromEntries(updatedEntries); | ||
let patch = this._lastTree.calculatePatch(currentTree); | ||
this._lastTree = currentTree; | ||
return patch; | ||
} | ||
return root; | ||
} | ||
currentEntries() { | ||
return this._lastTree.entries; | ||
} | ||
module.exports = class ColocatedTemplateProcessor extends Plugin { | ||
constructor(tree, options) { | ||
super([tree], options); | ||
inputHasFile(relativePath) { | ||
return !!this.currentEntries().find(e => e.relativePath === relativePath); | ||
} | ||
detectRootName() { | ||
let entries = this.currentEntries().filter(e => !e.isDirectory()); | ||
let [first] = entries; | ||
let parts = first.relativePath.split('/'); | ||
let root; | ||
if (parts[0].startsWith('@')) { | ||
root = parts.slice(0, 2).join('/'); | ||
} else { | ||
root = parts[0]; | ||
} | ||
if (!entries.every(e => e.relativePath.startsWith(root))) { | ||
root = null; | ||
} | ||
return root; | ||
} | ||
build() { | ||
let files = walkSync(this.inputPaths[0], { directories: false }); | ||
let patch = this.calculatePatch(); | ||
if (files.length === 0) { | ||
// nothing to do, bail | ||
// We skip building if this is a rebuild with a zero-length patch | ||
if (patch.length === 0) { | ||
return; | ||
} | ||
let root = detectRootName(files); | ||
let root = this.detectRootName(); | ||
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 processedColocatedFiles = new Set(); | ||
let filePathParts = path.parse(filePath); | ||
let inputPath = path.join(this.inputPaths[0], filePath); | ||
for (let operation of patch) { | ||
let [method, relativePath] = operation; | ||
// 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 filePathParts = path.parse(relativePath); | ||
let isInsideComponentsFolder = filePath.startsWith(`${root}/components/`); | ||
let isOutsideComponentsFolder = !relativePath.startsWith(`${root}/components/`); | ||
let isPodsTemplate = filePathParts.name === 'template' && filePathParts.ext === '.hbs'; | ||
let isNotColocationExtension = !['.hbs', '.js', '.ts', '.coffee'].includes(filePathParts.ext); | ||
let isDirectoryOperation = ['rmdir', 'mkdir'].includes(method); | ||
let basePath = path.posix.join(filePathParts.dir, filePathParts.name); | ||
let relativeTemplatePath = basePath + '.hbs'; | ||
// 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; | ||
// if the change in question has nothing to do with colocated templates | ||
// just apply the patch to the outputPath | ||
if ( | ||
isOutsideComponentsFolder || | ||
isPodsTemplate || | ||
isNotColocationExtension || | ||
isDirectoryOperation | ||
) { | ||
logger.debug(`default operation for non-colocation modification: ${relativePath}`); | ||
FSTree.applyPatch(this.inputPaths[0], this.outputPath, [operation]); | ||
continue; | ||
} | ||
if (filePathParts.name === 'template') { | ||
filesToCopy.push(filePath); | ||
return; | ||
// we have already processed this colocated file, carry on | ||
if (processedColocatedFiles.has(basePath)) { | ||
continue; | ||
} | ||
processedColocatedFiles.add(basePath); | ||
let hasBackingClass = false; | ||
let backingClassPath = path.join(filePathParts.dir, filePathParts.name); | ||
let hasTemplate = this.inputHasFile(basePath + '.hbs'); | ||
let backingClassPath = basePath; | ||
if (fs.existsSync(path.join(this.inputPaths[0], backingClassPath + '.js'))) { | ||
if (this.inputHasFile(basePath + '.js')) { | ||
backingClassPath += '.js'; | ||
hasBackingClass = true; | ||
} else if (fs.existsSync(path.join(this.inputPaths[0], backingClassPath + '.ts'))) { | ||
} else if (this.inputHasFile(basePath + '.ts')) { | ||
backingClassPath += '.ts'; | ||
hasBackingClass = true; | ||
} else if (fs.existsSync(path.join(this.inputPaths[0], backingClassPath + '.coffee'))) { | ||
} else if (this.inputHasFile(basePath + '.coffee')) { | ||
backingClassPath += '.coffee'; | ||
@@ -94,33 +120,38 @@ hasBackingClass = true; | ||
let templateContents = fs.readFileSync(inputPath, { encoding: 'utf8' }); | ||
let originalJsContents = null; | ||
let jsContents = null; | ||
let prefix = ''; | ||
let hbsInvocationOptions = { | ||
contents: templateContents, | ||
moduleName: filePath, | ||
parseOptions: { | ||
srcName: filePath, | ||
}, | ||
}; | ||
let hbsInvocation = `hbs(${JSON.stringify(templateContents)}, ${JSON.stringify( | ||
hbsInvocationOptions | ||
)})`; | ||
let prefix = `import { hbs } from 'ember-cli-htmlbars';\nconst __COLOCATED_TEMPLATE__ = ${hbsInvocation};\n`; | ||
if (backingClassPath.endsWith('.coffee')) { | ||
prefix = `import { hbs } from 'ember-cli-htmlbars'\n__COLOCATED_TEMPLATE__ = ${hbsInvocation}\n`; | ||
if (hasTemplate) { | ||
let templatePath = path.join(this.inputPaths[0], basePath + '.hbs'); | ||
let templateContents = fs.readFileSync(templatePath, { encoding: 'utf8' }); | ||
let hbsInvocationOptions = { | ||
contents: templateContents, | ||
moduleName: relativeTemplatePath, | ||
parseOptions: { | ||
srcName: relativeTemplatePath, | ||
}, | ||
}; | ||
let hbsInvocation = `hbs(${JSON.stringify(templateContents)}, ${JSON.stringify( | ||
hbsInvocationOptions | ||
)})`; | ||
prefix = `import { hbs } from 'ember-cli-htmlbars';\nconst __COLOCATED_TEMPLATE__ = ${hbsInvocation};\n`; | ||
if (backingClassPath.endsWith('.coffee')) { | ||
prefix = `import { hbs } from 'ember-cli-htmlbars'\n__COLOCATED_TEMPLATE__ = ${hbsInvocation}\n`; | ||
} | ||
} | ||
logger.debug( | ||
`processing colocated template: ${filePath} (template-only: ${hasBackingClass})` | ||
); | ||
if (hasBackingClass) { | ||
// add the template, call setComponentTemplate | ||
jsContents = fs.readFileSync(path.join(this.inputPaths[0], backingClassPath), { | ||
encoding: 'utf8', | ||
}); | ||
jsContents = originalJsContents = fs.readFileSync( | ||
path.join(this.inputPaths[0], backingClassPath), | ||
{ | ||
encoding: 'utf8', | ||
} | ||
); | ||
if (!jsContents.includes('export default')) { | ||
let message = `\`${filePath}\` does not contain a \`default export\`. Did you forget to export the component class?`; | ||
let message = `\`${relativePath}\` does not contain a \`default export\`. Did you forget to export the component class?`; | ||
jsContents = `${jsContents}\nthrow new Error(${JSON.stringify(message)});`; | ||
@@ -137,27 +168,51 @@ prefix = ''; | ||
let outputPath = path.join(this.outputPath, backingClassPath); | ||
let jsOutputPath = path.join(this.outputPath, backingClassPath); | ||
// TODO: don't speculatively mkdirSync (likely do in a try/catch with ENOENT) | ||
mkdirp.sync(path.dirname(outputPath)); | ||
fs.writeFileSync(outputPath, jsContents, { encoding: 'utf8' }); | ||
}); | ||
switch (method) { | ||
case 'unlink': { | ||
if (filePathParts.ext === '.hbs' && hasBackingClass) { | ||
fs.writeFileSync(jsOutputPath, originalJsContents, { encoding: 'utf8' }); | ||
filesToCopy.forEach(filePath => { | ||
let inputPath = path.join(this.inputPaths[0], filePath); | ||
let outputPath = path.join(this.outputPath, filePath); | ||
logger.debug(`removing colocated template for: ${basePath}`); | ||
} else if (filePathParts.ext !== '.hbs' && hasTemplate) { | ||
fs.writeFileSync(jsOutputPath, jsContents, { encoding: 'utf8' }); | ||
logger.debug( | ||
`converting colocated template with backing class to template only: ${basePath}` | ||
); | ||
} else { | ||
// Copied from https://github.com/stefanpenner/fs-tree-diff/blob/v2.0.1/lib/index.ts#L38-L68 | ||
try { | ||
fs.unlinkSync(jsOutputPath); | ||
} catch (e) { | ||
if (typeof e === 'object' && e !== null && e.code === 'ENOENT') { | ||
return; | ||
} | ||
throw e; | ||
} | ||
} | ||
break; | ||
} | ||
case 'change': | ||
case 'create': { | ||
fs.writeFileSync(jsOutputPath, jsContents, { encoding: 'utf8' }); | ||
// avoid copying file over top of a previously written one | ||
if (fs.existsSync(outputPath)) { | ||
return; | ||
logger.debug( | ||
`writing colocated template: ${basePath} (template-only: ${!hasBackingClass})` | ||
); | ||
break; | ||
} | ||
default: { | ||
throw new Error( | ||
`ember-cli-htmlbars: Unexpected operation when patching files for colocation.\n\tOperation:\n${JSON.stringify( | ||
[method, relativePath] | ||
)}\n\tKnown files:\n${JSON.stringify( | ||
this.currentEntries().map(e => e.relativePath), | ||
null, | ||
2 | ||
)}` | ||
); | ||
} | ||
} | ||
logger.debug(`copying unchanged file: ${filePath}`); | ||
// TODO: don't speculatively mkdirSync (likely do in a try/catch with ENOENT) | ||
mkdirp.sync(path.dirname(outputPath)); | ||
copyFileSync(inputPath, outputPath); | ||
}); | ||
logger.info(`copied over (unchanged): ${filesToCopy.length} files`); | ||
} | ||
} | ||
}; |
{ | ||
"name": "ember-cli-htmlbars", | ||
"version": "4.0.9", | ||
"version": "4.1.0", | ||
"description": "A library for adding htmlbars to ember CLI", | ||
@@ -41,7 +41,6 @@ "keywords": [ | ||
"ember-cli-babel-plugin-helpers": "^1.1.0", | ||
"fs-copy-file-sync": "^1.1.1", | ||
"fs-tree-diff": "^2.0.1", | ||
"hash-for-dep": "^1.5.1", | ||
"heimdalljs-logger": "^0.1.10", | ||
"json-stable-stringify": "^1.0.1", | ||
"mkdirp": "^0.5.1", | ||
"semver": "^6.3.0", | ||
@@ -52,8 +51,8 @@ "strip-bom": "^4.0.0", | ||
"devDependencies": { | ||
"@babel/core": "^7.6.4", | ||
"@babel/plugin-proposal-class-properties": "^7.5.5", | ||
"@babel/plugin-proposal-decorators": "^7.6.0", | ||
"@babel/plugin-transform-runtime": "^7.6.2", | ||
"@babel/plugin-transform-typescript": "^7.6.3", | ||
"@babel/runtime": "^7.6.3", | ||
"@babel/core": "^7.7.5", | ||
"@babel/plugin-proposal-class-properties": "^7.7.4", | ||
"@babel/plugin-proposal-decorators": "^7.7.4", | ||
"@babel/plugin-transform-runtime": "^7.7.6", | ||
"@babel/plugin-transform-typescript": "^7.7.4", | ||
"@babel/runtime": "^7.7.6", | ||
"@ember/optional-features": "^1.1.0", | ||
@@ -92,3 +91,3 @@ "babel-eslint": "^10.0.3", | ||
"prettier": "^1.18.2", | ||
"qunit-dom": "^0.9.1", | ||
"qunit-dom": "^0.9.2", | ||
"release-it": "^12.4.3", | ||
@@ -95,0 +94,0 @@ "release-it-lerna-changelog": "^1.0.3" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
49062
14
873
+ Addedfs-tree-diff@^2.0.1
- Removedfs-copy-file-sync@^1.1.1
- Removedmkdirp@^0.5.1
- Removedfs-copy-file-sync@1.1.1(transitive)