diff --git a/.gitignore b/.gitignore
index d9ca27c5..783b3693 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ npm-debug.log
coverage
/.tsbuildinfo.*
dist/
+/.turbo
scss
docs/api
diff --git a/Makefile b/Makefile
index b0ee4649..ada69ffa 100644
--- a/Makefile
+++ b/Makefile
@@ -13,9 +13,13 @@ cat_docs_command = cat ./docs/_API-header.md ./docs/_API-body.md > ./docs/API.md
clean:
rm -rf dist .tsbuildinfo.*
-build: clean
+build:
tsc --build ./tsconfig.build.json
cp ./shell/app.scss ./dist/shell/app.scss
+ # When the package is installed from the registry, NPM sets the executable
+ # bit on `bin` files automatically. It doesn't do the same in workspaces,
+ # though, so we handle it explicitly here.
+ chmod a+x $$(node -p "Object.values(require('./package.json').bin).join(' ')")
docs-build:
${doc_command}
diff --git a/README.md b/README.md
index abf8e6d8..e19e48ba 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ This watches for changes in `frontend-base` and rebuilds the packaged tarball on
```sh
nvm use
npm ci
-npm run pack:watch
+npm run watch:pack
```
#### In the consuming application
diff --git a/docs/decisions/0010-typescript-compilation-and-local-dev-workflow.rst b/docs/decisions/0010-typescript-compilation-and-local-dev-workflow.rst
index 3c463382..3740b52a 100644
--- a/docs/decisions/0010-typescript-compilation-and-local-dev-workflow.rst
+++ b/docs/decisions/0010-typescript-compilation-and-local-dev-workflow.rst
@@ -122,7 +122,7 @@ To develop a local dependency (e.g., ``@openedx/frontend-base``) against a
consuming project:
1. In the dependency: ``npm run pack`` (or use a watcher like ``nodemon`` with
- ``npm run pack:watch``)
+ ``npm run watch:pack``)
2. In the consumer: install from the tarball and run the dev server (or use the
`autoinstall tool`_ from the ``frontend-dev-utils`` package)
diff --git a/docs/how_tos/migrate-frontend-app.md b/docs/how_tos/migrate-frontend-app.md
index f3aa2aa1..68df23b6 100644
--- a/docs/how_tos/migrate-frontend-app.md
+++ b/docs/how_tos/migrate-frontend-app.md
@@ -110,17 +110,27 @@ Add frontend-base to dependencies
Run:
```sh
-npm i --save-peer @openedx/frontend-base@alpha
+npm i --save-peer @openedx/frontend-base
```
Your package.json should now have a line like this:
```diff
"peerDependencies": {
-+ "@openedx/frontend-base": "^1.0.0-alpha.0",
++ "@openedx/frontend-base": "^1.0.0",
},
```
+But change it to look like this:
+
+```diff
+"peerDependencies": {
++ "@openedx/frontend-base": "^1.0.0 || 0.0.0-dev",
+},
+```
+
+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.
+
Edit package.json scripts
-------------------------
@@ -134,7 +144,7 @@ With the exception of any custom scripts, replace the `scripts` section of your
"i18n_extract": "openedx formatjs extract",
"lint": "openedx lint .",
"lint:fix": "openedx lint --fix .",
- "prepack": "npm run build",
+ "prepack": "npm run clean && npm run build",
"snapshot": "openedx test --updateSnapshot",
"test": "openedx test --coverage --passWithNoTests"
},
@@ -158,11 +168,13 @@ Also:
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:
+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.
+
```makefile
clean:
rm -rf dist
-build: clean
+build:
tsc --project tsconfig.build.json
find src -type f \( -name '*.scss' -o -path '*/assets/*' \) -exec sh -c '\
for f in "$$@"; do \
@@ -250,6 +262,9 @@ node_modules
npm-debug.log
coverage
dist/
+packages/
+/.turbo
+/turbo.json
/*.tgz
### i18n ###
@@ -958,3 +973,145 @@ Refactor slots
First, rename `src/plugin-slots`, if it exists, to `src/slots`. Modify imports and documentation across the codebase accordingly.
Next, the frontend-base equivalent to `` is ``, 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.
+
+
+Set up npm workspaces for local development
+===========================================
+
+Frontend apps support `npm workspaces `_ so that developers can work on the app and its dependencies (such as ``frontend-base``) simultaneously, with changes reflected automatically.
+
+Add the workspaces field to package.json
+-----------------------------------------
+
+```diff
++ "workspaces": [
++ "packages/*"
++ ],
+```
+
+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.
+
+Add a turbo.site.json file
+--------------------------
+
+Create a ``turbo.site.json`` at the repository root. This configures `Turborepo `_ to build workspace packages in dependency order and run persistent tasks (watch and dev server) concurrently:
+
+```json
+{
+ "$schema": "https://turbo.build/schema.json",
+ "tasks": {
+ "build": {
+ "dependsOn": ["^build"],
+ "outputs": ["dist/**"],
+ "cache": false
+ },
+ "clean": {
+ "cache": false
+ },
+ "watch:build": {
+ "dependsOn": ["^build"],
+ "persistent": true,
+ "cache": false
+ },
+ "//#dev:site": {
+ "dependsOn": ["^build"],
+ "persistent": true,
+ "cache": false
+ }
+ }
+}
+```
+
+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).
+
+Add a nodemon.json file
+------------------------
+
+Create a ``nodemon.json`` at the repository root. This configures the ``watch:build`` script to rebuild automatically when source files change:
+
+```json
+{
+ "watch": [
+ "src"
+ ],
+ "ext": "js,jsx,ts,tsx,scss"
+}
+```
+
+Add workspace-aware scripts
+----------------------------
+
+Install ``turbo`` and ``nodemon`` as dev dependencies:
+
+```sh
+npm install --save-dev turbo nodemon
+```
+
+Then add the following scripts to ``package.json``:
+
+```json
+"build:packages": "make build-packages",
+"clean:packages": "make clean-packages",
+"dev:site": "make dev-site",
+"dev:packages": "make dev-packages",
+"watch:build": "nodemon --exec 'npm run build'",
+```
+
+And add the corresponding Makefile targets:
+
+```makefile
+TURBO = TURBO_TELEMETRY_DISABLED=1 turbo --dangerously-disable-package-manager-check
+
+# turbo.site.json is the standalone turbo config for this package. It is
+# renamed to avoid conflicts with turbo v2's workspace validation, which
+# rejects root task syntax (//#) and requires "extends" in package-level
+# turbo.json files, such as when running in a site repository. The targets
+# below copy it into place before running turbo and clean up after.
+turbo.json: turbo.site.json
+ cp $< $@
+
+# NPM doesn't bin-link workspace packages during install, so it must be done manually.
+bin-link:
+ [ -f packages/frontend-base/package.json ] && npm rebuild --ignore-scripts @openedx/frontend-base || true
+
+build-packages: turbo.json
+ $(TURBO) run build; rm -f turbo.json
+ $(MAKE) bin-link
+
+clean-packages: turbo.json
+ $(TURBO) run clean; rm -f turbo.json
+
+dev-packages: build-packages turbo.json
+ $(TURBO) run watch:build dev:site; rm -f turbo.json
+
+dev-site: bin-link
+ npm run dev
+```
+
+- ``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.
+- ``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``.
+- ``clean:packages`` runs the ``clean`` script in each workspace package.
+- ``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``).
+- ``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.
+
+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.
+
+Using workspaces
+-----------------
+
+To develop against a local ``frontend-base``:
+
+```sh
+mkdir -p packages/frontend-base
+sudo mount --bind /path/to/frontend-base packages/frontend-base
+npm install
+npm run dev:packages
+```
+
+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).
+
+When done, unmount with:
+
+```sh
+sudo umount packages/frontend-base
+```
diff --git a/nodemon.json b/nodemon.json
new file mode 100644
index 00000000..1a8a4d4e
--- /dev/null
+++ b/nodemon.json
@@ -0,0 +1,16 @@
+{
+ "watch": [
+ "runtime",
+ "shell",
+ "tools",
+ "index.ts",
+ "types.ts"
+ ],
+ "ext": "ts,tsx,js,jsx,json,scss,css",
+ "ignore": [
+ "node_modules/**",
+ ".git/**",
+ "pack/**"
+ ],
+ "delay": 250
+}
diff --git a/nodemon.pack.json b/nodemon.pack.json
deleted file mode 100644
index 25e9069a..00000000
--- a/nodemon.pack.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "watch": ["runtime", "shell", "tools", "index.ts", "types.ts"],
- "ext": "ts,tsx,js,jsx,json,scss,css",
- "ignore": ["node_modules/**", ".git/**", "pack/**"],
- "delay": "250ms",
- "exec": "npm run pack"
-}
diff --git a/package.json b/package.json
index f18e64dc..ef3e1683 100644
--- a/package.json
+++ b/package.json
@@ -25,13 +25,14 @@
"clean": "make clean",
"dev": "npm run build && node ./dist/tools/cli/openedx.js dev:shell",
"docs": "jsdoc -c jsdoc.json",
- "docs:watch": "nodemon -w runtime -w docs/template -w README.md -e js,jsx,ts,tsx --exec npm run docs",
"lint": "eslint .; npm run lint:tools; npm --prefix ./test-site run lint",
"lint:tools": "cd ./tools && eslint . && cd ..",
"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",
- "pack:watch": "nodemon --config nodemon.pack.json",
"prepack": "npm run build",
- "test": "jest"
+ "test": "jest",
+ "watch:build": "nodemon --exec 'npm run build'",
+ "watch:docs": "nodemon --watch docs/template --watch README.md --exec npm run docs",
+ "watch:pack": "nodemon --exec 'npm run pack'"
},
"repository": {
"type": "git",