diff --git a/.eslintrc.js b/.eslintrc.js index 5607f550..f5239313 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,7 @@ module.exports = deepmerge(tslint, { '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/interface-name-prefix': 1, + 'no-unused-expressions': 'off', }, settings: { react: { diff --git a/CHANGELOG.md b/CHANGELOG.md index 0844add4..65fcbd5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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.4.0 + +- [feat] support appending extra attributes for scripts when using `loadScriptMode = script`. ([#276](https://github.com/ice-lab/icestark/issues/276)) +- [fix] unexpectable sandbox's cleaning up when load modules. ([#293](https://github.com/ice-lab/icestark/issues/293)) +- [fix] missing `ErrorComponent` causes React rendering's error. ([#312](https://github.com/ice-lab/icestark/issues/312)) + ## 2.3.2 - [refact] compatible with sandbox spell error. diff --git a/package.json b/package.json index d7b2b694..d99ee737 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ice/stark", - "version": "2.3.2", + "version": "2.4.0", "description": "Icestark is a JavaScript library for multiple projects, Ice workbench solution.", "scripts": { "install:deps": "rm -rf node_modules && rm -rf ./packages/*/node_modules && yarn install && lerna exec -- npm install", diff --git a/packages/icestark-data/CHANGELOG.md b/packages/icestark-data/CHANGELOG.md new file mode 100644 index 00000000..9e820021 --- /dev/null +++ b/packages/icestark-data/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.3 + +- [feat] support `Symbol` key for index. ([#298](https://github.com/ice-lab/icestark/issues/298)) \ No newline at end of file diff --git a/packages/icestark-data/package.json b/packages/icestark-data/package.json index 2415a2fb..fdd0cad6 100644 --- a/packages/icestark-data/package.json +++ b/packages/icestark-data/package.json @@ -1,6 +1,6 @@ { "name": "@ice/stark-data", - "version": "0.1.2", + "version": "0.1.3", "description": "icestark-data is a JavaScript library for icestark, used for communication.", "scripts": { "build": "rm -rf lib && tsc", diff --git a/packages/icestark-data/src/event.ts b/packages/icestark-data/src/event.ts index c3e7d198..e41878bd 100644 --- a/packages/icestark-data/src/event.ts +++ b/packages/icestark-data/src/event.ts @@ -5,11 +5,13 @@ import { setCache, getCache } from './cache'; const eventNameSpace = 'event'; +type StringSymbolUnion = string | symbol; + interface Hooks { - emit(key: string, value: any): void; - on(key: string, callback: (value: any) => void): void; - off(key: string, callback?: (value: any) => void): void; - has(key: string): boolean; + emit(key: StringSymbolUnion, value: any): void; + on(key: StringSymbolUnion, callback: (value: any) => void): void; + off(key: StringSymbolUnion, callback?: (value: any) => void): void; + has(key: StringSymbolUnion): boolean; } class Event implements Hooks { @@ -19,11 +21,11 @@ class Event implements Hooks { this.eventEmitter = {}; } - emit(key: string, ...args) { + emit(key: StringSymbolUnion, ...args) { const keyEmitter = this.eventEmitter[key]; if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) { - warn(`event.emit: no callback is called for ${key}`); + warn(`event.emit: no callback is called for ${String(key)}`); return; } @@ -32,12 +34,11 @@ class Event implements Hooks { }); } - on(key: string, callback: (value: any) => void) { - if (typeof key !== 'string') { - warn('event.on: key should be string'); + on(key: StringSymbolUnion, callback: (value: any) => void) { + if (typeof key !== 'string' && typeof key !== 'symbol') { + warn('event.on: key should be string / symbol'); return; } - if (callback === undefined || typeof callback !== 'function') { warn('event.on: callback is required, should be function'); return; @@ -50,14 +51,15 @@ class Event implements Hooks { this.eventEmitter[key].push(callback); } - off(key: string, callback?: (value: any) => void) { - if (typeof key !== 'string') { - warn('event.off: key should be string'); + off(key: StringSymbolUnion, callback?: (value: any) => void) { + if (typeof key !== 'string' && typeof key !== 'symbol') { + warn('event.off: key should be string / symbol'); return; + } if (!isArray(this.eventEmitter[key])) { - warn(`event.off: ${key} has no callback`); + warn(`event.off: ${String(key)} has no callback`); return; } @@ -69,7 +71,7 @@ class Event implements Hooks { this.eventEmitter[key] = this.eventEmitter[key].filter(cb => cb !== callback); } - has(key: string) { + has(key: StringSymbolUnion) { const keyEmitter = this.eventEmitter[key]; return isArray(keyEmitter) && keyEmitter.length > 0; } diff --git a/packages/icestark-data/src/store.ts b/packages/icestark-data/src/store.ts index 5da47e3e..b69c8cd0 100644 --- a/packages/icestark-data/src/store.ts +++ b/packages/icestark-data/src/store.ts @@ -6,15 +6,18 @@ import { setCache, getCache } from './cache'; const storeNameSpace = 'store'; +type StringSymbolUnion = string | symbol; + +// eslint-disable-next-line @typescript-eslint/interface-name-prefix interface IO { - set(key: any, value?: any): void; - get(key?: string): void; + set(key: string | symbol | object, value?: any): void; + get(key?: StringSymbolUnion): void; } interface Hooks { - on(key: string, callback: (value: any) => void, force?: boolean): void; - off(key: string, callback?: (value: any) => void): void; - has(key: string): boolean; + on(key: StringSymbolUnion, callback: (value: any) => void, force?: boolean): void; + off(key: StringSymbolUnion, callback?: (value: any) => void): void; + has(key: StringSymbolUnion): boolean; } class Store implements IO, Hooks { @@ -27,16 +30,16 @@ class Store implements IO, Hooks { this.storeEmitter = {}; } - _getValue(key: string) { + _getValue(key: StringSymbolUnion) { return this.store[key]; } - _setValue(key: string, value: any) { + _setValue(key: StringSymbolUnion, value: any) { this.store[key] = value; this._emit(key); } - _emit(key) { + _emit(key: StringSymbolUnion) { const keyEmitter = this.storeEmitter[key]; if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) { @@ -49,39 +52,41 @@ class Store implements IO, Hooks { }); } - get(key?: string) { + get(key?: StringSymbolUnion) { if (key === undefined) { return this.store; } - if (typeof key !== 'string') { - warn(`store.get: key should be string`); + if (typeof key !== 'string' && typeof key !== 'symbol') { + warn(`store.get: key should be string / symbol`); return null; } return this._getValue(key); } - set(key: any, value?: any) { - if (typeof key !== 'string') { - if (!isObject(key)) { - warn('store.set: key should be string / object'); - return; - } + set(key: string | symbol | object, value?: T) { + if (typeof key !== 'string' + && typeof key !== 'symbol' + && !isObject(key)) { + warn('store.set: key should be string / symbol / object'); + return; + } + if (isObject(key)) { Object.keys(key).forEach(k => { const v = key[k]; this._setValue(k, v); }); + } else { + this._setValue(key as StringSymbolUnion, value); } - - this._setValue(key, value); } - on(key: string, callback: (value: any) => void, force?: boolean) { - if (typeof key !== 'string') { - warn('store.on: key should be string'); + on(key: StringSymbolUnion, callback: (value: any) => void, force?: boolean) { + if (typeof key !== 'string' && typeof key !== 'symbol') { + warn('store.on: key should be string / symbol'); return; } @@ -101,14 +106,14 @@ class Store implements IO, Hooks { } } - off(key: string, callback?: (value: any) => void) { - if (typeof key !== 'string') { - warn('store.off: key should be string'); + off(key: StringSymbolUnion, callback?: (value: any) => void) { + if (typeof key !== 'string' && typeof key !== 'symbol') { + warn('store.off: key should be string / symbol'); return; } if (!isArray(this.storeEmitter[key])) { - warn(`store.off: ${key} has no callback`); + warn(`store.off: ${String(key)} has no callback`); return; } @@ -120,7 +125,7 @@ class Store implements IO, Hooks { this.storeEmitter[key] = this.storeEmitter[key].filter(cb => cb !== callback); } - has(key: string) { + has(key: StringSymbolUnion) { const keyEmitter = this.storeEmitter[key]; return isArray(keyEmitter) && keyEmitter.length > 0; } diff --git a/packages/icestark-data/tests/event.spec.tsx b/packages/icestark-data/tests/event.spec.tsx index 0217a9e0..074c7122 100644 --- a/packages/icestark-data/tests/event.spec.tsx +++ b/packages/icestark-data/tests/event.spec.tsx @@ -13,7 +13,7 @@ describe('event', () => { }; event.on([]); - expect(warnMockFn).toBeCalledWith('event.on: key should be string'); + expect(warnMockFn).toBeCalledWith('event.on: key should be string / symbol'); event.on('testOn'); expect(warnMockFn).toBeCalledWith('event.on: callback is required, should be function'); @@ -33,7 +33,7 @@ describe('event', () => { }; event.off([]); - expect(warnMockFn).toBeCalledWith('event.off: key should be string'); + expect(warnMockFn).toBeCalledWith('event.off: key should be string / symbol'); event.off('testOff'); expect(warnMockFn).toBeCalledWith('event.off: testOff has no callback'); diff --git a/packages/icestark-data/tests/store.spec.tsx b/packages/icestark-data/tests/store.spec.tsx index 14f51415..ee33c45a 100644 --- a/packages/icestark-data/tests/store.spec.tsx +++ b/packages/icestark-data/tests/store.spec.tsx @@ -15,7 +15,7 @@ describe('store', () => { expect(store.get()).toStrictEqual({}); store.get([]); - expect(warnMockFn).toBeCalledWith('store.get: key should be string'); + expect(warnMockFn).toBeCalledWith('store.get: key should be string / symbol'); expect(store.get('test')).toBeUndefined(); }); @@ -27,7 +27,7 @@ describe('store', () => { }; store.set([]); - expect(warnMockFn).toBeCalledWith('store.set: key should be string / object'); + expect(warnMockFn).toBeCalledWith('store.set: key should be string / symbol / object'); const testArray = []; const testObj = {}; @@ -50,7 +50,7 @@ describe('store', () => { }; store.on([]); - expect(warnMockFn).toBeCalledWith('store.on: key should be string'); + expect(warnMockFn).toBeCalledWith('store.on: key should be string / symbol'); store.on('testOn'); expect(warnMockFn).toBeCalledWith('store.on: callback is required, should be function'); @@ -75,7 +75,7 @@ describe('store', () => { }; store.off([]); - expect(warnMockFn).toBeCalledWith('store.off: key should be string'); + expect(warnMockFn).toBeCalledWith('store.off: key should be string / symbol'); store.off('testOff'); expect(warnMockFn).toBeCalledWith('store.off: testOff has no callback'); diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 15850587..8605cdb8 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -55,14 +55,14 @@ export default class AppRouter extends React.Component {}, // eslint-disable-next-line react/jsx-filename-extension - ErrorComponent: ({ err }) =>
{ err || 'Error' }
, + ErrorComponent: ({ err }: { err: string | Error}) =>
{ typeof err === 'string' ? err : err?.message }
, LoadingComponent:
Loading...
, NotFoundComponent:
NotFound
, shouldAssetsRemove: () => true, diff --git a/src/apps.ts b/src/apps.ts index d4ed5231..32be4a4d 100644 --- a/src/apps.ts +++ b/src/apps.ts @@ -2,12 +2,22 @@ import Sandbox, { SandboxConstructor, SandboxProps } from '@ice/sandbox'; import * as isEmpty from 'lodash.isempty'; import { NOT_LOADED, NOT_MOUNTED, LOADING_ASSETS, UNMOUNTED, LOAD_ERROR, MOUNTED } from './util/constant'; import { matchActivePath, MatchOptions, PathData, PathOptions } from './util/matchPath'; -import { createSandbox, getUrlAssets, getEntryAssets, appendAssets, loadAndAppendCssAssets, emptyAssets, Assets } from './util/handleAssets'; +import { + createSandbox, + getUrlAssets, + getEntryAssets, + loadAndAppendCssAssets, + loadAndAppendJsAssets, + emptyAssets, + Assets, +} from './util/handleAssets'; import { setCache } from './util/cache'; import { loadBundle } from './util/loader'; import { globalConfiguration, StartConfiguration } from './start'; import { getLifecyleByLibrary, getLifecyleByRegister } from './util/getLifecycle'; +export type ScriptAttributes = string[] | ((url: string) => string[]); + interface ActiveFn { (url: string): boolean; } @@ -44,6 +54,10 @@ export interface BaseConfig extends PathOptions { props?: object; cached?: boolean; title?: string; + /** + * custom script attributes,only effective when scripts load by `` + */ + scriptAttributes?: ScriptAttributes; } interface LifeCycleFn { @@ -146,8 +160,8 @@ export async function loadAppModule(appConfig: AppConfig) { let lifecycle: ModuleLifeCycle = {}; onLoadingApp(appConfig); - const appSandbox = createSandbox(appConfig.sandbox); - const { url, container, entry, entryContent, name } = appConfig; + const appSandbox = createSandbox(appConfig.sandbox) as Sandbox; + const { url, container, entry, entryContent, name, scriptAttributes = [] } = appConfig; const appAssets = url ? getUrlAssets(url) : await getEntryAssets({ root: container, entry, @@ -167,7 +181,10 @@ export async function loadAppModule(appConfig: AppConfig) { await loadAndAppendCssAssets(appAssets); lifecycle = await loadBundle(appAssets.jsList, appSandbox); } else { - await appendAssets(appAssets, appSandbox, fetch); + await Promise.all([ + loadAndAppendCssAssets(appAssets), + loadAndAppendJsAssets(appAssets, { sandbox: appSandbox, fetch, scriptAttributes }), + ]); lifecycle = getLifecyleByLibrary() || @@ -180,8 +197,6 @@ export async function loadAppModule(appConfig: AppConfig) { onFinishLoading(appConfig); - // clear appSandbox - appSandbox?.clear(); return combineLifecyle(lifecycle, appConfig); } diff --git a/src/util/handleAssets.ts b/src/util/handleAssets.ts index 3936a7a2..ee386d76 100644 --- a/src/util/handleAssets.ts +++ b/src/util/handleAssets.ts @@ -1,8 +1,11 @@ +/* eslint-disable no-param-reassign */ import * as urlParse from 'url-parse'; import Sandbox, { SandboxProps, SandboxConstructor } from '@ice/sandbox'; import { PREFIX, DYNAMIC, STATIC, IS_CSS_REGEX } from './constant'; import { warn, error } from './message'; +import { toArray, isDev, formatMessage, builtInScriptAttributesMap, looseBoolean2Boolean } from './helpers'; import { Fetch, defaultFetch } from '../start'; +import type { ScriptAttributes } from '../apps'; const COMMENT_REGEX = //g; @@ -108,12 +111,77 @@ export function appendCSS( }); } +/** + * append custom attribute for element + */ +function setAttributeForScriptNode (element: HTMLScriptElement, { + id, + src, + scriptAttributes, +}: { id: string; src: string; scriptAttributes: ScriptAttributes }) { + /* + * stamped by icestark for recycle when needed. + */ + element.setAttribute(PREFIX, DYNAMIC); + element.id = id; + + + element.type = 'text/javascript'; + element.src = src; + + /* + * `async=false` is required to make sure all js resources execute sequentially. + */ + element.async = false; + + /* + * `type` is not allowed to set currently. + */ + const unableReachedAttributes = [PREFIX, 'id', 'type', 'src', 'async']; + + const attrs = typeof (scriptAttributes) === 'function' + ? scriptAttributes(src) + : scriptAttributes; + + if (!Array.isArray(attrs)) { + isDev && ( + console.warn(formatMessage('scriptAttributes should be Array or Function that returns Array.')) + ); + return; + } + + attrs.forEach(attr => { + const [attrKey, attrValue] = attr.split('='); + if (unableReachedAttributes.includes(attrKey)) { + (isDev ? console.warn : console.log)(formatMessage(`${attrKey} will be ignored by icestark.`)); + return; + } + + if (builtInScriptAttributesMap.has(attrKey)) { + /* + * built in attribute like ["crossorigin=use-credentials"]、["nomodule"] should be set as follow: + * script.crossOrigin = 'use-credentials'; + * script.noModule = true; + */ + const nonLooseBooleanAttrValue = looseBoolean2Boolean(attrValue); + element[builtInScriptAttributesMap.get(attrKey)] = nonLooseBooleanAttrValue === undefined || nonLooseBooleanAttrValue; + } else { + /* + * none built in attribute added by `setAttribute` + */ + element.setAttribute(attrKey, attrValue); + } + }); + +} + /** * Create script element (without inline) and append to root */ export function appendExternalScript( root: HTMLElement | ShadowRoot, asset: string | Asset, + scriptAttributes: ScriptAttributes, id: string, ): Promise { return new Promise((resolve, reject) => { @@ -121,18 +189,18 @@ export function appendExternalScript( if (!root) reject(new Error(`no root element for js assert: ${content || asset}`)); const element: HTMLScriptElement = document.createElement('script'); - // inline script + // inline script if (type && type === AssetTypeEnum.INLINE) { element.innerHTML = content; root.appendChild(element); resolve(); return; } - element.setAttribute(PREFIX, DYNAMIC); - element.id = id; - element.type = 'text/javascript'; - element.src = content || (asset as string); - element.async = false; + setAttributeForScriptNode(element, { + id, + src: content ||(asset as string), + scriptAttributes, + }); element.addEventListener( 'error', @@ -145,12 +213,11 @@ export function appendExternalScript( }); } -export function getUrlAssets(url: string | string[]) { - const urls = Array.isArray(url) ? url : [url]; +export function getUrlAssets(urls: string | string[]) { const jsList = []; const cssList = []; - urls.forEach(url => { + toArray(urls).forEach(url => { // //icestark.com/index.css -> true // //icestark.com/index.css?timeSamp=1575443657834 -> true // //icestark.com/index.css?query=test.js -> false @@ -194,11 +261,6 @@ export function fetchStyles(cssList: Asset[], fetch = defaultFetch) { ); } -export async function appendAssets(assets: Assets, sandbox?: Sandbox, fetch = defaultFetch) { - await loadAndAppendCssAssets(assets); - await loadAndAppendJsAssets(assets, sandbox, fetch); -} - export function parseUrl(entry: string): ParsedConfig { const { origin, pathname } = urlParse(entry); return { @@ -485,7 +547,17 @@ export async function loadAndAppendCssAssets(assets: Assets) { * @param {Sandbox} [sandbox] * @returns */ -export async function loadAndAppendJsAssets(assets: Assets, sandbox?: Sandbox, fetch = defaultFetch) { +export async function loadAndAppendJsAssets( + assets: Assets, + { + sandbox, + fetch = defaultFetch, + scriptAttributes = [], + }: { + sandbox?: Sandbox; + fetch?: Fetch; + scriptAttributes?: ScriptAttributes; + }) { const jsRoot: HTMLElement = document.getElementsByTagName('head')[0]; const { jsList } = assets; @@ -505,13 +577,13 @@ export async function loadAndAppendJsAssets(assets: Assets, sandbox?: Sandbox, f if (hasInlineScript) { // make sure js assets loaded in order if has inline scripts await jsList.reduce((chain, asset, index) => { - return chain.then(() => appendExternalScript(jsRoot, asset, `${PREFIX}-js-${index}`)); + return chain.then(() => appendExternalScript(jsRoot, asset, scriptAttributes, `${PREFIX}-js-${index}`)); }, Promise.resolve()); return; } await Promise.all( - jsList.map((asset, index) => appendExternalScript(jsRoot, asset, `${PREFIX}-js-${index}`)), + jsList.map((asset, index) => appendExternalScript(jsRoot, asset, scriptAttributes, `${PREFIX}-js-${index}`)), ); } diff --git a/src/util/helpers.ts b/src/util/helpers.ts new file mode 100644 index 00000000..a2d96d4b --- /dev/null +++ b/src/util/helpers.ts @@ -0,0 +1,26 @@ +export const isDev = process.env.NODE_ENV === 'development'; + +export const toArray = (any: T | T[]): T[] => { + return Array.isArray(any) ? any : [any]; +}; + +export const formatMessage = (msg: string): string => { + return `[icestark]: ${msg}`; +}; + + +/* + * all built in