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:
ClarkXia 2022-07-14 09:56:56 +08:00
parent 064e290ec6
commit 48603cdf8f
29 changed files with 1943 additions and 716 deletions

View File

@ -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,
});

View File

@ -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"
},

View File

@ -0,0 +1 @@
chrome 55

View File

@ -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()],
});

View File

@ -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

View File

@ -0,0 +1,7 @@
import { defineAppConfig } from 'ice';
export default defineAppConfig({
app: {
rootId: 'app',
},
});

View File

@ -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;

View File

@ -0,0 +1,3 @@
body {
font-size: 14px;
}

View File

@ -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;
}

14
examples/rax-project/src/typings.d.ts vendored Normal file
View File

@ -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;
}

View File

@ -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"]
}

View File

@ -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 || '/');

View File

@ -18,6 +18,7 @@
],
"dependencies": {
"consola": "^2.15.3",
"lodash.merge": "^4.6.2",
"rax-compat": "^0.1.0",
"stylesheet-loader": "^0.9.1"
},

View File

@ -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) {

View File

@ -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"
},

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,
});
}

View File

@ -40,7 +40,7 @@ describe('createElement', () => {
render(createElement(
'div',
{
onDisappear: func
onDisappear: func,
},
str
));

View File

@ -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"
}
}

View File

@ -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'>;

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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();
});
});

View File

@ -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 {