Skip to content

Commit d67c9ef

Browse files
authored
Merge pull request #2141 from embroider-build/side-watch-packages
Add better broccoli-side-watch package
2 parents b3fcd9b + ff8ad4c commit d67c9ef

File tree

13 files changed

+3559
-3235
lines changed

13 files changed

+3559
-3235
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
/packages/webpack/**/*.d.ts
1313
/packages/hbs-loader/**/*.js
1414
/packages/hbs-loader/**/*.d.ts
15+
/packages/broccoli-side-watch/**/*.js
16+
/packages/broccoli-side-watch/**/*.d.ts
1517
/test-packages/support/**/*.js
1618
/test-packages/**/*.d.ts
1719
/test-packages/release/src/*.js

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
/packages/webpack/**/*.d.ts
2222
/packages/hbs-loader/**/*.js
2323
/packages/hbs-loader/**/*.d.ts
24+
/packages/broccoli-side-watch/**/*.js
25+
/packages/broccoli-side-watch/**/*.d.ts
2426
/test-packages/support/**/*.js
2527
/test-packages/**/*.d.ts
2628
/test-packages/release/src/*.js
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/node_modules
2+
/src/**/*.js
3+
/src/**/*.d.ts
4+
/src/**/*.map
5+
/tests/**/*.js
6+
/tests/**/*.d.ts
7+
/tests/**/*.map
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# @embroider/broccoli-side-watch
2+
3+
A micro library that allows watching folders for changes outside the `app` folder in Ember apps
4+
5+
## Usage
6+
7+
Let's assume you have a v2 addon with a package name of `grand-prix` somewhere in your monorepo that also contains your Ember app.
8+
9+
Every time you change something in the source of that addon, you can rebuild it by watching the addon's build (currently using rollup). However, by default the host Ember app doesn't rebuild automatically, so you have to restart the Ember app every time this happens which is a slog.
10+
11+
With this library, you can add the following to your `ember-cli-build.js` to vastly improve your life as a developer:
12+
13+
```js
14+
const sideWatch = require('@embroider/broccoli-side-watch');
15+
16+
const app = new EmberApp(defaults, {
17+
trees: {
18+
app: sideWatch('app', { watching: [
19+
'grand-prix', // this will resolve the package by name and watch all its importable code
20+
'../grand-prix/dist', // or you point to a specific directory to be watched
21+
] }),
22+
},
23+
});
24+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
testMatch: [
4+
'<rootDir>/tests/**/*.test.js',
5+
],
6+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@embroider/broccoli-side-watch",
3+
"version": "1.0.0",
4+
"description": "Watch changes in other folders to rebuild Ember app",
5+
"keywords": [
6+
"ember"
7+
],
8+
"main": "src/index.js",
9+
"files": [
10+
"src/**/*.js",
11+
"src/**/*.d.ts",
12+
"src/**/*.js.map"
13+
],
14+
"scripts": {
15+
"test": "jest"
16+
},
17+
"author": "Balint Erdi",
18+
"license": "MIT",
19+
"dependencies": {
20+
"@embroider/shared-internals": "workspace:^",
21+
"broccoli-merge-trees": "^4.2.0",
22+
"broccoli-plugin": "^4.0.7",
23+
"broccoli-source": "^3.0.1",
24+
"resolve-package-path": "^4.0.1"
25+
},
26+
"devDependencies": {
27+
"broccoli-node-api": "^1.7.0",
28+
"broccoli-test-helper": "^2.0.0",
29+
"scenario-tester": "^4.0.0",
30+
"typescript": "^5.1.6"
31+
}
32+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { dirname, join, resolve } from 'path';
2+
import mergeTrees from 'broccoli-merge-trees';
3+
import { WatchedDir } from 'broccoli-source';
4+
import { getWatchedDirectories, packageName } from '@embroider/shared-internals';
5+
import resolvePackagePath from 'resolve-package-path';
6+
import Plugin from 'broccoli-plugin';
7+
8+
import type { InputNode } from 'broccoli-node-api';
9+
10+
class BroccoliNoOp extends Plugin {
11+
constructor(path: string) {
12+
super([new WatchedDir(path)]);
13+
}
14+
build() {}
15+
}
16+
17+
interface SideWatchOptions {
18+
watching?: string[];
19+
cwd?: string;
20+
}
21+
22+
/*
23+
Doesn't change your actualTree, but causes a rebuild when any of opts.watching
24+
trees change.
25+
26+
This is helpful when your build pipeline doesn't naturally watch some
27+
dependencies that you're actively developing. For example, right now
28+
@embroider/webpack doesn't rebuild itself when non-ember libraries change.
29+
*/
30+
export default function sideWatch(actualTree: InputNode, opts: SideWatchOptions = {}) {
31+
const cwd = opts.cwd ?? process.cwd();
32+
33+
if (!opts.watching || !Array.isArray(opts.watching)) {
34+
console.warn(
35+
'broccoli-side-watch expects a `watching` array. Returning the original tree without watching any additional trees.'
36+
);
37+
return actualTree;
38+
}
39+
40+
return mergeTrees([
41+
actualTree,
42+
...opts.watching
43+
.flatMap(w => {
44+
const pkgName = packageName(w);
45+
46+
if (pkgName) {
47+
// if this refers to a package name, we watch all importable directories
48+
49+
const pkgJsonPath = resolvePackagePath(pkgName, cwd);
50+
if (!pkgJsonPath) {
51+
throw new Error(
52+
`You specified "${pkgName}" as a package for broccoli-side-watch, but this package is not resolvable from ${cwd} `
53+
);
54+
}
55+
56+
const pkgPath = dirname(pkgJsonPath);
57+
58+
return getWatchedDirectories(pkgPath).map(relativeDir => join(pkgPath, relativeDir));
59+
} else {
60+
return [w];
61+
}
62+
})
63+
.map(path => {
64+
return new BroccoliNoOp(resolve(cwd, path));
65+
}),
66+
]);
67+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
import { UnwatchedDir } from 'broccoli-source';
4+
import sideWatch from '../src';
5+
import { Project } from 'scenario-tester';
6+
import { join } from 'path';
7+
import { createBuilder } from 'broccoli-test-helper';
8+
9+
async function generateProject() {
10+
const project = new Project('my-app', {
11+
files: {
12+
src: {
13+
'index.js': 'export default 123',
14+
},
15+
other: {
16+
'index.js': 'export default 456;',
17+
},
18+
},
19+
});
20+
21+
await project.write();
22+
23+
return project;
24+
}
25+
26+
describe('broccoli-side-watch', function () {
27+
test('it returns existing tree without options', async function () {
28+
const project = await generateProject();
29+
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));
30+
31+
const node = sideWatch(existingTree);
32+
33+
expect(node).toEqual(existingTree);
34+
});
35+
36+
test('it watches additional relative paths', async function () {
37+
const project = await generateProject();
38+
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));
39+
40+
const node = sideWatch(existingTree, { watching: ['./other'], cwd: project.baseDir });
41+
const output = createBuilder(node);
42+
await output.build();
43+
44+
expect(output.read()).toEqual({ 'index.js': 'export default 123' });
45+
46+
const watchedNode = node
47+
.__broccoliGetInfo__()
48+
.inputNodes[1].__broccoliGetInfo__()
49+
.inputNodes[0].__broccoliGetInfo__();
50+
expect(watchedNode).toHaveProperty('watched', true);
51+
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'other'));
52+
});
53+
54+
test('it watches additional absolute paths', async function () {
55+
const project = await generateProject();
56+
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));
57+
58+
const node = sideWatch(existingTree, { watching: [join(project.baseDir, './other')] });
59+
const output = createBuilder(node);
60+
await output.build();
61+
62+
expect(output.read()).toEqual({ 'index.js': 'export default 123' });
63+
64+
const watchedNode = node
65+
.__broccoliGetInfo__()
66+
.inputNodes[1].__broccoliGetInfo__()
67+
.inputNodes[0].__broccoliGetInfo__();
68+
expect(watchedNode).toHaveProperty('watched', true);
69+
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'other'));
70+
});
71+
72+
test('it watches additional package', async function () {
73+
const project = await generateProject();
74+
project.addDependency(
75+
new Project('some-dep', '0.0.0', {
76+
files: {
77+
'index.js': `export default 'some';`,
78+
},
79+
})
80+
);
81+
await project.write();
82+
83+
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));
84+
85+
const node = sideWatch(existingTree, { watching: ['some-dep'], cwd: project.baseDir });
86+
const output = createBuilder(node);
87+
await output.build();
88+
89+
expect(output.read()).toEqual({ 'index.js': 'export default 123' });
90+
91+
const watchedNode = node
92+
.__broccoliGetInfo__()
93+
.inputNodes[1].__broccoliGetInfo__()
94+
.inputNodes[0].__broccoliGetInfo__();
95+
expect(watchedNode).toHaveProperty('watched', true);
96+
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'node_modules/some-dep'));
97+
});
98+
99+
test('it watches additional package with exports', async function () {
100+
const project = await generateProject();
101+
project.addDependency(
102+
new Project('some-dep', '0.0.0', {
103+
files: {
104+
'package.json': JSON.stringify({
105+
exports: {
106+
'./*': {
107+
types: './declarations/*.d.ts',
108+
default: './dist/*.js',
109+
},
110+
},
111+
}),
112+
src: {
113+
'index.ts': `export default 'some';`,
114+
},
115+
dist: {
116+
'index.js': `export default 'some';`,
117+
},
118+
declarations: {
119+
'index.d.ts': `export default 'some';`,
120+
},
121+
},
122+
})
123+
);
124+
await project.write();
125+
126+
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));
127+
128+
const node = sideWatch(existingTree, { watching: ['some-dep'], cwd: project.baseDir });
129+
const output = createBuilder(node);
130+
await output.build();
131+
132+
expect(output.read()).toEqual({ 'index.js': 'export default 123' });
133+
134+
const watchedNode = node
135+
.__broccoliGetInfo__()
136+
.inputNodes[1].__broccoliGetInfo__()
137+
.inputNodes[0].__broccoliGetInfo__();
138+
expect(watchedNode).toHaveProperty('watched', true);
139+
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'node_modules/some-dep/dist'));
140+
});
141+
});

packages/shared-internals/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
"babel-import-util": "^2.0.0",
3232
"debug": "^4.3.2",
3333
"ember-rfc176-data": "^0.3.17",
34-
"js-string-escape": "^1.0.1",
35-
"resolve-package-path": "^4.0.1",
36-
"typescript-memoize": "^1.0.1",
3734
"fs-extra": "^9.1.0",
35+
"is-subdir": "^1.2.0",
36+
"js-string-escape": "^1.0.1",
3837
"lodash": "^4.17.21",
3938
"minimatch": "^3.0.4",
40-
"semver": "^7.3.5"
39+
"pkg-entry-points": "^1.1.0",
40+
"resolve-package-path": "^4.0.1",
41+
"semver": "^7.3.5",
42+
"typescript-memoize": "^1.0.1"
4143
},
4244
"devDependencies": {
4345
"broccoli-node-api": "^1.7.0",
@@ -52,6 +54,7 @@
5254
"@types/semver": "^7.3.6",
5355
"@types/tmp": "^0.1.0",
5456
"fixturify": "^2.1.1",
57+
"scenario-tester": "^4.0.0",
5558
"tmp": "^0.1.0",
5659
"typescript": "^5.1.6"
5760
},

packages/shared-internals/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export { locateEmbroiderWorkingDir } from './working-dir';
2626

2727
export * from './dep-validation';
2828
export * from './colocation';
29+
export { getWatchedDirectories } from './watch-utils';

0 commit comments

Comments
 (0)