Skip to content

Commit 1d4193d

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 c1555c5 commit 1d4193d

8 files changed

Lines changed: 165 additions & 15 deletions

File tree

.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: 141 additions & 2 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
},
@@ -158,11 +158,13 @@ Also:
158158
159159
Last but not least, add `clean:` and `build:` targets to your `Makefile`. The build target compiles TypeScript to JavaScript, copies all SCSS and asset files from `src/` into `dist/` preserving directory structure, and finally uses `tsc-alias` to rewrite `@src` path aliases to relative paths:
160160

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.
162+
161163
```makefile
162164
clean:
163165
rm -rf dist
164166

165-
build: clean
167+
build:
166168
tsc --project tsconfig.build.json
167169
find src -type f \( -name '*.scss' -o -path '*/assets/*' \) -exec sh -c '\
168170
for f in "$$@"; do \
@@ -250,6 +252,9 @@ node_modules
250252
npm-debug.log
251253
coverage
252254
dist/
255+
packages/
256+
/.turbo
257+
/turbo.json
253258
/*.tgz
254259
255260
### i18n ###
@@ -958,3 +963,137 @@ Refactor slots
958963
First, rename `src/plugin-slots`, if it exists, to `src/slots`. Modify imports and documentation across the codebase accordingly.
959964

960965
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.
966+
967+
968+
Set up npm workspaces for local development
969+
===========================================
970+
971+
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.
972+
973+
Add the workspaces field to package.json
974+
-----------------------------------------
975+
976+
```diff
977+
+ "workspaces": [
978+
+ "packages/*"
979+
+ ],
980+
```
981+
982+
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.
983+
984+
Add a turbo.site.json file
985+
--------------------------
986+
987+
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:
988+
989+
```json
990+
{
991+
"$schema": "https://turbo.build/schema.json",
992+
"tasks": {
993+
"build": {
994+
"dependsOn": ["^build"],
995+
"outputs": ["dist/**"],
996+
"cache": false
997+
},
998+
"clean": {
999+
"cache": false
1000+
},
1001+
"watch:build": {
1002+
"dependsOn": ["^build"],
1003+
"persistent": true,
1004+
"cache": false
1005+
},
1006+
"//#dev:site": {
1007+
"dependsOn": ["^build"],
1008+
"persistent": true,
1009+
"cache": false
1010+
}
1011+
}
1012+
}
1013+
```
1014+
1015+
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).
1016+
1017+
Add a nodemon.json file
1018+
------------------------
1019+
1020+
Create a ``nodemon.json`` at the repository root. This configures the ``watch:build`` script to rebuild automatically when source files change:
1021+
1022+
```json
1023+
{
1024+
"watch": [
1025+
"src"
1026+
],
1027+
"ext": "js,jsx,ts,tsx,scss"
1028+
}
1029+
```
1030+
1031+
Add workspace-aware scripts
1032+
----------------------------
1033+
1034+
Install ``turbo`` and ``nodemon`` as dev dependencies:
1035+
1036+
```sh
1037+
npm install --save-dev turbo nodemon
1038+
```
1039+
1040+
Then add the following scripts to ``package.json``:
1041+
1042+
```json
1043+
"build:packages": "make build-packages",
1044+
"clean:packages": "make clean-packages",
1045+
"dev:site": "npm run dev",
1046+
"dev:packages": "make dev-packages",
1047+
"watch:build": "nodemon --exec 'npm run build'",
1048+
```
1049+
1050+
And add the corresponding Makefile targets:
1051+
1052+
```makefile
1053+
TURBO = TURBO_TELEMETRY_DISABLED=1 turbo --dangerously-disable-package-manager-check
1054+
1055+
# turbo.site.json is the standalone turbo config for this package. It is
1056+
# renamed to avoid conflicts with turbo v2's workspace validation, which
1057+
# rejects root task syntax (//#) and requires "extends" in package-level
1058+
# turbo.json files, such as when running in a site repository. The targets
1059+
# below copy it into place before running turbo and clean up after.
1060+
turbo.json: turbo.site.json
1061+
cp $< $@
1062+
1063+
build-packages: turbo.json
1064+
$(TURBO) run build; rm -f turbo.json
1065+
1066+
clean-packages: turbo.json
1067+
$(TURBO) run clean; rm -f turbo.json
1068+
1069+
dev-packages: turbo.json
1070+
$(TURBO) run watch:build dev:site; rm -f turbo.json
1071+
```
1072+
1073+
- ``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.
1074+
- ``build:packages`` builds all workspace packages in dependency order (e.g., ``frontend-base`` before the app).
1075+
- ``clean:packages`` runs the ``clean`` script in each workspace package.
1076+
- ``dev:site`` is an alias for ``npm run dev`` that turbo uses as a root-only task (``//#dev:site``).
1077+
- ``dev:packages`` builds dependencies, then concurrently watches workspace packages for changes and starts the dev server.
1078+
1079+
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.
1080+
1081+
Using workspaces
1082+
-----------------
1083+
1084+
To develop against a local ``frontend-base``:
1085+
1086+
```sh
1087+
mkdir -p packages/frontend-base
1088+
sudo mount --bind /path/to/frontend-base packages/frontend-base
1089+
npm install
1090+
npm run dev:packages
1091+
```
1092+
1093+
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).
1094+
1095+
When done, unmount with:
1096+
1097+
```sh
1098+
sudo umount packages/frontend-base
1099+
```

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)