- commit
- 6197c58c3ffb8ec29a476fbdba21a20765fbb29e
- parent
- d2be209052904698912c6c0192745b4e873d6866
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 14 | ++++++--- |
| M | src/ | 6 | +++- |
| M | tests/ | 48 | ++++++++++++++++++++++++++++++ |
@@ -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 = {}) {@@ -54,7 +54,11 @@ function normalizePack(pack) {
5454 copy.palette = palette;5555 }5656 const errors = validateMod(copy);57- if (errors.length) throw new TypeError(`Invalid shared pack:\n- ${errors.join('\n- ')}`);57+ if (errors.length) {58+ const failure = new TypeError(`Invalid shared pack:\n- ${errors.join('\n- ')}`);59+ failure.errors = errors;60+ throw failure;61+ }5862 return copy;5963 }6064 @@ -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);