clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Expose mod warnings through local and shared previews

Dami committed 6197c58
commit
6197c58c3ffb8ec29a476fbdba21a20765fbb29e
parent
d2be209052904698912c6c0192745b4e873d6866
author
Dami <dami.etoile@gmail.com>
3 files changed, +63 -5
Msrc/pack-service.js14++++++---
Msrc/share.js6+++-
Mtests/pack-service.test.js48++++++++++++++++++++++++++++++
M src/pack-service.js +10 -4
@@ -1,5 +1,5 @@
11 /** Presentation-independent pack actions for the stage and developer tools. */2-import { loadMod } from './mods.js';2+import { collectModWarnings, loadMod } from './mods.js';33 import { createCatalog } from './catalog.js';44 import { decodeSharedPack, encodeSharedPack, exportCommonJS } from './share.js';55 import { cloneData } from './data/clone.js';
@@ -10,6 +10,9 @@ function errorDetails(error) {
1010 }1111 1212 function playerError(detail, source) {13+ if (/^manifest\.features(?:\[\d+\])?: unsupported feature\b/.test(detail)) {14+ return { message: 'This pack needs a newer Miau.', detail };15+ }1316 const levelMatch = /^levels\[(\d+)\](?:\.([^:]+))?:\s*(.*)$/.exec(detail);1417 if (!levelMatch) return { message: `Pack data: ${detail}`, detail };1518 const index = Number(levelMatch[1]);
@@ -37,7 +40,7 @@ function playerError(detail, source) {
3740 }3841 3942 function result(pack, extras = {}) {40- return { ok: true, pack, conflict: extras.conflict ?? pack?.conflict ?? null, errors: [], ...extras };43+ return { ok: true, pack, conflict: extras.conflict ?? pack?.conflict ?? null, errors: [], warnings: [], ...extras };4144 }4245 4346 function failure(error, source, extras = {}) {
@@ -47,6 +50,7 @@ function failure(error, source, extras = {}) {
4750 pack: null,4851 conflict: extras.conflict || null,4952 errors: errorDetails(error).map(detail => playerError(detail, context)),53+ warnings: [],5054 ...extras,5155 };5256 }
@@ -58,6 +62,7 @@ function manifestFromView(view) {
5862 ...(view.author !== undefined && view.author !== null ? { author: view.author } : {}),5963 version: view.version || '1.0.0',6064 ...(view.engine !== undefined ? { engine: view.engine } : {}),65+ ...(Array.isArray(view.features) ? { features: cloneData(view.features) } : {}),6166 ...(view.forkOf !== undefined && view.forkOf !== null ? { forkOf: view.forkOf } : {}),6267 };6368 }
@@ -85,6 +90,7 @@ function createPackView(catalog, source, origin, previews, conflict = null) {
8590 author: data.manifest.author ?? null,8691 version: data.manifest.version,8792 engine: data.manifest.engine,93+ features: Array.isArray(data.manifest.features) ? cloneData(data.manifest.features) : null,8894 forkOf: data.manifest.forkOf ?? null,8995 origin: origin || null,9096 vocabulary: cloneData(data.vocabulary || []),
@@ -132,7 +138,7 @@ export function createPackService(options = {}) {
132138 function importPackText(source, filename = 'pack.cjs') {133139 try {134140 const authored = loadMod(source, { filename });135- return result(createPackView(catalog, authored, 'local', previews, conflictFor(catalog, authored)));141+ return result(createPackView(catalog, authored, 'local', previews, conflictFor(catalog, authored)), { warnings: collectModWarnings(authored) });136142 } catch (error) { return failure(error, error?.source); }137143 }138144 function decodePackLink(hash) {
@@ -143,7 +149,7 @@ export function createPackService(options = {}) {
143149 const payload = fragment.slice('#pack='.length);144150 if (!payload || payload.includes('&')) throw new TypeError('Pack link is incomplete.');145151 const authored = decodeSharedPack(payload);146- return result(createPackView(catalog, authored, 'url', previews, conflictFor(catalog, authored)));152+ return result(createPackView(catalog, authored, 'url', previews, conflictFor(catalog, authored)), { warnings: collectModWarnings(authored) });147153 } catch (error) { return failure(error); }148154 }149155 function installPack(input, options = {}) {
M src/share.js +5 -1
M tests/pack-service.test.js +48 -0
@@ -55,6 +55,54 @@ test('link decoding is data-only and does not install until confirmed', () => {
5555 assert.equal(receiver.listPacks().find(pack => pack.id === 'gift-pack').origin, 'url');5656 });5757 58+test('local and shared previews expose rule warnings outside portable pack data', () => {59+ const warned = structuredClone(example);60+ warned.levels[0].objects[2].text = 'SHUT';61+ const sender = createPackService({ baseUrl: 'https://fcbc.n.zip/' });62+ const local = sender.importPackText(exportCommonJS(warned), 'warning.cjs');63+ assert.equal(local.ok, true);64+ assert.equal(local.errors.length, 0);65+ assert.equal(local.warnings.length, 1);66+ assert.match(local.warnings[0].message, /SHUT/);67+ assert.equal(local.warnings[0].levelId, 'hello');68+ assert.equal('warnings' in local.pack, false);69+70+ assert.equal(sender.installPack(local.pack).ok, true);71+ const link = sender.packLink('gift-pack');72+ assert.equal(link.ok, true);73+ const receiver = createPackService();74+ const shared = receiver.decodePackLink(new URL(link.url).hash);75+ assert.equal(shared.ok, true);76+ assert.deepEqual(shared.warnings, local.warnings);77+ assert.equal('warnings' in shared.pack, false);78+ assert.equal(receiver.listPacks().some(pack => pack.id === 'gift-pack'), false);79+});80+81+test('an unsupported required feature has a player-readable preview error', () => {82+ const unsupported = structuredClone(example);83+ unsupported.manifest.features = ['pairs'];84+ const service = createPackService();85+ const preview = service.importPackText(`module.exports = ${JSON.stringify(unsupported)};`, 'future.cjs');86+ assert.equal(preview.ok, false);87+ assert.equal(preview.errors[0].message, 'This pack needs a newer Miau.');88+ assert.match(preview.errors[0].detail, /manifest\.features\[0\]: unsupported feature/);89+ assert.deepEqual(preview.warnings, []);90+ assert.equal(service.listPacks().some(pack => pack.id === 'gift-pack'), false);91+});92+93+test('Gift-cloned previews retain required feature declarations through installation and sharing', () => {94+ const featured = structuredClone(example);95+ featured.manifest.features = ['automata'];96+ const service = createPackService({ baseUrl: 'https://fcbc.n.zip/' });97+ const preview = service.importPackText(`module.exports = ${JSON.stringify(featured)};`, 'featured.cjs');98+ assert.equal(preview.ok, true);99+ const installed = service.installPack(structuredClone(preview.pack));100+ assert.equal(installed.ok, true);101+ assert.deepEqual(installed.pack.features, ['automata']);102+ assert.deepEqual(service.catalog.getPack('gift-pack').manifest.features, ['automata']);103+ assert.match(service.packLink('gift-pack').url, /#pack=miau1\./);104+});105+58106 test('duplicate choices replace or fork with fresh identity', () => {59107 const service = createPackService();60108 assert.equal(service.installPack(example).ok, true);