Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
99d5b45
Merge pull request #287 from pwang7/MUSE-1194
supnate Mar 19, 2026
0fe38b8
Merge pull request #290 from pwang7/MUSE-1223
supnate Mar 30, 2026
4550188
Merge pull request #293 from pwang7/MUSE-1243
supnate Apr 23, 2026
9bdc0b4
Merge pull request #294 from pwang7/MUSE-1252
supnate Apr 23, 2026
8877db9
Merge pull request #295 from pwang7/MUSE-1252
supnate Apr 23, 2026
cb682d4
Merge pull request #296 from pwang7/MUSE-1253
supnate Apr 23, 2026
708a39b
Merge pull request #297 from pwang7/MUSE-1252
supnate Apr 23, 2026
de40469
Merge pull request #298 from pwang7/MUSE-1244
supnate Apr 23, 2026
62557b7
Merge pull request #299 from pwang7/MUSE-1265
supnate May 6, 2026
41c6cea
Merge pull request #300 from pwang7/MUSE-1280
supnate May 12, 2026
2eb76eb
Merge pull request #301 from zhijgu/MUSE-1269
May 21, 2026
297c1c1
Merge pull request #302 from pwang7/MUSE-1312
supnate May 29, 2026
1cbcb34
Merge pull request #303 from pwang7/MUSE-1310
supnate Jun 5, 2026
6806a9c
Merge pull request #304 from pwang7/MUSE-1309
supnate Jun 5, 2026
e68034e
Merge pull request #305 from pwang7/MUSE-1331
supnate Jun 8, 2026
7fc22b5
Merge pull request #306 from pwang7/MUSE-1308
supnate Jun 9, 2026
c50b2ae
Merge pull request #307 from pwang7/MUSE-1308
supnate Jun 9, 2026
413072f
Merge pull request #308 from pwang7/MUSE-1333
supnate Jun 11, 2026
ba46770
Merge pull request #309 from pwang7/MUSE-1336
supnate Jun 11, 2026
f563116
Merge pull request #317 from pwang7/MUSE-1290
supnate Jul 15, 2026
4a315c9
Merge pull request #319 from gling/MUSE-1359
supnate Jul 15, 2026
db46058
Merge pull request #320 from gling/upgrade-antd-v6
glin7976 Jul 30, 2026
42bc75e
Merge pull request #321 from gling/MUSE-1418
supnate Aug 14, 2026
096a33a
Merge pull request #322 from pwang7/MUSE-1425
supnate Aug 24, 2026
49491e4
Prevent activeMatch from being forwarded to menu DOM elements
eugef Aug 24, 2026
d216872
Merge branch 'main' into MUSE-1428
glin7976 Aug 25, 2026
dc6ed89
Merge pull request #324 from gling/MUSE-1428
supnate Aug 25, 2026
e8d179f
Merge pull request #323 from efidelin/main
supnate Aug 27, 2026
baf3ef3
Merge remote-tracking branch 'corp-git/main' into sdk-upgrade-dev
supnate Aug 27, 2026
47a6d9b
Merge pull request #153 from supnate/sdk-upgrade-dev
supnate Aug 27, 2026
699cd6b
add new plugin template
supnate Sep 7, 2026
dd11e35
Merge pull request #154 from supnate/sdk-upgrade-dev
supnate Sep 7, 2026
ec7c48a
Prevent DOM XSS in muse-boot-default error UI and restrict forcePlugins.
glin7976 Sep 7, 2026
6e57989
Merge pull request #155 from glin7976/fix/boot-force-plugins-xss
supnate Sep 8, 2026
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
11 changes: 11 additions & 0 deletions plugin-templates/msp2606/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/
build/
coverage/
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
1 change: 1 addition & 0 deletions plugin-templates/msp2606/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
shamefully-hoist=true
12 changes: 12 additions & 0 deletions plugin-templates/msp2606/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# <mypluginname>

Muse plugin created with the `msp2606` SDK preset.

## Development

```bash
pnpm install
pnpm start
```

Update `muse.devConfig` in `package.json` to select the Muse application and environment used for local development.
44 changes: 44 additions & 0 deletions plugin-templates/msp2606/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "<mypluginname>",
"version": "1.0.0",
"private": true,
"type": "module",
"muse": {
"msp": "msp2606",
"type": "normal",
"devConfig": {
"app": "myapp",
"env": "staging"
}
},
"scripts": {
"start": "vite",
"build": "vite build",
"build:dev": "cross-env NODE_ENV=development vite build --mode development",
"build:test": "cross-env MUSE_TEST_BUILD=true NODE_ENV=production vite build --mode e2e-test"
},
"dependencies": {
"js-plugin": "1.1.0"
},
"devDependencies": {
"@ebay/muse-core": "^2.0.0",
"@ebay/muse-lib-antd": "^2.0.0",
"@ebay/muse-lib-react": "^2.0.0",
"@ebay/muse-vite-plugin": "^2.0.0",
"@vitejs/plugin-react": "^6.0.1",
"cross-env": "^10.1.0",
"vite": "^8.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
1 change: 1 addition & 0 deletions plugin-templates/msp2606/src/ext/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
11 changes: 11 additions & 0 deletions plugin-templates/msp2606/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import plugin from 'js-plugin';
import * as ext from './ext';
import reducer from './reducer';
import route from './route';

plugin.register({
...ext,
name: '<mypluginname>',
route,
reducer,
});
3 changes: 3 additions & 0 deletions plugin-templates/msp2606/src/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const reducer = (state = {}) => state;

export default reducer;
5 changes: 5 additions & 0 deletions plugin-templates/msp2606/src/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const route = {
childRoutes: [],
};

export default route;
7 changes: 7 additions & 0 deletions plugin-templates/msp2606/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import museVitePlugin from '@ebay/muse-vite-plugin';

export default defineConfig(() => ({
plugins: [react(), museVitePlugin()],
}));
6 changes: 3 additions & 3 deletions ui-plugins/muse-boot-default/MUSE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,10 @@ const MyComponent = () => {

### Example 5: Force Loading Specific Plugin Versions

For debugging or testing, use the `forcePlugins` query parameter:
For debugging, local development, or Muse e2e, use the `forcePlugins` query parameter. It is applied when `isDev`, `isLocal`, or `isE2eTest` is true. Plugin names, types (`boot` / `init` / `lib` / `normal`), and versions must be valid; other values are ignored.

```
https://myapp.com?forcePlugins=@ebay/my-plugin@1.2.3;other-plugin@2.0.0
https://myapp.com?forcePlugins=@ebay/my-plugin@1.2.3;other-plugin!normal@2.0.0
```

This overrides the deployed plugin versions with specific versions.
Expand All @@ -394,7 +394,7 @@ This overrides the deployed plugin versions with specific versions.
- Handles plugin loading order: boot → init → lib → normal
- Init plugins can use `initEntries` or `waitFor` to perform async initialization
- Lib plugins with `isAppEntry: true` register app entry functions
- The `forcePlugins` query parameter is useful for debugging specific plugin versions
- The `forcePlugins` query parameter is useful for debugging specific plugin versions in local/dev or Muse e2e
- Service worker registration is automatic but can be customized
- All plugin loading happens in parallel for performance (within each type group)
- The loading UI provides user feedback during the bootstrap process
2 changes: 1 addition & 1 deletion ui-plugins/muse-boot-default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"url": "https://github.com/ebay/Muse",
"directory": "ui-plugins/muse-boot-default"
},
"version": "2.0.2",
"version": "2.0.3",
"main": "index.js",
"license": "MIT",
"type": "module",
Expand Down
50 changes: 8 additions & 42 deletions ui-plugins/muse-boot-default/src/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import loading from './loading';
import error from './error';
import registerSw from './registerSw';
import { loadInParallel, loadInSerial, getPluginId } from './utils';
import { applyForcePlugins, isForcePluginsAllowed } from './forcePlugins';
import msgEngine from './msgEngine';
import './urlListener';
import './style.css';
Expand Down Expand Up @@ -105,51 +106,16 @@ async function start() {
);
}

/* Handle forcePlugins query parameter */
/* Handle forcePlugins query parameter (local/dev or Muse e2e) */
const searchParams = new URLSearchParams(window.location.search);
const forcePluginStr = searchParams.get('forcePlugins');
if (forcePluginStr) {
const forcePluginById = forcePluginStr
.split(';')
.filter(Boolean)
.reduce((p, c) => {
const separator = '@';
const limit = 2;
let prefix = '';
if (c.startsWith('@') && c[0] === separator) {
// Starts with @, means it's a scoped plugin
c = c.substring(1);
prefix = '@';
}
const arr = c.split(separator, limit);
if (arr.length === limit) {
const [name, type] = arr[0].split('!');
p[`${prefix}${name}`] = {
version: arr[1],
type: type,
};
}
return p;
}, {});
// Update or remove plugins from the list based on forcePlugins
plugins = plugins
.map((p) => {
if (!forcePluginById[p.name]) return p;
const newPlugin = { ...p, version: forcePluginById[p.name].version };
delete forcePluginById[p.name];
return newPlugin;
})
.filter((p) => p.version !== 'null');

// Need to get the type of plugin from muse registry directly.
for (const p in forcePluginById) {
if (forcePluginById[p].version !== 'null') {
plugins.push({
name: p,
type: forcePluginById[p].type,
version: forcePluginById[p].version,
});
}
if (isForcePluginsAllowed(mg)) {
plugins = applyForcePlugins(plugins, forcePluginStr);
} else {
console.warn(
'[muse-boot] forcePlugins is ignored outside local/dev and Muse e2e.',
);
}
}

Expand Down
82 changes: 82 additions & 0 deletions ui-plugins/muse-boot-default/src/boot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ vi.mock('./style.css', () => ({}));
import loading from './loading.js';
import error from './error.js';
import msgEngine from './msgEngine.js';
import { loadInParallel } from './utils.js';
import { bootstrap } from './boot.js';

function makeMuseGlobal(overrides = {}) {
Expand Down Expand Up @@ -261,6 +262,87 @@ describe('bootstrap', () => {

expect(loader).toHaveBeenCalledTimes(1);
});

it('ignores forcePlugins in deployed environments', async () => {
const prevLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: { search: '?forcePlugins=poc!init@1.0.0' },
});
window.MUSE_GLOBAL.appEntries = [{ name: 'main', func: vi.fn().mockResolvedValue(undefined) }];
vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.spyOn(document.head, 'appendChild').mockImplementation((el) => {
if (el.tagName === 'SCRIPT' && el.textContent?.includes('__onMusePluginsLoaded')) {
window.MUSE_GLOBAL.__onMusePluginsLoaded?.();
}
});

try {
await runBootstrap();
const initPlugins = loadInParallel.mock.calls[0]?.[0] || [];
expect(initPlugins.some((p) => p.name === 'poc')).toBe(false);
} finally {
Object.defineProperty(window, 'location', { configurable: true, value: prevLocation });
}
});

it('applies valid forcePlugins in dev', async () => {
const prevLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: { search: '?forcePlugins=extra!init@1.0.1' },
});
window.MUSE_GLOBAL = makeMuseGlobal({
isDev: true,
appEntries: [{ name: 'main', func: vi.fn().mockResolvedValue(undefined) }],
});
vi.spyOn(document.head, 'appendChild').mockImplementation((el) => {
if (el.tagName === 'SCRIPT' && el.textContent?.includes('__onMusePluginsLoaded')) {
window.MUSE_GLOBAL.__onMusePluginsLoaded?.();
}
});

try {
await runBootstrap();
const initPlugins = loadInParallel.mock.calls[0][0];
expect(initPlugins).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'extra', type: 'init', version: '1.0.1' }),
]),
);
} finally {
Object.defineProperty(window, 'location', { configurable: true, value: prevLocation });
}
});

it('applies valid forcePlugins when isE2eTest is true', async () => {
const prevLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: { search: '?forcePlugins=extra!init@1.0.1' },
});
window.MUSE_GLOBAL = makeMuseGlobal({
isE2eTest: true,
appEntries: [{ name: 'main', func: vi.fn().mockResolvedValue(undefined) }],
});
vi.spyOn(document.head, 'appendChild').mockImplementation((el) => {
if (el.tagName === 'SCRIPT' && el.textContent?.includes('__onMusePluginsLoaded')) {
window.MUSE_GLOBAL.__onMusePluginsLoaded?.();
}
});

try {
await runBootstrap();
const initPlugins = loadInParallel.mock.calls[0][0];
expect(initPlugins).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'extra', type: 'init', version: '1.0.1' }),
]),
);
} finally {
Object.defineProperty(window, 'location', { configurable: true, value: prevLocation });
}
});
});

describe('bootstrap - appConfig.entry selection', () => {
Expand Down
61 changes: 45 additions & 16 deletions ui-plugins/muse-boot-default/src/error.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
function isSafeHref(href) {
if (!href || href === '#') return true;
try {
const url = new URL(href, window.location.origin);
return url.protocol === 'https:' || url.protocol === 'http:';
} catch {
return false;
}
}

const error = {
errors: [],
init() {
const errorDiv = document.createElement('div');
errorDiv.innerHTML = ``;
errorDiv.id = 'muse-error-node';
document.body.appendChild(errorDiv);
this.mountNode = errorDiv;
Expand All @@ -14,22 +23,42 @@ const error = {
},
update() {
if (!this.mountNode) this.init();
this.mountNode.replaceChildren();

const inner = document.createElement('div');
inner.className = 'muse-error-node-inner';

const heading = document.createElement('h4');
heading.textContent = 'Failed to load:';
inner.appendChild(heading);

if (this.errors.length === 1) {
const div = document.createElement('div');
div.textContent = String(this.errors[0] ?? '');
inner.appendChild(div);
} else {
const ul = document.createElement('ul');
this.errors.forEach((err) => {
const li = document.createElement('li');
li.textContent = String(err ?? '');
ul.appendChild(li);
});
inner.appendChild(ul);
}

const note = document.createElement('p');
note.appendChild(
document.createTextNode('* Unexpected error happened, please refresh to retry or '),
);
const supportLink = window.MUSE_GLOBAL?.appConfig?.supportLink || '#';
const contact = document.createElement('a');
contact.textContent = 'contact support';
contact.href = isSafeHref(supportLink) ? supportLink : '#';
note.appendChild(contact);
note.appendChild(document.createTextNode('.'));
inner.appendChild(note);

const content =
this.errors.length === 1
? `<div>${this.errors[0]}</div>`
: `<ul>
${this.errors.map((err) => '<li>' + err + '</li>').join('')}
</ul>`;
this.mountNode.innerHTML = `
<div class="muse-error-node-inner">
<h4>Failed to load:</h4>
${content}
<p>* Unexpected error happened, please refresh to retry or <a href="${
window.MUSE_GLOBAL.appConfig?.supportLink || '#'
}">contact support</a>.</p>
</div>
`;
this.mountNode.appendChild(inner);
},
};

Expand Down
14 changes: 14 additions & 0 deletions ui-plugins/muse-boot-default/src/error.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,19 @@ describe('error', () => {
error.update();
expect(error.mountNode.innerHTML).toContain('href="#"');
});

it('renders untrusted error text without executing HTML', () => {
error.showMessage('<img src=x onerror="alert(1)">');
expect(error.mountNode.querySelector('img')).toBeNull();
expect(error.mountNode.textContent).toContain('<img src=x onerror="alert(1)">');
});

it('rejects javascript: support links', () => {
setupMuseGlobal({ appConfig: { supportLink: 'javascript:alert(1)' } });
error.errors = ['oops'];
error.init();
error.update();
expect(error.mountNode.querySelector('a').getAttribute('href')).toBe('#');
});
});
});
Loading
Loading