Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions packages/all/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@ Seeded admin: `admin@objectos.ai` / `admin123`.
## How it works

This piggy-backs on the runtime's **real** local-install mechanism. When you
install an App from the marketplace, `MarketplaceInstallLocalPlugin` writes it to
`.objectstack/installed-packages/<manifestId>.json` (a wrapper
`{ packageId, manifestId, version, manifest, … }` whose `manifest` is the App's
full compiled artifact) and rehydrates it on boot. The compile command reads
that same folder and composes everything into one static environment artifact.
install an App from the marketplace, `MarketplaceInstallLocalPlugin` persists it
as a wrapper entry `{ packageId, manifestId, version, manifest, … }` (where
`manifest` is the App's full compiled artifact). The compile command uses that
same wrapper format and composes everything into one static environment artifact.

```
packages/*/dist/objectstack.json ← each template, compiled & self-contained
│ (1) install → wrapper entry, runtime's exact format
.objectstack/installed-packages/<id>.json ← the REAL local-install folder
.objectstack/marketplace-packages/<id>.json ← compile-input store (see note below)
│ (2) compile (scripts/compile-marketplace.mjs)
dist/objectstack.json ← ONE environment artifact: apps[9]
Expand All @@ -44,25 +43,34 @@ dist/objectstack.json ← ONE environment artifact: apps
objectstack dev all --artifact dist/objectstack.json
```

1. **install** — populate `.objectstack/installed-packages/` from the workspace
templates, in the runtime's exact wrapper format, so the folder is
indistinguishable from a real marketplace install. Already-present entries are
left untouched — if you installed an App via the marketplace UI (or dropped a
compiled artifact into the folder), it's picked up as-is.
1. **install** — populate `.objectstack/marketplace-packages/` from the workspace
templates, in the runtime's exact wrapper format. Already-present entries are
left untouched. Genuine marketplace installs in `.objectstack/installed-packages/`
are also folded in if present.
2. **compile** — unwrap each installed entry to its artifact, concatenate all
metadata (objects, apps, views, flows, hooks, data…), de-duplicate
environment-level singletons (`roles` / `permissions` by name, `requires` as a
set), and synthesize one environment manifest with `apps[N]`.
3. **serve** — `objectstack dev all --artifact …` loads the JSON directly and
boots a single runtime hosting every app.

> **Why `marketplace-packages/`, not the runtime's `installed-packages/`?**
> The runtime auto-**rehydrates** `.objectstack/installed-packages/` at boot. If
> we used that folder as our compile input, every app would be registered
> **twice** when serving `--artifact` — once by the composed env and once by the
> rehydrate pass (observed as `id=undefined` / `Overwriting package: undefined`
> in the boot log, which destabilized the server). Keeping the compile input in a
> separate `marketplace-packages/` folder makes the composed artifact the single
> source of truth at serve time. (Found and fixed during browser testing.)

Scripts:

| Command | Does |
|---|---|
| `pnpm compile` | (re)build `dist/objectstack.json` from the installed store |
| `pnpm dev` | compile, then boot on `:4000` with a fresh ephemeral DB + seeded admin |
| `pnpm start` | boot the already-compiled artifact (no recompile, persistent DB) |
| `pnpm clean` | remove the compile store + `dist/` (start over) |

## Why this does not break ADR-0019 ("one app per package")

Expand All @@ -84,12 +92,35 @@ do not apply. This is the same reason `composeStacks()` exists in
`@objectstack/spec`: composition is an environment-assembly primitive, not a
package-authoring one.

## Caveats
## Verified

Booted via `objectstack dev all --artifact dist/objectstack.json` and exercised
through the browser as a business user:

- **Setup / first-run** — login + seeded admin + console launcher render; all 11
tiles (9 apps + System Settings + Studio) present.
- **All 9 apps load and serve seed data** from the single composed runtime
(records per app: compliance 6, content 14, contracts 6, expense 5, helpdesk 9,
hr 7, procurement 4+5, pm 3, todo 16).
- **Dashboards aggregate correctly** — e.g. helpdesk Agent Workbench shows
SLA-breaching 6 / angry-customers 2 / awaiting-triage 1 from seed data.

## Caveats & known findings

- **Generic role/permission names collide.** `todo` and `content` both ship a
`lead` / `contributor` role and permission; the compiler keeps the first and
shadows the rest (logged on compile). Fine for a "see everything" dev
environment; for production isolation each app would namespace its roles.
- **Partial zh-CN coverage (per-template, not aggregator).** Under a Chinese
locale, object/field labels are translated, but several dashboard titles, KPI
widget labels, and view/page nav items remain English (e.g. helpdesk's "My
Workbench", "SLA Breaching"; the `pm` app name). Each template needs to extend
its translation bundle to cover dashboards/views — out of scope for the
aggregator.
- **Empty *personal* dashboards for a fresh admin.** "My Work"-style dashboards
filter to the current user; seed records are owned by seed users, so the admin
sees 0 there until records are assigned. Org-wide dashboard widgets still
populate.
- **Throwaway, not a product.** This package is for local exploration, demos,
and cross-template QA — not something you publish to the marketplace.
- Rebuild a template (`pnpm --filter @objectlab/<name> build`) then
Expand Down
3 changes: 2 additions & 1 deletion packages/all/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"scripts": {
"compile": "node scripts/compile-marketplace.mjs",
"dev": "pnpm run compile && objectstack dev all --artifact dist/objectstack.json -p 4000 --fresh",
"start": "objectstack dev all --artifact dist/objectstack.json -p 4000"
"start": "objectstack dev all --artifact dist/objectstack.json -p 4000",
"clean": "rm -rf .objectstack/marketplace-packages dist"
},
"dependencies": {
"@objectstack/account": "^7.4.1",
Expand Down
37 changes: 29 additions & 8 deletions packages/all/scripts/compile-marketplace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ const HERE = dirname(fileURLToPath(import.meta.url));
const ALL_DIR = resolve(HERE, '..'); // packages/all (the runtime cwd for `dev all`)
const PACKAGES_DIR = resolve(ALL_DIR, '..'); // packages

// The real on-disk location the runtime's MarketplaceInstallLocalPlugin uses:
// storageDir = resolve(process.cwd(), '.objectstack/installed-packages')
const INSTALLED_DIR = join(ALL_DIR, '.objectstack', 'installed-packages');
// Compile-input store. We deliberately do NOT use the runtime's live-install
// folder (`.objectstack/installed-packages/`): the MarketplaceInstallLocalPlugin
// auto-rehydrates THAT folder at boot, which would double-register every app on
// top of the `--artifact` env we serve (id=undefined / "Overwriting package").
// This is a compile-time cache, distinct from the runtime's live store, so the
// composed artifact is the single source of truth at serve time.
const INSTALLED_DIR = join(ALL_DIR, '.objectstack', 'marketplace-packages');
// If a genuine marketplace install populated the runtime's live folder, fold it
// in too — but warn, because serving `--artifact` from this cwd would also
// rehydrate it (run `start` from a clean cwd, or uninstall, to avoid that).
const LIVE_INSTALL_DIR = join(ALL_DIR, '.objectstack', 'installed-packages');
const OUT = join(ALL_DIR, 'dist', 'objectstack.json');

// Mirror MarketplaceInstallLocalPlugin.safeFilename so files we write are
Expand Down Expand Up @@ -114,14 +122,14 @@ function installFromWorkspace() {
return installed;
}

/** Read every installed entry; unwrap to the App's full artifact. */
function readInstalledArtifacts() {
if (!existsSync(INSTALLED_DIR)) return [];
/** Read every installed entry from a store; unwrap to the App's full artifact. */
function readStore(dir) {
if (!existsSync(dir)) return [];
const out = [];
for (const file of readdirSync(INSTALLED_DIR).filter((f) => f.endsWith('.json')).sort()) {
for (const file of readdirSync(dir).filter((f) => f.endsWith('.json')).sort()) {
let entry;
try {
entry = JSON.parse(readFileSync(join(INSTALLED_DIR, file), 'utf8'));
entry = JSON.parse(readFileSync(join(dir, file), 'utf8'));
} catch {
log(` ! ${file}: invalid JSON, skipped`);
continue;
Expand All @@ -135,6 +143,19 @@ function readInstalledArtifacts() {
return out;
}

/** Read installed Apps: the compile cache, plus any genuine live installs. */
function readInstalledArtifacts() {
const cache = readStore(INSTALLED_DIR);
const live = readStore(LIVE_INSTALL_DIR);
if (live.length > 0) {
log(` ⓘ folding in ${live.length} live marketplace install(s) from .objectstack/installed-packages/`);
log(' (the runtime rehydrates that folder too — run `start` from a clean cwd to avoid double-load)');
}
// De-dupe by namespace; cache wins (it's the deterministic workspace copy).
const seen = new Set(cache.map((e) => e.source));
return [...cache, ...live.filter((e) => !seen.has(e.source))];
}

/** `compile` — compose installed Apps into one environment artifact. */
function compile(store) {
const env = {
Expand Down
Loading