diff --git a/lib/colocated-broccoli-plugin.js b/lib/colocated-broccoli-plugin.js index 50fada34..397e1988 100644 --- a/lib/colocated-broccoli-plugin.js +++ b/lib/colocated-broccoli-plugin.js @@ -1,88 +1,113 @@ '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 { @@ -90,37 +115,46 @@ module.exports = class ColocatedTemplateProcessor extends Plugin { 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 = ''; } @@ -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`); + } } }; diff --git a/node-tests/colocated-broccoli-plugin-test.js b/node-tests/colocated-broccoli-plugin-test.js index f90a74a7..2f4f283d 100644 --- a/node-tests/colocated-broccoli-plugin-test.js +++ b/node-tests/colocated-broccoli-plugin-test.js @@ -25,6 +25,7 @@ describe('ColocatedTemplateCompiler', function() { it('works for template only component', async function() { input.write({ 'app-name-here': { + 'router.js': '// stuff here', components: { 'foo.hbs': `{{yield}}`, }, @@ -41,6 +42,7 @@ describe('ColocatedTemplateCompiler', function() { assert.deepStrictEqual(output.read(), { 'app-name-here': { + 'router.js': '// stuff here', components: { 'foo.js': stripIndent` @@ -59,11 +61,26 @@ describe('ColocatedTemplateCompiler', function() { await output.build(); assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + 'router.js': '// other stuff here', + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/router.js': 'change' }, + 'has only related changes' + ); }); it('works for component with template and class', async function() { input.write({ 'app-name-here': { + 'router.js': '// stuff here', components: { 'foo.hbs': `{{yield}}`, 'foo.js': stripIndent` @@ -85,6 +102,7 @@ describe('ColocatedTemplateCompiler', function() { assert.deepStrictEqual(output.read(), { 'app-name-here': { + 'router.js': '// stuff here', components: { 'foo.js': stripIndent` import { hbs } from 'ember-cli-htmlbars'; @@ -103,6 +121,20 @@ describe('ColocatedTemplateCompiler', function() { await output.build(); assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + 'router.js': '// other stuff here', + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/router.js': 'change' }, + 'has only related changes' + ); }); it('works for typescript component class with template', async function() { @@ -431,4 +463,667 @@ describe('ColocatedTemplateCompiler', function() { }); it('does not break class decorator usage'); + + describe('changes', function() { + it('initial template only, add a JS file', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': + stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import templateOnly from '@ember/component/template-only'; + + export default templateOnly();` + '\n', + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'change' }, + 'has only related changes' + ); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'content is correct after updating' + ); + }); + + it('initial JS only, add a template', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `{{yield}}`, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'change' }, + 'has only related changes' + ); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'content is correct after updating' + ); + }); + + it('initial JS only, delete JS', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.js': null, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'unlink' }, + 'has only related changes' + ); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: {}, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'content is correct after updating' + ); + }); + + it('initial template only, delete template', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': + stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import templateOnly from '@ember/component/template-only'; + + export default templateOnly();` + '\n', + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': null, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'unlink' }, + 'has only related changes' + ); + }); + + it('initial template, update template', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.hbs': `{{yield}}`, + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': + stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import templateOnly from '@ember/component/template-only'; + + export default templateOnly();` + '\n', + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': 'whoops!', + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'change' }, + 'has only related changes' + ); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': + stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("whoops!", {"contents":"whoops!","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import templateOnly from '@ember/component/template-only'; + + export default templateOnly();` + '\n', + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'updated content is correct' + ); + }); + + it('initial JS + template, update template', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + 'foo.hbs': '{{yield}}', + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.hbs': `whoops!`, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'change' }, + 'has only related changes' + ); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("whoops!", {"contents":"whoops!","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'content is correct after updating' + ); + }); + + it('initial JS + template, update JS', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + 'foo.hbs': '{{yield}}', + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooBarComponent extends Component {} + `, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooBarComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'content is correct after updating' + ); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'change' }, + 'has only related changes' + ); + }); + + it('initial JS + template, delete JS file', async function() { + input.write({ + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + 'foo.hbs': '{{yield}}', + }, + templates: { + 'application.hbs': `{{outlet}}`, + }, + }, + }); + + let tree = new ColocatedTemplateCompiler(input.path()); + + output = createBuilder(tree); + await output.build(); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import Component from '@glimmer/component'; + + export default class FooComponent extends Component {} + `, + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'initial content is correct' + ); + + await output.build(); + + assert.deepStrictEqual(output.changes(), {}, 'NOOP update has no changes'); + + input.write({ + 'app-name-here': { + components: { + 'foo.js': null, + }, + }, + }); + + await output.build(); + + assert.deepStrictEqual( + output.changes(), + { 'app-name-here/components/foo.js': 'change' }, + 'has only related changes' + ); + + assert.deepStrictEqual( + output.read(), + { + 'app-name-here': { + 'router.js': '// stuff here', + components: { + 'foo.js': + stripIndent` + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = hbs("{{yield}}", {"contents":"{{yield}}","moduleName":"app-name-here/components/foo.hbs","parseOptions":{"srcName":"app-name-here/components/foo.hbs"}}); + import templateOnly from '@ember/component/template-only'; + + export default templateOnly();` + '\n', + }, + templates: { + 'application.hbs': '{{outlet}}', + }, + }, + }, + 'content is correct after updating' + ); + }); + }); }); diff --git a/package.json b/package.json index bab9c8e9..7c6094f5 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,10 @@ "broccoli-plugin": "^3.0.0", "common-tags": "^1.8.0", "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", "strip-bom": "^4.0.0", "walk-sync": "^2.0.2" diff --git a/yarn.lock b/yarn.lock index 5832a8f1..8abdfda7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4790,11 +4790,6 @@ from2@^2.1.0, from2@^2.1.1: inherits "^2.0.1" readable-stream "^2.0.0" -fs-copy-file-sync@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fs-copy-file-sync/-/fs-copy-file-sync-1.1.1.tgz#11bf32c096c10d126e5f6b36d06eece776062918" - integrity sha512-2QY5eeqVv4m2PfyMiEuy9adxNP+ajf+8AR05cEi+OAzPcOj90hvFImeZhTmKLBgSd9EvG33jsD7ZRxsx9dThkQ== - fs-extra@^0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" @@ -6851,9 +6846,8 @@ mocha@^6.2.2: yargs-unparser "1.6.0" "module-name-inliner@link:./tests/dummy/lib/module-name-inliner": - version "0.1.0" - dependencies: - ember-cli-version-checker "*" + version "0.0.0" + uid "" morgan@^1.9.1: version "1.9.1"