MemberJunction / MemberJunction/MJ
mj app install routes manifest `shared` packages into dynamicPackages.client, dragging server-only code into the MJExplorer bundle
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 308
Description
## Summary
`mj app install` copies every manifest `shared` package into `dynamicPackages.client`. `shared` says nothing about browser safety, so an app whose shared closure reaches server-only code puts that code in the MJExplorer bundle. Installing `bizapps-accounting` fails the Explorer build with 16 `Could not resolve` errors for Node builtins.
## Environment
MJ `next`, CLI `@memberjunction/cli/6.1.0-edge.4`, Node 24.14.1, pnpm 10.33.0, macOS arm64. Host is MJ in a `mj dev workspace` layout with five BizApps installed.
## What happened
After `mj app install https://github.com/MemberJunction/bizapps-accounting`, regenerating the class-registration manifests and rebuilding, `ng build` for MJExplorer failed with 16 errors of the form:
```
X [ERROR] Could not resolve "node:fs"
X [ERROR] Could not resolve "node:path"
X [ERROR] Could not resolve "child_process"
X [ERROR] Could not resolve "node:crypto"
X [ERROR] Could not resolve "node:async_hooks"
X [ERROR] Could not resolve "node:url"
```
The chain:
```
mj.config.cjs dynamicPackages.client
└─ @mj-biz-apps/accounting-actions (manifest role "library", section "shared")
└─ @mj-biz-apps/accounting-core-entities-server
└─ @memberjunction/actions, @memberjunction/integration-engine ← server-only
```
`mj codegen manifest --open-app-client-bootstrap` turns each `dynamicPackages.client` entry into a side-effect import in the Explorer class-registrations manifest, so the whole closure lands in the browser bundle.
Worth noting: `@mj-biz-apps/accounting-actions` ends up in `dynamicPackages.client` and *not* in `dynamicPackages.server`. Actions execute server-side, so the browser is the one place it should not be, and it is the only place it is.
## Root cause
`packages/OpenApp/Engine/src/install/config-manager.ts`:
- `GetServerPackagesFromManifest` (:324-341) iterates `server` plus `shared` and emits an entry only when the package declares a `startupExport`.
- `GetClientPackagesFromManifest` (:349-362) iterates `client` plus `shared` and emits **every** package unconditionally, because client entries are side-effect imports rather than named startup calls.
So a `shared` package with no `startupExport` — which is what `role: "library"` packages are — is filtered out of the server list and passed straight into the client list. `bizapps-accounting/mj-app.json` declares exactly that:
```json
"shared": [
{ "name": "@mj-biz-apps/accounting-entities", "role": "library" },
{ "name": "@mj-biz-apps/accounting-actions", "role": "library" }
]
```
The manifest schema (`packages/OpenApp/Engine/src/manifest/manifest-schema.ts:76-88`) offers `server`, `client` and `shared`, and the doc comment on `GetClientPackagesFromManifest` reads "shared packages run in both the server and client bundles". But `shared` in practice means "not exclusively one tier", which is not the same claim. `@mj-biz-apps/accounting-actions` is shared between the server runtime and the entity layer, not between server and browser. Nothing in the schema or the installer distinguishes the two, and nothing verifies that a package routed to the client can actually be bundled for a browser.
This is not an accident of one repo's packaging. I checked the other four installed apps' shared closures rather than assuming: `orders-actions`, `contracts-actions`, `common-actions` and `tasks-actions` depend only on `zod`, and `orders-core-entities-server` depends only on `orders-entities`. Accounting's is the only chain that currently reaches `@memberjunction/actions` and `integration-engine`. So today it looks like one app's problem, but the routing rule guarantees the same failure for any app whose shared closure touches server code, and there is no signal at install time that it has happened.
## Workaround
Set `Enabled: false` on the `@mj-biz-apps/accounting-actions` entry in `dynamicPackages.client` in `mj.config.cjs`, remove `@mj-biz-apps/accounting-actions` and `@mj-biz-apps/accounting-core-entities-server` from `packages/MJExplorer/package.json`, and regenerate the Explorer manifest with `--no-sync-deps` so the dependency sync does not add them back. The app stays fully `Active`; the Explorer build goes to 0 resolve errors.
## Notes
Candidate directions: route `shared` to the server list only and require an explicit `client` entry for anything the browser needs; split the manifest so browser-safe shared packages are declared distinctly; or have the installer check a routed client package's closure for Node builtins and server-only MJ packages and fail the install loudly instead of deferring it to the Explorer build. Happy to send a PR once the intended meaning of `shared` is settled.
Contributor guide
Research direction
Start in packages/OpenApp/Engine/src/install/config-manager.ts with GetServerPackagesFromManifest and GetClientPackagesFromManifest, then read the manifest schema at packages/OpenApp/Engine/src/manifest/manifest-schema.ts. Compare the generated dynamicPackages entries with the Explorer class-registrations manifest and the reported Node builtin resolution errors. Done means the manifest semantics are enforced consistently and server-only shared dependencies no longer enter the browser bundle.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- build-system, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100