Skip to content

Commit

Permalink
Implement basic patching strategy.
Browse files Browse the repository at this point in the history
  • Loading branch information
rwjblue committed Dec 10, 2019
1 parent 4300bd7 commit 82bc9d4
Show file tree
Hide file tree
Showing 4 changed files with 831 additions and 102 deletions.
225 changes: 133 additions & 92 deletions lib/colocated-broccoli-plugin.js
Original file line number Diff line number Diff line change
@@ -1,126 +1,160 @@
'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');
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
) {
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';
hasBackingClass = true;
} else {
backingClassPath += '.js';
hasBackingClass = false;
}

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

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`;
let prefix = '';

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})`
`processing colocated template: ${relativePath} (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)});`;
prefix = '';
}
Expand All @@ -132,29 +166,36 @@ module.exports = class ColocatedTemplateProcessor extends Plugin {

jsContents = prefix + jsContents;

let outputPath = 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' });
});

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;
let jsOutputPath = path.join(this.outputPath, backingClassPath);

switch (method) {
case 'unlink': {
if (filePathParts.ext === '.hbs' && hasBackingClass) {
fs.writeFileSync(jsOutputPath, originalJsContents, { encoding: 'utf8' });
} else if (filePathParts.ext !== '.hbs' && hasTemplate) {
fs.writeFileSync(jsOutputPath, jsContents, { encoding: 'utf8' });
} 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' });
break;
}
default: {
throw new Error(`Unexpected operation ${JSON.stringify([method, relativePath])}`);
}
}

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`);
}
}
};
Loading

0 comments on commit 82bc9d4

Please sign in to comment.