Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add better broccoli-side-watch package #2141

Merged
merged 9 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
/packages/webpack/**/*.d.ts
/packages/hbs-loader/**/*.js
/packages/hbs-loader/**/*.d.ts
/packages/broccoli-side-watch/**/*.js
/packages/broccoli-side-watch/**/*.d.ts
/test-packages/support/**/*.js
/test-packages/**/*.d.ts
/test-packages/release/src/*.js
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
/packages/webpack/**/*.d.ts
/packages/hbs-loader/**/*.js
/packages/hbs-loader/**/*.d.ts
/packages/broccoli-side-watch/**/*.js
/packages/broccoli-side-watch/**/*.d.ts
/test-packages/support/**/*.js
/test-packages/**/*.d.ts
/test-packages/release/src/*.js
Expand Down
7 changes: 7 additions & 0 deletions packages/broccoli-side-watch/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/src/**/*.js
/src/**/*.d.ts
/src/**/*.map
/tests/**/*.js
/tests/**/*.d.ts
/tests/**/*.map
24 changes: 24 additions & 0 deletions packages/broccoli-side-watch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# @embroider/broccoli-side-watch

A micro library that allows watching folders for changes outside the `app` folder in Ember apps

## Usage

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.

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.

With this library, you can add the following to your `ember-cli-build.js` to vastly improve your life as a developer:

```js
const sideWatch = require('@embroider/broccoli-side-watch');

const app = new EmberApp(defaults, {
trees: {
app: sideWatch('app', { watching: [
'grand-prix', // this will resolve the package by name and watch all its importable code
'../grand-prix/dist', // or you point to a specific directory to be watched
] }),
},
});
```
6 changes: 6 additions & 0 deletions packages/broccoli-side-watch/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node',
testMatch: [
'<rootDir>/tests/**/*.test.js',
],
};
32 changes: 32 additions & 0 deletions packages/broccoli-side-watch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@embroider/broccoli-side-watch",
"version": "1.0.0",
"description": "Watch changes in other folders to rebuild Ember app",
"keywords": [
"ember"
],
"main": "src/index.js",
"files": [
"src/**/*.js",
"src/**/*.d.ts",
"src/**/*.js.map"
],
"scripts": {
"test": "jest"
},
"author": "Balint Erdi",
"license": "MIT",
"dependencies": {
"@embroider/shared-internals": "workspace:^",
"broccoli-merge-trees": "^4.2.0",
"broccoli-plugin": "^4.0.7",
"broccoli-source": "^3.0.1",
"resolve-package-path": "^4.0.1"
},
"devDependencies": {
"broccoli-node-api": "^1.7.0",
"broccoli-test-helper": "^2.0.0",
"scenario-tester": "^4.0.0",
"typescript": "^5.1.6"
}
}
67 changes: 67 additions & 0 deletions packages/broccoli-side-watch/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { dirname, join, resolve } from 'path';
import mergeTrees from 'broccoli-merge-trees';
import { WatchedDir } from 'broccoli-source';
import { getWatchedDirectories, packageName } from '@embroider/shared-internals';
import resolvePackagePath from 'resolve-package-path';
import Plugin from 'broccoli-plugin';

import type { InputNode } from 'broccoli-node-api';

class BroccoliNoOp extends Plugin {
constructor(path: string) {
super([new WatchedDir(path)]);
}
build() {}
}

interface SideWatchOptions {
watching?: string[];
cwd?: string;
}

/*
Doesn't change your actualTree, but causes a rebuild when any of opts.watching
trees change.

This is helpful when your build pipeline doesn't naturally watch some
dependencies that you're actively developing. For example, right now
@embroider/webpack doesn't rebuild itself when non-ember libraries change.
*/
export default function sideWatch(actualTree: InputNode, opts: SideWatchOptions = {}) {
const cwd = opts.cwd ?? process.cwd();

if (!opts.watching || !Array.isArray(opts.watching)) {
console.warn(
'broccoli-side-watch expects a `watching` array. Returning the original tree without watching any additional trees.'
);
return actualTree;
}

return mergeTrees([
actualTree,
...opts.watching
.flatMap(w => {
const pkgName = packageName(w);

if (pkgName) {
// if this refers to a package name, we watch all importable directories

const pkgJsonPath = resolvePackagePath(pkgName, cwd);
if (!pkgJsonPath) {
throw new Error(
`You specified "${pkgName}" as a package for broccoli-side-watch, but this package is not resolvable from ${cwd} `
);
}

const pkgPath = dirname(pkgJsonPath);

return getWatchedDirectories(pkgPath).map(relativeDir => join(pkgPath, relativeDir));
} else {
return [w];
}
})
.map(path => {
return new BroccoliNoOp(resolve(cwd, path));
}),
]);
}
141 changes: 141 additions & 0 deletions packages/broccoli-side-watch/tests/side-watch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use strict';

import { UnwatchedDir } from 'broccoli-source';
import sideWatch from '../src';
import { Project } from 'scenario-tester';
import { join } from 'path';
import { createBuilder } from 'broccoli-test-helper';

async function generateProject() {
const project = new Project('my-app', {
files: {
src: {
'index.js': 'export default 123',
},
other: {
'index.js': 'export default 456;',
},
},
});

await project.write();

return project;
}

describe('broccoli-side-watch', function () {
test('it returns existing tree without options', async function () {
const project = await generateProject();
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));

const node = sideWatch(existingTree);

expect(node).toEqual(existingTree);
});

test('it watches additional relative paths', async function () {
const project = await generateProject();
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));

const node = sideWatch(existingTree, { watching: ['./other'], cwd: project.baseDir });
const output = createBuilder(node);
await output.build();

expect(output.read()).toEqual({ 'index.js': 'export default 123' });

const watchedNode = node
.__broccoliGetInfo__()
.inputNodes[1].__broccoliGetInfo__()
.inputNodes[0].__broccoliGetInfo__();
expect(watchedNode).toHaveProperty('watched', true);
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'other'));
});

test('it watches additional absolute paths', async function () {
const project = await generateProject();
const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));

const node = sideWatch(existingTree, { watching: [join(project.baseDir, './other')] });
const output = createBuilder(node);
await output.build();

expect(output.read()).toEqual({ 'index.js': 'export default 123' });

const watchedNode = node
.__broccoliGetInfo__()
.inputNodes[1].__broccoliGetInfo__()
.inputNodes[0].__broccoliGetInfo__();
expect(watchedNode).toHaveProperty('watched', true);
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'other'));
});

test('it watches additional package', async function () {
const project = await generateProject();
project.addDependency(
new Project('some-dep', '0.0.0', {
files: {
'index.js': `export default 'some';`,
},
})
);
await project.write();

const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));

const node = sideWatch(existingTree, { watching: ['some-dep'], cwd: project.baseDir });
const output = createBuilder(node);
await output.build();

expect(output.read()).toEqual({ 'index.js': 'export default 123' });

const watchedNode = node
.__broccoliGetInfo__()
.inputNodes[1].__broccoliGetInfo__()
.inputNodes[0].__broccoliGetInfo__();
expect(watchedNode).toHaveProperty('watched', true);
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'node_modules/some-dep'));
});

test('it watches additional package with exports', async function () {
const project = await generateProject();
project.addDependency(
new Project('some-dep', '0.0.0', {
files: {
'package.json': JSON.stringify({
exports: {
'./*': {
types: './declarations/*.d.ts',
default: './dist/*.js',
},
},
}),
src: {
'index.ts': `export default 'some';`,
},
dist: {
'index.js': `export default 'some';`,
},
declarations: {
'index.d.ts': `export default 'some';`,
},
},
})
);
await project.write();

const existingTree = new UnwatchedDir(join(project.baseDir, 'src'));

const node = sideWatch(existingTree, { watching: ['some-dep'], cwd: project.baseDir });
const output = createBuilder(node);
await output.build();

expect(output.read()).toEqual({ 'index.js': 'export default 123' });

const watchedNode = node
.__broccoliGetInfo__()
.inputNodes[1].__broccoliGetInfo__()
.inputNodes[0].__broccoliGetInfo__();
expect(watchedNode).toHaveProperty('watched', true);
expect(watchedNode).toHaveProperty('sourceDirectory', join(project.baseDir, 'node_modules/some-dep/dist'));
});
});
11 changes: 7 additions & 4 deletions packages/shared-internals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
"babel-import-util": "^2.0.0",
"debug": "^4.3.2",
"ember-rfc176-data": "^0.3.17",
"js-string-escape": "^1.0.1",
"resolve-package-path": "^4.0.1",
"typescript-memoize": "^1.0.1",
"fs-extra": "^9.1.0",
"is-subdir": "^1.2.0",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.21",
"minimatch": "^3.0.4",
"semver": "^7.3.5"
"pkg-entry-points": "^1.1.0",
void-mAlex marked this conversation as resolved.
Show resolved Hide resolved
"resolve-package-path": "^4.0.1",
"semver": "^7.3.5",
"typescript-memoize": "^1.0.1"
},
"devDependencies": {
"broccoli-node-api": "^1.7.0",
Expand All @@ -52,6 +54,7 @@
"@types/semver": "^7.3.6",
"@types/tmp": "^0.1.0",
"fixturify": "^2.1.1",
"scenario-tester": "^4.0.0",
"tmp": "^0.1.0",
"typescript": "^5.1.6"
},
Expand Down
1 change: 1 addition & 0 deletions packages/shared-internals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export { locateEmbroiderWorkingDir } from './working-dir';

export * from './dep-validation';
export * from './colocation';
export { getWatchedDirectories } from './watch-utils';
Loading