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
7 changes: 5 additions & 2 deletions apps/extension/scripts/write-mdbase-manifest.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";

import { format } from "prettier";
import { format, resolveConfig } from "prettier";

import { extensionEnvironment } from "./extension-manifest.mjs";
import { buildReaderManifest } from "../../reader/scripts/reader-manifest.mjs";
Expand All @@ -21,4 +21,7 @@ const manifest = {
};
const target = resolve(root, "src/generated/mdbase-app.json");
await mkdir(resolve(target, ".."), { recursive: true });
await writeFile(target, await format(JSON.stringify(manifest), { parser: "json" }));
await writeFile(
target,
await format(JSON.stringify(manifest), { ...(await resolveConfig(target)), parser: "json" }),
);
71 changes: 60 additions & 11 deletions apps/extension/src/generated/mdbase-app.json

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions apps/reader/mdbase/packs/mdbase.view-1.0.0.json

Large diffs are not rendered by default.

71 changes: 60 additions & 11 deletions apps/reader/public/.well-known/mdbase-app.json

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions apps/reader/scripts/reader-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { parse as parseYaml } from "yaml";
export const READER_TYPE_PACK_VERSION = "1.0.0-beta.1";

const projectRoot = resolve(import.meta.dirname, "..");
// Reader saves library views as mdbase.view records; this is the published
// mdbase-contracts provision, embedded byte-for-byte.
const viewPackPath = resolve(projectRoot, "mdbase", "packs", "mdbase.view-1.0.0.json");
const resources = [
{
kind: "contract",
Expand Down Expand Up @@ -65,6 +68,8 @@ export async function buildReaderManifest({
};
});

const viewPack = JSON.parse(await readFile(viewPackPath, "utf8"));

return {
manifest_version: 1,
id: "dev.mdbase.reader",
Expand All @@ -74,18 +79,12 @@ export async function buildReaderManifest({
redirect_uris: [appUrl],
requirements: {
access: "full_collection",
contracts,
contracts: [...contracts, ...viewPack.provides],
// Contract readiness and collection setup are derived from `contracts` and
// `provisions`; version 2 has no capability identifiers for them.
capabilities: {
contract_version: 2,
required: [
"collection.read",
"records.create",
"records.edit",
"records.delete",
"views.manage",
],
required: ["collection.read", "records.create", "records.edit", "records.delete"],
},
files: {
required: ["list", "read", "add"],
Expand All @@ -106,6 +105,7 @@ export async function buildReaderManifest({
},
resources: packResources.map(({ source, document }) => ({ source, document })),
},
viewPack,
],
},
};
Expand Down
63 changes: 42 additions & 21 deletions apps/reader/scripts/verify-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,49 @@ const validation = validateAppManifest(manifest);
if (!validation.valid) {
fail(`Reader manifest is invalid: ${formatValidationIssues(validation.issues)}`);
}
const [pack] = manifest.provisions?.type_packs ?? [];
if (!pack || manifest.requirements.contracts.length !== 2) {
fail("Reader must provide one type pack containing its two required contracts.");
const packs = manifest.provisions?.type_packs ?? [];
const provided = packs.flatMap((pack) =>
pack.provides.map(({ id, version }) => `${id}@${version}`),
);
const required = manifest.requirements.contracts.map(({ id, version }) => `${id}@${version}`);
if (provided.sort().join() !== required.sort().join()) {
fail("Reader's type packs must provide exactly its required contracts.");
}

const documents = new Map(pack.resources.map((resource) => [resource.source, resource.document]));
for (const resource of pack.manifest.resources) {
const document = documents.get(resource.source);
if (!document) {
fail(`Type-pack source '${resource.source}' is missing.`);
}
if (digest(document) !== resource.digest) {
fail(`Type-pack source '${resource.source}' does not match its digest.`);
for (const pack of packs) {
const documents = new Map(pack.resources.map((resource) => [resource.source, resource.document]));
for (const resource of pack.manifest.resources) {
const document = documents.get(resource.source);
if (!document) {
fail(`Type-pack source '${resource.source}' is missing.`);
}
if (digest(document) !== resource.digest) {
fail(`Type-pack source '${resource.source}' does not match its digest.`);
}
}
}

const collectionRoot = await mkdtemp(join(tmpdir(), "mdbase-reader-pack-"));
try {
await writeFile(join(collectionRoot, "mdbase.yaml"), "spec_version: 0.3.0\n");
const provision = { manifest: pack.manifest, resources: pack.resources };
const assessment = await assessTypePack(collectionRoot, provision, {
installedBy: manifest.id,
});
assertValid(assessment, "assessment");
const installed = await applyTypePack(collectionRoot, provision, {
installedBy: manifest.id,
expectedAssessmentDigest: assessment.result.assessment_digest,
});
assertValid(installed, "installation");
for (const pack of packs) {
const provision = { manifest: pack.manifest, resources: pack.resources };
const assessment = await assessTypePack(collectionRoot, provision, {
installedBy: manifest.id,
});
assertValid(assessment, "assessment");
const installed = await applyTypePack(collectionRoot, provision, {
installedBy: manifest.id,
expectedAssessmentDigest: assessment.result.assessment_digest,
});
assertValid(installed, "installation");
}

await mkdir(join(collectionRoot, "views"));
await writeFile(
join(collectionRoot, "views", "example.md"),
"---\ntype: view\nid: reader.library.example\nversion: 1\nname: Example\nviews:\n - id: all\n name: All\n---\n",
);
await mkdir(join(collectionRoot, "sources"));
await writeFile(
join(collectionRoot, "sources", "example.md"),
Expand Down Expand Up @@ -81,6 +94,14 @@ try {
if (!source.valid || source.view.title !== "Example source") {
fail("Reader source contract did not project a valid source view.");
}
const view = await opened.collection.getContractView(
"views/example.md",
"mdbase.view",
"1.0.0",
);
if (!view.valid || view.view.name !== "Example") {
fail("A Reader view record did not project a valid mdbase.view contract view.");
}
} finally {
await opened.collection.close();
}
Expand Down
7 changes: 5 additions & 2 deletions apps/reader/scripts/write-manifest.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";

import { format } from "prettier";
import { format, resolveConfig } from "prettier";

import { buildReaderManifest } from "./reader-manifest.mjs";

Expand All @@ -10,7 +10,10 @@ const manifest = await buildReaderManifest({
origin: process.env.MDBASE_READER_ORIGIN ?? "https://reader.mdbase.dev",
basePath: process.env.MDBASE_READER_BASE_PATH ?? "/",
});
const document = await format(JSON.stringify(manifest), { parser: "json" });
const document = await format(JSON.stringify(manifest), {
...(await resolveConfig(projectRoot)),
parser: "json",
});
const targets = [
resolve(projectRoot, "public", ".well-known", "mdbase-app.json"),
resolve(projectRoot, "src", "generated", "mdbase-app.json"),
Expand Down
21 changes: 8 additions & 13 deletions apps/reader/src/connect-workspace-views.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function document(id: string, views: NamedView[]): ViewDocument {
id,
name: id,
source: {
path: `views/${id}.mdbase.view`,
path: `views/${id}.md`,
format: "mdbase.view",
revision: "rev-1",
writable: true,
Expand Down Expand Up @@ -81,8 +81,8 @@ describe("Reader saved-view discovery", () => {
expect(result.map(({ name }) => name)).toEqual(["All sources", "Reading queue", "Finished"]);
expect(result[0]).toEqual(defaultLibraryView);
expect(result[1]).toMatchObject({
key: "views/shared.mdbase.view::Reading queue",
path: "views/shared.mdbase.view",
key: "views/shared.md::Reading queue",
path: "views/shared.md",
revision: "rev-1",
writable: true,
owned: true,
Expand Down Expand Up @@ -116,29 +116,24 @@ describe("Reader saved-view discovery", () => {
});

it("can still reopen a newly saved Reader-marked view", async () => {
const path = "views/saved.mdbase.view";
const path = "views/reader.library.reading-queue.md";
const views = repository([
document("saved", [
document("reader.library.reading-queue", [
namedView("Reading queue", {
type: "table",
options: { readerViewVersion: 1 },
}),
]),
]);
vi.mocked(views.save).mockResolvedValue({
path,
format: "mdbase.view",
revision: "rev-1",
document: "",
});
vi.mocked(views.save).mockResolvedValue({ path, revision: "rev-1" });
const result = await gateway(views).saveLibraryView({
name: "Reading queue",
configuration: defaultLibraryViewConfiguration,
});
expect(result).toMatchObject({ path, name: "Reading queue", owned: true });
expect(views.save).toHaveBeenCalledWith({
name: "Reading queue",
document: expect.stringContaining("readerViewVersion: 1"),
path,
frontmatter: expect.objectContaining({ type: "view", name: "Reading queue" }),
});
});
});
46 changes: 28 additions & 18 deletions apps/reader/src/connect-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ import {

import {
annotationViewConfiguration,
buildAnnotationViewDocument,
buildSourceAnnotationsViewDocument,
buildAnnotationView,
buildSourceAnnotationsView,
readerViewKind,
sourceAnnotationsViewName,
} from "./mdbase-annotation-views.js";
import {
buildLibraryViewDocument,
buildLibraryView,
defaultLibraryView,
libraryViewConfiguration,
libraryViewKey,
Expand All @@ -34,7 +33,7 @@ import type {
ReaderLibrarySnapshot,
ReaderWorkspaceGateway,
} from "./workspace-model.js";
import type { LibraryViewRepository } from "@mdbase-reader/connect";
import type { JsonObject, LibraryViewRepository } from "@mdbase-reader/connect";
import type {
Annotation,
AnnotationDeletionPlan,
Expand Down Expand Up @@ -200,17 +199,22 @@ export class ConnectWorkspaceGateway implements ReaderWorkspaceGateway {
if (!this.libraryViewRepository) {
throw new Error("Saved mdbase views are unavailable for this collection.");
}
const saved = await this.libraryViewRepository.save({
document: await (request.annotations
? buildAnnotationViewDocument({
name: request.name,
configuration: request.annotations,
...(request.fieldShapes ? { fieldShapes: request.fieldShapes } : {}),
})
: buildLibraryViewDocument(request)),
...(request.existing?.path ? { path: request.existing.path } : { name: request.name }),
...(request.existing?.revision ? { revision: request.existing.revision } : {}),
});
const frontmatter = request.annotations
? buildAnnotationView({
name: request.name,
configuration: request.annotations,
...(request.fieldShapes ? { fieldShapes: request.fieldShapes } : {}),
})
: buildLibraryView(request);
const saved = await this.libraryViewRepository.save(
request.existing?.path
? {
path: request.existing.path,
frontmatter,
replace: request.existing.revision ? { revision: request.existing.revision } : {},
}
: { path: viewPath(frontmatter), frontmatter },
);
const views = await this.listLibraryViews();
const match = views.find(
(view) =>
Expand Down Expand Up @@ -251,9 +255,10 @@ export class ConnectWorkspaceGateway implements ReaderWorkspaceGateway {
if (existing) {
return { path: existing.source.path, created: false };
}
const frontmatter = buildSourceAnnotationsView();
const saved = await this.libraryViewRepository.save({
document: await buildSourceAnnotationsViewDocument(),
name: sourceAnnotationsViewName,
path: viewPath(frontmatter),
frontmatter,
});
return { path: saved.path, created: true };
}
Expand Down Expand Up @@ -596,3 +601,8 @@ function replaceInOrder(
): readonly SourceSummary[] {
return sources.map((source) => (source.id === replacement.id ? replacement : source));
}

/** New views live beside each other under `views/`, named by their stable view ID. */
function viewPath(frontmatter: JsonObject): string {
return `views/${String(frontmatter["id"])}.md`;
}
Loading
Loading