Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

See [https://github.com/ice-lab/icestark/releases](https://github.com/ice-lab/icestark/releases) for what has changed in each version of icestark.

# 2.8.5

- [feat] support `freezeRuntime` option to freeze the runtime library.

# 2.8.4

- [fix] automatically switch to "fetch" mode when runtime is set.
Expand Down
98 changes: 98 additions & 0 deletions packages/icestark/__tests__/handleAssets.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
getUrlAssets,
isAbsoluteUrl,
replaceImportIdentifier,
fetchScripts,
} from '../src/util/handleAssets';
import { setCache } from '../src/util/cache';
import globalConfiguration from '../src/util/globalConfiguration';

const originalLocation = window.location;

Expand Down Expand Up @@ -755,3 +757,99 @@ describe('replaceImportIdentifier', () => {
expect(target).toContain('window.__vite_plugin_react_preamble_installed__ = true')
})
});

describe('freezeRuntime', () => {
beforeEach(() => {
// Clean up global state
delete (window as any).__icestark_locked_globals;
delete (window as any)['[email protected]'];
delete (window as any)['[email protected]_value'];

fetchMock.mockReset();

fetchMock.mockResolvedValue({
text: () => Promise.resolve('console.log("React loaded");')
});
});

afterEach(() => {
fetchMock.mockClear();
});

test('should freeze runtime libraries when freezeRuntime is enabled', async () => {
// Mock enabling freeze functionality
const originalConfig = globalConfiguration.freezeRuntime;
globalConfiguration.freezeRuntime = true;

fetchMock.mockResolvedValue({
text: () => Promise.resolve('console.log("React loaded");')
});

const jsList = [
{
type: AssetTypeEnum.RUNTIME,
library: 'React',
version: '17.0.0',
content: 'https://unpkg.com/[email protected]/umd/react.production.min.js',
loaded: false,
},
];

const scriptTexts = await fetchScripts(jsList, fetchMock);

const combinedScript = scriptTexts.join('');
expect(combinedScript).toContain('Object.defineProperty(window, \'[email protected]\'');
globalConfiguration.freezeRuntime = originalConfig;
});

test('should not freeze runtime libraries when freezeRuntime is disabled', async () => {
// Ensure freeze functionality is disabled
const originalConfig = globalConfiguration.freezeRuntime;
globalConfiguration.freezeRuntime = false;

fetchMock.mockResolvedValue({
text: () => Promise.resolve('console.log("React loaded");')
});

const jsList = [
{
type: AssetTypeEnum.RUNTIME,
library: 'React',
version: '17.0.0',
content: 'https://unpkg.com/[email protected]/umd/react.production.min.js',
loaded: false,
},
];

const scriptTexts = await fetchScripts(jsList, fetchMock);

const combinedScript = scriptTexts.join('');
expect(combinedScript).not.toContain('Object.defineProperty(window, \'[email protected]\'');

globalConfiguration.freezeRuntime = originalConfig;
});

test('should prevent modification of frozen runtime libraries', () => {
Object.defineProperty(window, '[email protected]', {
get: function() { return (this as any)['[email protected]_value']; },
set: function(value) {
if ((this as any)['[email protected]_value'] === undefined) {
(this as any)['[email protected]_value'] = value;
}
// Ignore subsequent modifications
},
configurable: false,
enumerable: true
});

(window as any)['[email protected]'] = { version: '17.0.0' };
expect((window as any)['[email protected]']).toEqual({ version: '17.0.0' });

(window as any)['[email protected]'] = { version: '18.0.0' };
expect((window as any)['[email protected]']).toEqual({ version: '17.0.0' });

// Cleanup
delete (window as any)['[email protected]'];
delete (window as any)['[email protected]_value'];
});
});
2 changes: 1 addition & 1 deletion packages/icestark/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ice/stark",
"version": "2.8.4",
"version": "2.8.5",
"description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.",
"scripts": {
"build": "rm -rf lib && tsc",
Expand Down
5 changes: 4 additions & 1 deletion packages/icestark/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface AppRouterProps {
basename?: string;
fetch?: Fetch;
prefetch?: Prefetch;
freezeRuntime?: boolean;
}

interface AppRouterState {
Expand All @@ -58,6 +59,7 @@ export default class AppRouter extends React.Component<React.PropsWithChildren<A
basename: '',
fetch: window.fetch,
prefetch: false,
freezeRuntime: false,
};

private unmounted = false;
Expand Down Expand Up @@ -90,7 +92,7 @@ export default class AppRouter extends React.Component<React.PropsWithChildren<A
* status `started` used to make sure parent's `componentDidMount` to be invoked eariler then child's,
* for mounting child component needs global configuration be settled.
*/
const { shouldAssetsRemove, onAppEnter, onAppLeave, fetch, basename } = this.props;
const { shouldAssetsRemove, onAppEnter, onAppLeave, fetch, basename, freezeRuntime } = this.props;
start({
onAppLeave,
onAppEnter,
Expand All @@ -100,6 +102,7 @@ export default class AppRouter extends React.Component<React.PropsWithChildren<A
reroute: this.handleRouteChange,
fetch,
basename,
freezeRuntime,
...(shouldAssetsRemove ? { shouldAssetsRemove } : {}),
});

Expand Down
7 changes: 7 additions & 0 deletions packages/icestark/src/util/globalConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export interface StartConfiguration {
fetch?: Fetch;
prefetch?: Prefetch;
basename?: string;
/**
* Whether to freeze the runtime library to prevent accidental modifications.
* When set to true, the versioned runtime library (e.g., window['[email protected]'])
* will not be modifiable after its initial assignment.
*/
freezeRuntime?: boolean;
}

const globalConfiguration: StartConfiguration = {
Expand All @@ -42,6 +48,7 @@ const globalConfiguration: StartConfiguration = {
fetch: window.fetch,
prefetch: false,
basename: '',
freezeRuntime: false,
};

export default globalConfiguration;
Expand Down
46 changes: 44 additions & 2 deletions packages/icestark/src/util/handleAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { toArray, isDev, formatMessage, builtInScriptAttributesMap, looseBoolean
import { formatErrMessage, ErrorCode } from './error';
import type { Fetch } from './globalConfiguration';
import type { ScriptAttributes } from '../apps';
import globalConfiguration from './globalConfiguration';

const COMMENT_REGEX = /<!--.*?-->/g;
const BASE_LOOSE_REGEX = /<base\s[^>]*href=['"]?([^'"]*)['"]?[^>]*>/;
Expand All @@ -20,6 +21,36 @@ const cachedProcessedContent: object = {};

const defaultFetch = window?.fetch.bind(window);

const GLOBAL_LOCKED_MAP = new Map<string, boolean>();

/**
* Generate runtime library freeze code.
* Used to prevent micro apps from reloading the same version of a runtime library.
* @param versionedLibKey The versioned library key, formatted as "library@version"
* @returns The generated freeze code string
*/
function generateRuntimeLockCode(versionedLibKey: string): string {
const valueKey = `__${versionedLibKey}_value`;
const warningMessage = `${versionedLibKey} is locked, cannot be modified`;

return `
Object.defineProperty(window, '${versionedLibKey}', {
get: function() {
return this['${valueKey}'];
},
set: function(value) {
if (this['${valueKey}'] === undefined) {
this['${valueKey}'] = value;
} else {
console.warn('${warningMessage}');
}
},
configurable: false,
enumerable: true
});
`;
}

export enum AssetTypeEnum {
INLINE = 'inline',
EXTERNAL = 'external',
Expand Down Expand Up @@ -319,10 +350,21 @@ export async function fetchScripts(jsList: Asset[], fetch: Fetch = defaultFetch)
const { library, version } = asset;
const globalLib = `window['${library}']`;
const backupLib = `window['__${library}__']`;
const versionedLib = `window['${library}@${version}']`;
const versionedLibKey = `${library}@${version}`;
const versionedLib = `window['${versionedLibKey}']`;
const backupCode = `if (${globalLib}) {${backupLib} = ${globalLib};}\n`;
const restoreCode = `if (${backupLib}) {${globalLib} = ${backupLib};${backupLib} = undefined;}\n`;
jsBeforeRuntime = `${jsBeforeRuntime}${backupCode}${asset.loaded ? `${globalLib} = ${versionedLib};` : ''}`;

let lockCode = '';
if (
globalConfiguration.freezeRuntime &&
!GLOBAL_LOCKED_MAP.has(versionedLibKey) &&
typeof Object.defineProperty === 'function'
) {
lockCode = generateRuntimeLockCode(versionedLibKey);
GLOBAL_LOCKED_MAP.set(versionedLibKey, true);
}
jsBeforeRuntime = `${jsBeforeRuntime}${backupCode}${asset.loaded ? `${globalLib} = ${versionedLib};` : ''}${lockCode}`;
jsAfterRuntime = `${jsAfterRuntime}${asset.loaded ? '' : `${versionedLib} = ${globalLib};`}${restoreCode}`;
}
});
Expand Down
Loading