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",