Skip to content

Commit 9a339b9

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. Also, ensure bin entry points are executable after build, and add bin-linking workaround to migration guide. Part of #184 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5387ffc commit 9a339b9

8 files changed

Lines changed: 187 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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ 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
19+
# When the package is installed from the registry, NPM sets the executable
20+
# bit on `bin` files automatically. It doesn't do the same in workspaces,
21+
# though, so we handle it explicitly here.
22+
chmod a+x $$(node -p "Object.values(require('./package.json').bin).join(' ')")
1923

2024
docs-build:
2125
${doc_command}

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: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ Your package.json should now have a line like this:
121121
},
122122
```
123123

124+
But change it to look like this:
125+
126+
```diff
127+
"peerDependencies": {
128+
+ "@openedx/frontend-base": "alpha || 0.0.0-dev",
129+
},
130+
```
131+
132+
The `alpha` dist-tag resolves to the latest published pre-release, which is what you want in the main branch. The `0.0.0-dev` alternative is the placeholder version used in the source checkout — semantic-release replaces it with the real version at publish time, but in npm workspaces the package still needs to satisfy peer dependency checks. The `||` lets both scenarios work.
133+
124134
Edit package.json scripts
125135
-------------------------
126136

@@ -134,7 +144,7 @@ With the exception of any custom scripts, replace the `scripts` section of your
134144
"i18n_extract": "openedx formatjs extract",
135145
"lint": "openedx lint .",
136146
"lint:fix": "openedx lint --fix .",
137-
"prepack": "npm run build",
147+
"prepack": "npm run clean && npm run build",
138148
"snapshot": "openedx test --updateSnapshot",
139149
"test": "openedx test --coverage --passWithNoTests"
140150
},
@@ -158,11 +168,13 @@ Also:
158168
159169
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:
160170

171+
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.
172+
161173
```makefile
162174
clean:
163175
rm -rf dist
164176

165-
build: clean
177+
build:
166178
tsc --project tsconfig.build.json
167179
find src -type f \( -name '*.scss' -o -path '*/assets/*' \) -exec sh -c '\
168180
for f in "$$@"; do \
@@ -250,6 +262,9 @@ node_modules
250262
npm-debug.log
251263
coverage
252264
dist/
265+
packages/
266+
/.turbo
267+
/turbo.json
253268
/*.tgz
254269
255270
### i18n ###
@@ -958,3 +973,145 @@ Refactor slots
958973
First, rename `src/plugin-slots`, if it exists, to `src/slots`. Modify imports and documentation across the codebase accordingly.
959974

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

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)