-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a test for testing the watch mode behavior and expectations of our
rollup plugins
- Loading branch information
1 parent
7d18d20
commit d3a8b31
Showing
1 changed file
with
312 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
import path from 'path'; | ||
import { baseV2Addon } from './scenarios'; | ||
import { PreparedApp, Scenarios } from 'scenario-tester'; | ||
import fs from 'node:fs/promises'; | ||
import { spawn } from 'node:child_process'; | ||
import QUnit from 'qunit'; | ||
import merge from 'lodash/merge'; | ||
|
||
const { module: Qmodule, test } = QUnit; | ||
|
||
Scenarios.fromProject(() => baseV2Addon()) | ||
.map('v2-addon-dev-watch', async addon => { | ||
addon.pkg.name = 'v2-addon'; | ||
addon.pkg.files = ['dist']; | ||
addon.pkg.exports = { | ||
'./*': './dist/*.js', | ||
'./addon-main.js': './addon-main.js', | ||
'./package.json': './package.json', | ||
}; | ||
addon.pkg.scripts = { | ||
build: 'node ./node_modules/rollup/dist/bin/rollup -c ./rollup.config.mjs', | ||
start: 'node ./node_modules/rollup/dist/bin/rollup -c ./rollup.config.mjs --watch', | ||
}; | ||
|
||
merge(addon.files, { | ||
'babel.config.json': ` | ||
{ | ||
"presets": [ | ||
["@babel/preset-env", { | ||
"targets": ["last 1 firefox versions"] | ||
}] | ||
], | ||
"plugins": [ | ||
"@embroider/addon-dev/template-colocation-plugin", | ||
["babel-plugin-ember-template-compilation"], | ||
["@babel/plugin-proposal-decorators", { "legacy": true }], | ||
[ "@babel/plugin-proposal-class-properties" ] | ||
] | ||
} | ||
`, | ||
'rollup.config.mjs': ` | ||
import { babel } from '@rollup/plugin-babel'; | ||
import { Addon } from '@embroider/addon-dev/rollup'; | ||
const addon = new Addon({ | ||
srcDir: 'src', | ||
destDir: 'dist', | ||
}); | ||
const reexportMappings = { | ||
'components/demo/namespace-me.js': 'components/demo/namespace/namespace-me.js', | ||
}; | ||
export default { | ||
output: addon.output(), | ||
plugins: [ | ||
addon.publicEntrypoints([ | ||
'components/**/*.js', | ||
]), | ||
addon.appReexports([ | ||
'components/demo/index.js', | ||
'components/demo/out.js', | ||
'components/demo/namespace-me.js', | ||
], { | ||
mapFilename: (name) => reexportMappings[name] || name, | ||
}), | ||
addon.hbs(), | ||
addon.dependencies(), | ||
babel({ babelHelpers: 'bundled' }), | ||
addon.clean(), | ||
], | ||
}; | ||
`, | ||
src: { | ||
components: { | ||
demo: { | ||
'button.hbs': ` | ||
<button {{on 'click' @onClick}}> | ||
flip | ||
</button> | ||
`, | ||
'out.hbs': ` | ||
<out>{{yield}}</out> | ||
`, | ||
'namespace-me.hbs': ` | ||
namespaced component | ||
`, | ||
'index.js': ` | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import FlipButton from './button'; | ||
import Out from './out'; | ||
export default class ExampleComponent extends Component { | ||
Button = FlipButton; | ||
Out = Out; | ||
@tracked active = false; | ||
flip = () => (this.active = !this.active); | ||
} | ||
`, | ||
'index.hbs': `Hello there! | ||
<this.Out>{{this.active}}</this.Out> | ||
<this.Button @onClick={{this.flip}} /> | ||
<div data-test="should-transform">{{transformMe}}</div> | ||
`, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
addon.linkDependency('@embroider/addon-shim', { baseDir: __dirname }); | ||
addon.linkDependency('@embroider/addon-dev', { baseDir: __dirname }); | ||
addon.linkDependency('babel-plugin-ember-template-compilation', { baseDir: __dirname }); | ||
addon.linkDevDependency('@babel/core', { baseDir: __dirname }); | ||
addon.linkDevDependency('@babel/plugin-proposal-class-properties', { baseDir: __dirname }); | ||
addon.linkDevDependency('@babel/plugin-proposal-decorators', { baseDir: __dirname }); | ||
addon.linkDevDependency('@babel/preset-env', { baseDir: __dirname }); | ||
addon.linkDevDependency('@rollup/plugin-babel', { baseDir: __dirname }); | ||
addon.linkDevDependency('rollup', { baseDir: __dirname }); | ||
}) | ||
.forEachScenario(scenario => { | ||
Qmodule(scenario.name, function (hooks) { | ||
let addon: PreparedApp; | ||
let watcher: DevWatcher | undefined; | ||
|
||
hooks.before(async () => { | ||
addon = await scenario.prepare(); | ||
|
||
// run the build *once* so we have a known stable state | ||
await addon.execute('pnpm build'); | ||
}); | ||
|
||
hooks.afterEach(async () => { | ||
watcher?.stop(); | ||
}); | ||
|
||
Qmodule('Watching the addon via rollup -c -w', function () { | ||
test('the package.json is not updated since it would be the same', async function (assert) { | ||
watcher = new DevWatcher(addon); | ||
|
||
await watcher.start(); | ||
|
||
let someFile = path.join(addon.dir, 'src/components/demo/index.hbs'); | ||
let manifestPath = path.join(addon.dir, 'package.json'); | ||
|
||
await isNotModified({ | ||
filePath: manifestPath, | ||
assert, | ||
fn: async () => { | ||
let someContent = await fs.readFile(someFile); | ||
|
||
// 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 waiting *a lot more*, should be pretty safe. | ||
await aBit(100); | ||
await fs.writeFile(someFile, someContent); | ||
}, | ||
}); | ||
}); | ||
|
||
test('the package.json *is* updated, since app-js changed', async function (assert) { | ||
watcher = new DevWatcher(addon); | ||
|
||
await watcher.start(); | ||
|
||
let someFile = path.join(addon.dir, 'src/components/demo/index.hbs'); | ||
let manifestPath = path.join(addon.dir, 'package.json'); | ||
|
||
await becomesModified({ | ||
filePath: manifestPath, | ||
assert, | ||
fn: async () => { | ||
let someContent = await fs.readFile(someFile); | ||
|
||
await fs.rm(someFile); | ||
await watcher?.settled(); | ||
|
||
// 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 waiting *a lot more*, should be pretty safe. | ||
await aBit(100); | ||
await fs.writeFile(someFile, someContent); | ||
}, | ||
}); | ||
}); | ||
|
||
test('the package.json *is* updated, since public assets changed', async function (assert) { | ||
watcher = new DevWatcher(addon); | ||
|
||
await watcher.start(); | ||
|
||
let someFile = path.join(addon.dir, 'src/components/demo/index.hbs'); | ||
let manifestPath = path.join(addon.dir, 'package.json'); | ||
|
||
await becomesModified({ | ||
filePath: manifestPath, | ||
assert, | ||
fn: async () => { | ||
let someContent = await fs.readFile(someFile); | ||
|
||
await fs.rm(someFile); | ||
await watcher?.settled(); | ||
|
||
// 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 waiting *a lot more*, should be pretty safe. | ||
await aBit(100); | ||
await fs.writeFile(someFile, someContent); | ||
}, | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
class DevWatcher { | ||
#addon: PreparedApp; | ||
#singletonAbort?: AbortController; | ||
#waitForBuildPromise?: Promise<unknown>; | ||
#lastBuild?: string; | ||
|
||
constructor(addon: PreparedApp) { | ||
this.#addon = addon; | ||
} | ||
|
||
start = () => { | ||
if (this.#singletonAbort) this.#singletonAbort.abort(); | ||
|
||
this.#singletonAbort = new AbortController(); | ||
|
||
// Use execa, because addon.execute returns a normal promise, rather than an inspectable promise-like | ||
// potential feature enhancement? | ||
let process = spawn('pnpm', ['start'], { cwd: this.#addon.dir, signal: this.#singletonAbort.signal }); | ||
|
||
let settle: (...args: unknown[]) => void; | ||
this.#waitForBuildPromise = new Promise((resolve, _reject) => { | ||
settle = resolve; | ||
}); | ||
|
||
if (!process.stdout) { | ||
throw new Error(`Failed to start process, pnpm start`); | ||
} | ||
|
||
process.stdout.on('data', data => { | ||
// TODO: | ||
console.log('=> data', data.toString()); | ||
setTimeout(() => settle?.(), 7000); | ||
}); | ||
|
||
return this.settled(); | ||
}; | ||
|
||
stop = () => this.#singletonAbort?.abort(); | ||
settled = () => this.#waitForBuildPromise; | ||
get lastBuild() { | ||
return this.#lastBuild; | ||
} | ||
} | ||
|
||
async function becomesModified({ | ||
filePath, | ||
assert, | ||
fn, | ||
}: { | ||
filePath: string; | ||
assert: Assert; | ||
fn: () => Promise<void>; | ||
}) { | ||
let oldStat = (await fs.stat(filePath)).mtimeMs; | ||
|
||
await fn(); | ||
|
||
let newStat = (await fs.stat(filePath)).mtimeMs; | ||
|
||
assert.notStrictEqual( | ||
oldStat, | ||
newStat, | ||
`Expected ${filePath} to be modified. Latest: ${newStat}, previously: ${oldStat}` | ||
); | ||
} | ||
|
||
async function isNotModified({ filePath, assert, fn }: { filePath: string; assert: Assert; fn: () => Promise<void> }) { | ||
let oldStat = (await fs.stat(filePath)).mtimeMs; | ||
|
||
await fn(); | ||
|
||
let newStat = (await fs.stat(filePath)).mtimeMs; | ||
|
||
assert.strictEqual( | ||
oldStat, | ||
newStat, | ||
`Expected ${filePath} to be unchanged. Latest: ${newStat}, and pre-fn: ${oldStat}` | ||
); | ||
} | ||
|
||
function aBit(ms: number) { | ||
return new Promise(resolve => setTimeout(resolve, ms)); | ||
} |