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
22 changes: 22 additions & 0 deletions .changeset/adr-0046-docs-runtime-serving.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@objectstack/cli": patch
"@objectstack/metadata": patch
---

fix(ADR-0046): serve package docs at runtime, not just in the compiled artifact

Package docs (`src/docs/*.md`) compiled into a bundle were never reaching the
runtime, so `GET /meta/doc` returned an empty list and the docs were invisible
even though `os build` produced them.

Two gaps:

- **`os dev` / `os serve` (config-load path)** re-derives metadata from
`defineStack(...)`, which never carries the markdown docs — those are
collected only at compile time. `serve.ts` now collects `src/docs/*.md` into
the stack on the config-load path too (collection only — additive, never
blocks boot), so docs serve in dev exactly as from a built artifact.
- **The MetadataPlugin artifact loader** (`ARTIFACT_FIELD_TO_TYPE`) omitted the
`docs` → `doc` mapping, so the bundle's `docs` array was skipped when loading
through that path. Added the mapping (with a regression test) for parity with
the ObjectQL engine's `metadataArrayKeys`.
25 changes: 25 additions & 0 deletions packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,31 @@ export default class Serve extends Command {
config = merged;
}

// Package docs (ADR-0046): flat `src/docs/*.md` are collected into the
// stack at COMPILE time (compile.ts step 3d). The config-load path here
// re-derives metadata from `defineStack(...)`, which never carries the
// markdown docs — so without this, `os dev`/`os serve` against a config
// serves ZERO docs (GET /meta/doc empty) even though `os build` produces
// them and an artifact boot serves them. Mirror compile's collection so
// docs render under /docs/<name> in dev exactly as from a built artifact.
// Collection only (no lint-fail): docs are additive; never block boot.
if (!useArtifactFallback) {
try {
const { collectDocsFromSrc } = await import('../utils/collect-docs.js');
const collected = collectDocsFromSrc(absolutePath);
if (collected.docs.length > 0) {
const byName = new Map<string, any>();
for (const d of (Array.isArray((config as any).docs) ? (config as any).docs : [])) {
if (d?.name) byName.set(d.name, d);
}
for (const d of collected.docs) byName.set(d.name, d);
config = { ...config, docs: Array.from(byName.values()) };
}
} catch {
/* docs are additive — never block boot on collection */
}
}

// Boot-mode dispatch: this open-core CLI only supports `standalone`
// (and the artifact-fallback shortcut). Cloud / multi-environment
// boot modes live in a separate distribution and are no longer
Expand Down
44 changes: 44 additions & 0 deletions packages/metadata/src/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,50 @@ describe('MetadataPlugin._parseAndRegisterArtifact — view name resolution (PR-
});
});

// ─────────────────────────────────────────────────────────────────────────
// ADR-0046 regression: a compiled artifact carries package docs in a
// top-level `docs: DocSchema[]` array. The artifact loader registers only
// the metadata fields enumerated in ARTIFACT_FIELD_TO_TYPE; `docs` was
// omitted, so the bundle's docs were silently dropped and GET /meta/doc
// returned an empty list even though the package shipped docs. The field
// must map to the `doc` type so docs register like any other item.
// ─────────────────────────────────────────────────────────────────────────
describe('MetadataPlugin._parseAndRegisterArtifact — package docs (ADR-0046)', () => {
it('registers `doc` items from the artifact `docs` array', async () => {
const plugin = new MetadataPlugin({
watch: false,
config: { bootstrap: 'eager' },
environmentId: 'proj_test',
});
const mgr = (plugin as any).manager as NodeMetadataManager;
const fakeCtx = {
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
} as any;

const artifact = {
id: 'com.example.docs',
name: 'test',
version: '0.0.0',
type: 'app',
scope: 'app',
namespace: 'test',
defaultDatasource: 'memory',
docs: [
{ name: 'test_index', label: 'Overview', content: '# Overview\n' },
{ name: 'test_guide', content: '# Guide\n' },
],
};

await (plugin as any)._parseAndRegisterArtifact(fakeCtx, artifact, 'test-artifact');

const index = await mgr.get('doc', 'test_index');
expect(index).toBeDefined();
expect((index as any)?.content).toContain('# Overview');
const guide = await mgr.get('doc', 'test_guide');
expect(guide).toBeDefined();
});
});

// ─────────────────────────────────────────────────────────────────────────
// Filesystem-scanner provenance: when the host declares its package id
// (options.packageId — the project's `defineStack` manifest id), scanned
Expand Down
1 change: 1 addition & 0 deletions packages/metadata/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const ARTIFACT_FIELD_TO_TYPE: Record<string, string> = {
analyticsCubes: 'analytics_cube',
connectors: 'connector',
emailTemplates: 'email_template',
docs: 'doc',
data: 'dataset',
};

Expand Down