@mh-gg/schema 0.1.1-alpha.20260613T085325975Z
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +39 -0
- package/schemas/app-composition.schema.json +684 -0
- package/schemas/example-app.schema.json +64 -0
- package/schemas/example-demo.schema.json +45 -0
- package/schemas/micro-plugin.schema.json +20 -0
- package/src/actions.cjs +31 -0
- package/src/app.d.ts +119 -0
- package/src/builder/app.cjs +157 -0
- package/src/builder/effects.cjs +116 -0
- package/src/builder/guards.cjs +35 -0
- package/src/builder/index.cjs +11 -0
- package/src/builder/matterhornApp/build.cjs +137 -0
- package/src/builder/matterhornApp/bundle.cjs +89 -0
- package/src/builder/matterhornApp/demo.cjs +161 -0
- package/src/builder/matterhornApp/demoAliases.cjs +50 -0
- package/src/builder/matterhornApp/descriptors.cjs +59 -0
- package/src/builder/matterhornApp/exports.cjs +95 -0
- package/src/builder/matterhornApp/frontend.cjs +80 -0
- package/src/builder/matterhornApp/plugins.cjs +95 -0
- package/src/builder/matterhornApp/shared.cjs +105 -0
- package/src/builder/matterhornApp.cjs +7 -0
- package/src/builder/model.cjs +172 -0
- package/src/builder/notifications.cjs +51 -0
- package/src/builder/refs.cjs +33 -0
- package/src/builder/schema.cjs +101 -0
- package/src/builder/streamKey.cjs +27 -0
- package/src/composition.cjs +157 -0
- package/src/configured.cjs +41 -0
- package/src/configured.d.ts +1 -0
- package/src/imports/loader.cjs +86 -0
- package/src/index.cjs +12 -0
- package/src/index.d.ts +168 -0
- package/src/json.cjs +70 -0
- package/src/jsonSchema/validator.cjs +131 -0
- package/src/microPlugin.cjs +112 -0
- package/src/model/collections.cjs +57 -0
- package/src/model/effects.cjs +149 -0
- package/src/model/expressions.cjs +85 -0
- package/src/model/guards.cjs +139 -0
- package/src/model/index.cjs +9 -0
- package/src/model/partitionOperands.cjs +89 -0
- package/src/model/partitionValidator.cjs +95 -0
- package/src/model/payload.cjs +93 -0
- package/src/model/plugin.cjs +146 -0
- package/src/notifications.cjs +41 -0
- package/src/notifications.d.ts +63 -0
- package/src/registry.cjs +63 -0
- package/src/runtime-exports.d.ts +65 -0
- package/src/streamKey.d.ts +15 -0
- package/src/types/actionTypes.cjs +164 -0
- package/src/types/coreActionPayloadSchemas.cjs +35 -0
- package/src/types/coreFeatures.cjs +29 -0
- package/src/types/entities.cjs +131 -0
- package/src/types/entityAliases.cjs +84 -0
- package/src/types/generator.cjs +145 -0
- package/src/types/index.cjs +5 -0
- package/src/types/interfaceBlock.cjs +41 -0
- package/src/types/pluginEntities.cjs +83 -0
- package/src/types/schema.cjs +148 -0
- package/src/types/standardPluginEntityTypes.cjs +135 -0
- package/test/content-lww-contract.test.cjs +104 -0
- package/test/partitionValidator.test.cjs +92 -0
- package/test/schema-array-effects.test.cjs +116 -0
- package/test/schema-builder-types.test.cjs +383 -0
- package/test/schema-composition.test.cjs +144 -0
- package/test/schema-configured-builders.test.cjs +286 -0
- package/test/schema-imports.test.cjs +66 -0
- package/test/schema-model-interpreter.test.cjs +273 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const {
|
|
2
|
+
computeAppProtocolHash,
|
|
3
|
+
definePlayerPlugin,
|
|
4
|
+
hashCanonical,
|
|
5
|
+
manifestHash,
|
|
6
|
+
resolvePluginGraph
|
|
7
|
+
} = require('@mh-gg/base');
|
|
8
|
+
const { createActionDispatchersFromComposition } = require('../../actions.cjs');
|
|
9
|
+
const {
|
|
10
|
+
appCompositionSchemaHash,
|
|
11
|
+
resolveCompositionHostPluginEntries,
|
|
12
|
+
validateAppCompositionSchema
|
|
13
|
+
} = require('../../composition.cjs');
|
|
14
|
+
const { schemaDefinedHostPluginEntry } = require('../../model/plugin.cjs');
|
|
15
|
+
const { matterhornAppDescriptorFromBuilt } = require('./descriptors.cjs');
|
|
16
|
+
const { normalizeHostPluginEntries, primaryHostPluginDescriptorForBuilt } = require('./plugins.cjs');
|
|
17
|
+
const { DEFAULT_PUBLISHER, DEFAULT_TRUST, mergedOptions, slugFromAppId } = require('./shared.cjs');
|
|
18
|
+
|
|
19
|
+
function buildMatterhornApp(app, options = {}) {
|
|
20
|
+
const merged = mergedOptions(app, options);
|
|
21
|
+
const composition = validateAppCompositionSchema(merged.compositionSchema || app.toJSON(), { registry: merged.registry });
|
|
22
|
+
const appId = merged.appId || composition.app.id;
|
|
23
|
+
const version = merged.version || composition.app.version || '1.0.0';
|
|
24
|
+
const name = merged.name || composition.app.name || appId;
|
|
25
|
+
const slug = merged.slug || slugFromAppId(appId);
|
|
26
|
+
const packageName = merged.packageName || slug;
|
|
27
|
+
const compositionHostPlugins = resolveCompositionHostPluginEntries(composition, merged.registry);
|
|
28
|
+
const primarySchemaPlugin = !merged.hostPlugin ? schemaDefinedHostPluginEntry(composition, { packageName }) : undefined;
|
|
29
|
+
const hostPluginEntries = normalizeHostPluginEntries({
|
|
30
|
+
...merged,
|
|
31
|
+
packageName,
|
|
32
|
+
hostPlugins: [primarySchemaPlugin, ...(merged.hostPlugins || []), ...compositionHostPlugins].filter(Boolean)
|
|
33
|
+
}, version);
|
|
34
|
+
const graph = resolvePluginGraph(hostPluginEntries.map((entry) => entry.plugin));
|
|
35
|
+
const hostPluginDescriptors = graph.descriptors;
|
|
36
|
+
const appProtocolHash = computeAppProtocolHash({ appPackId: appId, appVersion: version, hostPlugins: hostPluginDescriptors });
|
|
37
|
+
const pluginGraphHash = graph.pluginGraphHash;
|
|
38
|
+
const publisher = merged.publisher || DEFAULT_PUBLISHER;
|
|
39
|
+
const trust = merged.trust || DEFAULT_TRUST;
|
|
40
|
+
const compositionHash = appCompositionSchemaHash(composition);
|
|
41
|
+
|
|
42
|
+
const hostPack = {
|
|
43
|
+
kind: 'matterhorn.host-pack',
|
|
44
|
+
id: `${appId}.host`,
|
|
45
|
+
appPackId: appId,
|
|
46
|
+
version,
|
|
47
|
+
plugins: hostPluginEntries.map((entry) => ({
|
|
48
|
+
id: entry.plugin.id,
|
|
49
|
+
version: entry.plugin.version,
|
|
50
|
+
source: entry.source || `workspace:${entry.packageName}#${entry.exportName}`,
|
|
51
|
+
integrity: hashCanonical({ package: entry.packageName, export: entry.exportName, version: entry.plugin.version || version }),
|
|
52
|
+
...(entry.dependsOn && entry.dependsOn.length ? { dependsOn: entry.dependsOn } : {}),
|
|
53
|
+
...(entry.conflictsWith && entry.conflictsWith.length ? { conflictsWith: entry.conflictsWith } : {})
|
|
54
|
+
})),
|
|
55
|
+
compatibility: { appProtocolHash, pluginGraphHash },
|
|
56
|
+
runtime: { minMatterhornVersion: '0.1.0', sandbox: 'process' },
|
|
57
|
+
capabilities: merged.hostCapabilities || { required: ['room.state', 'room.roles'], optional: ['relay.event-cache'] },
|
|
58
|
+
composition: {
|
|
59
|
+
schemaHash: compositionHash,
|
|
60
|
+
primaryPlugin: composition.primaryPlugin,
|
|
61
|
+
plugins: composition.plugins,
|
|
62
|
+
sharedScopes: composition.sharedScopes || {},
|
|
63
|
+
views: composition.views,
|
|
64
|
+
actions: composition.actions,
|
|
65
|
+
...(composition.notifications === undefined ? {} : { notifications: composition.notifications })
|
|
66
|
+
},
|
|
67
|
+
trust
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const pluginIds = hostPluginDescriptors.map((descriptor) => descriptor.id);
|
|
71
|
+
const support = { appPackId: appId, appPackRange: `^${version}`, appProtocolHash, pluginIds, compositionSchemaHash: compositionHash };
|
|
72
|
+
const playerPack = {
|
|
73
|
+
kind: 'matterhorn.player-pack',
|
|
74
|
+
id: merged.playerPackId || `${appId}.player`,
|
|
75
|
+
name: `${name} Player`,
|
|
76
|
+
version,
|
|
77
|
+
publisher,
|
|
78
|
+
entrypoints: merged.entrypoints || {
|
|
79
|
+
default: `https://apps.matterhorn.gg/${slug}/player/${version}/`,
|
|
80
|
+
admin: `https://apps.matterhorn.gg/${slug}/admin/${version}/`
|
|
81
|
+
},
|
|
82
|
+
supports: [support],
|
|
83
|
+
recommendedFor: merged.recommendedFor || { devices: ['desktop', 'tablet'], roles: ['admin', 'member'] },
|
|
84
|
+
mode: 'external',
|
|
85
|
+
trust: {
|
|
86
|
+
integrity: hashCanonical({ package: packageName, export: merged.playerPluginExport || 'playerPlugin', version }),
|
|
87
|
+
signatures: trust.signatures
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const builtForPrimary = { config: { ...merged, hostPlugin: merged.hostPlugin }, compositionSchema: composition, hostPluginDescriptors, hostPlugins: hostPluginEntries.map((entry) => entry.plugin) };
|
|
92
|
+
const primaryHostPluginDescriptor = primaryHostPluginDescriptorForBuilt(builtForPrimary);
|
|
93
|
+
const appPack = {
|
|
94
|
+
kind: 'matterhorn.app-pack',
|
|
95
|
+
id: appId,
|
|
96
|
+
name,
|
|
97
|
+
version,
|
|
98
|
+
publisher,
|
|
99
|
+
matterhornVersion: '>=0.1 <0.2',
|
|
100
|
+
hostPack: { url: `workspace:${packageName}#${merged.hostPackExport || 'hostPack'}`, integrity: manifestHash(hostPack) },
|
|
101
|
+
playerPacks: [{ id: playerPack.id, name: playerPack.name, url: `workspace:${packageName}#${merged.playerPackExport || 'playerPack'}`, integrity: manifestHash(playerPack), recommendedFor: playerPack.recommendedFor }],
|
|
102
|
+
compatibility: {
|
|
103
|
+
appProtocolHash,
|
|
104
|
+
operationSchemaHash: primaryHostPluginDescriptor.operationSchemaHash,
|
|
105
|
+
stateSchemaHash: primaryHostPluginDescriptor.stateSchemaHash,
|
|
106
|
+
pluginGraphHash,
|
|
107
|
+
compositionSchemaHash: compositionHash
|
|
108
|
+
},
|
|
109
|
+
composition,
|
|
110
|
+
capabilities: merged.appCapabilities || { required: ['room.state', 'room.roles', 'relay.route'], optional: ['relay.event-cache'] },
|
|
111
|
+
trust: { createdAt: merged.createdAt || '2026-05-27T00:00:00Z', signatures: trust.signatures }
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const generatedActions = createActionDispatchersFromComposition(composition, { registry: merged.registry });
|
|
115
|
+
const playerPlugin = definePlayerPlugin({
|
|
116
|
+
id: merged.playerPluginId || `${appId}.player-plugin`,
|
|
117
|
+
version,
|
|
118
|
+
meta: { name: `${name} Player Plugin`, publisher },
|
|
119
|
+
supports: [support],
|
|
120
|
+
routes: merged.routes || composition.routes || [{ path: '/', component: `${name.replace(/[^A-Za-z0-9]/g, '')}Page`, requiredPlugins: pluginIds }],
|
|
121
|
+
actions: { ...generatedActions, ...(merged.actions || {}) },
|
|
122
|
+
optimisticReducers: merged.optimisticReducers || {},
|
|
123
|
+
navigation: { defaultRoute: '/', adminRoute: '/admin', ...(merged.navigation || {}) },
|
|
124
|
+
deviceHints: { desktop: true, tablet: true, ...(merged.deviceHints || {}) }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return { appPack, appProtocolHash, hostPack, hostPluginDescriptors, hostPlugins: hostPluginEntries.map((entry) => entry.plugin), playerPack, playerPlugin, pluginGraphHash, compositionSchema: composition, publisher, trust, config: { ...merged, appId, version, name, slug, packageName } };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createMatterhornAppDescriptor(app, options = {}) {
|
|
131
|
+
return matterhornAppDescriptorFromBuilt(buildMatterhornApp(app, options));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
buildMatterhornApp,
|
|
136
|
+
createMatterhornAppDescriptor
|
|
137
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const { cloneJson } = require('../../json.cjs');
|
|
4
|
+
const { buildMatterhornApp } = require('./build.cjs');
|
|
5
|
+
const { matterhornAppDescriptorFromBuilt, schemaArtifactsForBuilt } = require('./descriptors.cjs');
|
|
6
|
+
const { APP_MATTERHORN_META } = require('./shared.cjs');
|
|
7
|
+
|
|
8
|
+
const BUNDLE_DESCRIPTOR_KEYS = new Set([
|
|
9
|
+
'id',
|
|
10
|
+
'name',
|
|
11
|
+
'version',
|
|
12
|
+
'appPack',
|
|
13
|
+
'hostPack',
|
|
14
|
+
'hostPlugins',
|
|
15
|
+
'playerPacks',
|
|
16
|
+
'playerPlugins',
|
|
17
|
+
'deployment',
|
|
18
|
+
'frontend',
|
|
19
|
+
'matterhorn'
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function bundleDescriptorExtras(matterhornApp) {
|
|
23
|
+
const extras = {};
|
|
24
|
+
for (const [key, value] of Object.entries(matterhornApp)) {
|
|
25
|
+
if (!BUNDLE_DESCRIPTOR_KEYS.has(key) && value !== undefined) extras[key] = cloneJson(value);
|
|
26
|
+
}
|
|
27
|
+
return extras;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanBundlePluginRef(plugin) {
|
|
31
|
+
const ref = {
|
|
32
|
+
kind: 'matterhorn.registry-host-plugin',
|
|
33
|
+
key: plugin.key,
|
|
34
|
+
id: plugin.id,
|
|
35
|
+
version: plugin.version
|
|
36
|
+
};
|
|
37
|
+
if (plugin.config !== undefined) ref.config = cloneJson(plugin.config);
|
|
38
|
+
return ref;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bundleHostPluginRefs(built) {
|
|
42
|
+
const primaryId = built.compositionSchema.primaryPlugin.id;
|
|
43
|
+
const refs = [{
|
|
44
|
+
kind: 'matterhorn.schema-model-host-plugin',
|
|
45
|
+
id: primaryId,
|
|
46
|
+
version: built.compositionSchema.primaryPlugin.version,
|
|
47
|
+
composition: cloneJson(built.compositionSchema)
|
|
48
|
+
}];
|
|
49
|
+
for (const plugin of built.compositionSchema.plugins || []) refs.push(cleanBundlePluginRef(plugin));
|
|
50
|
+
return refs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createMatterhornAppBundle(app, options = {}) {
|
|
54
|
+
const built = buildMatterhornApp(app, options);
|
|
55
|
+
const matterhornApp = matterhornAppDescriptorFromBuilt(built);
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
kind: 'matterhorn.app-bundle',
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
id: matterhornApp.id,
|
|
60
|
+
name: matterhornApp.name,
|
|
61
|
+
version: matterhornApp.version,
|
|
62
|
+
appPack: matterhornApp.appPack,
|
|
63
|
+
hostPack: matterhornApp.hostPack,
|
|
64
|
+
hostPlugins: bundleHostPluginRefs(built),
|
|
65
|
+
playerPacks: matterhornApp.playerPacks,
|
|
66
|
+
deployment: matterhornApp.deployment,
|
|
67
|
+
...(matterhornApp.frontend ? { frontend: matterhornApp.frontend } : {}),
|
|
68
|
+
...(matterhornApp.matterhorn ? { matterhorn: matterhornApp.matterhorn } : {}),
|
|
69
|
+
...bundleDescriptorExtras(matterhornApp),
|
|
70
|
+
compositionSchema: cloneJson(built.compositionSchema),
|
|
71
|
+
artifacts: schemaArtifactsForBuilt(built)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function emitMatterhornAppBundle(app, options = {}) {
|
|
76
|
+
const bundle = createMatterhornAppBundle(app, options);
|
|
77
|
+
const meta = app?.[APP_MATTERHORN_META] || {};
|
|
78
|
+
const outDir = path.resolve(options.outDir || options.packageRoot || meta.packageRoot || process.cwd());
|
|
79
|
+
const fileName = options.fileName || `${bundle.id}.json`;
|
|
80
|
+
const filePath = path.join(outDir, fileName);
|
|
81
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
82
|
+
fs.writeFileSync(filePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
83
|
+
return Object.freeze({ bundlePath: filePath, bundle });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
createMatterhornAppBundle,
|
|
88
|
+
emitMatterhornAppBundle
|
|
89
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const { manifestHash } = require('@mh-gg/base');
|
|
2
|
+
const { createSignedPartyEvent, nostrEventToPartyEvent, partyEventToNostrEvent } = require('@mh-gg/event');
|
|
3
|
+
const {
|
|
4
|
+
HostPluginRuntime,
|
|
5
|
+
createMemoryOperationLog,
|
|
6
|
+
createMemoryRoomStore
|
|
7
|
+
} = require('@mh-gg/host-runtime');
|
|
8
|
+
const { ensureOperationIdentity } = require('@mh-gg/protocol');
|
|
9
|
+
const { cloneJson } = require('../../json.cjs');
|
|
10
|
+
const {
|
|
11
|
+
createRecordPrefixes,
|
|
12
|
+
rememberCreatedDemoAlias,
|
|
13
|
+
rewriteDemoOperation
|
|
14
|
+
} = require('./demoAliases.cjs');
|
|
15
|
+
|
|
16
|
+
const DEMO_FILE_SIGNER = Object.freeze({
|
|
17
|
+
privateKey: '0000000000000000000000000000000000000000000000000000000000000001',
|
|
18
|
+
pubkey: '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function actorForDemo(value = {}) {
|
|
22
|
+
const role = value.role || 'admin';
|
|
23
|
+
const memberId = value.memberId || role;
|
|
24
|
+
return {
|
|
25
|
+
memberId,
|
|
26
|
+
deviceId: value.deviceId || `dev_${memberId}`,
|
|
27
|
+
role,
|
|
28
|
+
displayName: value.displayName || memberId,
|
|
29
|
+
...value
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pluginIdFromDemoRef(ref, app) {
|
|
34
|
+
if (!ref || ref === 'primary' || ref === '$primary') return app.hostPlugin.id;
|
|
35
|
+
const plugin = app.appPack.composition.plugins.find((item) => item.key === ref || item.id === ref);
|
|
36
|
+
return plugin?.id || ref;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function payloadForDemoOperation(item, roomId) {
|
|
40
|
+
const payload = cloneJson(item.payload || {});
|
|
41
|
+
if (item.type !== 'file.upload' || !payload.event) return payload;
|
|
42
|
+
|
|
43
|
+
const partyEvent = nostrEventToPartyEvent(payload.event);
|
|
44
|
+
if (partyEvent.kind !== 'file.upload' || partyEvent.partyId === roomId) return payload;
|
|
45
|
+
|
|
46
|
+
const signed = createSignedPartyEvent({
|
|
47
|
+
kind: 'file.upload',
|
|
48
|
+
partyId: roomId,
|
|
49
|
+
identity: DEMO_FILE_SIGNER,
|
|
50
|
+
payload: partyEvent.payload,
|
|
51
|
+
createdAt: partyEvent.createdAt
|
|
52
|
+
});
|
|
53
|
+
return { ...payload, event: partyEventToNostrEvent(signed) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function operationsFromDemoDefinition(demo, app, roomId = demo?.roomId) {
|
|
57
|
+
if (!demo) return [];
|
|
58
|
+
const appPackHash = manifestHash(app.appPack);
|
|
59
|
+
return (demo.operations || []).map((item, index) => {
|
|
60
|
+
const actor = actorForDemo(item.actor);
|
|
61
|
+
const createdAt = item.createdAt || ((demo.startTime || 1700000000000) + index + 1);
|
|
62
|
+
const operation = {
|
|
63
|
+
clientOperationId: item.id,
|
|
64
|
+
roomId,
|
|
65
|
+
appPackId: app.appPack.id,
|
|
66
|
+
appPackHash,
|
|
67
|
+
pluginId: pluginIdFromDemoRef(item.plugin || item.pluginId, app),
|
|
68
|
+
type: item.type,
|
|
69
|
+
actor,
|
|
70
|
+
seq: item.seq || index + 1,
|
|
71
|
+
createdAt,
|
|
72
|
+
hlc: item.hlc,
|
|
73
|
+
payload: payloadForDemoOperation(item, roomId),
|
|
74
|
+
auth: { credentialId: `cred_${actor.memberId}`, signature: 'sig' }
|
|
75
|
+
};
|
|
76
|
+
return ensureOperationIdentity(operation, { now: createdAt, nodeId: actor.deviceId || actor.memberId || 'demo' });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function countLive(value) {
|
|
81
|
+
if (Array.isArray(value)) return value.filter((item) => !item?.deletedAt && !item?.archivedAt && !item?.deleted).length;
|
|
82
|
+
if (value && typeof value === 'object') return Object.values(value).filter((item) => !item?.deletedAt && !item?.archivedAt && !item?.deleted).length;
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function addSharedPluginSummary(summary, state) {
|
|
87
|
+
const plugins = state.plugins || {};
|
|
88
|
+
const comments = plugins['com.matterhorn.examples.plugins.comments'];
|
|
89
|
+
if (comments?.comments) summary.sharedComments = countLive(comments.comments);
|
|
90
|
+
const mediaRooms = plugins['com.matterhorn.examples.plugins.media-rooms'];
|
|
91
|
+
if (mediaRooms?.rooms) summary.mediaRooms = countLive(mediaRooms.rooms);
|
|
92
|
+
const screenShares = plugins['com.matterhorn.examples.plugins.screen-share'];
|
|
93
|
+
if (screenShares?.shares) summary.activeScreenShares = Object.values(screenShares.shares).filter((share) => !share.stoppedAt).length;
|
|
94
|
+
const presence = plugins['com.matterhorn.examples.plugins.presence'];
|
|
95
|
+
if (presence?.members) summary.onlineMembers = countLive(presence.members);
|
|
96
|
+
const embeds = plugins['com.matterhorn.examples.plugins.embeds'];
|
|
97
|
+
if (embeds?.embeds) summary.embeds = countLive(embeds.embeds);
|
|
98
|
+
const reactions = plugins['com.matterhorn.examples.plugins.reactions'];
|
|
99
|
+
if (reactions?.reactions) summary.scopedReactions = countLive(reactions.reactions);
|
|
100
|
+
return summary;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function genericSummary(state, pluginId) {
|
|
104
|
+
const pluginState = state.plugins?.[pluginId] || {};
|
|
105
|
+
const summary = { version: state.version };
|
|
106
|
+
for (const [key, value] of Object.entries(pluginState)) {
|
|
107
|
+
if (Array.isArray(value) || (value && typeof value === 'object' && key !== 'details' && key !== 'settings')) summary[key] = countLive(value);
|
|
108
|
+
}
|
|
109
|
+
if (pluginState.messages && typeof pluginState.messages === 'object') {
|
|
110
|
+
const messages = Object.values(pluginState.messages);
|
|
111
|
+
summary.visibleMessages = messages.filter((message) => !message.deletedAt).length;
|
|
112
|
+
summary.pinnedMessages = messages.filter((message) => message.pinnedAt).length;
|
|
113
|
+
}
|
|
114
|
+
if (pluginState.tasks && typeof pluginState.tasks === 'object') {
|
|
115
|
+
const tasks = Object.values(pluginState.tasks);
|
|
116
|
+
summary.totalTasks = tasks.length;
|
|
117
|
+
summary.openTasks = tasks.filter((task) => task.status !== 'done' && !task.archivedAt).length;
|
|
118
|
+
summary.completedTasks = tasks.filter((task) => task.status === 'done' && !task.archivedAt).length;
|
|
119
|
+
}
|
|
120
|
+
if (pluginState.notes && typeof pluginState.notes === 'object') summary.noteCount = countLive(pluginState.notes);
|
|
121
|
+
if (pluginState.details?.title) summary.title = pluginState.details.title;
|
|
122
|
+
return summary;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function replayDemo(app, operations, roomId, summary) {
|
|
126
|
+
const runtime = new HostPluginRuntime({
|
|
127
|
+
room: {
|
|
128
|
+
id: roomId,
|
|
129
|
+
appPack: {
|
|
130
|
+
id: app.appPack.id,
|
|
131
|
+
version: app.appPack.version,
|
|
132
|
+
hash: manifestHash(app.appPack),
|
|
133
|
+
protocolHash: app.appPack.compatibility.appProtocolHash
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
plugins: app.hostPlugins,
|
|
137
|
+
store: createMemoryRoomStore(),
|
|
138
|
+
operationLog: createMemoryOperationLog(),
|
|
139
|
+
authenticateActor: async (_auth, actor) => actor
|
|
140
|
+
});
|
|
141
|
+
await runtime.start();
|
|
142
|
+
const aliases = new Map();
|
|
143
|
+
const createPrefixes = createRecordPrefixes(app);
|
|
144
|
+
for (const operation of operations) {
|
|
145
|
+
const operationForReplay = rewriteDemoOperation(operation, aliases);
|
|
146
|
+
const result = await runtime.handleOperation(operationForReplay);
|
|
147
|
+
if (!result.ok) throw new Error(result.reason || result.error || `Demo operation ${operation.type} failed`);
|
|
148
|
+
rememberCreatedDemoAlias(operationForReplay, result, aliases, createPrefixes);
|
|
149
|
+
}
|
|
150
|
+
const state = await runtime.getState();
|
|
151
|
+
const guestView = await runtime.publicView(actorForDemo({ memberId: 'guest', role: 'guest' }));
|
|
152
|
+
const computedSummary = summary ? summary(state) : genericSummary(state, app.hostPlugin.id);
|
|
153
|
+
addSharedPluginSummary(computedSummary, state);
|
|
154
|
+
return { runtime, operations, state, guestView, summary: computedSummary };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
genericSummary,
|
|
159
|
+
operationsFromDemoDefinition,
|
|
160
|
+
replayDemo
|
|
161
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { ensureOperationIdentity } = require('@mh-gg/protocol');
|
|
2
|
+
|
|
3
|
+
function createRecordPrefixes(app) {
|
|
4
|
+
const prefixes = new Map();
|
|
5
|
+
const primaryPluginId = app.hostPlugin?.id || app.appPack?.composition?.primaryPlugin?.id;
|
|
6
|
+
const primaryOperations = app.appPack?.composition?.primaryPlugin?.model?.operations || {};
|
|
7
|
+
for (const [type, descriptor] of Object.entries(primaryOperations)) {
|
|
8
|
+
const createEffect = (descriptor.effects || []).find((effect) => effect?.kind === 'createRecord' && effect.idPrefix);
|
|
9
|
+
if (createEffect && primaryPluginId) prefixes.set(`${primaryPluginId}:${type}`, createEffect.idPrefix);
|
|
10
|
+
}
|
|
11
|
+
return prefixes;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function replaceDemoAliases(value, aliases) {
|
|
15
|
+
if (typeof value === 'string') return aliases.get(value) || value;
|
|
16
|
+
if (Array.isArray(value)) return value.map((item) => replaceDemoAliases(item, aliases));
|
|
17
|
+
if (value && typeof value === 'object') {
|
|
18
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, replaceDemoAliases(item, aliases)]));
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function rememberCreatedDemoAlias(operation, result, aliases, createPrefixes) {
|
|
24
|
+
const prefix = createPrefixes.get(`${operation.pluginId}:${operation.type}`) || inferredCreatePrefix(operation.type);
|
|
25
|
+
const legacyId = operation.clientOperationId;
|
|
26
|
+
const ledgerId = result?.acceptedLedgerId || result?.acceptedSnowflakeId;
|
|
27
|
+
if (!prefix || !legacyId || !ledgerId) return;
|
|
28
|
+
aliases.set(`${prefix}_${legacyId}`, `${prefix}_${ledgerId}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inferredCreatePrefix(type) {
|
|
32
|
+
if (typeof type !== 'string') return undefined;
|
|
33
|
+
if (!/(^|\.)(create|add|send|upload)$/.test(type)) return undefined;
|
|
34
|
+
const prefix = type.split('.')[0]?.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
35
|
+
return prefix || undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rewriteDemoOperation(operation, aliases) {
|
|
39
|
+
const rewritten = replaceDemoAliases(operation, aliases);
|
|
40
|
+
return ensureOperationIdentity(rewritten, {
|
|
41
|
+
now: rewritten.createdAt,
|
|
42
|
+
nodeId: rewritten.actor?.deviceId || rewritten.actor?.memberId || 'demo'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
createRecordPrefixes,
|
|
48
|
+
rememberCreatedDemoAlias,
|
|
49
|
+
rewriteDemoOperation
|
|
50
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { cloneJson } = require('../../json.cjs');
|
|
2
|
+
const { generateAppTypeDeclaration } = require('../../types/index.cjs');
|
|
3
|
+
const { frontendDescriptor } = require('./frontend.cjs');
|
|
4
|
+
const { typeNamespaceForBuilt } = require('./shared.cjs');
|
|
5
|
+
|
|
6
|
+
function matterhornAppDescriptorFromBuilt(built) {
|
|
7
|
+
const config = built.config;
|
|
8
|
+
const frontend = frontendDescriptor(config);
|
|
9
|
+
const matterhornMetadata = config.matterhornApp || (config.matterhorn ? { matterhorn: config.matterhorn } : {});
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
id: config.appId,
|
|
12
|
+
name: config.name,
|
|
13
|
+
version: config.version,
|
|
14
|
+
appPack: built.appPack,
|
|
15
|
+
hostPack: built.hostPack,
|
|
16
|
+
hostPlugins: built.hostPlugins,
|
|
17
|
+
playerPacks: [built.playerPack],
|
|
18
|
+
playerPlugins: [built.playerPlugin],
|
|
19
|
+
deployment: {
|
|
20
|
+
kind: 'matterhorn.self-contained-app',
|
|
21
|
+
host: { runner: 'matterhorn-example-host' },
|
|
22
|
+
relay: { mode: 'embedded', autoStart: true, acceptsPeerRelays: true },
|
|
23
|
+
frontendDelivery: { mode: frontend?.bundle ? 'relay-chunks' : 'external' },
|
|
24
|
+
...(config.deployment || {})
|
|
25
|
+
},
|
|
26
|
+
...(frontend ? { frontend } : {}),
|
|
27
|
+
...matterhornMetadata
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matterhornAppMetadataForBuilt(built) {
|
|
32
|
+
const typeNamespace = typeNamespaceForBuilt(built);
|
|
33
|
+
const generatedTypes = generateAppTypeDeclaration(built.compositionSchema, { namespace: typeNamespace, registry: built.config.registry });
|
|
34
|
+
return {
|
|
35
|
+
typeNamespace,
|
|
36
|
+
generatedTypes
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function schemaArtifactsForBuilt(built) {
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
composition: cloneJson(built.compositionSchema),
|
|
43
|
+
model: cloneJson(built.compositionSchema.primaryPlugin.model || {}),
|
|
44
|
+
actions: Object.fromEntries((built.compositionSchema.actions || []).map((action) => [action.name, cloneJson({
|
|
45
|
+
plugin: action.plugin,
|
|
46
|
+
type: action.type,
|
|
47
|
+
payloadDefaults: action.payloadDefaults,
|
|
48
|
+
payloadSchema: action.payloadSchema,
|
|
49
|
+
requiredRole: action.requiredRole,
|
|
50
|
+
label: action.label
|
|
51
|
+
})]))
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
matterhornAppDescriptorFromBuilt,
|
|
57
|
+
matterhornAppMetadataForBuilt,
|
|
58
|
+
schemaArtifactsForBuilt
|
|
59
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const { maybeClone, pascalCase } = require('./shared.cjs');
|
|
2
|
+
const { buildMatterhornApp } = require('./build.cjs');
|
|
3
|
+
const { matterhornAppDescriptorFromBuilt, matterhornAppMetadataForBuilt } = require('./descriptors.cjs');
|
|
4
|
+
const { operationsFromDemoDefinition, replayDemo } = require('./demo.cjs');
|
|
5
|
+
const { primaryHostPluginForBuilt } = require('./plugins.cjs');
|
|
6
|
+
|
|
7
|
+
function createMatterhornAppExports(app, options = {}) {
|
|
8
|
+
const built = buildMatterhornApp(app, options);
|
|
9
|
+
const config = built.config;
|
|
10
|
+
const matterhornAppDescriptor = matterhornAppDescriptorFromBuilt(built);
|
|
11
|
+
const hostPlugin = primaryHostPluginForBuilt(built);
|
|
12
|
+
const metadata = matterhornAppMetadataForBuilt(built);
|
|
13
|
+
const generatedTypes = metadata.generatedTypes;
|
|
14
|
+
const schemaArtifacts = Object.freeze({
|
|
15
|
+
schema: app.toJSON(),
|
|
16
|
+
types: generatedTypes,
|
|
17
|
+
files: Object.freeze({
|
|
18
|
+
schema: 'matterhorn.schema.json',
|
|
19
|
+
types: 'matterhorn.types.d.ts'
|
|
20
|
+
})
|
|
21
|
+
});
|
|
22
|
+
const matterhornApp = Object.freeze({
|
|
23
|
+
...matterhornAppDescriptor,
|
|
24
|
+
types: Object.freeze({ namespace: metadata.typeNamespace, declaration: generatedTypes }),
|
|
25
|
+
artifacts: schemaArtifacts
|
|
26
|
+
});
|
|
27
|
+
const demo = config.demo;
|
|
28
|
+
const exportPrefix = config.exportPrefix || config.slug;
|
|
29
|
+
const constantPrefix = (config.constantPrefix || exportPrefix).replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
|
|
30
|
+
const pascal = pascalCase(exportPrefix);
|
|
31
|
+
const example = Object.freeze({
|
|
32
|
+
id: config.example?.id || `examples/${config.slug}`,
|
|
33
|
+
title: config.example?.title || `${config.name} Matterhorn example`,
|
|
34
|
+
appPack: config.appId,
|
|
35
|
+
hostPlugin: hostPlugin.id
|
|
36
|
+
});
|
|
37
|
+
const appDefinition = Object.freeze({
|
|
38
|
+
slug: config.slug,
|
|
39
|
+
packageName: config.packageName,
|
|
40
|
+
appId: config.appId,
|
|
41
|
+
pluginId: hostPlugin.id,
|
|
42
|
+
version: config.version,
|
|
43
|
+
name: config.name,
|
|
44
|
+
frontend: maybeClone(config.frontend)
|
|
45
|
+
});
|
|
46
|
+
const appExport = {
|
|
47
|
+
built,
|
|
48
|
+
matterhornApp,
|
|
49
|
+
appPack: built.appPack,
|
|
50
|
+
hostPack: built.hostPack,
|
|
51
|
+
hostPlugin,
|
|
52
|
+
hostPlugins: built.hostPlugins,
|
|
53
|
+
hostPluginDescriptors: built.hostPluginDescriptors,
|
|
54
|
+
playerPack: built.playerPack,
|
|
55
|
+
playerPlugin: built.playerPlugin,
|
|
56
|
+
playerPacks: [built.playerPack],
|
|
57
|
+
playerPlugins: [built.playerPlugin],
|
|
58
|
+
compositionSchema: built.compositionSchema,
|
|
59
|
+
example,
|
|
60
|
+
generatedTypes,
|
|
61
|
+
schemaArtifacts,
|
|
62
|
+
appDefinition,
|
|
63
|
+
demoDefinition: demo,
|
|
64
|
+
[`${constantPrefix}_APP_ID`]: config.appId,
|
|
65
|
+
[`${constantPrefix}_PLUGIN_ID`]: hostPlugin.id,
|
|
66
|
+
[`${constantPrefix}_VERSION`]: config.version,
|
|
67
|
+
[`${exportPrefix}AppPack`]: built.appPack,
|
|
68
|
+
[`${exportPrefix}HostPack`]: built.hostPack,
|
|
69
|
+
[`${exportPrefix}HostPlugin`]: hostPlugin,
|
|
70
|
+
[`${exportPrefix}HostPlugins`]: built.hostPlugins,
|
|
71
|
+
[`${exportPrefix}HostPluginDescriptors`]: built.hostPluginDescriptors,
|
|
72
|
+
[`${exportPrefix}PlayerPack`]: built.playerPack,
|
|
73
|
+
[`${exportPrefix}PlayerPacks`]: [built.playerPack],
|
|
74
|
+
[`${exportPrefix}PlayerPlugin`]: built.playerPlugin,
|
|
75
|
+
[`${exportPrefix}PlayerPlugins`]: [built.playerPlugin],
|
|
76
|
+
[`${exportPrefix}CompositionSchema`]: built.compositionSchema,
|
|
77
|
+
[`${exportPrefix}GeneratedTypes`]: generatedTypes,
|
|
78
|
+
[`${exportPrefix}SchemaArtifacts`]: schemaArtifacts
|
|
79
|
+
};
|
|
80
|
+
appExport.createDemoOperations = (roomId = demo?.roomId || `${config.slug}_demo`) => operationsFromDemoDefinition(demo, appExport, roomId);
|
|
81
|
+
appExport[`create${pascal}DemoOperations`] = appExport.createDemoOperations;
|
|
82
|
+
appExport.createDemo = async (demoOptions = {}) => {
|
|
83
|
+
if (!demo) throw new Error(`${config.name} does not define a demo`);
|
|
84
|
+
const roomId = demoOptions.roomId || demo.roomId;
|
|
85
|
+
return replayDemo(appExport, appExport.createDemoOperations(roomId), roomId, config.summary);
|
|
86
|
+
};
|
|
87
|
+
appExport[`create${pascal}Demo`] = appExport.createDemo;
|
|
88
|
+
for (const [alias, target] of Object.entries(config.exportAliases || {})) {
|
|
89
|
+
if (appExport[target] === undefined) throw new Error(`Export alias ${alias} points at missing ${target}`);
|
|
90
|
+
appExport[alias] = appExport[target];
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze(appExport);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { createMatterhornAppExports };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
function defaultBundleDevCommand() {
|
|
4
|
+
return {
|
|
5
|
+
command: 'npm',
|
|
6
|
+
args: ['run', 'dev:frontend', '--', '--host', '127.0.0.1', '--port', '${bundlePort}', '--strictPort']
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function defaultBundleBuildCommand() {
|
|
11
|
+
return { command: 'npm', args: ['run', 'build:frontend'] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cleanBackgroundColor(value) {
|
|
15
|
+
if (typeof value !== 'string') return undefined;
|
|
16
|
+
const color = value.trim();
|
|
17
|
+
if (!color || color.length > 128) return undefined;
|
|
18
|
+
return color;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanIconText(value, max = 64 * 1024) {
|
|
22
|
+
if (typeof value !== 'string') return undefined;
|
|
23
|
+
const cleaned = value.trim();
|
|
24
|
+
if (!cleaned || cleaned.length > max) return undefined;
|
|
25
|
+
return cleaned;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cleanIcon(value) {
|
|
29
|
+
if (typeof value === 'string') {
|
|
30
|
+
const iconPath = cleanIconText(value, 512);
|
|
31
|
+
return iconPath ? { path: iconPath } : undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
34
|
+
const iconPath = cleanIconText(value.path, 512);
|
|
35
|
+
const svg = cleanIconText(value.svg);
|
|
36
|
+
const label = cleanIconText(value.label, 120);
|
|
37
|
+
if (!iconPath && !svg) return undefined;
|
|
38
|
+
return {
|
|
39
|
+
...(iconPath ? { path: iconPath } : {}),
|
|
40
|
+
...(svg ? { svg } : {}),
|
|
41
|
+
...(label ? { label } : {})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function frontendDescriptor(options) {
|
|
46
|
+
const frontend = options.frontend;
|
|
47
|
+
if (frontend === false || frontend === undefined) return undefined;
|
|
48
|
+
const appId = options.appId;
|
|
49
|
+
const slug = options.slug;
|
|
50
|
+
const name = options.name;
|
|
51
|
+
const root = path.resolve(frontend.root || options.packageRoot || process.cwd());
|
|
52
|
+
const defaultPort = frontend.defaultPort || frontend.port || 42730;
|
|
53
|
+
const bundlePort = frontend.bundlePort || frontend.port || frontend.defaultPort || 42730;
|
|
54
|
+
const bundleOverrides = frontend.bundle && typeof frontend.bundle === 'object' ? frontend.bundle : {};
|
|
55
|
+
const devEntry = bundleOverrides.devEntry || frontend.devEntry || 'src/index.ts';
|
|
56
|
+
const backgroundColor = cleanBackgroundColor(frontend.backgroundColor) || cleanBackgroundColor(bundleOverrides.backgroundColor);
|
|
57
|
+
const icon = cleanIcon(frontend.icon) || cleanIcon(bundleOverrides.icon);
|
|
58
|
+
return {
|
|
59
|
+
kind: frontend.kind || 'matterhorn-player-bundle',
|
|
60
|
+
appId,
|
|
61
|
+
root,
|
|
62
|
+
defaultPort,
|
|
63
|
+
locations: frontend.locations || [{ type: 'local-path', ref: './frontend', label: frontend.label || `${name} frontend package` }],
|
|
64
|
+
...(backgroundColor ? { backgroundColor } : {}),
|
|
65
|
+
...(icon ? { icon } : {}),
|
|
66
|
+
bundle: {
|
|
67
|
+
mountPath: bundleOverrides.mountPath || frontend.mountPath || `/matterhorn/apps/${appId}/`,
|
|
68
|
+
defaultPort: bundleOverrides.defaultPort || bundlePort,
|
|
69
|
+
devEntry,
|
|
70
|
+
builtEntry: bundleOverrides.builtEntry || frontend.builtEntry || `matterhorn-${slug}.js`,
|
|
71
|
+
healthPath: bundleOverrides.healthPath || frontend.healthPath || devEntry,
|
|
72
|
+
dev: bundleOverrides.dev || frontend.dev || defaultBundleDevCommand(),
|
|
73
|
+
build: bundleOverrides.build || frontend.build || defaultBundleBuildCommand(),
|
|
74
|
+
dist: bundleOverrides.dist || frontend.dist || 'frontend/dist',
|
|
75
|
+
...(bundleOverrides.prebuilt === undefined ? {} : { prebuilt: bundleOverrides.prebuilt })
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { frontendDescriptor };
|