Skip to content

Commit b13dabc

Browse files
arbrandesclaude
andcommitted
feat: support npm workspaces for local development
Decouple clean from build in the Makefile so that watch mode can rebuild without wiping dist/. Add nodemon.json and watch:build, watch:docs, watch:pack scripts to standardize file watching. Part of #184 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 094119f commit b13dabc

File tree

8 files changed

+166
-16
lines changed

8 files changed

+166
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ npm-debug.log
33
coverage
44
/.tsbuildinfo.*
55
dist/
6+
/.turbo
67
scss
78
docs/api
89

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ cat_docs_command = cat ./docs/_API-header.md ./docs/_API-body.md > ./docs/API.md
1313
clean:
1414
rm -rf dist .tsbuildinfo.*
1515

16-
build: clean
16+
build:
1717
tsc --build ./tsconfig.build.json
1818
cp ./shell/app.scss ./dist/shell/app.scss
1919

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ This watches for changes in `frontend-base` and rebuilds the packaged tarball on
7878
```sh
7979
nvm use
8080
npm ci
81-
npm run pack:watch
81+
npm run watch:pack
8282
```
8383

8484
#### In the consuming application

docs/decisions/0010-typescript-compilation-and-local-dev-workflow.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ To develop a local dependency (e.g., ``@openedx/frontend-base``) against a
122122
consuming project:
123123

124124
1. In the dependency: ``npm run pack`` (or use a watcher like ``nodemon`` with
125-
``npm run pack:watch``)
125+
``npm run watch:pack``)
126126
2. In the consumer: install from the tarball and run the dev server (or use the
127127
`autoinstall tool`_ from the ``frontend-dev-utils`` package)
128128

docs/how_tos/migrate-frontend-app.md

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ With the exception of any custom scripts, replace the `scripts` section of your
134134
"i18n_extract": "openedx formatjs extract",
135135
"lint": "openedx lint .",
136136
"lint:fix": "openedx lint --fix .",
137-
"prepack": "npm run build",
137+
"prepack": "npm run clean && npm run build",
138138
"snapshot": "openedx test --updateSnapshot",
139139
"test": "openedx test --coverage --passWithNoTests"
140140
},
@@ -156,13 +156,15 @@ Also:
156156
> **Why change `fedx-scripts` to `openedx`?**
157157
> A few reasons. One, the Open edX project shouldn't be using the name of an internal community of practice at edX for its frontend tooling. Two, some dependencies of your MFE invariably still use frontend-build for their own build needs. This means that they already installed `fedx-scripts` into your `node_modules/.bin` folder. Only one version can be in there, so we need a new name. Seemed like a great time for a naming refresh. |
158158
159-
Last but not least, add `clean:` and `build:` targets to your `Makefile`. The build target compiles TypeScript to JavaScript, uses `tsc-alias` to rewrite `@src` path aliases to relative paths, and copies all SCSS files from `src/` into `dist/` preserving directory structure:
159+
Last but not least, add `clean:` and `build:` targets to your `Makefile`. The build target compiles TypeScript to JavaScript, uses `tsc-alias` to rewrite `@src` path aliases to relative paths, and copies all SCSS files from `src/` into `dist/` preserving directory structure.
160+
161+
Note that `build` intentionally does *not* depend on `clean`. This allows incremental rebuilds during development (especially in workspace mode, where a watcher triggers `build` on every change). The `prepack` script in `package.json` runs `clean && build` explicitly, so published packages always start fresh.
160162

161163
```makefile
162164
clean:
163165
rm -rf dist
164166

165-
build: clean
167+
build:
166168
tsc --project tsconfig.build.json
167169
tsc-alias -p tsconfig.build.json
168170
find src -type f -name '*.scss' -exec sh -c '\
@@ -248,6 +250,9 @@ node_modules
248250
npm-debug.log
249251
coverage
250252
dist/
253+
packages/
254+
/.turbo
255+
/turbo.json
251256
/*.tgz
252257
253258
### i18n ###
@@ -952,3 +957,137 @@ Refactor slots
952957
First, rename `src/plugin-slots`, if it exists, to `src/slots`. Modify imports and documentation across the codebase accordingly.
953958

954959
Next, the frontend-base equivalent to `<PluginSlot />` is `<Slot />`, and has a different API. This includes a change in the slot ID, according to the [new slot naming ADR](../decisions/0009-slot-naming-and-lifecycle.rst) in this repository. Rename them accordingly. You can refer to the `src/shell/dev` in this repository for examples.
960+
961+
962+
Set up npm workspaces for local development
963+
===========================================
964+
965+
Frontend apps support `npm workspaces <https://docs.npmjs.com/cli/using-npm/workspaces>`_ so that developers can work on the app and its dependencies (such as ``frontend-base``) simultaneously, with changes reflected automatically.
966+
967+
Add the workspaces field to package.json
968+
-----------------------------------------
969+
970+
```diff
971+
+ "workspaces": [
972+
+ "packages/*"
973+
+ ],
974+
```
975+
976+
This tells npm to look in ``packages/`` for local overrides of published packages. The ``packages/`` directory is gitignored (see the `.gitignore` step above), since it contains development-only bind-mounted checkouts.
977+
978+
Add a turbo.site.json file
979+
--------------------------
980+
981+
Create a ``turbo.site.json`` at the repository root. This configures `Turborepo <https://turbo.build/>`_ to build workspace packages in dependency order and run persistent tasks (watch and dev server) concurrently:
982+
983+
```json
984+
{
985+
"$schema": "https://turbo.build/schema.json",
986+
"tasks": {
987+
"build": {
988+
"dependsOn": ["^build"],
989+
"outputs": ["dist/**"],
990+
"cache": false
991+
},
992+
"clean": {
993+
"cache": false
994+
},
995+
"watch:build": {
996+
"dependsOn": ["^build"],
997+
"persistent": true,
998+
"cache": false
999+
},
1000+
"//#dev:site": {
1001+
"dependsOn": ["^build"],
1002+
"persistent": true,
1003+
"cache": false
1004+
}
1005+
}
1006+
}
1007+
```
1008+
1009+
The file is named ``turbo.site.json`` rather than ``turbo.json`` to avoid conflicts with turbo v2's workspace validation. When a site repository includes your app as an npm workspace, turbo scans for ``turbo.json`` in each package directory and rejects root task syntax (``//#``) and configs without ``"extends"``. By using a different filename, the config is invisible to turbo during workspace runs, and only activated via the Makefile when running standalone (see below).
1010+
1011+
Add a nodemon.json file
1012+
------------------------
1013+
1014+
Create a ``nodemon.json`` at the repository root. This configures the ``watch:build`` script to rebuild automatically when source files change:
1015+
1016+
```json
1017+
{
1018+
"watch": [
1019+
"src"
1020+
],
1021+
"ext": "js,jsx,ts,tsx,scss"
1022+
}
1023+
```
1024+
1025+
Add workspace-aware scripts
1026+
----------------------------
1027+
1028+
Install ``turbo`` and ``nodemon`` as dev dependencies:
1029+
1030+
```sh
1031+
npm install --save-dev turbo nodemon
1032+
```
1033+
1034+
Then add the following scripts to ``package.json``:
1035+
1036+
```json
1037+
"build:packages": "make build-packages",
1038+
"clean:packages": "make clean-packages",
1039+
"dev:site": "npm run dev",
1040+
"dev:packages": "make dev-packages",
1041+
"watch:build": "nodemon --exec 'npm run build'",
1042+
```
1043+
1044+
And add the corresponding Makefile targets:
1045+
1046+
```makefile
1047+
TURBO = TURBO_TELEMETRY_DISABLED=1 turbo --dangerously-disable-package-manager-check
1048+
1049+
# turbo.site.json is the standalone turbo config for this package. It is
1050+
# renamed to avoid conflicts with turbo v2's workspace validation, which
1051+
# rejects root task syntax (//#) and requires "extends" in package-level
1052+
# turbo.json files, such as when running in a site repository. The targets
1053+
# below copy it into place before running turbo and clean up after.
1054+
turbo.json: turbo.site.json
1055+
cp $< $@
1056+
1057+
build-packages: turbo.json
1058+
$(TURBO) run build; rm -f turbo.json
1059+
1060+
clean-packages: turbo.json
1061+
$(TURBO) run clean; rm -f turbo.json
1062+
1063+
dev-packages: turbo.json
1064+
$(TURBO) run watch:build dev:site; rm -f turbo.json
1065+
```
1066+
1067+
- ``watch:build`` uses ``nodemon`` to watch for source changes (as configured in ``nodemon.json``) and re-runs ``npm run build`` on each change. Turbo runs this in each workspace package that defines it.
1068+
- ``build:packages`` builds all workspace packages in dependency order (e.g., ``frontend-base`` before the app).
1069+
- ``clean:packages`` runs the ``clean`` script in each workspace package.
1070+
- ``dev:site`` is an alias for ``npm run dev`` that turbo uses as a root-only task (``//#dev:site``).
1071+
- ``dev:packages`` builds dependencies, then concurrently watches workspace packages for changes and starts the dev server.
1072+
1073+
The Makefile targets copy ``turbo.site.json`` to ``turbo.json`` before invoking turbo, then remove the copy afterward. This ensures turbo finds its expected config when running standalone, without leaving a ``turbo.json`` that would conflict in a workspace context. The ``--dangerously-disable-package-manager-check`` flag and ``TURBO_TELEMETRY_DISABLED=1`` are also set here, keeping turbo invocation details in one place.
1074+
1075+
Using workspaces
1076+
-----------------
1077+
1078+
To develop against a local ``frontend-base``:
1079+
1080+
```sh
1081+
mkdir -p packages/frontend-base
1082+
sudo mount --bind /path/to/frontend-base packages/frontend-base
1083+
npm install
1084+
npm run dev:packages
1085+
```
1086+
1087+
Bind mounts are used instead of symlinks because Node.js resolves symlinks to real paths, which breaks hoisted dependency resolution. Docker volume mounts work equally well (and are what ``tutor dev`` uses).
1088+
1089+
When done, unmount with:
1090+
1091+
```sh
1092+
sudo umount packages/frontend-base
1093+
```

nodemon.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"watch": [
3+
"runtime",
4+
"shell",
5+
"tools",
6+
"index.ts",
7+
"types.ts"
8+
],
9+
"ext": "ts,tsx,js,jsx,json,scss,css",
10+
"ignore": [
11+
"node_modules/**",
12+
".git/**",
13+
"pack/**"
14+
],
15+
"delay": 250
16+
}

nodemon.pack.json

Lines changed: 0 additions & 7 deletions
This file was deleted.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
"clean": "make clean",
2626
"dev": "npm run build && node ./dist/tools/cli/openedx.js dev:shell",
2727
"docs": "jsdoc -c jsdoc.json",
28-
"docs:watch": "nodemon -w runtime -w docs/template -w README.md -e js,jsx,ts,tsx --exec npm run docs",
2928
"lint": "eslint .; npm run lint:tools; npm --prefix ./test-site run lint",
3029
"lint:tools": "cd ./tools && eslint . && cd ..",
3130
"pack": "mkdir -p pack && npm pack --silent --pack-destination pack >/dev/null && mv \"$(ls -t pack/*.tgz | head -n 1)\" pack/openedx-frontend-base.tgz",
32-
"pack:watch": "nodemon --config nodemon.pack.json",
3331
"prepack": "npm run build",
34-
"test": "jest"
32+
"test": "jest",
33+
"watch:build": "nodemon --exec 'npm run build'",
34+
"watch:docs": "nodemon --watch docs/template --watch README.md --exec npm run docs",
35+
"watch:pack": "nodemon --exec 'npm run pack'"
3536
},
3637
"repository": {
3738
"type": "git",

0 commit comments

Comments
 (0)