mirror of https://github.com/alibaba/ice.git
feat: support swc options for plugin-rax-compat and onappear on rax-compat (#349)
* feat: support swc options for plugin-rax-compat * chore: example * test: fix test case * test: fix test case * test: fix test case * fix: outputdir * fix: onappear for no ref cases * fix: merge options * fix: appear polyfill * chore: remove appear polyfill dependency * fix: add swc helpers * fix: onappear on rax-compat * refactor: format code and not to use react.frowardRef Co-authored-by: ZeroLing <zhuoling.lcl@alibaba-inc.com>
This commit is contained in:
parent
064e290ec6
commit
48603cdf8f
|
|
@ -1,7 +1,6 @@
|
|||
import { defineConfig } from '@ice/app';
|
||||
import SpeedMeasurePlugin from 'speed-measure-webpack-plugin';
|
||||
import auth from '@ice/plugin-auth';
|
||||
import compatRax from '@ice/plugin-rax-compat';
|
||||
|
||||
export default defineConfig({
|
||||
publicPath: '/',
|
||||
|
|
@ -22,6 +21,6 @@ export default defineConfig({
|
|||
return webpackConfig;
|
||||
},
|
||||
dropLogLevel: 'warn',
|
||||
plugins: [auth(), compatRax()],
|
||||
plugins: [auth()],
|
||||
eslint: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,11 +14,6 @@
|
|||
"@ice/plugin-rax-compat": "workspace:*",
|
||||
"@ice/runtime": "workspace:*",
|
||||
"ahooks": "^3.3.8",
|
||||
"rax": "^1.2.2",
|
||||
"rax-image": "^2.4.1",
|
||||
"rax-is-valid-element": "^1.0.0",
|
||||
"rax-text": "^2.2.0",
|
||||
"rax-view": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
chrome 55
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from '@ice/app';
|
||||
import compatRax from '@ice/plugin-rax-compat';
|
||||
|
||||
export default defineConfig({
|
||||
ssr: false,
|
||||
ssg: false,
|
||||
publicPath: '/',
|
||||
plugins: [compatRax()],
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "rax-project",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "ice start",
|
||||
"build": "ice build"
|
||||
},
|
||||
"description": "",
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ice/app": "workspace:*",
|
||||
"@ice/plugin-rax-compat": "workspace:*",
|
||||
"@ice/runtime": "workspace:*",
|
||||
"rax": "^1.2.2",
|
||||
"rax-image": "^2.4.1",
|
||||
"rax-is-valid-element": "^1.0.0",
|
||||
"rax-text": "^2.2.0",
|
||||
"rax-view": "^2.3.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"browserslist": "^4.19.3",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"webpack": "^5.73.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -0,0 +1,7 @@
|
|||
import { defineAppConfig } from 'ice';
|
||||
|
||||
export default defineAppConfig({
|
||||
app: {
|
||||
rootId: 'app',
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Meta, Title, Links, Main, Scripts } from 'ice';
|
||||
|
||||
function Document() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="description" content="ICE 3.0 Demo" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover" />
|
||||
<Meta />
|
||||
<Title />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Main />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default Document;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.title {
|
||||
color: red;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
.data {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.homeContainer {
|
||||
align-items: center;
|
||||
margin-top: 200rpx;
|
||||
}
|
||||
|
||||
.homeTitle {
|
||||
font-size: 45rpx;
|
||||
font-weight: bold;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.homeInfo {
|
||||
font-size: 36rpx;
|
||||
margin: 8rpx 0;
|
||||
color: #555;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
declare module '*.module.less' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"compileOnSave": false,
|
||||
"buildOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"module": "esnext",
|
||||
"target": "es6",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["es6", "dom"],
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"rootDir": "./",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": false,
|
||||
"importHelpers": true,
|
||||
"strictNullChecks": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"ice": [".ice"]
|
||||
}
|
||||
},
|
||||
"include": ["src", ".ice", "ice.config.*"],
|
||||
"exclude": ["node_modules", "build", "public"]
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ const plugin: Plugin<PluginOptions> = ({ onGetConfig, onHook, context, generator
|
|||
// Get server compiler by hooks
|
||||
onHook(`before.${command as 'start' | 'build'}.run`, async ({ serverCompiler, taskConfigs, urls }) => {
|
||||
const taskConfig = taskConfigs.find(({ name }) => name === 'web').config;
|
||||
outputDir = taskConfig.outputDir;
|
||||
outputDir = path.isAbsolute(taskConfig.outputDir) ? taskConfig.outputDir : path.join(rootDir, taskConfig.outputDir);
|
||||
|
||||
// Need absolute path for pha dev.
|
||||
publicPath = command === 'start' ? getDevPath(urls.lanUrlForTerminal) : (taskConfig.publicPath || '/');
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"consola": "^2.15.3",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"rax-compat": "^0.1.0",
|
||||
"stylesheet-loader": "^0.9.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createRequire } from 'module';
|
|||
import type { Plugin } from '@ice/types';
|
||||
import type { RuleSetRule } from 'webpack';
|
||||
import consola from 'consola';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
|
|
@ -33,6 +34,20 @@ let warnOnce = false;
|
|||
function getPlugin(options: CompatRaxOptions): Plugin {
|
||||
return ({ onGetConfig }) => {
|
||||
onGetConfig((config) => {
|
||||
// Reset jsc.transform.react.runtime to classic.
|
||||
config.swcOptions = merge(config.swcOptions || {}, {
|
||||
compilationConfig: {
|
||||
jsc: {
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'classic',
|
||||
pragma: 'createElement',
|
||||
pragmaFrag: 'Fragment',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Object.assign(config.alias, alias);
|
||||
if (options.inlineStyle) {
|
||||
if (!warnOnce) {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
"compat"
|
||||
],
|
||||
"dependencies": {
|
||||
"appear-polyfill": "^0.1.2",
|
||||
"@swc/helpers": "^0.4.3",
|
||||
"style-unit": "^3.0.4",
|
||||
"create-react-class": "^15.7.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,19 +5,11 @@ import type {
|
|||
ReactNode,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import { createElement as _createElement, useEffect, forwardRef } from 'react';
|
||||
import { setupAppear } from 'appear-polyfill';
|
||||
import { createElement as _createElement, useEffect, useCallback, useRef } from 'react';
|
||||
import { cached, convertUnit } from 'style-unit';
|
||||
import { observerElement } from './visibility';
|
||||
import { isFunction, isObject, isNumber } from './type';
|
||||
|
||||
let appearSetup = false;
|
||||
function setupAppearOnce() {
|
||||
if (!appearSetup) {
|
||||
setupAppear();
|
||||
appearSetup = true;
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/alibaba/rax/blob/master/packages/driver-dom/src/index.js
|
||||
// opacity -> opa
|
||||
// fontWeight -> ntw
|
||||
|
|
@ -34,7 +26,6 @@ function setupAppearOnce() {
|
|||
// borderImageOutset|borderImageSlice|borderImageWidth -> erim
|
||||
const NON_DIMENSIONAL_REG = /opa|ntw|ne[ch]|ex(?:s|g|n|p|$)|^ord|zoo|grid|orp|ows|mnc|^columns$|bs|erim|onit/i;
|
||||
|
||||
|
||||
/**
|
||||
* Compat createElement for rax export.
|
||||
* Reference: https://github.com/alibaba/rax/blob/master/packages/rax/src/createElement.js#L13
|
||||
|
|
@ -53,8 +44,11 @@ export function createElement<P extends {
|
|||
type: FunctionComponent<P> | string,
|
||||
props?: Attributes & P | null,
|
||||
...children: ReactNode[]): ReactElement {
|
||||
// Get a shallow copy of props, to avoid mutating the original object.
|
||||
const rest = Object.assign({}, props);
|
||||
const { onAppear, onDisappear } = rest;
|
||||
|
||||
// Delete props that are not allowed in react.
|
||||
delete rest.onAppear;
|
||||
delete rest.onDisappear;
|
||||
|
||||
|
|
@ -64,21 +58,53 @@ export function createElement<P extends {
|
|||
rest.style = compatStyleProps;
|
||||
}
|
||||
|
||||
// Create backend element.
|
||||
const args = [type, rest];
|
||||
let element: any = _createElement.apply(null, args.concat(children as any));
|
||||
|
||||
// Polyfill for appear and disappear event.
|
||||
// Compat for visibility events.
|
||||
if (isFunction(onAppear) || isFunction(onDisappear)) {
|
||||
setupAppearOnce();
|
||||
element = _createElement(forwardRef(AppearOrDisappear), {
|
||||
onAppear: onAppear,
|
||||
onDisappear: onDisappear,
|
||||
ref: rest.ref,
|
||||
}, element);
|
||||
return _createElement(
|
||||
VisibilityChange,
|
||||
{
|
||||
onAppear,
|
||||
onDisappear,
|
||||
// Passing child ref to `VisibilityChange` to avoid creating a new ref.
|
||||
childRef: rest.ref,
|
||||
// Using forwardedRef as a prop to the backend react element.
|
||||
forwardRef: (ref: RefObject<any>) => _createElement(type, Object.assign({ ref }, rest), ...children),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _createElement(type, rest, ...children);
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
function VisibilityChange({
|
||||
onAppear,
|
||||
onDisappear,
|
||||
childRef,
|
||||
forwardRef,
|
||||
}: any) {
|
||||
const fallbackRef = useRef(null); // `fallbackRef` used if `childRef` is not provided.
|
||||
const ref = childRef || fallbackRef;
|
||||
|
||||
const listen = useCallback((eventName: string, handler: Function) => {
|
||||
const { current } = ref;
|
||||
if (current != null) {
|
||||
if (isFunction(handler)) {
|
||||
observerElement(current as HTMLElement);
|
||||
current.addEventListener(eventName, handler);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
const { current } = ref;
|
||||
if (current) {
|
||||
current.removeEventListener(eventName, handler);
|
||||
}
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => listen('appear', onAppear), [ref, onAppear, listen]);
|
||||
useEffect(() => listen('disappear', onDisappear), [ref, onDisappear, listen]);
|
||||
|
||||
return forwardRef(ref);
|
||||
}
|
||||
|
||||
const isDimensionalProp = cached((prop: string) => !NON_DIMENSIONAL_REG.test(prop));
|
||||
|
|
@ -102,31 +128,3 @@ function compatStyle<S = object>(style?: S): S | void {
|
|||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
// Appear HOC Component.
|
||||
function AppearOrDisappear(props: any, ref: RefObject<EventTarget>) {
|
||||
const { onAppear, onDisappear } = props;
|
||||
|
||||
listen('appear', onAppear);
|
||||
listen('disappear', onDisappear);
|
||||
|
||||
function listen(eventName: string, handler: EventListenerOrEventListenerObject) {
|
||||
if (isFunction(handler) && ref) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
const { current } = ref;
|
||||
if (current != null) {
|
||||
current.addEventListener(eventName, handler);
|
||||
}
|
||||
return () => {
|
||||
const { current } = ref;
|
||||
if (current) {
|
||||
current.removeEventListener(eventName, handler);
|
||||
}
|
||||
};
|
||||
}, [ref, handler]);
|
||||
}
|
||||
}
|
||||
|
||||
return props.children;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,652 @@
|
|||
/**
|
||||
* An IntersectionObserver registry. This registry exists to hold a strong
|
||||
* reference to IntersectionObserver instances currently observing a target
|
||||
* element. Without this registry, instances without another reference may be
|
||||
* garbage collected.
|
||||
*/
|
||||
const registry = [];
|
||||
|
||||
|
||||
/**
|
||||
* Creates the global IntersectionObserverEntry constructor.
|
||||
* https://w3c.github.io/IntersectionObserver/#intersection-observer-entry
|
||||
* @param {Object} entry A dictionary of instance properties.
|
||||
* @constructor
|
||||
*/
|
||||
export function IntersectionObserverEntry(entry) {
|
||||
this.time = entry.time;
|
||||
this.target = entry.target;
|
||||
this.rootBounds = entry.rootBounds;
|
||||
this.boundingClientRect = entry.boundingClientRect;
|
||||
this.intersectionRect = entry.intersectionRect || getEmptyRect();
|
||||
this.isIntersecting = !!entry.intersectionRect;
|
||||
|
||||
// Calculates the intersection ratio.
|
||||
const targetRect = this.boundingClientRect;
|
||||
const targetArea = targetRect.width * targetRect.height;
|
||||
const { intersectionRect } = this;
|
||||
const intersectionArea = intersectionRect.width * intersectionRect.height;
|
||||
|
||||
// Sets intersection ratio.
|
||||
if (targetArea) {
|
||||
// Round the intersection ratio to avoid floating point math issues:
|
||||
// https://github.com/w3c/IntersectionObserver/issues/324
|
||||
this.intersectionRatio = Number((intersectionArea / targetArea).toFixed(4));
|
||||
} else {
|
||||
// If area is zero and is intersecting, sets to 1, otherwise to 0
|
||||
this.intersectionRatio = this.isIntersecting ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default class IntersectionObserver {
|
||||
/**
|
||||
* The minimum interval within which the document will be checked for
|
||||
* intersection changes.
|
||||
*/
|
||||
THROTTLE_TIMEOUT = 100;
|
||||
|
||||
/**
|
||||
* The frequency in which the polyfill polls for intersection changes.
|
||||
* this can be updated on a per instance basis and must be set prior to
|
||||
* calling `observe` on the first target.
|
||||
*/
|
||||
POLL_INTERVAL = null;
|
||||
|
||||
/**
|
||||
* Use a mutation observer on the root element
|
||||
* to detect intersection changes.
|
||||
*/
|
||||
USE_MUTATION_OBSERVER = true;
|
||||
|
||||
/**
|
||||
* Creates the global IntersectionObserver constructor.
|
||||
* https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
|
||||
* @param {Function} callback The function to be invoked after intersection
|
||||
* changes have queued. The function is not invoked if the queue has
|
||||
* been emptied by calling the `takeRecords` method.
|
||||
* @param {Object=} optOptions Optional configuration options.
|
||||
* @constructor
|
||||
*/
|
||||
constructor(callback, optOptions) {
|
||||
const options = optOptions || {};
|
||||
|
||||
if (typeof callback != 'function') {
|
||||
throw new Error('callback must be a function');
|
||||
}
|
||||
|
||||
if (options.root && options.root.nodeType != 1) {
|
||||
throw new Error('root must be an Element');
|
||||
}
|
||||
|
||||
// Throttles `this._checkForIntersections`.
|
||||
this._checkForIntersections = throttle(this._checkForIntersections, this.THROTTLE_TIMEOUT);
|
||||
|
||||
// Private properties.
|
||||
this._callback = callback;
|
||||
this._observationTargets = [];
|
||||
this._queuedEntries = [];
|
||||
this._rootMarginValues = this._parseRootMargin(options.rootMargin);
|
||||
|
||||
// Public properties.
|
||||
this.thresholds = this._initThresholds(options.threshold);
|
||||
this.root = options.root || null;
|
||||
this.rootMargin = this._rootMarginValues.map((margin) => margin.value + margin.unit).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts observing a target element for intersection changes based on
|
||||
* the thresholds values.
|
||||
* @param {Element} target The DOM element to observe.
|
||||
*/
|
||||
observe(target) {
|
||||
const isTargetAlreadyObserved = this._observationTargets.some((item) => item.element === target);
|
||||
|
||||
if (isTargetAlreadyObserved) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(target && target.nodeType == 1)) {
|
||||
throw new Error('target must be an Element');
|
||||
}
|
||||
|
||||
this._registerInstance();
|
||||
this._observationTargets.push({ element: target, entry: null });
|
||||
this._monitorIntersections();
|
||||
this._checkForIntersections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops observing a target element for intersection changes.
|
||||
* @param {Element} target The DOM element to observe.
|
||||
*/
|
||||
unobserve(target) {
|
||||
this._observationTargets =
|
||||
this._observationTargets.filter((item) => {
|
||||
return item.element !== target;
|
||||
});
|
||||
if (!this._observationTargets.length) {
|
||||
this._unmonitorIntersections();
|
||||
this._unregisterInstance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops observing all target elements for intersection changes.
|
||||
*/
|
||||
disconnect() {
|
||||
this._observationTargets = [];
|
||||
this._unmonitorIntersections();
|
||||
this._unregisterInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns any queue entries that have not yet been reported to the
|
||||
* callback and clears the queue. This can be used in conjunction with the
|
||||
* callback to obtain the absolute most up-to-date intersection information.
|
||||
* @return {Array} The currently queued entries.
|
||||
*/
|
||||
takeRecords() {
|
||||
const records = this._queuedEntries.slice();
|
||||
this._queuedEntries = [];
|
||||
return records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the threshold value from the user configuration object and
|
||||
* returns a sorted array of unique threshold values. If a value is not
|
||||
* between 0 and 1 and error is thrown.
|
||||
* @private
|
||||
* @param {Array|number=} optThreshold An optional threshold value or
|
||||
* a list of threshold values, defaulting to [0].
|
||||
* @return {Array} A sorted list of unique and valid threshold values.
|
||||
*/
|
||||
_initThresholds(optThreshold) {
|
||||
let threshold = optThreshold || [0];
|
||||
if (!Array.isArray(threshold)) threshold = [threshold];
|
||||
|
||||
return threshold.sort().filter((t, i, a) => {
|
||||
if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
|
||||
throw new Error('threshold must be a number between 0 and 1 inclusively');
|
||||
}
|
||||
return t !== a[i - 1];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the rootMargin value from the user configuration object
|
||||
* and returns an array of the four margin values as an object containing
|
||||
* the value and unit properties. If any of the values are not properly
|
||||
* formatted or use a unit other than px or %, and error is thrown.
|
||||
* @private
|
||||
* @param {string=} optRootMargin An optional rootMargin value,
|
||||
* defaulting to '0px'.
|
||||
* @return {Array<Object>} An array of margin objects with the keys
|
||||
* value and unit.
|
||||
*/
|
||||
_parseRootMargin(optRootMargin) {
|
||||
let marginString = optRootMargin || '0px';
|
||||
let margins = marginString.split(/\s+/).map((margin) => {
|
||||
let parts = /^(-?\d*\.?\d+)(px|%)$/.exec(margin);
|
||||
if (!parts) {
|
||||
throw new Error('rootMargin must be specified in pixels or percent');
|
||||
}
|
||||
return { value: parseFloat(parts[1]), unit: parts[2] };
|
||||
});
|
||||
|
||||
// Handles shorthand.
|
||||
margins[1] = margins[1] || margins[0];
|
||||
margins[2] = margins[2] || margins[0];
|
||||
margins[3] = margins[3] || margins[1];
|
||||
|
||||
return margins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts polling for intersection changes if the polling is not already
|
||||
* happening, and if the page's visibility state is visible.
|
||||
* @private
|
||||
*/
|
||||
_monitorIntersections() {
|
||||
if (!this._monitoringIntersections) {
|
||||
this._monitoringIntersections = true;
|
||||
|
||||
// If a poll interval is set, use polling instead of listening to
|
||||
// resize and scroll events or DOM mutations.
|
||||
if (this.POLL_INTERVAL) {
|
||||
this._monitoringInterval = setInterval(this._checkForIntersections, this.POLL_INTERVAL);
|
||||
} else {
|
||||
addEvent(window, 'resize', this._checkForIntersections, true);
|
||||
addEvent(document, 'scroll', this._checkForIntersections, true);
|
||||
|
||||
if (this.USE_MUTATION_OBSERVER && 'MutationObserver' in window) {
|
||||
this._domObserver = new MutationObserver(this._checkForIntersections);
|
||||
this._domObserver.observe(document, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops polling for intersection changes.
|
||||
* @private
|
||||
*/
|
||||
_unmonitorIntersections() {
|
||||
if (this._monitoringIntersections) {
|
||||
this._monitoringIntersections = false;
|
||||
|
||||
clearInterval(this._monitoringInterval);
|
||||
this._monitoringInterval = null;
|
||||
|
||||
removeEvent(window, 'resize', this._checkForIntersections, true);
|
||||
removeEvent(document, 'scroll', this._checkForIntersections, true);
|
||||
|
||||
if (this._domObserver) {
|
||||
this._domObserver.disconnect();
|
||||
this._domObserver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans each observation target for intersection changes and adds them
|
||||
* to the internal entries queue. If new entries are found, it
|
||||
* schedules the callback to be invoked.
|
||||
* @NOTE Using arrow function to bind to `this` instance.
|
||||
* @private
|
||||
*/
|
||||
_checkForIntersections = () => {
|
||||
let rootIsInDom = this._rootIsInDom();
|
||||
let rootRect = rootIsInDom ? this._getRootRect() : getEmptyRect();
|
||||
|
||||
this._observationTargets.forEach(function (item) {
|
||||
let target = item.element;
|
||||
let targetRect = getBoundingClientRect(target);
|
||||
let rootContainsTarget = this._rootContainsTarget(target);
|
||||
let oldEntry = item.entry;
|
||||
let intersectionRect = rootIsInDom && rootContainsTarget &&
|
||||
this._computeTargetAndRootIntersection(target, rootRect);
|
||||
|
||||
let newEntry = item.entry = new IntersectionObserverEntry({
|
||||
time: now(),
|
||||
target: target,
|
||||
boundingClientRect: targetRect,
|
||||
rootBounds: rootRect,
|
||||
intersectionRect: intersectionRect,
|
||||
});
|
||||
|
||||
if (!oldEntry) {
|
||||
this._queuedEntries.push(newEntry);
|
||||
} else if (rootIsInDom && rootContainsTarget) {
|
||||
// If the new entry intersection ratio has crossed any of the
|
||||
// thresholds, add a new entry.
|
||||
if (this._hasCrossedThreshold(oldEntry, newEntry)) {
|
||||
this._queuedEntries.push(newEntry);
|
||||
}
|
||||
} else {
|
||||
// If the root is not in the DOM or target is not contained within
|
||||
// root but the previous entry for this target had an intersection,
|
||||
// add a new record indicating removal.
|
||||
if (oldEntry && oldEntry.isIntersecting) {
|
||||
this._queuedEntries.push(newEntry);
|
||||
}
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (this._queuedEntries.length) {
|
||||
this._callback(this.takeRecords(), this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Accepts a target and root rect computes the intersection between then
|
||||
* following the algorithm in the spec.
|
||||
* TODO(philipwalton): at this time clip-path is not considered.
|
||||
* https://w3c.github.io/IntersectionObserver/#calculate-intersection-rect-algo
|
||||
* @param {Element} target The target DOM element
|
||||
* @param {Object} rootRect The bounding rect of the root after being
|
||||
* expanded by the rootMargin value.
|
||||
* @return {?Object} The final intersection rect object or undefined if no
|
||||
* intersection is found.
|
||||
* @private
|
||||
*/
|
||||
_computeTargetAndRootIntersection(target, rootRect) {
|
||||
// If the element isn't displayed, an intersection can't happen.
|
||||
if (window.getComputedStyle(target).display == 'none') return;
|
||||
|
||||
let targetRect = getBoundingClientRect(target);
|
||||
let intersectionRect = targetRect;
|
||||
let parent = getParentNode(target);
|
||||
let atRoot = false;
|
||||
|
||||
while (!atRoot) {
|
||||
let parentRect = null;
|
||||
let parentComputedStyle = parent.nodeType == 1
|
||||
? window.getComputedStyle(parent) : {};
|
||||
|
||||
// If the parent isn't displayed, an intersection can't happen.
|
||||
if (parentComputedStyle.display === 'none') return;
|
||||
|
||||
if (parent === this.root || parent === document) {
|
||||
atRoot = true;
|
||||
parentRect = rootRect;
|
||||
} else {
|
||||
// If the element has a non-visible overflow, and it's not the <body>
|
||||
// or <html> element, update the intersection rect.
|
||||
// Note: <body> and <html> cannot be clipped to a rect that's not also
|
||||
// the document rect, so no need to compute a new intersection.
|
||||
if (parent !== document.body &&
|
||||
parent !== document.documentElement &&
|
||||
parentComputedStyle.overflow !== 'visible') {
|
||||
parentRect = getBoundingClientRect(parent);
|
||||
}
|
||||
}
|
||||
|
||||
// If either of the above conditionals set a new parentRect,
|
||||
// calculate new intersection data.
|
||||
if (parentRect) {
|
||||
intersectionRect = computeRectIntersection(parentRect, intersectionRect);
|
||||
|
||||
if (!intersectionRect) break;
|
||||
}
|
||||
parent = getParentNode(parent);
|
||||
}
|
||||
return intersectionRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root rect after being expanded by the rootMargin value.
|
||||
* @return {Object} The expanded root rect.
|
||||
* @private
|
||||
*/
|
||||
_getRootRect() {
|
||||
let rootRect;
|
||||
if (this.root) {
|
||||
rootRect = getBoundingClientRect(this.root);
|
||||
} else {
|
||||
// Use <html>/<body> instead of window since scroll bars affect size.
|
||||
let html = document.documentElement;
|
||||
let { body } = document;
|
||||
rootRect = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: html.clientWidth || body.clientWidth,
|
||||
width: html.clientWidth || body.clientWidth,
|
||||
bottom: html.clientHeight || body.clientHeight,
|
||||
height: html.clientHeight || body.clientHeight,
|
||||
};
|
||||
}
|
||||
return this._expandRectByRootMargin(rootRect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a rect and expands it by the rootMargin value.
|
||||
* @param {Object} rect The rect object to expand.
|
||||
* @return {Object} The expanded rect.
|
||||
* @private
|
||||
*/
|
||||
_expandRectByRootMargin(rect) {
|
||||
let margins = this._rootMarginValues.map((margin, i) => {
|
||||
return margin.unit === 'px' ? margin.value
|
||||
: margin.value * (i % 2 ? rect.width : rect.height) / 100;
|
||||
});
|
||||
let newRect = {
|
||||
top: rect.top - margins[0],
|
||||
right: rect.right + margins[1],
|
||||
bottom: rect.bottom + margins[2],
|
||||
left: rect.left - margins[3],
|
||||
};
|
||||
newRect.width = newRect.right - newRect.left;
|
||||
newRect.height = newRect.bottom - newRect.top;
|
||||
|
||||
return newRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an old and new entry and returns true if at least one of the
|
||||
* threshold values has been crossed.
|
||||
* @param {?IntersectionObserverEntry} oldEntry The previous entry for a
|
||||
* particular target element or null if no previous entry exists.
|
||||
* @param {IntersectionObserverEntry} newEntry The current entry for a
|
||||
* particular target element.
|
||||
* @return {boolean} Returns true if a any threshold has been crossed.
|
||||
* @private
|
||||
*/
|
||||
_hasCrossedThreshold(oldEntry, newEntry) {
|
||||
// To make comparing easier, an entry that has a ratio of 0
|
||||
// but does not actually intersect is given a value of -1
|
||||
const oldRatio = oldEntry && oldEntry.isIntersecting
|
||||
? oldEntry.intersectionRatio || 0 : -1;
|
||||
const newRatio = newEntry.isIntersecting
|
||||
? newEntry.intersectionRatio || 0 : -1;
|
||||
|
||||
// Ignore unchanged ratios
|
||||
if (oldRatio === newRatio) return;
|
||||
|
||||
for (let i = 0; i < this.thresholds.length; i++) {
|
||||
const threshold = this.thresholds[i];
|
||||
|
||||
// Return true if an entry matches a threshold or if the new ratio
|
||||
// and the old ratio are on the opposite sides of a threshold.
|
||||
if (threshold == oldRatio || threshold == newRatio ||
|
||||
threshold < oldRatio !== threshold < newRatio) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the root element is an element and is in the DOM.
|
||||
* @return {boolean} True if the root element is an element and is in the DOM.
|
||||
* @private
|
||||
*/
|
||||
_rootIsInDom() {
|
||||
return !this.root || containsDeep(document, this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the target element is a child of root.
|
||||
* @param {Element} target The target element to check.
|
||||
* @return {boolean} True if the target element is a child of root.
|
||||
* @private
|
||||
*/
|
||||
_rootContainsTarget(target) {
|
||||
return containsDeep(this.root || document, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the instance to the global IntersectionObserver registry if it isn't
|
||||
* already present.
|
||||
* @private
|
||||
*/
|
||||
_registerInstance() {
|
||||
if (registry.indexOf(this) < 0) {
|
||||
registry.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the instance from the global IntersectionObserver registry.
|
||||
* @private
|
||||
*/
|
||||
_unregisterInstance() {
|
||||
const index = registry.indexOf(this);
|
||||
if (index !== -1) registry.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of the performance.now() method or null in browsers
|
||||
* that don't support the API.
|
||||
* @return {number} The elapsed time since the page was requested.
|
||||
*/
|
||||
function now() {
|
||||
return window.performance && performance.now && performance.now();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Throttles a function and delays its execution, so it's only called at most
|
||||
* once within a given time period.
|
||||
* @param {Function} fn The function to throttle.
|
||||
* @param {number} timeout The amount of time that must pass before the
|
||||
* function can be called again.
|
||||
* @return {Function} The throttled function.
|
||||
*/
|
||||
function throttle(fn, timeout) {
|
||||
let timer = null;
|
||||
return function () {
|
||||
if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
fn();
|
||||
timer = null;
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds an event handler to a DOM node ensuring cross-browser compatibility.
|
||||
* @param {Node} node The DOM node to add the event handler to.
|
||||
* @param {string} event The event name.
|
||||
* @param {Function} fn The event handler to add.
|
||||
* @param {boolean} opt_useCapture Optionally adds the even to the capture
|
||||
* phase. Note: this only works in modern browsers.
|
||||
*/
|
||||
function addEvent(node, event, fn, opt_useCapture) {
|
||||
if (typeof node.addEventListener == 'function') {
|
||||
node.addEventListener(event, fn, opt_useCapture || false);
|
||||
} else if (typeof node.attachEvent == 'function') {
|
||||
node.attachEvent(`on${event}`, fn);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes a previously added event handler from a DOM node.
|
||||
* @param {Node} node The DOM node to remove the event handler from.
|
||||
* @param {string} event The event name.
|
||||
* @param {Function} fn The event handler to remove.
|
||||
* @param {boolean} opt_useCapture If the event handler was added with this
|
||||
* flag set to true, it should be set to true here in order to remove it.
|
||||
*/
|
||||
function removeEvent(node, event, fn, opt_useCapture) {
|
||||
if (typeof node.removeEventListener == 'function') {
|
||||
node.removeEventListener(event, fn, opt_useCapture || false);
|
||||
} else if (typeof node.detatchEvent == 'function') {
|
||||
node.detatchEvent(`on${event}`, fn);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the intersection between two rect objects.
|
||||
* @param {Object} rect1 The first rect.
|
||||
* @param {Object} rect2 The second rect.
|
||||
* @return {?Object} The intersection rect or undefined if no intersection
|
||||
* is found.
|
||||
*/
|
||||
function computeRectIntersection(rect1, rect2) {
|
||||
const top = Math.max(rect1.top, rect2.top);
|
||||
const bottom = Math.min(rect1.bottom, rect2.bottom);
|
||||
const left = Math.max(rect1.left, rect2.left);
|
||||
const right = Math.min(rect1.right, rect2.right);
|
||||
const width = right - left;
|
||||
const height = bottom - top;
|
||||
|
||||
return width >= 0 && height >= 0 && { top, bottom, left, right, width, height };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shims the native getBoundingClientRect for compatibility with older IE.
|
||||
* @param {Element} el The element whose bounding rect to get.
|
||||
* @return {Object} The (possibly shimmed) rect of the element.
|
||||
*/
|
||||
function getBoundingClientRect(el) {
|
||||
let rect;
|
||||
|
||||
try {
|
||||
rect = el.getBoundingClientRect();
|
||||
} catch (err) {
|
||||
// Ignore Windows 7 IE11 "Unspecified error"
|
||||
// https://github.com/w3c/IntersectionObserver/pull/205
|
||||
}
|
||||
|
||||
if (!rect) return getEmptyRect();
|
||||
|
||||
// Older IE
|
||||
if (!(rect.width && rect.height)) {
|
||||
rect = {
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
left: rect.left,
|
||||
width: rect.right - rect.left,
|
||||
height: rect.bottom - rect.top,
|
||||
};
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an empty rect object. An empty rect is returned when an element
|
||||
* is not in the DOM.
|
||||
* @return {Object} The empty rect.
|
||||
*/
|
||||
function getEmptyRect() {
|
||||
return {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if a parent element contains a child element (including inside
|
||||
* shadow DOM).
|
||||
* @param {Node} parent The parent element.
|
||||
* @param {Node} child The child element.
|
||||
* @return {boolean} True if the parent node contains the child node.
|
||||
*/
|
||||
function containsDeep(parent, child) {
|
||||
let node = child;
|
||||
while (node) {
|
||||
if (node === parent) return true;
|
||||
|
||||
node = getParentNode(node);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the parent node of an element or its host element if the parent node
|
||||
* is a shadow root.
|
||||
* @param {Node} node The node whose parent to get.
|
||||
* @return {Node|null} The parent node or null if no parent exists.
|
||||
*/
|
||||
function getParentNode(node) {
|
||||
let parent = node.parentNode;
|
||||
|
||||
if (parent && parent.nodeType == 11 && parent.host) {
|
||||
// If the parent is a shadow root, return the host element.
|
||||
return parent.host;
|
||||
}
|
||||
|
||||
if (parent && parent.assignedSlot) {
|
||||
// If the parent is distributed in a <slot>, return the parent of a slot.
|
||||
return parent.assignedSlot.parentNode;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
// Handle appear and disappear event.
|
||||
// Fork from https://github.com/raxjs/appear-polyfill
|
||||
// @ts-ignore
|
||||
import PolyfilledIntersectionObserver from './intersection-observer';
|
||||
|
||||
enum VisibilityChangeEvent {
|
||||
appear = 'appear',
|
||||
disappear = 'disappear',
|
||||
}
|
||||
|
||||
enum VisibilityChangeDirection {
|
||||
up = 'up',
|
||||
down = 'down',
|
||||
}
|
||||
|
||||
// Shared intersectionObserver instance.
|
||||
let intersectionObserver: any;
|
||||
const IntersectionObserver = (function () {
|
||||
if (typeof window !== 'undefined' &&
|
||||
'IntersectionObserver' in window &&
|
||||
'IntersectionObserverEntry' in window &&
|
||||
'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
|
||||
// features are natively supported
|
||||
return window.IntersectionObserver;
|
||||
} else {
|
||||
// polyfilled IntersectionObserver
|
||||
return PolyfilledIntersectionObserver;
|
||||
}
|
||||
})();
|
||||
|
||||
function generateThreshold(number: number) {
|
||||
const thresholds = [];
|
||||
for (let index = 0; index < number; index++) {
|
||||
thresholds.push(index / number);
|
||||
}
|
||||
|
||||
return thresholds;
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
// @ts-ignore
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: generateThreshold(10),
|
||||
};
|
||||
|
||||
export function createIntersectionObserver(options = defaultOptions) {
|
||||
intersectionObserver = new IntersectionObserver(handleIntersect, options);
|
||||
}
|
||||
|
||||
export function destroyIntersectionObserver() {
|
||||
if (intersectionObserver) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function observerElement(element: HTMLElement | Node) {
|
||||
if (!intersectionObserver) createIntersectionObserver();
|
||||
|
||||
if (element === document) element = document.documentElement;
|
||||
|
||||
intersectionObserver.observe(element);
|
||||
}
|
||||
|
||||
function handleIntersect(entries: IntersectionObserverEntry[]) {
|
||||
entries.forEach((entry) => {
|
||||
const {
|
||||
target,
|
||||
boundingClientRect,
|
||||
intersectionRatio,
|
||||
} = entry;
|
||||
// No `top` value in polyfill.
|
||||
const currentY = boundingClientRect.y || boundingClientRect.top;
|
||||
const beforeY = parseInt(target.getAttribute('data-before-current-y')) || currentY;
|
||||
|
||||
// is in view
|
||||
if (
|
||||
intersectionRatio > 0.01 &&
|
||||
!isTrue(target.getAttribute('data-appeared')) &&
|
||||
!appearOnce(target as HTMLElement, VisibilityChangeEvent.appear)
|
||||
) {
|
||||
target.setAttribute('data-appeared', 'true');
|
||||
target.setAttribute('data-has-appeared', 'true');
|
||||
target.dispatchEvent(createEvent(VisibilityChangeEvent.appear, {
|
||||
direction: currentY > beforeY ? VisibilityChangeDirection.up : VisibilityChangeDirection.down,
|
||||
}));
|
||||
} else if (
|
||||
intersectionRatio === 0 &&
|
||||
isTrue(target.getAttribute('data-appeared')) &&
|
||||
!appearOnce(target as HTMLElement, VisibilityChangeEvent.disappear)
|
||||
) {
|
||||
target.setAttribute('data-appeared', 'false');
|
||||
target.setAttribute('data-has-disappeared', 'true');
|
||||
target.dispatchEvent(createEvent(VisibilityChangeEvent.appear, {
|
||||
direction: currentY > beforeY ? VisibilityChangeDirection.up : VisibilityChangeDirection.down,
|
||||
}));
|
||||
}
|
||||
|
||||
target.setAttribute('data-before-current-y', String(currentY));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* need appear again when node has isonce or data-once
|
||||
*/
|
||||
function appearOnce(node: HTMLElement, type: VisibilityChangeEvent) {
|
||||
const isOnce = isTrue(node.getAttribute('isonce')) || isTrue(node.getAttribute('data-once'));
|
||||
const appearType = type === VisibilityChangeEvent.appear ? 'data-has-appeared' : 'data-has-disappeared';
|
||||
|
||||
return isOnce && isTrue(node.getAttribute(appearType));
|
||||
}
|
||||
|
||||
function isTrue(flag: any) {
|
||||
return flag && flag !== 'false';
|
||||
}
|
||||
|
||||
function createEvent(eventName: string, data: any) {
|
||||
return new CustomEvent(eventName, {
|
||||
bubbles: false,
|
||||
cancelable: true,
|
||||
detail: data,
|
||||
});
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ describe('createElement', () => {
|
|||
render(createElement(
|
||||
'div',
|
||||
{
|
||||
onDisappear: func
|
||||
onDisappear: func,
|
||||
},
|
||||
str
|
||||
));
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"bugs": "https://github.com/ice-lab/ice-next/issues",
|
||||
"homepage": "https://next.ice.work",
|
||||
"devDependencies": {
|
||||
"@builder/swc": "^0.2.0",
|
||||
"@ice/route-manifest": "^1.0.0",
|
||||
"@ice/runtime": "^1.0.0",
|
||||
"build-scripts": "^2.0.0-23",
|
||||
|
|
@ -39,9 +40,9 @@
|
|||
"fork-ts-checker-webpack-plugin": "7.2.6",
|
||||
"react": "^18.0.0",
|
||||
"terser": "^5.12.1",
|
||||
"typescript": "^4.6.4",
|
||||
"unplugin": "^0.3.2",
|
||||
"webpack": "^5.73.0",
|
||||
"webpack-dev-server": "^4.7.4",
|
||||
"typescript": "^4.6.4"
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ForkTsCheckerWebpackPluginOptions } from 'fork-ts-checker-webpack-
|
|||
import type { UnpluginOptions } from 'unplugin';
|
||||
import type Server from 'webpack-dev-server';
|
||||
import type { ECMA } from 'terser';
|
||||
import type { Config as CompilationConfig } from '@builder/swc';
|
||||
|
||||
// get type definitions from terser-webpack-plugin
|
||||
interface CustomOptions {
|
||||
|
|
@ -27,6 +28,7 @@ interface ConfigurationCtx extends Config {
|
|||
interface SwcOptions {
|
||||
jsxTransform?: boolean;
|
||||
removeExportExprs?: string[];
|
||||
compilationConfig?: CompilationConfig;
|
||||
}
|
||||
|
||||
type Experimental = Pick<Configuration, 'experiments'>;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const compilationPlugin = (options: Options): UnpluginOptions => {
|
|||
filename: id,
|
||||
};
|
||||
|
||||
const { jsxTransform, removeExportExprs } = swcOptions;
|
||||
const { jsxTransform, removeExportExprs, compilationConfig } = swcOptions;
|
||||
|
||||
let needTransform = false;
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ const compilationPlugin = (options: Options): UnpluginOptions => {
|
|||
}
|
||||
|
||||
try {
|
||||
const output = await transform(source, programmaticOptions);
|
||||
const output = await transform(source, merge(programmaticOptions, compilationConfig || {}));
|
||||
const { code } = output;
|
||||
let { map } = output;
|
||||
// FIXME: swc transform should return the sourcemap which the type is object
|
||||
|
|
|
|||
1549
pnpm-lock.yaml
1549
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,41 @@
|
|||
import { expect, test, describe, afterAll } from 'vitest';
|
||||
import { buildFixture, setupBrowser } from '../utils/build';
|
||||
import { startFixture, setupStartBrowser } from '../utils/start';
|
||||
import { Page } from '../utils/browser';
|
||||
|
||||
const example = 'rax-project';
|
||||
|
||||
describe(`build ${example}`, () => {
|
||||
let page: Page = null;
|
||||
let browser = null;
|
||||
|
||||
test('open /', async () => {
|
||||
await buildFixture(example);
|
||||
const res = await setupBrowser({ example });
|
||||
|
||||
page = res.page;
|
||||
browser = res.browser;
|
||||
expect(await page.$$text('div')).toStrictEqual(['']);
|
||||
}, 120000);
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`start ${example}`, () => {
|
||||
let page: Page = null;
|
||||
let browser = null;
|
||||
|
||||
test('setup devServer', async () => {
|
||||
const { devServer, port } = await startFixture(example);
|
||||
const res = await setupStartBrowser({ server: devServer, port });
|
||||
page = res.page;
|
||||
browser = res.browser;
|
||||
expect((await page.$$text('span'))[0]).toStrictEqual('Welcome to Your Rax App');
|
||||
}, 120000);
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
});
|
||||
|
|
@ -12,7 +12,8 @@ interface ISetupBrowser {
|
|||
example: string;
|
||||
outputDir?: string;
|
||||
defaultHtml?: string;
|
||||
}): Promise<IReturn>;
|
||||
disableJS?: boolean;
|
||||
}): Promise<ReturnValue>;
|
||||
}
|
||||
|
||||
interface IReturn {
|
||||
|
|
@ -29,12 +30,12 @@ export const buildFixture = async function(example: string) {
|
|||
}
|
||||
|
||||
export const setupBrowser: SetupBrowser = async (options) => {
|
||||
const { example, outputDir = 'build', defaultHtml = 'index.html' } = options;
|
||||
const { example, outputDir = 'build', defaultHtml = 'index.html', disableJS = true } = options;
|
||||
const rootDir = path.join(__dirname, `../../examples/${example}`);
|
||||
const port = await getPort();
|
||||
const browser = new Browser({ cwd: path.join(rootDir, outputDir), port });
|
||||
await browser.start();
|
||||
const disableJS = true;
|
||||
await browser.start('disableJS', disableJS);
|
||||
console.log()
|
||||
// when preview html generate by build, the path will not match the router info, so hydrate will not found the route component
|
||||
const page = await browser.page(`http://127.0.0.1:${port}/${defaultHtml}`, disableJS);
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in New Issue