From 97878a64b6c5bcf7c4ff4e32bf78c1c3c3a18039 Mon Sep 17 00:00:00 2001 From: patrickpircher Date: Fri, 20 Sep 2024 23:27:13 +0200 Subject: [PATCH] backport #1855 addon-dev: incremental updates to output --- packages/addon-dev/package.json | 3 +- packages/addon-dev/src/rollup-gjs-plugin.ts | 4 +- packages/addon-dev/src/rollup-hbs-plugin.ts | 144 +++++++------- .../src/rollup-incremental-plugin.ts | 79 ++++++++ packages/addon-dev/src/rollup.ts | 12 +- packages/shared-internals/src/colocation.ts | 57 ++++++ packages/shared-internals/src/index.ts | 1 + pnpm-lock.yaml | 49 +---- tests/scenarios/v2-addon-dev-watch-test.ts | 180 +++++++++++++++++- 9 files changed, 404 insertions(+), 125 deletions(-) create mode 100644 packages/addon-dev/src/rollup-incremental-plugin.ts create mode 100644 packages/shared-internals/src/colocation.ts diff --git a/packages/addon-dev/package.json b/packages/addon-dev/package.json index d30c35fba..fb89e6367 100644 --- a/packages/addon-dev/package.json +++ b/packages/addon-dev/package.json @@ -40,7 +40,6 @@ "fs-extra": "^10.0.0", "minimatch": "^3.0.4", "rollup-plugin-copy-assets": "^2.0.3", - "rollup-plugin-delete": "^2.0.0", "walk-sync": "^3.0.0", "yargs": "^17.0.1" }, @@ -52,7 +51,7 @@ "@types/yargs": "^17.0.3", "rollup": "^3.23.0", "tmp": "^0.1.0", - "typescript": "^5.1.6" + "typescript": "^5.4.5" }, "engines": { "node": "12.* || 14.* || >= 16" diff --git a/packages/addon-dev/src/rollup-gjs-plugin.ts b/packages/addon-dev/src/rollup-gjs-plugin.ts index af90405bd..375f9dce5 100644 --- a/packages/addon-dev/src/rollup-gjs-plugin.ts +++ b/packages/addon-dev/src/rollup-gjs-plugin.ts @@ -1,6 +1,5 @@ import { createFilter } from '@rollup/pluginutils'; import type { Plugin } from 'rollup'; -import { readFileSync } from 'fs'; import { Preprocessor } from 'content-tag'; const PLUGIN_NAME = 'rollup-gjs-plugin'; @@ -14,11 +13,10 @@ export default function rollupGjsPlugin( return { name: PLUGIN_NAME, - load(id: string) { + transform(input: string, id: string) { if (!gjsFilter(id)) { return null; } - let input = readFileSync(id, 'utf8'); let code = processor.process(input, { filename: id, inline_source_map, diff --git a/packages/addon-dev/src/rollup-hbs-plugin.ts b/packages/addon-dev/src/rollup-hbs-plugin.ts index db35a35fd..a0841511b 100644 --- a/packages/addon-dev/src/rollup-hbs-plugin.ts +++ b/packages/addon-dev/src/rollup-hbs-plugin.ts @@ -1,8 +1,15 @@ +import type { Plugin, PluginContext } from 'rollup'; import { createFilter } from '@rollup/pluginutils'; -import type { Plugin, PluginContext, CustomPluginOptions } from 'rollup'; -import { readFileSync } from 'fs'; -import { correspondingTemplate, hbsToJS } from '@embroider/core'; import minimatch from 'minimatch'; +import { + hbsToJS, + templateOnlyComponentSource, + needsSyntheticComponentJS, + syntheticJStoHBS, +} from '@embroider/core'; +import { extname } from 'path'; + +const hbsFilter = createFilter('**/*.hbs?([?]*)'); export default function rollupHbsPlugin({ excludeColocation, @@ -12,48 +19,88 @@ export default function rollupHbsPlugin({ return { name: 'rollup-hbs-plugin', async resolveId(source: string, importer: string | undefined, options) { + if (options.custom?.embroider?.isExtensionSearch) { + return null; + } + let resolution = await this.resolve(source, importer, { skipSelf: true, - ...options, }); - if (resolution) { - return resolution; - } else { - return maybeSynthesizeComponentJS( - this, - source, - importer, - options, - excludeColocation + if (!resolution && extname(source) === '') { + resolution = await this.resolve(source + '.hbs', importer, { + skipSelf: true, + }); + } + + if (!resolution) { + let hbsSource = syntheticJStoHBS(source); + if (hbsSource) { + resolution = await this.resolve(hbsSource, importer, { + skipSelf: true, + custom: { + embroider: { + isExtensionSearch: true, + }, + }, + }); + } + + if (!resolution) { + return null; + } + } + + if (resolution && resolution.id.endsWith('.hbs')) { + let isExcluded = excludeColocation?.some((glob) => + minimatch(resolution!.id, glob) ); + if (isExcluded) { + return resolution; + } + } + + let syntheticId = needsSyntheticComponentJS(source, resolution.id); + if (syntheticId) { + this.addWatchFile(source); + return { + id: syntheticId, + meta: { + 'rollup-hbs-plugin': { + type: 'template-only-component-js', + }, + }, + }; } }, load(id: string) { - if (hbsFilter(id)) { - return getHbsToJSCode(id); - } - let meta = getMeta(this, id); - if (meta) { - if (meta?.type === 'template-js') { - const hbsFile = id.replace(/\.js$/, '.hbs'); - return getHbsToJSCode(hbsFile); - } + if (getMeta(this, id)?.type === 'template-only-component-js') { + this.addWatchFile(id); return { - code: templateOnlyComponent, + code: templateOnlyComponentSource(), }; } }, + + transform(code: string, id: string) { + let hbsFilename = id.replace(/\.\w{1,3}$/, '') + '.hbs'; + if (hbsFilename !== id) { + this.addWatchFile(hbsFilename); + if (getMeta(this, id)?.type === 'template-only-component-js') { + this.addWatchFile(id); + } + } + if (!hbsFilter(id)) { + return null; + } + return hbsToJS(code); + }, }; } -const templateOnlyComponent = - `import templateOnly from '@ember/component/template-only';\n` + - `export default templateOnly();\n`; - type Meta = { - type: 'template-only-component-js' | 'template-js'; + type: 'template-only-component-js'; }; function getMeta(context: PluginContext, id: string): Meta | null { @@ -64,44 +111,3 @@ function getMeta(context: PluginContext, id: string): Meta | null { return null; } } - -function getHbsToJSCode(file: string): { code: string } { - let input = readFileSync(file, 'utf8'); - let code = hbsToJS(input); - return { - code, - }; -} - -async function maybeSynthesizeComponentJS( - context: PluginContext, - source: string, - importer: string | undefined, - options: { custom?: CustomPluginOptions; isEntry: boolean }, - excludeColocation: string[] | undefined -) { - let hbsFilename = correspondingTemplate(source); - let templateResolution = await context.resolve(hbsFilename, importer, { - skipSelf: true, - ...options, - }); - if (!templateResolution) { - return null; - } - let type = excludeColocation?.some((glob) => minimatch(hbsFilename, glob)) - ? 'template-js' - : 'template-only-component-js'; - // we're trying to resolve a JS module but only the corresponding HBS - // file exists. Synthesize the JS. The meta states if the hbs corresponds - // to a template-only component or a simple template like a route template. - return { - id: templateResolution.id.replace(/\.hbs$/, '.js'), - meta: { - 'rollup-hbs-plugin': { - type, - }, - }, - }; -} - -const hbsFilter = createFilter('**/*.hbs'); diff --git a/packages/addon-dev/src/rollup-incremental-plugin.ts b/packages/addon-dev/src/rollup-incremental-plugin.ts new file mode 100644 index 000000000..19cca3033 --- /dev/null +++ b/packages/addon-dev/src/rollup-incremental-plugin.ts @@ -0,0 +1,79 @@ +import walkSync from 'walk-sync'; +import { rmSync } from 'fs'; +import { join } from 'path'; +import type { Plugin } from 'rollup'; +import { existsSync } from 'fs-extra'; + +export default function incremental(): Plugin { + const generatedAssets = new Map(); + const generatedFiles = new Set(); + + function isEqual(v1: string | Uint8Array, v2: string | Uint8Array): boolean { + if (typeof v1 === 'string' && typeof v2 === 'string') { + return v1 === v2; + } + if (Buffer.isBuffer(v1) && Buffer.isBuffer(v2)) { + return v1.equals(v2); + } + return false; + } + + let firstTime = true; + + function initGeneratedFiles(outDir: string) { + if (existsSync(outDir)) { + const files = walkSync(outDir, { + globs: ['*/**'], + directories: false, + }); + for (const file of files) { + generatedFiles.add(file); + } + } + } + + function deleteRemovedFiles(bundle: Record, outDir: string) { + for (const file of generatedFiles) { + if (!bundle[file]) { + generatedAssets.delete(file); + rmSync(join(outDir, file)); + } + } + generatedFiles.clear(); + for (const file of Object.keys(bundle)) { + generatedFiles.add(file); + } + } + + function syncFiles(bundle: Record) { + for (const checkKey of Object.keys(bundle)) { + if (bundle[checkKey]) { + let module = bundle[checkKey] as any; + let code = module.source || module.code; + if ( + generatedAssets.has(checkKey) && + isEqual(code, generatedAssets.get(checkKey)) + ) { + delete bundle[checkKey]; + } else { + generatedAssets.set(checkKey, code); + } + } + } + } + + return { + name: 'incremental', + generateBundle(options, bundle) { + if (firstTime) { + firstTime = false; + initGeneratedFiles(options.dir!); + } + if (existsSync(options.dir!)) { + deleteRemovedFiles(bundle, options.dir!); + } + + syncFiles(bundle); + }, + }; +} diff --git a/packages/addon-dev/src/rollup.ts b/packages/addon-dev/src/rollup.ts index 50d9f3bc8..ce035cc40 100644 --- a/packages/addon-dev/src/rollup.ts +++ b/packages/addon-dev/src/rollup.ts @@ -2,14 +2,13 @@ import { default as hbs } from './rollup-hbs-plugin'; import { default as gjs } from './rollup-gjs-plugin'; import { default as publicEntrypoints } from './rollup-public-entrypoints'; import { default as appReexports } from './rollup-app-reexports'; -import type { Options as DelOptions } from 'rollup-plugin-delete'; -import { default as clean } from 'rollup-plugin-delete'; import { default as keepAssets } from './rollup-keep-assets'; import { default as dependencies } from './rollup-addon-dependencies'; import { default as publicAssets, type PublicAssetsOptions, } from './rollup-public-assets'; +import { default as clean } from './rollup-incremental-plugin'; import type { Plugin } from 'rollup'; export class Addon { @@ -64,10 +63,11 @@ export class Addon { return gjs(options); } - // By default rollup does not clear the output directory between builds. This - // does that. - clean(options: DelOptions) { - return clean({ targets: `${this.#destDir}/*`, ...options }); + // this does incremental updates to the dist files and also deletes files that are not part of the generated bundle + // rollup already supports incremental transforms of files, + // this extends it to the dist files + clean() { + return clean(); } // V2 Addons are allowed to contain imports of .css files. This tells rollup diff --git a/packages/shared-internals/src/colocation.ts b/packages/shared-internals/src/colocation.ts new file mode 100644 index 000000000..33b4a974c --- /dev/null +++ b/packages/shared-internals/src/colocation.ts @@ -0,0 +1,57 @@ +import { existsSync } from 'fs-extra'; +import { cleanUrl } from './paths'; +import type PackageCache from './package-cache'; +import { sep } from 'path'; +import { resolve as resolveExports } from 'resolve.exports'; + +export function syntheticJStoHBS(source: string): string | null { + // explicit js is the only case we care about here. Synthetic template JS is + // only ever JS (never TS or anything else). And extensionless imports are + // handled by the default resolving system doing extension search. + if (cleanUrl(source).endsWith('.js')) { + return source.replace(/.js(\?.*)?/, '.hbs$1'); + } + + return null; +} + +export function needsSyntheticComponentJS(requestedSpecifier: string, foundFile: string): string | null { + requestedSpecifier = cleanUrl(requestedSpecifier); + foundFile = cleanUrl(foundFile); + if ( + discoveredImplicitHBS(requestedSpecifier, foundFile) && + !foundFile.split(sep).join('/').endsWith('/template.hbs') && + !correspondingJSExists(foundFile) + ) { + return foundFile.slice(0, -3) + 'js'; + } + return null; +} + +function discoveredImplicitHBS(source: string, id: string): boolean { + return !source.endsWith('.hbs') && id.endsWith('.hbs'); +} + +function correspondingJSExists(id: string): boolean { + return ['js', 'ts'].some(ext => existsSync(id.slice(0, -3) + ext)); +} + +export function isInComponents(url: string, packageCache: Pick) { + const id = cleanUrl(url); + + const pkg = packageCache.ownerOfFile(id); + if (!pkg?.isV2App()) { + return false; + } + + let tryResolve = resolveExports(pkg.packageJSON, './components', { + browser: true, + conditions: ['default', 'imports'], + }); + let componentsDir = tryResolve?.[0] ?? './components'; + return ('.' + id.slice(pkg?.root.length).split(sep).join('/')).startsWith(componentsDir); +} + +export function templateOnlyComponentSource() { + return `import templateOnly from '@ember/component/template-only';\nexport default templateOnly();\n`; +} diff --git a/packages/shared-internals/src/index.ts b/packages/shared-internals/src/index.ts index 2cb7f5b42..e6a9201ce 100644 --- a/packages/shared-internals/src/index.ts +++ b/packages/shared-internals/src/index.ts @@ -25,3 +25,4 @@ export { export { locateEmbroiderWorkingDir } from './working-dir'; export * from './dep-validation'; +export * from './colocation'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 465b972a4..40c5d5afb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: rollup-plugin-copy-assets: specifier: ^2.0.3 version: 2.0.3(rollup@3.29.4) - rollup-plugin-delete: - specifier: ^2.0.0 - version: 2.0.0 walk-sync: specifier: ^3.0.0 version: 3.0.0 @@ -107,7 +104,7 @@ importers: specifier: ^0.1.0 version: 0.1.0 typescript: - specifier: ^5.1.6 + specifier: ^5.4.5 version: 5.5.3 packages/addon-shim: @@ -8896,6 +8893,7 @@ packages: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 + dev: true /ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} @@ -12270,20 +12268,6 @@ packages: is-descriptor: 1.0.3 isobject: 3.0.1 - /del@5.1.0: - resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} - engines: {node: '>=8'} - dependencies: - globby: 10.0.2 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 3.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - dev: false - /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -18495,20 +18479,6 @@ packages: merge2: 1.4.1 slash: 3.0.0 - /globby@10.0.2: - resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} - engines: {node: '>=8'} - dependencies: - '@types/glob': 7.2.0 - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - glob: 7.2.3 - ignore: 5.3.1 - merge2: 1.4.1 - slash: 3.0.0 - dev: false - /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -18976,6 +18946,7 @@ packages: /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + dev: true /indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} @@ -19342,14 +19313,10 @@ packages: resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} dev: true - /is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - dev: false - /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + dev: true /is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} @@ -21774,6 +21741,7 @@ packages: engines: {node: '>=8'} dependencies: aggregate-error: 3.1.0 + dev: true /p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} @@ -22892,13 +22860,6 @@ packages: rollup: 3.29.4 dev: false - /rollup-plugin-delete@2.0.0: - resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==} - engines: {node: '>=10'} - dependencies: - del: 5.1.0 - dev: false - /rollup-pluginutils@2.8.2: resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} dependencies: diff --git a/tests/scenarios/v2-addon-dev-watch-test.ts b/tests/scenarios/v2-addon-dev-watch-test.ts index 365e92110..e57bd905d 100644 --- a/tests/scenarios/v2-addon-dev-watch-test.ts +++ b/tests/scenarios/v2-addon-dev-watch-test.ts @@ -3,6 +3,7 @@ import { baseV2Addon } from './scenarios'; import type { PreparedApp } from 'scenario-tester'; import { Scenarios } from 'scenario-tester'; import fs from 'fs/promises'; +import { existsSync } from 'fs'; import QUnit from 'qunit'; import merge from 'lodash/merge'; import { DevWatcher, becomesModified, isNotModified } from './helpers'; @@ -51,6 +52,7 @@ Scenarios.fromProject(() => baseV2Addon()) plugins: [ addon.publicEntrypoints(['components/**/*.js']), addon.appReexports(['components/**/*.js']), + addon.gjs(), addon.hbs(), addon.dependencies(), addon.publicAssets('custom-public'), @@ -67,11 +69,13 @@ Scenarios.fromProject(() => baseV2Addon()) }, src: { components: { + 'test.gts': '', 'button.hbs': ` `, + 'other.hbs': '
', 'out.hbs': `{{yield}}`, 'demo.js': ` import Component from '@glimmer/component'; @@ -134,6 +138,181 @@ Scenarios.fromProject(() => baseV2Addon()) }); Qmodule('Watching the addon via rollup -c -w', function () { + Qmodule('files are correctly synced', function (hooks) { + let watcher: DevWatcher | null = null; + let demoHbs = ''; + let demoJs = ''; + let distPath = ''; + let distPathDemoComp = ''; + let srcPathOther = ''; + let distPathOther = ''; + let distAppReExportPathOther = ''; + + let origContent = ''; + let demoContent = ''; + let demoJsContent = ''; + + hooks.before(async () => { + demoHbs = path.join(addon.dir, 'src/components/demo.hbs'); + demoJs = path.join(addon.dir, 'src/components/demo.js'); + distPath = path.join(addon.dir, 'dist/components/test.js'); + distPathDemoComp = path.join(addon.dir, 'dist/components/demo.js'); + srcPathOther = path.join(addon.dir, 'src/components/other.hbs'); + distPathOther = path.join(addon.dir, 'dist/components/other.js'); + distAppReExportPathOther = path.join(addon.dir, 'dist/_app_/components/other.js'); + + origContent = (await fs.readFile(srcPathOther)).toString(); + demoContent = (await fs.readFile(demoHbs)).toString(); + demoJsContent = (await fs.readFile(demoJs)).toString(); + watcher = new DevWatcher(addon); + await watcher.start(); + }); + + hooks.after(async () => { + await watcher?.stop(); + }); + + test('deleting a component from src should delete it from dist', async function (assert) { + assert.strictEqual( + existsSync(distAppReExportPathOther), + true, + `Expected ${distAppReExportPathOther} to exist` + ); + + await fs.rm(srcPathOther); + await watcher?.nextBuild(); + assert.strictEqual( + existsSync(distAppReExportPathOther), + false, + `Expected ${distAppReExportPathOther} to be deleted` + ); + }); + + test('create a component in src should create it in dist', async function (assert) { + await fs.writeFile(srcPathOther, origContent); + await watcher?.nextBuild(); + assert.strictEqual( + existsSync(distAppReExportPathOther), + true, + `Expected ${distAppReExportPathOther} to exist` + ); + }); + + test('updating hbs modifies generated colocated js', async function (assert) { + await becomesModified({ + filePath: distPathDemoComp, + assert, + // Update a component + fn: async () => { + let someContent = await fs.readFile(demoHbs); + + // generally it's bad to introduce time dependencies to a test, but we need to wait long enough + // to guess for how long it'll take for the file system to update our file. + // + // the `stat` is measured in `ms`, so it's still pretty fast + await fs.writeFile(demoHbs, someContent + `\n`); + await aBit(10); + await watcher?.nextBuild(); + }, + }); + }); + + test('deleting hbs file updates dist component file', async function (assert) { + await becomesModified({ + filePath: distPathDemoComp, + assert, + // Update a component + fn: async () => { + // generally it's bad to introduce time dependencies to a test, but we need to wait long enough + // to guess for how long it'll take for the file system to update our file. + // + // the `stat` is measured in `ms`, so it's still pretty fast + await aBit(10); + await fs.rm(demoHbs); + await watcher?.nextBuild(); + }, + }); + }); + + test('updating hbs content should not update unrelated files', async function (assert) { + await fs.writeFile(demoHbs, demoContent); + await watcher?.nextBuild(); + + await isNotModified({ + filePath: distPath, + assert, + // Update a component + fn: async () => { + let someContent = await fs.readFile(demoHbs); + + // generally it's bad to introduce time dependencies to a test, but we need to wait long enough + // to guess for how long it'll take for the file system to update our file. + // + // the `stat` is measured in `ms`, so it's still pretty fast + await fs.writeFile(demoHbs, someContent + `\n\n`); + await aBit(10); + await watcher?.nextBuild(); + }, + }); + }); + + test('updating hbs content should not update resulting app re-exported component', async function (assert) { + distPath = path.join(addon.dir, 'dist/_app_/components/test.js'); + await isNotModified({ + filePath: distPath, + assert, + // Update a component + fn: async () => { + let someContent = await fs.readFile(demoHbs); + + // generally it's bad to introduce time dependencies to a test, but we need to wait long enough + // to guess for how long it'll take for the file system to update our file. + // + // the `stat` is measured in `ms`, so it's still pretty fast + await fs.writeFile(demoHbs, someContent + `\n`); + await aBit(10); + await watcher?.nextBuild(); + }, + }); + }); + + test('updating template only should update the dist output', async function (assert) { + await becomesModified({ + filePath: distPathOther, + assert, + // Update a component + fn: async () => { + await aBit(100); + let someContent = await fs.readFile(srcPathOther); + + // generally it's bad to introduce time dependencies to a test, but we need to wait long enough + // to guess for how long it'll take for the file system to update our file. + // + // the `stat` is measured in `ms`, so it's still pretty fast + await fs.writeFile(srcPathOther, someContent + `test\n`); + await aBit(10); + await watcher?.nextBuild(); + }, + }); + }); + + test('deleting demo.js should make demo a template only component', async function (assert) { + await aBit(100); + await fs.rm(demoJs); + await watcher?.nextBuild(); + let distPathDemoCompContent = await fs.readFile(distPathDemoComp); + assert.true(distPathDemoCompContent.includes('templateOnly')); + }); + + test('creating demo.js should make demo a template colocated component', async function (assert) { + await aBit(100); + void fs.writeFile(demoJs, demoJsContent); + await watcher?.nextBuild(); + let distPathDemoCompContent = await fs.readFile(distPathDemoComp); + assert.false(distPathDemoCompContent.includes('templateOnly')); + }); + }); + test('the package.json is not updated since it would be the same', async function (assert) { watcher = new DevWatcher(addon); @@ -166,7 +345,6 @@ Scenarios.fromProject(() => baseV2Addon()) await watcher.start(); let manifestPath = path.join(addon.dir, 'package.json'); - await becomesModified({ filePath: manifestPath, assert,