test: improve test case (#497)

* test: improve test case

* test: compat win32

* test: test case

* test: add test case

* test: add test case

* test: test case for rax-compat

* test: test case for routes

* fix: types error

* fix: to strict equal

* chore: remove unused props

* fix: types

* test: test case for run client app

* chore: remove empty router

* test: test case of run server app

* chore: remove log

* chore: do not remove file by test case

* fix: remove document dependency when run client app

* chore: lint

* chore: remove config

* fix: lint

* chore: lint warning

* fix: test case

* chore: update unplugin
This commit is contained in:
ClarkXia 2022-09-07 14:49:54 +08:00
parent c0c342d5d5
commit ad531405a8
93 changed files with 1864 additions and 468 deletions

View File

@ -1,7 +1,6 @@
# 忽略目录
build/
test/
tests/
fixtures/
node_modules/
dist/
out/
@ -11,7 +10,6 @@ compiled/
coverage/
# 忽略测试文件
/packages/*/__tests__
/packages/*/lib/
/packages/*/esm/
/packages/*/es2017/

View File

@ -16,6 +16,7 @@ const commonRules = {
'no-multiple-empty-lines': 1,
'react/jsx-no-bind': 0,
'import/order': 1,
'no-multi-assign': 0,
};
module.exports = getESLintConfig('react-ts', {
@ -32,7 +33,9 @@ module.exports = getESLintConfig('react-ts', {
'id-length': 0,
'no-use-before-define': 0,
'no-unused-vars': 0,
'@typescript-eslint/no-unused-vars': 1,
'@typescript-eslint/no-unused-vars': ['warn', {
varsIgnorePattern: '[iI]gnored|createElement',
}],
'@typescript-eslint/ban-ts-ignore': 0,
'@typescript-eslint/no-confusing-void-expression': 0,
'@typescript-eslint/promise-function-async': 0,

View File

@ -0,0 +1 @@
chrome 55

View File

@ -0,0 +1,3 @@
import { defineConfig } from '@ice/app';
export default defineConfig({});

View File

@ -0,0 +1,28 @@
{
"name": "app-config",
"version": "1.0.0",
"scripts": {
"start": "ice start",
"build": "ice build",
"build:splitChunks": "ice build --config splitChunks.config.mts"
},
"description": "",
"author": "",
"license": "MIT",
"dependencies": {
"@ice/app": "workspace:*",
"@ice/plugin-auth": "workspace:*",
"@ice/plugin-rax-compat": "workspace:*",
"@ice/runtime": "workspace:*",
"@uni/env": "^1.1.0",
"ahooks": "^3.3.8",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.2",
"speed-measure-webpack-plugin": "^1.5.0",
"webpack": "^5.73.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,13 @@
import { defineAppConfig } from 'ice';
export default defineAppConfig({
app: {
rootId: 'app',
strict: true,
errorBoundary: true,
},
router: {
type: 'browser',
basename: '/ice',
},
});

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" />
<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,5 @@
export default function Error() {
// @ts-ignore
window.test();
return <>error</>;
}

View File

@ -0,0 +1,3 @@
export default function Home() {
return <h1>home</h1>;
}

14
examples/app-config/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

@ -22,6 +22,14 @@ export default defineConfig({
return webpackConfig;
},
dropLogLevel: 'warn',
plugins: [auth(), custom],
plugins: [
auth(),
{
name: 'runtime-donot-exsist',
setup() {},
runtime: './test',
},
custom,
],
eslint: true,
});

View File

@ -1,6 +1,10 @@
import style from './cssWithEscapedSymbols.module.css';
console.log('style', style);
export default function Bar() {
return (
<div>
<div className={style.test}>
bar
</div>
);

View File

@ -0,0 +1,114 @@
.test {
background: red;
}
._test {
background: blue;
}
.className {
background: red;
}
#someId {
background: green;
}
.className .subClass {
color: green;
}
#someId .subClass {
color: blue;
}
.-a0-34a___f {
color: red;
}
.m_x_\@ {
margin-left: auto !important;
margin-right: auto !important;
}
.B\&W\? {
margin-left: auto !important;
margin-right: auto !important;
}
/* matches elements with class=":`(" */
.\3A \`\( {
color: aqua;
}
/* matches elements with class="1a2b3c" */
.\31 a2b3c {
color: aliceblue;
}
/* matches the element with id="#fake-id" */
#\#fake-id {
color: antiquewhite;
}
/* matches the element with id="-a-b-c-" */
#-a-b-c- {
color: azure;
}
/* matches the element with id="©" */
#© {
color: black;
}
. { background: lime; }
.© { background: lime; }
.😍 { background: lime; }
. { background: lime; }
. { background: lime; }
. { background: lime; }
.𝄞 { background: lime; }
.💩 { background: lime; }
.\? { background: lime; }
.\@ { background: lime; }
.\. { background: lime; }
.\3A \) { background: lime; }
.\3A \`\( { background: lime; }
.\31 23 { background: lime; }
.\31 a2b3c { background: lime; }
.\<p\> { background: lime; }
.\<\>\<\<\<\>\>\<\> { background: lime; }
.\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\. { background: lime; }
.\# { background: lime; }
.\#\# { background: lime; }
.\#\.\#\.\# { background: lime; }
.\_ { background: lime; }
.\{\} { background: lime; }
.\#fake\-id { background: lime; }
.foo\.bar { background: lime; }
.\3A hover { background: lime; }
.\3A hover\3A focus\3A active { background: lime; }
.\[attr\=value\] { background: lime; }
.f\/o\/o { background: lime; }
.f\\o\\o { background: lime; }
.f\*o\*o { background: lime; }
.f\!o\!o { background: lime; }
.f\'o\'o { background: lime; }
.f\~o\~o { background: lime; }
.f\+o\+o { background: lime; }
.foo\/bar {
background: hotpink;
}
.foo\\bar {
background: hotpink;
}
.foo\/bar\/baz {
background: hotpink;
}
.foo\\bar\\baz {
background: hotpink;
}

View File

@ -0,0 +1,3 @@
h2 {
color: #000;
}

View File

@ -0,0 +1,3 @@
.data {
margin-top: 10px;
}

View File

@ -0,0 +1,3 @@
.data {
margin-top: 10px;
}

View File

@ -4,7 +4,10 @@ import { Link, useData, useAppData, useConfig } from 'ice';
import { useAppContext } from '@ice/runtime';
import { useRequest } from 'ahooks';
import type { AppData } from 'ice';
import './index.css';
import styles from './index.module.css';
import lessStyles from './index.module.less';
import sassStyles from './index.module.scss';
const Bar = lazy(() => import('../components/bar'));
@ -33,8 +36,8 @@ export default function Home(props) {
<Bar />
</Suspense>
<div className={styles.data}>
<div>foo: {JSON.stringify(foo)}</div>
<div>users: {JSON.stringify(users)}</div>
<div className={lessStyles.data}>foo: {JSON.stringify(foo)}</div>
<div className={sassStyles.data}>users: {JSON.stringify(users)}</div>
<div>userInfo: {JSON.stringify(userInfo)}</div>
<div>data from: <span id="data-from">{data.from}</span></div>
</div>

View File

@ -83,7 +83,7 @@
"@types/temp": "^0.9.1",
"chokidar": "^3.5.3",
"react": "^18.2.0",
"unplugin": "^0.8.0",
"unplugin": "^0.9.0",
"webpack": "^5.73.0",
"webpack-dev-server": "^4.7.4"
},

View File

@ -223,7 +223,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
reCompileRouteConfig,
dataCache,
appConfig,
devPath: (routePaths[0] || '').replace(/^[\/\\]/, ''),
devPath: (routePaths[0] || '').replace(/^[/\\]/, ''),
spinner: buildSpinner,
});
} else if (command === 'build') {

View File

@ -6,7 +6,6 @@ import { less, sass, postcss } from '@ice/bundles';
import type { Plugin, PluginBuild, OnResolveArgs, OnResolveResult, OnLoadArgs, OnLoadResult } from 'esbuild';
const cssModulesStyleFilter = /\.module\.(css|sass|scss|less)$/;
const CSS_LOADER_NAMESPACE = 'css-loader-namespace';
const STYLE_HANDLER_NAMESPACE = 'style-handler-namespace';
type GenerateScopedNameFunction = (name: string, filename: string, css: string) => string;
@ -27,38 +26,20 @@ const cssModulesPlugin = (options: PluginOptions): Plugin => {
build.onResolve({ filter: cssModulesStyleFilter }, onResolve);
build.onLoad({ filter: /.*/, namespace: STYLE_HANDLER_NAMESPACE }, onStyleLoad(options));
build.onLoad({ filter: /.*/, namespace: CSS_LOADER_NAMESPACE }, onCSSLoad);
},
};
};
async function onResolve(args: OnResolveArgs): Promise<OnResolveResult> {
const { namespace, resolveDir } = args;
const { resolveDir } = args;
const absolutePath = path.resolve(resolveDir, args.path);
// This is the import in the `STYLE_HANDLER_NAMESPACE` namespace.
// Put the path in the `CSS_LOADER_NAMESPACE` namespace to tell esbuild to load the css file.
if (namespace === STYLE_HANDLER_NAMESPACE) {
return {
path: absolutePath,
namespace: CSS_LOADER_NAMESPACE,
};
}
// Otherwise, generate css and put it in the `STYLE_HANDLER_NAMESPACE` namespace to handle css file
// Generate css and put it in the `STYLE_HANDLER_NAMESPACE` namespace to handle css file
return {
path: absolutePath,
namespace: STYLE_HANDLER_NAMESPACE,
};
}
async function onCSSLoad(args: OnLoadArgs): Promise<OnLoadResult> {
const data = (await fse.readFile(args.path)).toString();
return {
contents: data,
loader: 'css',
};
}
/**
* parse less/scss/css-modules to css
*/

View File

@ -2,7 +2,6 @@ import { runClientApp, getAppConfig } from '@ice/runtime';
import * as app from '@/app';
import runtimeModules from './runtimeModules';
import routes from './routes';
import Document from '@/document';
const getRouterBasename = () => {
const appConfig = getAppConfig(app);
@ -13,7 +12,6 @@ runClientApp({
app,
runtimeModules,
routes,
Document,
basename: getRouterBasename(),
hydrate: <%- hydrate %>,
memoryRouter: <%- memoryRouter || false %>,

View File

@ -1,13 +1,13 @@
import type { AppConfig } from 'ice';
import { runApp } from 'ice';
import { request, type Request } from 'ice';
import page from '@/page';
import url from './a.png';
import page from '@/page';
console.log(url);
runApp({
request,
page,
} as AppConfig)
} as AppConfig);
export default () => { };

View File

@ -1,4 +1,4 @@
var a = 1;
let a = 1;
if (true) {
a = 2;
}

View File

@ -4,10 +4,10 @@ import c from 'c';
import d from 'd';
const [e, f, ...rest] = a;
const {h, j} = b;
const { h, j } = b;
const [x, ...m] = c;
const zz = 'x';
const {k, l, ...s} = d;
const { k, l, ...s } = d;
export function getConfig() {
return {

View File

@ -1,5 +1,5 @@
var j = 2;
var i = 2;
let j = 2;
let i = 2;
while (j < 3) {
j++;
}

View File

@ -0,0 +1,71 @@
import { expect, it, describe } from 'vitest';
import formatWebpackMessages from '../src/utils/formatWebpackMessages';
describe('webpack message formatting', () => {
it('format syntax error', () => {
const result = formatWebpackMessages({
errors: [{ message: 'Syntax error: Unterminated JSX contents (8:13)' }, { message: 'Module error' }],
warnings: [],
});
expect(result.errors.length).toBe(1);
});
it('formats aliased unknown export 1', () => {
const result = formatWebpackMessages({
errors: [{ message: 'export \'bar\' (imported as \'bar2\') was not found in \'./AppUnknownExport\'' }],
warnings: [],
});
expect(result.errors[0]).toBe('Attempted import error: \'bar\' is not exported from \'./AppUnknownExport\' (imported as \'bar2\').');
});
it('formats cannot find module sass', () => {
const result = formatWebpackMessages({
errors: [{ message: '\nCannot find module.import sass from \'sass\'' }],
warnings: [],
});
expect(result.errors[0]).toBe('To import Sass files, you first need to install sass.\nRun `npm install sass` or `yarn add sass` inside your workspace.');
});
it('formats module no found', () => {
const result = formatWebpackMessages({
errors: [{ message: '\nModule not found: Cannot find file: \'./ThisFileSouldNotExist\' in \'./src\'' }],
warnings: [],
});
expect(result.errors[0]).toBe('Cannot find file: \'./ThisFileSouldNotExist\' in \'./src\'');
});
it('remove leading newline', () => {
const result = formatWebpackMessages({
errors: [{ message: 'line1\n\n\n\nline3' }],
warnings: [],
});
expect(result.errors[0]).toBe('line1\n\nline3');
});
it('nested message', () => {
const result = formatWebpackMessages({
// @ts-ignore
errors: [[{ message: 'line1' }, { message: 'line2\nline3' }]],
warnings: [],
});
expect(result.errors[0]).toBe('line2\nline3');
});
it('string message', () => {
const result = formatWebpackMessages({
// @ts-ignore
errors: ['line2\nline3'],
warnings: [],
});
expect(result.errors[0]).toBe('line2\nline3');
});
it('eslint error', () => {
const result = formatWebpackMessages({
errors: [{ message: 'Line 4:13: Parsing error: \'b\' is not defined no-undef' }],
warnings: [],
});
expect(result.errors[0]).toBe('Syntax error: \'b\' is not defined no-undef (4:13)');
});
});

View File

@ -9,7 +9,7 @@ describe('generateExports', () => {
type: false,
}]);
expect(importStr).toBe('import Router from \'react-router\';');
expect(exportStr).toBe('Router,')
expect(exportStr).toBe('Router,');
});
it('type export', () => {
const { importStr, exportStr } = generateExports([{
@ -18,7 +18,7 @@ describe('generateExports', () => {
type: true,
}]);
expect(importStr).toBe('import type Router from \'react-router\';');
expect(exportStr).toBe('Router;')
expect(exportStr).toBe('Router;');
});
it('named exports', () => {
const { importStr, exportStr } = generateExports([{
@ -34,7 +34,7 @@ describe('generateExports', () => {
source: 'react-helmet',
specifier: 'Helmet',
exportAlias: {
'Helmet': 'Head',
Helmet: 'Head',
},
}]);
expect(importStr).toBe('import Helmet from \'react-helmet\';');
@ -52,40 +52,20 @@ const defaultExportData = [{
describe('checkExportData', () => {
it('basic usage', () => {
try {
checkExportData(defaultExportData, { source: 'react-dom', specifier: 'react-dom' }, 'test-api');
expect(true).toBe(true);
} catch (err) {
expect(true).toBe(false);
}
checkExportData(defaultExportData, { source: 'react-dom', specifier: 'react-dom' }, 'test-api');
});
it('duplicate named export', () => {
try {
checkExportData(defaultExportData, defaultExportData[0], 'test-api');
expect(true).toBe(false);
} catch (err) {
expect(true).toBe(true);
}
})
expect(() => checkExportData(defaultExportData, defaultExportData[0], 'test-api')).toThrow();
});
it('duplicate exports', () => {
try {
checkExportData(defaultExportData, defaultExportData, 'test-api');
expect(true).toBe(false);
} catch (err) {
expect(true).toBe(true);
}
})
expect(() => checkExportData(defaultExportData, defaultExportData, 'test-api')).toThrow();
});
it('duplicate default export', () => {
try {
checkExportData(defaultExportData, { source: 'react-dom', specifier: 'Switch' }, 'test-api');
expect(true).toBe(false);
} catch (err) {
expect(true).toBe(true);
}
})
expect(() => checkExportData(defaultExportData, { source: 'react-dom', specifier: 'Switch' }, 'test-api')).toThrow();
});
});
describe('removeExportData', () => {
@ -102,4 +82,4 @@ describe('removeExportData', () => {
const removed = removeExportData(defaultExportData, ['react-router', 'react-helmet']);
expect(removed.length).toBe(0);
});
})
});

View File

@ -0,0 +1,12 @@
import { expect, it, describe } from 'vitest';
import openBrowser from '../src/utils/openBrowser';
describe('openBrowser in node', () => {
it('open localhost', () => {
process.env.BROWSER = 'none';
const result = openBrowser('http://localhost/');
expect(result).toBe(false);
});
// TODO simulate open browser in node
});

View File

@ -1,6 +1,6 @@
import { expect, it, describe } from 'vitest';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { expect, it, describe } from 'vitest';
import { analyzeImports, getImportPath, resolveId, type Alias } from '../src/service/analyze';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -17,7 +17,7 @@ describe('resolveId', () => {
expect(id).toBe('/.ice/runApp/test');
});
it('alias: { ice$: \'/.ice/runApp\'}; id: ice/test', () => {
const alias = { 'ice$': '/.ice/runApp' };
const alias = { ice$: '/.ice/runApp' };
const id = resolveId('ice/test', alias);
expect(id).toBe('ice/test');
});
@ -27,7 +27,7 @@ describe('resolveId', () => {
expect(id).toBe(false);
});
it('alias: { foundnamejs: \'/user/folder\'}; id: foundnamejs', () => {
const alias = { 'foundnamejs': '/user/folder' };
const alias = { foundnamejs: '/user/folder' };
const id = resolveId('foundnamejs', alias);
expect(id).toBe('/user/folder');
});
@ -46,7 +46,7 @@ describe('getImportPath', () => {
expect(filePath.replace(/^[A-Za-z]:/, '')).toBe('/rootDir/page.js');
});
it('import from alias', () => {
const filePath = getImportPath('ice', '/rootDir/test.ts', { ice: '/rootDir/component.tsx'});
const filePath = getImportPath('ice', '/rootDir/test.ts', { ice: '/rootDir/component.tsx' });
expect(filePath).toBe('/rootDir/component.tsx');
});
it('import node_module dependency', () => {
@ -57,7 +57,7 @@ describe('getImportPath', () => {
describe('analyzeImports', () => {
it('basic usage', async () => {
const entryFile = path.join(__dirname, './fixtures/preAnalyze/app.ts');
const entryFile = path.join(__dirname, './fixtures/preAnalyze/app.ts');
const analyzeSet = await analyzeImports([entryFile], {
analyzeRelativeImport: true,
alias: {
@ -65,5 +65,5 @@ describe('analyzeImports', () => {
},
});
expect([...(analyzeSet || [])]).toStrictEqual(['runApp', 'request', 'store']);
})
});
});

View File

@ -1,7 +1,7 @@
import { afterAll, expect, it } from 'vitest';
import * as path from 'path';
import fse from 'fs-extra';
import { fileURLToPath } from 'url';
import { afterAll, expect, it } from 'vitest';
import fse from 'fs-extra';
import preBundleCJSDeps from '../src/service/preBundleCJSDeps';
import { scanImports } from '../src/service/analyze';
@ -16,7 +16,7 @@ it('prebundle cjs deps', async () => {
depsInfo: deps,
cacheDir,
alias,
taskConfig: { mode: 'production' }
taskConfig: { mode: 'production' },
});
expect(fse.pathExistsSync(path.join(cacheDir, 'deps', 'react.js'))).toBeTruthy();

View File

@ -3,7 +3,7 @@ import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { expect, it, describe } from 'vitest';
import { parse, type ParserOptions } from '@babel/parser';
import traverse from '@babel/traverse'
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import removeTopLevelCodePlugin from '../src/utils/babelPluginRemoveCode';
@ -19,7 +19,7 @@ const parserOptions: ParserOptions = {
'classPrivateMethods',
'typescript',
'decorators-legacy',
]
],
};
describe('remove top level code', () => {
@ -27,19 +27,19 @@ describe('remove top level code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/export-specifier.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['getConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`const getConfig = () => {};export { getConfig };`);
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('const getConfig = () => {};export { getConfig };');
});
it('remove variable export', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/export-variable.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['getConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`export const getConfig = () => {};`);
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export const getConfig = () => {};');
});
it('remove function export', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/function-exports.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['getConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`export function getConfig() {}`);
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function getConfig() {}');
});
it('remove if statement', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/if.ts'), 'utf-8'), parserOptions);
@ -51,7 +51,7 @@ describe('remove top level code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/import.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['getConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`export function getConfig() { return { a: 1 };}`);
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('export function getConfig() { return { a: 1 };}');
});
it('remove IIFE code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/iife.ts'), 'utf-8'), parserOptions);
@ -76,14 +76,14 @@ describe('remove top level code', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/vars.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['getConfig']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`import c from 'c';import d from 'd';const [x] = c;const { k} = d;export function getConfig() { return { x, k };}`);
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('import c from \'c\';import d from \'d\';const [x] = c;const { k} = d;export function getConfig() { return { x, k };}');
});
it('keep export default', () => {
const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/export-default.ts'), 'utf-8'), parserOptions);
traverse(ast, removeTopLevelCodePlugin(['default']));
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`const a = 1;export default a;`);
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('const a = 1;export default a;');
});
it('remove expression statement', () => {
@ -92,4 +92,4 @@ describe('remove top level code', () => {
const content = generate(ast).code;
expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe('');
});
})
});

View File

@ -1,6 +1,6 @@
import { expect, it, describe } from 'vitest';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { expect, it, describe } from 'vitest';
import { scanImports } from '../src/service/analyze';
import formatPath from '../src/utils/formatPath';
@ -30,7 +30,7 @@ describe('scan import', () => {
it('scan with depImports', async () => {
const deps = await scanImports(
[path.join(__dirname, './fixtures/scan/app.ts')],
{ alias, rootDir, depImports: { '@ice/runtime': { name: '@ice/runtime' }, react: { name: 'react' } } }
{ alias, rootDir, depImports: { '@ice/runtime': { name: '@ice/runtime' }, react: { name: 'react' } } },
);
expect(deps['@ice/runtime'].name).toEqual('@ice/runtime');
expect(deps['@ice/runtime'].pkgPath).toBeUndefined();

View File

@ -1,13 +1,13 @@
import { afterAll, expect, it } from 'vitest';
import * as path from 'path';
import fse from 'fs-extra';
import { fileURLToPath } from 'url';
import { afterAll, expect, it } from 'vitest';
import fse from 'fs-extra';
import esbuild from 'esbuild';
import { createUnplugin } from 'unplugin';
import preBundleCJSDeps from '../src/service/preBundleCJSDeps';
import { scanImports } from '../src/service/analyze';
import esbuild from 'esbuild';
import transformImport from '../src/esbuild/transformImport';
import aliasPlugin from '../src/esbuild/alias';
import { createUnplugin } from 'unplugin';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const alias = { '@': path.join(__dirname, './fixtures/scan') };
@ -22,7 +22,7 @@ it('transform module import', async () => {
depsInfo: deps,
cacheDir,
alias,
taskConfig: { mode: 'production' }
taskConfig: { mode: 'production' },
});
const transformImportPlugin = createUnplugin(() => transformImport(metadata, path.join(outdir, 'server'))).esbuild;
await esbuild.build({

View File

@ -26,7 +26,7 @@ function isObject(obj: any): obj is object {
}
// Support rpx unit.
function hijackElementProps(props: { style?: object } | object): object {
export function hijackElementProps(props: { style?: object } | object): object {
if (props && STYLE in props) {
const { style } = props;
if (isObject(style)) {

View File

@ -0,0 +1,22 @@
import { expect, it, describe } from 'vitest';
import { hijackElementProps } from '../src/';
describe('hijack element', () => {
it('hijackElementProps basic', () => {
const props = hijackElementProps({ data: '', number: 1, fontSize: '12rpx' });
expect(props).toStrictEqual({
data: '', number: 1, fontSize: '12rpx',
});
});
it('hijackElementProps style', () => {
const props = hijackElementProps({ style: { fontSize: 14, height: '12px', with: '12rpx' } });
expect(props).toStrictEqual({
style: {
fontSize: 14,
height: '12px',
with: '1.6vw',
},
});
});
});

View File

@ -1,4 +1,4 @@
var a = 1;
let a = 1;
if (true) {
a = 2;
}

View File

@ -4,10 +4,10 @@ import c from 'c';
import d from 'd';
const [e, f, ...rest] = a;
const {h, j} = b;
const { h, j } = b;
const [x, ...m] = c;
const zz = 'x';
const {k, l, ...s} = d;
const { k, l, ...s } = d;
export function getConfig() {
return {

View File

@ -1,5 +1,5 @@
var j = 2;
var i = 2;
let j = 2;
let i = 2;
while (j < 3) {
j++;
}

View File

@ -49,12 +49,12 @@ describe('rewrite app worker url', () => {
appWorker: {
url: 'pha-worker.js',
source: 'test',
}
},
})).toMatchObject({
appWorker: {
url: 'app-worker.js',
source: 'test',
}
},
});
});
@ -62,7 +62,7 @@ describe('rewrite app worker url', () => {
expect(rewriteAppWorker({})).toMatchObject({
appWorker: {
url: 'app-worker.js',
}
},
});
});
});
@ -78,11 +78,11 @@ describe('transform config keys', () => {
{
pageHeader: {
includedSafeArea: true,
}
}
]
},
},
],
},
{ isRoot: true }
{ isRoot: true },
);
expect(manifestJSON.name).toStrictEqual('name');
expect(manifestJSON.offline_resources).toStrictEqual(['//g.alicdn.com/.*']);
@ -115,7 +115,7 @@ describe('transform config keys', () => {
},
],
},
{ isRoot: true }
{ isRoot: true },
);
expect(manifestJSON?.data_prefetch?.length).toBe(1);
expect(manifestJSON?.data_prefetch![0].data).toMatchObject({
@ -154,11 +154,11 @@ describe('transform config keys', () => {
text: 'text-name',
icon: '',
activeIcon: '',
}
},
],
},
},
{ isRoot: true }
{ isRoot: true },
);
expect(manifestJSON.tab_bar).toBeTruthy();
@ -189,7 +189,7 @@ describe('transform config keys', () => {
},
],
},
{ isRoot: true }
{ isRoot: true },
);
expect(manifestJSON?.pages?.length).toBe(2);
expect(manifestJSON?.pages![0].data_prefetch).toMatchObject([
@ -209,7 +209,7 @@ describe('transform config keys', () => {
{
a: 123,
},
{ isRoot: false }
{ isRoot: false },
);
expect(manifestJSON).toMatchObject({ a: 123 });
@ -220,7 +220,7 @@ describe('transform config keys', () => {
{
a: 123,
},
{ isRoot: true }
{ isRoot: true },
);
expect(manifestJSON).toMatchObject({});
@ -233,7 +233,7 @@ describe('transform config keys', () => {
'U-Tag': '${storage.uTag}',
},
},
{ isRoot: false }
{ isRoot: false },
);
expect(manifestJSON).toMatchObject({ request_headers: { 'U-Tag': '${storage.uTag}' } });
});
@ -293,7 +293,7 @@ describe('parse manifest', async () => {
'home',
'about',
'app/nest',
'404'
'404',
],
};
const manifest = await parseManifest(phaManifest, {
@ -403,7 +403,7 @@ describe('parse manifest', async () => {
} catch (err) {
expect(true).toBe(true);
}
})
});
it('url failed with new URL', async () => {
const phaManifest = {
@ -417,7 +417,7 @@ describe('parse manifest', async () => {
});
expect(manifest.tab_bar?.url).toBe('{{xxx}}/tabBar');
expect(manifest.tab_bar?.name).toBe('{{xxx}}');
})
});
it('should not inject html when tabHeader & tabBar have url field', async () => {
const phaManifest = {
@ -426,14 +426,14 @@ describe('parse manifest', async () => {
pageHeader: {
source: 'pages/Header',
url: 'https://m.taobao.com',
}
}
},
},
],
tabBar: {
custom: true,
source: 'pages/CustomTabBar',
items: ['home', 'frame1'],
}
},
};
const manifest = await parseManifest(phaManifest, {
@ -477,8 +477,8 @@ describe('get multiple manifest', async () => {
'app/nest',
{
url: 'https://m.taobao.com',
}
]
},
],
},
'about',
],

View File

@ -165,7 +165,7 @@ export default class IntersectionObserver {
if (!Array.isArray(threshold)) threshold = [threshold];
return threshold.sort().filter((t, i, a) => {
if (typeof t != 'number' || isNaN(t) || t < 0 || t > 1) {
if (typeof t != 'number' || 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];
@ -515,12 +515,12 @@ function throttle(fn, timeout) {
* @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
* @param {boolean} useCapture Optionally adds the even to the capture
* phase. Note: this only works in modern browsers.
*/
function addEvent(node, event, fn, opt_useCapture) {
function addEvent(node, event, fn, useCapture) {
if (typeof node.addEventListener == 'function') {
node.addEventListener(event, fn, opt_useCapture || false);
node.addEventListener(event, fn, useCapture || false);
} else if (typeof node.attachEvent == 'function') {
node.attachEvent(`on${event}`, fn);
}
@ -532,12 +532,12 @@ function addEvent(node, event, fn, opt_useCapture) {
* @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
* @param {boolean} 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) {
function removeEvent(node, event, fn, useCapture) {
if (typeof node.removeEventListener == 'function') {
node.removeEventListener(event, fn, opt_useCapture || false);
node.removeEventListener(event, fn, useCapture || false);
} else if (typeof node.detatchEvent == 'function') {
node.detatchEvent(`on${event}`, fn);
}

View File

@ -1,5 +1,7 @@
import type { ReactElement } from 'react';
type ChildrenElement<T> = ChildrenElement<T>[] | T;
// Mocked `Rax.shared`.
const shared = {
get Element(): any {
@ -27,7 +29,7 @@ function warningCompat(message: string) {
console.error(`[RaxCompat] ${stack}`);
}
function flattenChildren(children: ReactElement) {
function flattenChildren(children: ChildrenElement<ReactElement>) {
if (children == null) {
return children;
}
@ -39,7 +41,7 @@ function flattenChildren(children: ReactElement) {
return result.length - 1 ? result : result[0];
}
function traverseChildren(children: ReactElement, result: ReactElement[]) {
function traverseChildren(children: ChildrenElement<ReactElement>, result: ReactElement[]) {
if (Array.isArray(children)) {
for (let i = 0, l = children.length; i < l; i++) {
traverseChildren(children[i], result);

View File

@ -72,7 +72,7 @@ function handleIntersect(entries: IntersectionObserverEntry[]) {
} = entry;
// No `top` value in polyfill.
const currentY = boundingClientRect.y || boundingClientRect.top;
const beforeY = parseInt(target.getAttribute('data-before-current-y')) || currentY;
const beforeY = parseInt(target.getAttribute('data-before-current-y'), 10) || currentY;
// is in view
if (

View File

@ -1,25 +1,25 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import Children from '../src/children';
import { render } from '@testing-library/react';
import Children from '../src/children';
const arrText = ['hello', 'rax'];
describe('children', () => {
it('should work with map', () => {
function Hello({ children }) {
return <div data-testid="test">
return (<div data-testid="test">
{
Children.map(children, (child, i) => {
if (i === 0) {
return <span>{arrText[1]}</span>
return <span>{arrText[1]}</span>;
}
return child;
})
}
</div>
</div>);
}
const instance = (
<Hello>
<span>{arrText[0]}</span>
@ -29,10 +29,10 @@ describe('children', () => {
const wrapper = render(instance);
const node = wrapper.queryByTestId('test');
expect(node.children.item(0).textContent).toBe(node.children.item(0).textContent);
expect(node.children.item(1).textContent).toBe(arrText[1]);
})
});
it('should work with forEach', () => {
function Hello({ children }) {
@ -40,9 +40,9 @@ describe('children', () => {
expect(child.type).toBe('span');
expect(child.props.children).toBe(arrText[i]);
});
return children
return children;
}
const instance = (
<Hello>
<span>{arrText[0]}</span>
@ -51,14 +51,14 @@ describe('children', () => {
);
render(instance);
})
});
it('should work with count', () => {
function Hello({ children }) {
expect(Children.count(children)).toBe(arrText.length);
return children
return children;
}
const instance = (
<Hello>
<span>{arrText[0]}</span>
@ -67,15 +67,15 @@ describe('children', () => {
);
render(instance);
})
});
it('should work with only', () => {
let child = <span>{arrText[0]}</span>;
function Hello({ children }) {
expect(Children.only(children)).toBe(child);
return children
return children;
}
const instance = (
<Hello>
{
@ -85,14 +85,14 @@ describe('children', () => {
);
render(instance);
})
});
it('should work with toArray', () => {
function Hello({ children }) {
expect(Children.toArray(children).length).toBe(arrText.length);
return children
return children;
}
const instance = (
<Hello>
<span>{arrText[0]}</span>
@ -101,7 +101,7 @@ describe('children', () => {
);
render(instance);
})
});
it('should escape keys', () => {
const zero = <div key="1" />;
@ -139,7 +139,7 @@ describe('children', () => {
null,
<span key="y" />,
kid,
kid && React.cloneElement(kid, {key: 'z'}),
kid && React.cloneElement(kid, { key: 'z' }),
<hr />,
],
);

View File

@ -1,8 +1,8 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import { render } from '@testing-library/react';
import { Component } from '../src/index';
import cloneElement from '../src/clone-element';
import { render } from '@testing-library/react';
describe('cloneElement', () => {
it('basic', () => {

View File

@ -1,7 +1,7 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import { Component, PureComponent, memo } from '../src/index';
import { render } from '@testing-library/react';
import { Component, PureComponent, memo } from '../src/index';
describe('component', () => {
it('Component should work', () => {
@ -31,8 +31,8 @@ describe('component', () => {
});
it('memo should work', () => {
const MyComponent = memo(function MyComponent(props: { text: string, id: string }) {
return <div id={props.id}>{props.text}</div>
const MyComponent = memo((props: { text: string; id: string }) => {
return <div id={props.id}>{props.text}</div>;
});
const wrapper = render(<MyComponent id="" text="memo demo" />);

View File

@ -1,6 +1,6 @@
import { expect, it, describe, vi } from 'vitest';
import { createElement } from '../src/index';
import { render } from '@testing-library/react';
import { createElement } from '../src/index';
describe('createElement', () => {
it('basic', () => {
@ -8,7 +8,7 @@ describe('createElement', () => {
const wrapper = render(createElement(
'div',
null,
str
str,
));
expect(wrapper.container.childNodes[0].textContent).toBe(str);
});
@ -17,32 +17,32 @@ describe('createElement', () => {
let appearFun = vi.spyOn({
func: () => {
expect(appearFun).toHaveBeenCalled();
}
}, 'func')
},
}, 'func');
const str = 'hello world';
render(createElement(
'div',
{
onAppear: appearFun
onAppear: appearFun,
},
str
str,
));
});
it('should work with onDisappear', () => {
const func = () => {
expect(disappearFun).toHaveBeenCalled();
}
};
let disappearFun = vi.spyOn({
func: func
}, 'func')
func: func,
}, 'func');
const str = 'hello world';
render(createElement(
'div',
{
onDisappear: func,
},
str
str,
));
});
@ -53,10 +53,10 @@ describe('createElement', () => {
{
'data-testid': 'rpxTest',
style: {
width: '300rpx'
}
width: '300rpx',
},
},
str
str,
));
const node = wrapper.queryByTestId('rpxTest');
@ -84,7 +84,7 @@ describe('createElement', () => {
{
'data-testid': 'maxlengthTest',
value: str,
maxlength: ''
maxlength: '',
},
));

View File

@ -1,23 +1,13 @@
import { expect, it, describe } from 'vitest';
import { Component } from '../src/index';
import createFactory from '../src/create-factory';
import { render } from '@testing-library/react';
import createFactory from '../src/create-factory';
describe('createFactory', () => {
it('basic', () => {
class CustomComponent extends Component {
text: string = '';
render() {
return <div>{this.props.text}</div>;
}
}
const factory = createFactory("div");
const div = factory(null,'div node');
const factory = createFactory('div');
const div = factory(null, 'div node');
const wrapper = render(div);
const node = wrapper.queryByTestId('div');
let res = wrapper.getAllByText('div node');
expect(res.length).toBe(1);

View File

@ -1,8 +1,8 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import createPortal from '../src/create-portal';
import { render } from '@testing-library/react';
import createPortal from '../src/create-portal';
describe('createPortal', () => {
it('basic', () => {
@ -11,13 +11,13 @@ describe('createPortal', () => {
const Portal = ({ children }) => {
return createPortal(children, div);
};
function App() {
return <div>
return (<div>
<Portal>
<text>Hello Rax</text>
</Portal>
</div>
</div>);
}
render(<App />);

View File

@ -1,16 +1,16 @@
import { expect, it, describe } from 'vitest';
import React from 'react';
import createReactClass from '../src/create-class';
import { render } from '@testing-library/react';
import createReactClass from '../src/create-class';
describe('createReactClass', () => {
it('basic', () => {
const ReactClass = createReactClass({
name: '',
id: '',
render: function() {
render: function () {
return <div data-testid={this.props.id}>Hello, {this.props.name}</div>;
}
},
});
const wrapper = render(<ReactClass id="reactClassId" name="raxCompat" />);

View File

@ -1,10 +1,10 @@
import { expect, it, describe, vi } from 'vitest';
import React, { Children } from 'react';
import React from 'react';
import { render } from '@testing-library/react';
import { useRef, useEffect } from '../src/index';
import { createElement } from '../src/create-element';
import findDOMNode from '../src/find-dom-node';
import { render } from '@testing-library/react';
import transformPrototypes from '../src/prototypes'
import transformPrototypes from '../src/prototypes';
describe('events', () => {
it('should work with onclick', () => {
@ -13,20 +13,20 @@ describe('events', () => {
const obj = {
handleClick: () => console.log('click'),
}
};
const click = vi.spyOn(obj, 'handleClick')
const click = vi.spyOn(obj, 'handleClick');
useEffect(() => {
const dom = findDOMNode(ref.current);
dom.click();
expect(click).toHaveBeenCalled()
expect(click).toHaveBeenCalled();
});
return createElement('div', {
onclick: obj.handleClick,
ref: ref
}, 'click me')
ref: ref,
}, 'click me');
}
render(<App />);
@ -34,82 +34,82 @@ describe('events', () => {
it('should work with ontouchstart', () => {
expect(transformPrototypes({
ontouchstart: () => { }
ontouchstart: () => { },
}).onTouchStart).toBeInstanceOf(Function);
});
it('should work with onclick', () => {
expect(transformPrototypes({
onclick: () => { }
onclick: () => { },
}).onClick).toBeInstanceOf(Function);
expect(transformPrototypes({
onclick: () => { }
onclick: () => { },
}).onclick).toBe(undefined);
});
it('should work with onClick', () => {
expect(transformPrototypes({
onClick: () => { }
onClick: () => { },
}).onClick).toBeInstanceOf(Function);
});
it('should work with ondblclick', () => {
expect(transformPrototypes({
ondblclick: () => { }
ondblclick: () => { },
}).onDoubleClick).toBeInstanceOf(Function);
expect(transformPrototypes({
ondblclick: () => { }
ondblclick: () => { },
}).ondblclick).toBe(undefined);
});
it('should work with onDblclick', () => {
expect(transformPrototypes({
onDblclick: () => { }
onDblclick: () => { },
}).onDoubleClick).toBeInstanceOf(Function);
expect(transformPrototypes({
onDblclick: () => { }
onDblclick: () => { },
}).onDblclick).toBe(undefined);
});
it('should work with onDoubleClick', () => {
expect(transformPrototypes({
onDoubleClick: () => { }
onDoubleClick: () => { },
}).onDoubleClick).toBeInstanceOf(Function);
});
it('should work with onmouseenter', () => {
expect(transformPrototypes({
onmouseenter: () => { }
onmouseenter: () => { },
}).onMouseEnter).toBeInstanceOf(Function);
expect(transformPrototypes({
onmouseenter: () => { }
onmouseenter: () => { },
}).onmouseenter).toBe(undefined);
});
it('should work with onpointerenter', () => {
expect(transformPrototypes({
onpointerenter: () => { }
onpointerenter: () => { },
}).onPointerEnter).toBeInstanceOf(Function);
expect(transformPrototypes({
onpointerenter: () => { }
onpointerenter: () => { },
}).onpointerenter).toBe(undefined);
});
it('should work with onchange', () => {
expect(transformPrototypes({
onchange: () => { }
onchange: () => { },
}).onChange).toBeInstanceOf(Function);
expect(transformPrototypes({
onchange: () => { }
onchange: () => { },
}).onchange).toBe(undefined);
});
it('should work with onbeforeinput', () => {
expect(transformPrototypes({
onbeforeinput: () => { }
onbeforeinput: () => { },
}).onBeforeInput).toBeInstanceOf(Function);
expect(transformPrototypes({
onbeforeinput: () => { }
onbeforeinput: () => { },
}).onbeforeinput).toBe(undefined);
});
});

View File

@ -1,8 +1,8 @@
import { expect, it, describe } from 'vitest';
import React from 'react';
import { render } from '@testing-library/react';
import { useRef, useEffect } from '../src/index';
import findDOMNode from '../src/find-dom-node';
import { render } from '@testing-library/react';
describe('findDomNode', () => {
it('basic', () => {

View File

@ -1,15 +1,15 @@
import { expect, it, describe } from 'vitest';
import React from 'react';
import Fragment from '../src/fragment';
import { render } from '@testing-library/react';
import Fragment from '../src/fragment';
describe('fragment', () => {
it('basic', () => {
function App() {
return <Fragment>
return (<Fragment>
<header>A heading</header>
<footer>A footer</footer>
</Fragment>;
</Fragment>);
}
const wrapper = render(<App />);

View File

@ -1,5 +1,6 @@
import { expect, it, describe, vi } from 'vitest';
import React from 'react';
import { render } from '@testing-library/react';
import {
useState,
useEffect,
@ -8,12 +9,11 @@ import {
useContext,
useRef,
} from '../src/hooks';
import { render } from '@testing-library/react';
describe('hooks', () => {
it('useState', () => {
function App() {
const [state, setState] = useState({ text: 'text' });
const [state] = useState({ text: 'text' });
expect(state.text).toBe('text');
return <div>{state.text}</div>;
}
@ -30,6 +30,8 @@ describe('hooks', () => {
setLoading(false);
expect(loading).toBe(false);
}, 1);
// Expect useEffect to execute once
// eslint-disable-next-line
}, []);
return <div>{loading ? 'loading...' : 'load end'}</div>;
}
@ -41,11 +43,11 @@ describe('hooks', () => {
let useEffectFunc = vi.spyOn({
func: () => {
expect(useEffectFunc).toHaveBeenCalled();
}
}, 'func')
},
}, 'func');
function App() {
useEffect(useEffectFunc, []);
return <div>useEffect</div>;
}
@ -56,11 +58,11 @@ describe('hooks', () => {
let useEffectFunc = vi.spyOn({
func: () => {
expect(useEffectFunc).toHaveBeenCalled();
}
}, 'func')
},
}, 'func');
function App() {
useLayoutEffect(useEffectFunc, []);
return <div>useEffect</div>;
}
@ -69,7 +71,7 @@ describe('hooks', () => {
it('useContext', () => {
const Context = createContext({
theme: 'dark'
theme: 'dark',
});
function App() {
const context = useContext(Context);
@ -79,7 +81,7 @@ describe('hooks', () => {
const wrapper = render(<App />);
wrapper.findAllByText('dark').then((res) => {
expect(res.length).toBe(1);
});
});
});
it('useRef', () => {
@ -87,15 +89,14 @@ describe('hooks', () => {
const inputEl = useRef(null);
useEffect(() => {
expect(inputEl.current).instanceOf(Element);
})
});
return (
<>
<input ref={inputEl} type="text" />
</>
<>
<input ref={inputEl} type="text" />
</>
);
}
render(<TextInputWithFocusButton />);
});
});

View File

@ -5,9 +5,9 @@ import isValidElement from '../src/is-valid-element';
describe('isValidElement', () => {
it('basic', () => {
function App() {
return <div>
return (<div>
<div>isValidElement</div>
</div>;
</div>);
}
expect(isValidElement(<App />)).toBe(true);

View File

@ -4,10 +4,24 @@ import { shared } from '../src/index';
describe('render', () => {
it('basic', () => {
console.log('shared.Element', shared)
console.log('shared.Element', shared);
expect(shared.Element).toBe(null);
expect(shared.Host).toBe(null);
expect(shared.Instance).toBe(null);
expect(shared.flattenChildren).instanceOf(Function);
});
it('flattenChildren null', () => {
// @ts-ignore e
expect(shared.flattenChildren(null)).toBe(null);
});
it('flattenChildren common', () => {
expect(shared.flattenChildren(<>div</>)).toStrictEqual(<React.Fragment>div</React.Fragment>);
});
it('flattenChildren array', () => {
const children = [[[<>div</>]]];
expect(shared.flattenChildren(children)).toStrictEqual(<React.Fragment>div</React.Fragment>);
});
});

View File

@ -1,6 +1,6 @@
import { expect, test, describe } from 'vitest';
import path from 'path';
import { fileURLToPath } from 'url';
import { expect, test, describe } from 'vitest';
import { generateRouteManifest, formatNestedRouteManifest } from '../src/index';
const __dirname = path.dirname(fileURLToPath(import.meta.url));

View File

@ -1,6 +1,6 @@
import { expect, test, describe } from 'vitest';
import path from 'path';
import { fileURLToPath } from 'url';
import { expect, test, describe } from 'vitest';
import { generateRouteManifest } from '../src/index';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -33,7 +33,7 @@ describe('generateRouteManifest function', () => {
});
test('invalid-routes', () => {
expect(() => generateRouteManifest(path.join(fixturesDir, 'invalid-routes'))).toThrow(`invalid character in 'src/pages/[a.pdf].tsx'. Only support char: -, \\w, /`);
expect(() => generateRouteManifest(path.join(fixturesDir, 'invalid-routes'))).toThrow('invalid character in \'src/pages/[a.pdf].tsx\'. Only support char: -, \\w, /');
});
test('ignore-routes', () => {
@ -43,11 +43,11 @@ describe('generateRouteManifest function', () => {
test('define-extra-routes', () => {
const routeManifest = generateRouteManifest(
path.join(fixturesDir, 'basic-routes'),
['About/index.tsx'],
path.join(fixturesDir, 'basic-routes'),
['About/index.tsx'],
(defineRoute) => {
defineRoute('/about-me', 'About/index.tsx');
}
},
);
expect(routeManifest).toMatchSnapshot();
});

View File

@ -54,7 +54,7 @@ export function Links(props) {
<>
{
routeLinks.map(link => {
const { block, ...routeLinkProps } = link;
const { block: ignored, ...routeLinkProps } = link;
return <link key={link.href} {...props} {...routeLinkProps} data-route-link />;
})
}
@ -104,7 +104,7 @@ export function Scripts(props) {
/>
{
routeScripts.map(script => {
const { block, ...routeScriptProps } = script;
const { block: ignored, ...routeScriptProps } = script;
return <script key={script.src} {...props} {...routeScriptProps} data-route-script />;
})
}

View File

@ -18,11 +18,11 @@ export default function getAppConfig(appExport: AppExport): AppConfig {
return {
app: {
...defaultAppConfig.app,
...(appConfig.app || {}),
...(app || {}),
},
router: {
...defaultAppConfig.router,
...(appConfig.router || {}),
...(router || {}),
},
...others,
};

View File

@ -15,7 +15,6 @@ export default function matchRoutes(
const matchRoutesFn = process.env.ICE_CORE_ROUTER === 'true' ? originMatchRoutes : matchRoutesSingle;
let matches = matchRoutesFn(routes as unknown as RouteObject[], location, basename);
if (!matches) return [];
return matches.map(({ params, pathname, pathnameBase, route }) => ({
params,
pathname,

View File

@ -147,7 +147,7 @@ export function createRouteElements(
});
}
function RouteComponent({ id }: { id: string }) {
export function RouteComponent({ id }: { id: string }) {
// get current route component from latest routeModules
const { routeModules } = useAppContext();
const { default: Component } = routeModules[id] || {};
@ -169,7 +169,6 @@ export function filterMatchesToLoad(prevMatches: RouteMatch[], currentMatches: R
let isNew = (match: RouteMatch, index: number) => {
// [a] -> [a, b]
if (!prevMatches[index]) return true;
// [a, b] -> [a, c]
return match.route.id !== prevMatches[index].route.id;
};

View File

@ -4,7 +4,7 @@ import { createHashHistory, createBrowserHistory, createMemoryHistory } from 'hi
import type { HashHistory, BrowserHistory, Action, Location, InitialEntry, MemoryHistory } from 'history';
import type {
AppContext, AppExport, RouteItem, AppRouterProps, RoutesData, RoutesConfig,
RouteWrapperConfig, RuntimeModules, RouteMatch, RouteModules, AppConfig, DocumentComponent,
RouteWrapperConfig, RuntimeModules, RouteMatch, RouteModules, AppConfig,
} from '@ice/types';
import { createHistory as createHistorySingle } from './single-router.js';
import { setHistory } from './history.js';
@ -22,7 +22,6 @@ interface RunClientAppOptions {
app: AppExport;
routes: RouteItem[];
runtimeModules: RuntimeModules;
Document: DocumentComponent;
hydrate: boolean;
basename?: string;
memoryRouter?: boolean;
@ -35,7 +34,6 @@ export default async function runClientApp(options: RunClientAppOptions) {
app,
routes,
runtimeModules,
Document,
basename,
hydrate,
memoryRouter,
@ -71,7 +69,7 @@ export default async function runClientApp(options: RunClientAppOptions) {
routesData = await loadRoutesData(matches, requestContext, routeModules);
}
if (!routesConfig) {
routesConfig = getRoutesConfig(matches, routesConfig, routeModules);
routesConfig = getRoutesConfig(matches, routesData, routeModules);
}
const appContext: AppContext = {
@ -98,15 +96,14 @@ export default async function runClientApp(options: RunClientAppOptions) {
await Promise.all(runtimeModules.map(m => runtime.loadModule(m)).filter(Boolean));
render({ runtime, Document, history });
render({ runtime, history });
}
interface RenderOptions {
history: History;
runtime: Runtime;
Document: DocumentComponent;
}
async function render({ history, runtime, Document }: RenderOptions) {
async function render({ history, runtime }: RenderOptions) {
const appContext = runtime.getAppContext();
const { appConfig } = appContext;
const render = runtime.getRender();
@ -122,7 +119,6 @@ async function render({ history, runtime, Document }: RenderOptions) {
AppProvider={AppProvider}
RouteWrappers={RouteWrappers}
AppRouter={AppRouter}
Document={Document}
/>,
);
}
@ -133,7 +129,6 @@ interface BrowserEntryProps {
AppProvider: React.ComponentType<any>;
RouteWrappers: RouteWrapperConfig[];
AppRouter: React.ComponentType<AppRouterProps>;
Document: DocumentComponent;
}
interface HistoryState {
@ -151,7 +146,6 @@ interface RouteState {
function BrowserEntry({
history,
appContext,
Document,
...rest
}: BrowserEntryProps) {
const {
@ -231,7 +225,7 @@ function BrowserEntry({
* Prepare for the next pages.
* Load modulesgetPageData and preLoad the custom assets.
*/
async function loadNextPage(
export async function loadNextPage(
currentMatches: RouteMatch[],
preRouteState: RouteState,
) {

View File

@ -1,13 +1,33 @@
import { expect, test, describe } from 'vitest';
import { defineAppConfig } from '../src/appConfig.js';
import { expect, it, describe } from 'vitest';
import getAppConfig, { defineAppConfig } from '../src/appConfig.js';
describe('AppConfig', () => {
test('defineAppConfig', () => {
it('getAppConfig', () => {
const appConfig = getAppConfig({
default: {
app: {
rootId: 'app',
},
},
auth: {},
});
expect(appConfig).toStrictEqual({
app: {
rootId: 'app',
strict: false,
},
router: {
type: 'browser',
},
});
});
it('defineAppConfig', () => {
const appConfig = {};
expect(defineAppConfig(appConfig)).toEqual(appConfig);
});
test('defineAppConfig fn', () => {
it('defineAppConfig fn', () => {
const appConfig = {};
expect(defineAppConfig(() => appConfig)).toEqual(appConfig);
});

View File

@ -0,0 +1,33 @@
import { expect, it, describe } from 'vitest';
import { createStaticNavigator } from '../src/server/navigator';
describe('mock server navigator', () => {
const staticNavigator = createStaticNavigator();
it('createHref', () => {
expect(staticNavigator.createHref('/')).toBe('/');
});
it('push', () => {
expect(() => staticNavigator.push('/')).toThrow();
});
it('replace', () => {
expect(() => staticNavigator.replace('/')).toThrow();
});
it('go', () => {
expect(() => staticNavigator.go(1)).toThrow();
});
it('back', () => {
expect(() => staticNavigator.back()).toThrow();
});
it('forward', () => {
expect(() => staticNavigator.forward()).toThrow();
});
it('block', () => {
expect(() => staticNavigator.block()).toThrow();
});
});

View File

@ -1,82 +0,0 @@
import { expect, test, describe } from 'vitest';
import { filterMatchesToLoad } from '../src/routes.js';
describe('routes', () => {
test('filter new matches', () => {
const oldMatches = [
{
pathname: '/',
route: {
id: '/page/layout'
}
},
{
pathname: '/',
route: {
id: '/page/home'
}
}
];
const newMatches = [
{
pathname: '/',
route: {
id: '/page/layout'
}
},
{
pathname: '/about',
route: {
id: '/page/about'
}
}
];
// @ts-ignore
const matches = filterMatchesToLoad(oldMatches, newMatches);
expect(
matches
).toEqual([{
pathname: '/about',
route: {
id: '/page/about'
}
}]);
});
test('filter matches with path changed', () => {
const oldMatches = [
{
pathname: '/users/123',
route: {
id: '/users/123'
}
}
];
const newMatches = [
{
pathname: '/users/456',
route: {
id: '/users/456'
}
}
];
// @ts-ignore
const matches = filterMatchesToLoad(oldMatches, newMatches);
expect(
matches
).toEqual([
{
pathname: '/users/456',
route: {
id: '/users/456'
}
}
]);
});
});

View File

@ -0,0 +1,322 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { expect, it, describe, beforeEach, afterEach, vi } from 'vitest';
import type { RouteComponent as IRouteComponent } from '@ice/types/esm/runtime';
import RouteWrapper from '../src/RouteWrapper';
import { AppContextProvider } from '../src/AppContext';
import {
filterMatchesToLoad,
createRouteElements,
RouteComponent,
loadRouteModules,
loadRoutesData,
getRoutesConfig,
} from '../src/routes.js';
describe('routes', () => {
let windowSpy;
beforeEach(() => {
windowSpy = vi.spyOn(global, 'window', 'get');
});
afterEach(() => {
windowSpy.mockRestore();
});
Object.defineProperty(window, 'open', open);
const homeItem = {
default: () => <></>,
getConfig: () => ({ title: 'home' }),
getData: async () => ({ type: 'getData' }),
getServerData: async () => ({ type: 'getServerData' }),
getStaticData: async () => ({ type: 'getStaticData' }),
};
const aboutItem = {
default: () => <></>,
getConfig: () => ({ title: 'about' }),
};
const routeModules = [
{
id: 'home',
load: async () => {
return homeItem as IRouteComponent;
},
},
{
id: 'about',
load: async () => {
return aboutItem as IRouteComponent;
},
},
];
it('route Component', () => {
const domstring = renderToString(
// @ts-ignore
<AppContextProvider value={{ routeModules: {
home: {
default: () => <div>home</div>,
},
} }}
>
<RouteComponent id="home" />
</AppContextProvider>,
);
expect(domstring).toBe('<div>home</div>');
});
it('route error', () => {
const currentEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
expect(() => renderToString(
// @ts-ignore
<AppContextProvider value={{ routeModules: {} }}>
<RouteComponent id="home" />
</AppContextProvider>,
)).toThrow();
process.env.NODE_ENV = currentEnv;
});
it('load route modules', async () => {
windowSpy.mockImplementation(() => ({}));
const routeModule = await loadRouteModules(routeModules, { home: homeItem });
expect(routeModule).toStrictEqual({
home: homeItem,
about: aboutItem,
});
});
it('load error module', async () => {
const routeModule = await loadRouteModules([{
id: 'error',
// @ts-ignore
load: async () => {
throw new Error('err');
return {};
},
}], {});
expect(routeModule).toStrictEqual({
error: undefined,
});
});
it('load route data', async () => {
const routeModule = await loadRouteModules(routeModules);
const routesDataSSG = await loadRoutesData(
// @ts-ignore
[{ route: routeModules[0] }],
{},
routeModule,
'SSG',
);
const routesDataSSR = await loadRoutesData(
// @ts-ignore
[{ route: routeModules[0] }],
{},
routeModule,
'SSR',
);
const routesDataCSR = await loadRoutesData(
// @ts-ignore
[{ route: routeModules[0] }],
{},
routeModule,
'CSR',
);
expect(routesDataSSG).toStrictEqual({
home: {
type: 'getStaticData',
},
});
expect(routesDataSSR).toStrictEqual({
home: {
type: 'getServerData',
},
});
expect(routesDataCSR).toStrictEqual({
home: {
type: 'getData',
},
});
});
it('load data from __ICE_DATA_LOADER__', async () => {
windowSpy.mockImplementation(() => ({
__ICE_DATA_LOADER__: async (id) => ({ id: `${id}_data` }),
}));
const routesData = await loadRoutesData(
// @ts-ignore
[{ route: routeModules[0] }],
{},
{},
'SSG',
);
expect(routesData).toStrictEqual({
home: {
id: 'home_data',
},
});
});
it('get routes config', async () => {
const routeModule = await loadRouteModules(routeModules);
const routesConfig = getRoutesConfig(
// @ts-ignore
[{ route: routeModules[0] }],
{ home: {} },
routeModule,
);
expect(routesConfig).toStrictEqual({
home: {
title: 'home',
},
});
});
it('get routes config when failed get route module', async () => {
const routesConfig = getRoutesConfig(
// @ts-ignore
[{ route: routeModules[0] }],
{ home: {} },
{},
);
expect(routesConfig).toStrictEqual({
home: {},
});
});
it('create route element', () => {
const routeElement = createRouteElements([{
path: '/',
id: 'home',
componentName: 'home',
}]);
expect(routeElement).toEqual([{
componentName: 'home',
element: (
<RouteWrapper id="home">
<RouteComponent
id="home"
/>
</RouteWrapper>
),
id: 'home',
path: '/',
}]);
});
it('create route with children', () => {
const routeElement = createRouteElements([{
path: '/',
id: 'home',
componentName: 'home',
children: [{
path: '/about',
id: 'about',
componentName: 'about',
}],
}]);
expect(routeElement).toEqual([{
componentName: 'home',
element: (
<RouteWrapper id="home">
<RouteComponent
id="home"
/>
</RouteWrapper>
),
children: [{
componentName: 'about',
element: (
<RouteWrapper id="about">
<RouteComponent
id="about"
/>
</RouteWrapper>
),
id: 'about',
path: '/about',
}],
id: 'home',
path: '/',
}]);
});
it('filter new matches', () => {
const oldMatches = [
{
pathname: '/',
route: {
id: '/page/layout',
},
},
{
pathname: '/',
route: {
id: '/page/home',
},
},
];
const newMatches = [
{
pathname: '/',
route: {
id: '/page/layout',
},
},
{
pathname: '/about',
route: {
id: '/page/about',
},
},
];
// @ts-ignore
const matches = filterMatchesToLoad(oldMatches, newMatches);
expect(
matches,
).toEqual([{
pathname: '/about',
route: {
id: '/page/about',
},
}]);
});
it('filter matches with path changed', () => {
const oldMatches = [
{
pathname: '/users/123',
route: {
id: '/users/123',
},
},
];
const newMatches = [
{
pathname: '/users/456',
route: {
id: '/users/456',
},
},
];
// @ts-ignore
const matches = filterMatchesToLoad(oldMatches, newMatches);
expect(
matches,
).toEqual([
{
pathname: '/users/456',
route: {
id: '/users/456',
},
},
]);
});
});

View File

@ -0,0 +1,69 @@
import { expect, it, vi, describe, beforeEach, afterEach } from 'vitest';
import { updateRoutesConfig } from '../src/routesConfig';
describe('routes config', () => {
let documentSpy;
const insertTags: any[] = [];
const appendTags: any[] = [];
beforeEach(() => {
documentSpy = vi.spyOn(global, 'document', 'get');
documentSpy.mockImplementation(() => ({
head: {
querySelector: () => ({
content: '',
}),
insertBefore: (tag) => {
insertTags.push(tag);
},
appendChild: (tag) => {
appendTags.push(tag);
tag.onload();
},
},
getElementById: () => null,
querySelectorAll: () => [],
createElement: (type) => {
const element = {
type,
setAttribute: (attr, value) => {
element[attr] = value;
},
};
return element;
},
}));
});
afterEach(() => {
documentSpy.mockRestore();
});
it('update routes config', async () => {
const routesConfig = {
home: {
title: 'home',
meta: [
{
name: 'theme-color',
content: '#eee',
},
],
links: [{
href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
rel: 'stylesheet',
}],
scripts: [{
src: 'https://cdn.jsdelivr.net/npm/lodash@2.4.1/dist/lodash.min.js',
}],
},
};
// @ts-ignore
await updateRoutesConfig([{ route: { id: 'home' } }], routesConfig);
expect(insertTags.length).toBe(1);
expect(insertTags[0]?.type).toBe('meta');
expect(appendTags.length).toBe(2);
expect(appendTags[0]?.type).toBe('link');
expect(appendTags[1]?.type).toBe('script');
});
});

View File

@ -0,0 +1,350 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { expect, it, vi, describe, beforeEach, afterEach } from 'vitest';
import runClientApp, { loadNextPage } from '../src/runClientApp';
import { useAppData } from '../src/AppData';
import { useConfig, useData } from '../src/RouteContext';
describe('run client app', () => {
let windowSpy;
let documentSpy;
const mockData = {
location: new URL('http://localhost:4000/'),
history: {
replaceState: vi.fn(),
},
addEventListener: vi.fn(),
};
beforeEach(() => {
process.env.ICE_CORE_ROUTER = 'true';
windowSpy = vi.spyOn(global, 'window', 'get');
documentSpy = vi.spyOn(global, 'document', 'get');
windowSpy.mockImplementation(() => mockData);
documentSpy.mockImplementation(() => ({
head: {
querySelector: () => ({
content: '',
}),
},
getElementById: () => null,
querySelectorAll: () => [],
}));
});
afterEach(() => {
windowSpy.mockRestore();
documentSpy.mockRestore();
});
let domstring = '';
const serverRuntime = async ({ setRender }) => {
setRender((container, element) => {
try {
domstring = renderToString(element as any);
} catch (err) {}
});
};
const wrapperRuntime = async ({ addWrapper }) => {
const RouteWrapper = ({ children }) => {
return <div>{children}</div>;
};
addWrapper(RouteWrapper, true);
};
const providerRuntmie = async ({ addProvider }) => {
const Provider = ({ children }) => {
return <div>{children}</div>;
};
addProvider(Provider);
// Add twice.
addProvider(Provider);
};
const basicRoutes = [
{
id: 'home',
path: '/',
componentName: 'Home',
load: async () => ({
default: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const appData = useAppData();
return (
<div>home{appData?.msg || ''}</div>
);
},
getConfig: () => ({ title: 'home' }),
getData: async () => ({ data: 'test' }),
}),
},
];
it('run client basic', async () => {
windowSpy.mockImplementation(() => ({
...mockData,
location: new URL('http://localhost:4000/?test=1&runtime=true&baisc'),
}));
await runClientApp({
app: {},
routes: basicRoutes,
runtimeModules: [serverRuntime],
hydrate: false,
});
expect(domstring).toBe('<div>home</div>');
});
it('run client single-router', async () => {
process.env.ICE_CORE_ROUTER = '';
await runClientApp({
app: {},
routes: basicRoutes,
runtimeModules: [serverRuntime],
hydrate: false,
});
process.env.ICE_CORE_ROUTER = 'true';
expect(domstring).toBe('<div>home</div>');
});
it('run client with wrapper', async () => {
await runClientApp({
app: {},
routes: basicRoutes,
runtimeModules: [serverRuntime, wrapperRuntime],
hydrate: true,
});
expect(domstring).toBe('<div><div>home</div></div>');
});
it('run client with app provider', async () => {
await runClientApp({
app: {},
routes: basicRoutes,
runtimeModules: [serverRuntime, providerRuntmie],
hydrate: true,
});
expect(domstring).toBe('<div><div><div>home</div></div></div>');
});
it('run client with empty route', async () => {
await runClientApp({
app: {},
routes: [],
runtimeModules: [serverRuntime],
hydrate: false,
});
});
it('run client with memory router', async () => {
const routes = [...basicRoutes, {
id: 'about',
path: '/about',
componentName: 'About',
load: async () => ({
default: () => {
return (
<div>about</div>
);
},
}),
}];
await runClientApp({
app: {
default: {
router: {
type: 'memory',
initialEntries: ['/about'],
},
},
},
routes,
runtimeModules: [serverRuntime],
hydrate: true,
});
expect(domstring).toBe('<div>about</div>');
await runClientApp({
app: {
default: {
},
},
routes,
runtimeModules: [serverRuntime],
hydrate: true,
});
});
it('run client with memory router - from context', async () => {
windowSpy.mockImplementation(() => ({
...mockData,
__ICE_APP_CONTEXT__: {
routePath: '/about',
},
}));
const routes = [...basicRoutes, {
id: 'about',
path: '/about',
componentName: 'About',
load: async () => ({
default: () => {
return (
<div>about</div>
);
},
}),
}];
await runClientApp({
app: {
},
routes,
runtimeModules: [serverRuntime],
hydrate: true,
memoryRouter: true,
});
expect(domstring).toBe('<div>about</div>');
});
it('run client with hash router', async () => {
await runClientApp({
app: {
default: {
router: {
type: 'hash',
},
},
},
routes: basicRoutes,
runtimeModules: [serverRuntime],
hydrate: true,
});
expect(domstring).toBe('<div>home</div>');
});
it('run client with app data', async () => {
let executed = false;
await runClientApp({
app: {
getAppData: async () => {
executed = true;
return { msg: '-getAppData' };
},
},
routes: basicRoutes,
runtimeModules: [serverRuntime],
hydrate: false,
});
expect(domstring).toBe('<div>home<!-- -->-getAppData</div>');
expect(executed).toBe(true);
});
it('run client with app data', async () => {
let useGlobalLoader = false;
let executed = false;
windowSpy.mockImplementation(() => ({
...mockData,
__ICE_DATA_LOADER__: async () => {
useGlobalLoader = true;
return { msg: '-globalData' };
},
}));
await runClientApp({
app: {
getAppData: async () => {
executed = true;
return { msg: 'app' };
},
},
routes: basicRoutes,
runtimeModules: [serverRuntime],
hydrate: false,
});
expect(executed).toBe(false);
expect(useGlobalLoader).toBe(true);
expect(domstring).toBe('<div>home<!-- -->-globalData</div>');
});
it('run client with AppErrorBoundary', async () => {
await runClientApp({
app: {
default: {
app: {
errorBoundary: true,
},
},
},
routes: [{
id: 'home',
path: '/',
componentName: 'Home',
load: async () => ({
default: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const config = useConfig();
// eslint-disable-next-line react-hooks/rules-of-hooks
const data = useData();
return (
<div>home{data?.data}{config.title}</div>
);
},
getConfig: () => ({ title: 'home' }),
getData: async () => ({ data: 'test' }),
}),
}],
runtimeModules: [serverRuntime],
hydrate: false,
});
expect(domstring).toBe('<div>home<!-- -->test<!-- -->home</div>');
});
it('load next page', async () => {
const homePage = {
default: () => <></>,
getConfig: () => ({ title: 'home' }),
getData: async () => ({ type: 'getDataHome' }),
};
const aboutPage = {
default: () => <></>,
getConfig: () => ({ title: 'about' }),
getData: async () => ({ type: 'getDataAbout' }),
};
const mockedModules = [
{
id: 'home',
load: async () => {
return homePage;
},
},
{
id: 'about',
load: async () => {
return aboutPage;
},
},
];
const { routesData, routesConfig, routeModules } = await loadNextPage(
// @ts-ignore
[{ route: mockedModules[0] }],
{
// @ts-ignore
matches: [{ route: mockedModules[1] }],
routesData: {},
routeModules: {},
},
);
expect(routesData).toStrictEqual({
home: { type: 'getDataHome' },
});
expect(routesConfig).toStrictEqual({
home: {
title: 'home',
},
});
expect(routeModules).toStrictEqual({
home: homePage,
});
});
});

View File

@ -0,0 +1,212 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import { renderToHTML, renderToResponse } from '../src/runServerApp';
import { Meta, Title, Links, Main, Scripts } from '../src/Document';
describe('run server app', () => {
process.env.ICE_CORE_ROUTER = 'true';
const basicRoutes = [
{
id: 'home',
path: 'home',
componentName: 'Home',
load: async () => ({
default: () => {
return (
<div>home</div>
);
},
getConfig: () => ({ title: 'home' }),
getData: async () => ({ data: 'test' }),
}),
},
];
const assetsManifest = {
publicPath: '/',
assets: {},
entries: [],
pages: {
home: ['js/home.js'],
},
};
const Document = () => (
<html>
<head>
<meta charSet="utf-8" />
<meta name="description" content="ICE 3.0 Demo" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Title />
<Links />
</head>
<body>
<Main />
<Scripts />
</body>
</html>
);
it('render to html', async () => {
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/home',
},
}, {
app: {},
assetsManifest,
runtimeModules: [],
routes: basicRoutes,
Document,
renderMode: 'SSR',
});
// @ts-ignore
expect(html?.value?.includes('<div>home</div>')).toBe(true);
// @ts-ignore
expect(html?.value?.includes('js/home.js')).toBe(true);
});
it('render to html basename', async () => {
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/home',
},
}, {
app: {},
assetsManifest,
runtimeModules: [],
routes: basicRoutes,
Document,
renderMode: 'SSR',
basename: '/ice',
});
// @ts-ignore
expect(html?.statusCode).toBe(404);
});
it('render to html serverOnlyBasename', async () => {
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/home',
},
}, {
app: {},
assetsManifest,
runtimeModules: [],
routes: basicRoutes,
Document,
renderMode: 'SSR',
serverOnlyBasename: '/',
basename: '/ice',
});
// @ts-ignore
expect(html?.statusCode).toBe(200);
});
it('render to 404 html', async () => {
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/about',
},
}, {
app: {},
assetsManifest,
runtimeModules: [],
routes: basicRoutes,
Document,
});
// @ts-ignore
expect(html?.statusCode).toBe(404);
});
it('router hash', async () => {
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/home',
},
}, {
app: {
default: {
router: {
type: 'hash',
},
},
},
assetsManifest,
runtimeModules: [],
routes: basicRoutes,
Document,
});
// @ts-ignore
expect(html?.statusCode).toBe(200);
// @ts-ignore
expect(html?.value?.includes('<div>home</div>')).toBe(false);
});
it('fallback to csr', async () => {
const html = await renderToHTML({
// @ts-ignore
req: {
url: '/home',
},
}, {
app: {},
assetsManifest,
runtimeModules: [],
routes: [{
id: 'home',
path: 'home',
componentName: 'Home',
load: async () => ({
default: () => {
throw new Error('err');
return (
<div>home</div>
);
},
}),
}],
Document,
});
// @ts-ignore
expect(html?.value?.includes('<div>home</div>')).toBe(false);
// @ts-ignore
expect(html?.value?.includes('js/home.js')).toBe(true);
});
it('render to response', async () => {
let htmlContent = '';
await renderToResponse({
// @ts-ignore
req: {
url: '/home',
},
res: {
destination: {},
// @ts-ignore
setHeader: () => {},
// @ts-ignore
end: (content) => {
htmlContent = content;
},
},
}, {
app: {},
assetsManifest,
runtimeModules: [],
routes: basicRoutes,
Document,
renderMode: 'SSR',
routePath: '/',
documentOnly: true,
});
expect(!!htmlContent).toBe(true);
expect(htmlContent.includes('<div>home</div')).toBe(false);
});
});

View File

@ -0,0 +1,67 @@
import React from 'react';
import { expect, it, describe } from 'vitest';
import {
useRoutes,
Router,
createHistory,
matchRoutes,
Link,
Outlet,
useParams,
useSearchParams,
useLocation,
useNavigate,
} from '../src/single-router';
describe('single route api', () => {
it('useRoutes', () => {
expect(useRoutes([{ element: <div>test</div> }])).toStrictEqual(
<React.Fragment>
<div>
test
</div>
</React.Fragment>);
});
it('Router', () => {
expect(Router({ children: <div>test</div> })).toStrictEqual(
<React.Fragment>
<div>
test
</div>
</React.Fragment>);
});
it('createHistory', () => {
expect(createHistory().location).toBe('');
});
it('matchRoutes', () => {
expect(matchRoutes([{}])[0].pathname).toBe('');
});
it('Link', () => {
expect(Link()).toBe(null);
});
it('Outlet', () => {
expect(Outlet()).toStrictEqual(<React.Fragment />);
});
it('useParams', () => {
expect(useParams()).toStrictEqual({});
});
it('useSearchParams', () => {
expect(useSearchParams()[0]).toStrictEqual({});
});
it('useLocation', () => {
expect(useLocation()).toStrictEqual({});
});
it('useNavigate', () => {
expect(useNavigate()).toStrictEqual({});
});
});

View File

@ -3,58 +3,58 @@ import { importStyle } from '../src/index';
describe('import style', () => {
it('simple import', async () => {
const sourceCode = `import { Button } from 'antd';`;
const sourceCode = 'import { Button } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result?.code).toBe(`${sourceCode}\nimport 'antd/es/button/style';`);
});
it('custom style', async () => {
const sourceCode = `import { Button } from 'antd';`;
const sourceCode = 'import { Button } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: (name) => `antd/es/${name.toLocaleLowerCase()}/style` });
expect(result?.code).toBe(`${sourceCode}\nimport 'antd/es/button/style';`);
});
it('mismatch import', async () => {
const sourceCode = `import { Button } from 'antd-mobile';`;
const sourceCode = 'import { Button } from \'antd-mobile\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result?.code).toBe(`${sourceCode}`);
});
it('multiple import', async () => {
const sourceCode = `import { Button, Table } from 'antd';`;
const sourceCode = 'import { Button, Table } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result?.code).toBe(`${sourceCode}\nimport 'antd/es/button/style';\nimport 'antd/es/table/style';`);
});
it('named import', async () => {
const sourceCode = `import { Button as Btn } from 'antd';`;
const sourceCode = 'import { Button as Btn } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result?.code).toBe(`${sourceCode}\nimport 'antd/es/button/style';`);
});
it('default import', async () => {
const sourceCode = `import * as antd from 'antd';`;
const sourceCode = 'import * as antd from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result?.code).toBe(`${sourceCode}`);
});
it('sourcemap', async () => {
const sourceCode = `import * as antd from 'antd';`;
const sourceCode = 'import * as antd from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true, sourceMap: true });
expect(!!result?.map).toBe(true);
});
it('none import', async () => {
const sourceCode = `export const a = 'antd';`;
const sourceCode = 'export const a = \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result).toBe(null);
});
it('parse error', async () => {
const sourceCode = `export antd, { Button } from 'antd';`;
const sourceCode = 'export antd, { Button } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result).toBe(null);
});
it('import error', async () => {
const sourceCode = `import antd, { Button } from 'antd';`;
const sourceCode = 'import antd, { Button } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: true });
expect(result?.code).toBe(sourceCode);
});
it('style false', async () => {
const sourceCode = `import { Button } from 'antd';`;
const sourceCode = 'import { Button } from \'antd\';';
const result = await importStyle(sourceCode, { libraryName: 'antd', style: false });
expect(result).toBe(null);
});

View File

@ -144,8 +144,12 @@ export interface RouteModules {
export interface AssetsManifest {
dataLoader?: string;
publicPath: string;
entries: string[];
pages: string[];
entries: {
[assetPath: string]: string[];
};
pages: {
[assetPath: string]: string[];
};
assets?: {
[assetPath: string]: string;
};

View File

@ -1,7 +1,7 @@
import { expect, describe, it } from 'vitest';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { expect, describe, it } from 'vitest';
import { redirectImport } from '../src/unPlugins/redirectImport';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -14,8 +14,8 @@ describe('redirect import', () => {
specifier: 'Head',
source: 'react-helmet',
exportAlias: {
'Head': 'Helmet',
}
Head: 'Helmet',
},
}, {
specifier: 'store',
source: '@ice/store',
@ -53,5 +53,5 @@ describe('redirect import', () => {
const code = fs.readFileSync(path.join(__dirname, './fixtures/multiple.js'), 'utf-8');
const transformed = await redirectImport(code, { exportData, targetSource: 'ice' });
expect(transformed).toBe('import request from \'axios\';\nimport store from \'@ice/store\';');
})
})
});
});

View File

@ -76,6 +76,35 @@ importers:
typescript: 4.7.4
vitest: 0.15.2_c8@7.12.0+jsdom@20.0.0
examples/app-config:
specifiers:
'@ice/app': workspace:*
'@ice/plugin-auth': workspace:*
'@ice/plugin-rax-compat': workspace:*
'@ice/runtime': workspace:*
'@types/react': ^18.0.0
'@types/react-dom': ^18.0.2
'@uni/env': ^1.1.0
ahooks: ^3.3.8
react: ^18.0.0
react-dom: ^18.0.0
speed-measure-webpack-plugin: ^1.5.0
webpack: ^5.73.0
dependencies:
'@ice/app': link:../../packages/ice
'@ice/plugin-auth': link:../../packages/plugin-auth
'@ice/plugin-rax-compat': link:../../packages/plugin-rax-compat
'@ice/runtime': link:../../packages/runtime
'@uni/env': 1.1.0
ahooks: 3.4.1_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
devDependencies:
'@types/react': 18.0.17
'@types/react-dom': 18.0.6
speed-measure-webpack-plugin: 1.5.0_webpack@5.74.0
webpack: 5.74.0
examples/basic-project:
specifiers:
'@ice/app': workspace:*
@ -539,7 +568,7 @@ importers:
semver: ^7.3.5
temp: ^0.9.4
trusted-cert: ^1.1.3
unplugin: ^0.8.0
unplugin: ^0.9.0
webpack: ^5.73.0
webpack-dev-server: ^4.7.4
dependencies:
@ -596,7 +625,7 @@ importers:
'@types/temp': 0.9.1
chokidar: 3.5.3
react: 18.2.0
unplugin: 0.8.1_3qmdnfoccgmcaqi5n264fyeyfi
unplugin: 0.9.5_3qmdnfoccgmcaqi5n264fyeyfi
webpack: 5.74.0_esbuild@0.14.54
webpack-dev-server: 4.8.1_webpack@5.74.0
@ -18258,31 +18287,6 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
/unplugin/0.8.1_3qmdnfoccgmcaqi5n264fyeyfi:
resolution: {integrity: sha512-o7rUZoPLG1fH4LKinWgb77gDtTE6mw/iry0Pq0Z5UPvZ9+HZ1/4+7fic7t58s8/CGkPrDpGq+RltO+DmswcR4g==}
peerDependencies:
esbuild: '>=0.13'
rollup: ^2.50.0
vite: ^2.3.0 || ^3.0.0-0
webpack: 4 || 5
peerDependenciesMeta:
esbuild:
optional: true
rollup:
optional: true
vite:
optional: true
webpack:
optional: true
dependencies:
acorn: 8.8.0
chokidar: 3.5.3
esbuild: 0.14.54
webpack: 5.74.0_esbuild@0.14.54
webpack-sources: 3.2.3
webpack-virtual-modules: 0.4.4
dev: true
/unplugin/0.9.5_3qmdnfoccgmcaqi5n264fyeyfi:
resolution: {integrity: sha512-luraheyfxwtvkvHpsOvMNv7IjLdORTWKZp0gWYNHGLi2ImON3iIZOj464qEyyEwLA/EMt12fC415HW9zRpOfTg==}
peerDependencies:

View File

@ -0,0 +1,38 @@
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture, setupBrowser } from '../utils/build';
import { startFixture, setupStartBrowser } from '../utils/start';
import type { Page } from '../utils/browser';
import type Browser from '../utils/browser';
const example = 'app-config';
describe(`build ${example}`, () => {
test('open /', async () => {
await buildFixture(example);
await setupBrowser({ example });
}, 120000);
});
describe(`start ${example}`, () => {
let page: Page;
let browser: Browser;
test('setup devServer', async () => {
const { devServer, port } = await startFixture(example);
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
await page.push('ice');
expect(await page.$$text('h1')).toStrictEqual(['home']);
}, 120000);
test('error page', async () => {
await page.push('ice/error');
await page.waitForNetworkIdle();
expect(await page.$$text('h1')).toStrictEqual(['Something went wrong.']);
}, 120000);
afterAll(async () => {
await browser.close();
});
});

View File

@ -1,10 +1,11 @@
import { expect, test, describe, afterAll } from 'vitest';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture, setupBrowser } from '../utils/build';
import { startFixture, setupStartBrowser } from '../utils/start';
import Browser, { Page } from '../utils/browser';
import type { Page } from '../utils/browser';
import type Browser from '../utils/browser';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -44,7 +45,7 @@ describe(`build ${example}`, () => {
test('disable splitChunks', async () => {
await buildFixture(example, {
config: 'splitChunks.config.mts'
config: 'splitChunks.config.mts',
});
const res = await setupBrowser({ example });
page = res.page;
@ -62,47 +63,73 @@ describe(`build ${example}`, () => {
describe(`start ${example}`, () => {
let page: Page;
let browser: Browser;
const rootDir = path.join(__dirname, `../../examples/${example}`);
test('setup devServer', async () => {
const { devServer, port } = await startFixture(example);
const { devServer, port } = await startFixture(example, {
mock: true,
force: true,
https: false,
analyzer: false,
open: false,
mode: 'start',
});
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
expect(await page.$$text('h2')).toStrictEqual(['Home Page']);
expect(await page.$$text('#data-from')).toStrictEqual(['getServerData']);
}, 120000);
// TODO: fix waitForNetworkIdle not resolved
test.skip('should update config during client routing', async () => {
const { devServer, port } = await startFixture(example);
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
test('update route', async () => {
const targetPath = path.join(rootDir, 'src/pages/blog.tsx');
const routeContent = fs.readFileSync(targetPath, 'utf-8');
const routeManifest = fs.readFileSync(path.join(rootDir, '.ice/route-manifest.json'), 'utf-8');
fs.writeFileSync(targetPath, routeContent);
await page.reload();
expect(JSON.parse(routeManifest)[0].children.length).toBe(3);
}, 120000);
test('update watched file: global.css', async () => {
const targetPath = path.join(rootDir, 'src/global.css');
const cssContent = fs.readFileSync(targetPath, 'utf-8');
fs.writeFileSync(targetPath, cssContent);
await page.reload();
});
test('update watched file: app.ts', async () => {
const targetPath = path.join(rootDir, 'src/app.tsx');
const appContent = fs.readFileSync(targetPath, 'utf-8');
fs.writeFileSync(targetPath, appContent);
await page.reload();
});
test('should update config during client routing', async () => {
expect(
await page.title()
await page.title(),
).toBe('Home');
expect(
await page.$$attr('meta[name="theme-color"]', 'content')
await page.$$attr('meta[name="theme-color"]', 'content'),
).toStrictEqual(['#000']);
await page.click('a[href="/about"]');
await page.push('about');
await page.waitForNetworkIdle();
expect(
await page.title()
await page.title(),
).toBe('About');
expect(
await page.$$attr('meta[name="theme-color"]', 'content')
await page.$$attr('meta[name="theme-color"]', 'content'),
).toStrictEqual(['#eee']);
expect(
await page.$$eval('link[href*="bootstrap"]', (els) => els.length)
await page.$$eval('link[href*="bootstrap"]', (els) => els.length),
).toBe(1);
expect(
await page.$$eval('script[src*="lodash"]', (els) => els.length)
await page.$$eval('script[src*="lodash"]', (els) => els.length),
).toBe(1);
}, 120000);

View File

@ -1,7 +1,7 @@
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture, setupBrowser } from '../utils/build';
import { startFixture, setupStartBrowser } from '../utils/start';
import Browser from '../utils/browser';
import type Browser from '../utils/browser';
import type { Page } from '../utils/browser';
const example = 'hash-router';
@ -15,8 +15,8 @@ describe(`build ${example}`, () => {
const res = await setupBrowser({ example, disableJS: false });
page = res.page as Page;
browser = res.browser;
await page.waitForFunction(`document.getElementsByTagName('h1').length > 0`);
await page.waitForFunction(`document.getElementsByTagName('h2').length > 0`);
await page.waitForFunction('document.getElementsByTagName(\'h1\').length > 0');
await page.waitForFunction('document.getElementsByTagName(\'h2\').length > 0');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Home']);
}, 120000);
@ -28,15 +28,13 @@ describe(`build ${example}`, () => {
describe(`start ${example}`, () => {
let page: Page;
let browser: Browser;
test('open /', async () => {
const { devServer, port } = await startFixture(example);
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
await page.waitForFunction(`document.getElementsByTagName('h1').length > 0`);
await page.waitForFunction(`document.getElementsByTagName('h2').length > 0`);
await page.waitForFunction('document.getElementsByTagName(\'h1\').length > 0');
await page.waitForFunction('document.getElementsByTagName(\'h2\').length > 0');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Home']);
}, 120000);

View File

@ -3,7 +3,7 @@ import * as fs from 'fs';
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture, setupBrowser } from '../utils/build';
import { startFixture, setupStartBrowser } from '../utils/start';
import { Page } from '../utils/browser';
import type { Page } from '../utils/browser';
const example = 'rax-project';
@ -39,7 +39,7 @@ describe(`start ${example}`, () => {
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
await page.waitForFunction(`document.getElementsByTagName('span').length > 0`);
await page.waitForFunction('document.getElementsByTagName(\'span\').length > 0');
expect((await page.$$text('span')).length).toEqual(3);
expect((await page.$$text('span'))[0]).toStrictEqual('Welcome to Your Rax App');
expect((await page.$$text('span'))[1]).toStrictEqual('More information about Rax');

View File

@ -1,7 +1,7 @@
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture, setupBrowser } from '../utils/build';
import { startFixture, setupStartBrowser } from '../utils/start';
import { Page } from '../utils/browser';
import type { Page } from '../utils/browser';
const example = 'routes-generate';

View File

@ -1,6 +1,6 @@
import { expect, test, describe, afterAll } from 'vitest';
import * as path from 'path';
import * as fs from 'fs';
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture } from '../utils/build';
const example = 'single-route';
@ -11,22 +11,20 @@ describe(`build ${example}`, () => {
test('optimize router', async () => {
await buildFixture(example, {
config: 'optimization.config.mts'
config: 'optimization.config.mts',
});
const dataLoaderPath = path.join(__dirname, `../../examples/${example}/build/js/framework.js`);
sizeWithOptimize = fs.statSync(dataLoaderPath).size;
}, 120000);
test('disable optimize router', async () => {
await buildFixture(example);
const dataLoaderPath = path.join(__dirname, `../../examples/${example}/build/js/framework.js`);
sizeWithoutOptimize = fs.statSync(dataLoaderPath).size;
}, 120000);
afterAll(async () => {
expect(sizeWithOptimize).toBeLessThan(sizeWithoutOptimize);
expect(sizeWithoutOptimize - sizeWithOptimize).toBeGreaterThan(6 * 1024) // reduce more than 6kb after minify
expect(sizeWithoutOptimize - sizeWithOptimize).toBeGreaterThan(6 * 1024); // reduce more than 6kb after minify
});
});

View File

@ -14,7 +14,7 @@ describe(`build ${example}`, () => {
const res = await setupBrowser({ example, disableJS: false });
page = res.page;
browser = res.browser;
await page.waitForFunction(`document.getElementsByTagName('button').length > 0`);
await page.waitForFunction('document.getElementsByTagName(\'button\').length > 0');
expect(await page.$$text('#username')).toStrictEqual(['name: ICE 3']);
expect(await page.$$text('#count')).toStrictEqual(['0']);
}, 120000);

View File

@ -1,15 +1,15 @@
import http from 'http';
import url from 'url';
import fse from 'fs-extra';
import path from 'path';
import fse from 'fs-extra';
import puppeteer from 'puppeteer';
export interface IPage extends puppeteer.Page {
html?: () => Promise<string>;
$text?: (selector: string, trim?: boolean) => Promise<string|null>;
$$text?: (selector: string, trim?: boolean) => Promise<(string|null)[]>;
$attr?: (selector: string, attr: string) => Promise<string|null>;
$$attr?: (selector: string, attr: string) => Promise<(string|null)[]>;
$text?: (selector: string, trim?: boolean) => Promise<string | null>;
$$text?: (selector: string, trim?: boolean) => Promise<(string | null)[]>;
$attr?: (selector: string, attr: string) => Promise<string | null>;
$$attr?: (selector: string, attr: string) => Promise<(string | null)[]>;
push?: (url: string, options?: puppeteer.WaitForOptions & { referer?: string }) => Promise<puppeteer.HTTPResponse>;
}
@ -24,7 +24,7 @@ export default class Browser {
private browser: puppeteer.Browser;
private baseUrl: string;
constructor (options: IBrowserOptions) {
constructor(options: BrowserOptions) {
const { server } = options;
if (server) {
this.server = server;
@ -74,17 +74,17 @@ export default class Browser {
}).listen(port, '127.0.0.1');
}
async start () {
async start() {
this.browser = await puppeteer.launch();
}
async close () {
if (!this.browser) { return }
async close() {
if (!this.browser) { return; }
await this.browser.close();
this.server.close();
}
async page (url: string, disableJS?: boolean) {
async page(url: string, disableJS?: boolean) {
this.baseUrl = url;
if (!this.browser) { throw new Error('Please call start() before page(url)'); }
const page: Page = await this.browser.newPage();
@ -98,11 +98,11 @@ export default class Browser {
page.html = () =>
page.evaluate(() => window.document.documentElement.outerHTML);
page.$text = (selector, trim) => page.$eval(selector, (el, trim) => {
return trim ? (el.textContent || '').replace(/^\s+|\s+$/g, '') : el.textContent
return trim ? (el.textContent || '').replace(/^\s+|\s+$/g, '') : el.textContent;
}, trim);
page.$$text = (selector, trim) =>
page.$$eval(selector, (els, trim) => els.map((el) => {
return trim ? (el.textContent || '').replace(/^\s+|\s+$/g, '') : el.textContent
return trim ? (el.textContent || '').replace(/^\s+|\s+$/g, '') : el.textContent;
}), trim);
page.$attr = (selector, attr) =>

View File

@ -1,9 +1,10 @@
import path from 'path';
import process from 'process';
import getPort from 'get-port';
import Browser, { Page } from './browser';
import createService from '../../packages/ice/src/createService';
import { fileURLToPath } from 'url';
import getPort from 'get-port';
import createService from '../../packages/ice/src/createService';
import type { Page } from './browser';
import Browser from './browser';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -22,14 +23,16 @@ interface IReturn {
}
// get builtIn plugins
export const buildFixture = async function(example: string, commandArgs?: Record<string, string>) {
export const buildFixture = async function (example: string, commandArgs?: Record<string, string>) {
const rootDir = path.join(__dirname, `../../examples/${example}`);
process.env.DISABLE_FS_CACHE = 'true';
const service = await createService({ rootDir, command: 'build', commandArgs: {
const service = await createService({ rootDir,
command: 'build',
commandArgs: {
...(commandArgs || {}),
} });
await service.run();
}
};
export const setupBrowser: SetupBrowser = async (options) => {
const { example, outputDir = 'build', defaultHtml = 'index.html', disableJS = true } = options;
@ -37,11 +40,12 @@ export const setupBrowser: SetupBrowser = async (options) => {
const port = await getPort();
const browser = new Browser({ cwd: path.join(rootDir, outputDir), port });
await browser.start();
console.log()
// when preview html generate by build, the path will not match the router info, so hydrate will not found the route component
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 {
browser,
page,
}
}
};
};

View File

@ -6,4 +6,4 @@ export default (order: string, cwd: string) => {
stdio: 'inherit',
cwd,
});
}
};

View File

@ -1,14 +1,15 @@
import path from 'path';
import getPort from 'get-port';
import Browser, { Page } from './browser';
import { Server } from 'http';
import createService from '../../packages/ice/src/createService';
import type { Server } from 'http';
import { fileURLToPath } from 'url';
import getPort from 'get-port';
import createService from '../../packages/ice/src/createService';
import type { Page } from './browser';
import Browser from './browser';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
interface ISetupBrowser {
(options: { port: number; defaultPath?: string; server: Server; }): Promise<IReturn>;
interface SetupBrowser {
(options: { port: number; defaultPath?: string; server: Server }): Promise<ReturnValue>;
}
interface IReturn {
@ -17,25 +18,28 @@ interface IReturn {
}
// get builtIn plugins
export const startFixture = async function (example: string) {
export const startFixture = async function (example: string, commandArgs?: Record<string, any>) {
const port = await getPort();
const rootDir = path.join(__dirname, `../../examples/${example}`);
const processCwdSpy = jest.spyOn(process, 'cwd');
processCwdSpy.mockReturnValue(rootDir);
process.env.DISABLE_FS_CACHE = 'true';
const service = await createService({ rootDir, command: 'start', commandArgs: {
const service = await createService({ rootDir,
command: 'start',
commandArgs: {
host: '0.0.0.0',
port,
open: false,
}});
...commandArgs,
} });
// @ts-ignore
const { compiler, devServer } = await service.run();
// wait generate assets manifest
await new Promise((resolve) => {
compiler.hooks.done.tap('done',() => {
compiler.hooks.done.tap('done', () => {
resolve(true);
})
});
});
// @ts-ignore
@ -60,7 +64,7 @@ export const startFixture = async function (example: string) {
}) as any as Server;
return {
port,
devServer
devServer,
};
};

View File

@ -3,4 +3,4 @@ export default ({ modifyUserConfig }) => {
modifyUserConfig('minify', false);
// disable sourceMap to speed-up fixture start
modifyUserConfig('sourceMap', false);
}
};

View File

@ -24,6 +24,8 @@ export default defineConfig({
include: ['**/packages/**'],
exclude: [
'**/bundles/compiled/**',
// App runtime has been tested by unit test case
'**/packages/runtime/esm/**',
'**/tests/**',
],
},

View File

@ -3,7 +3,7 @@ const path = require('path');
module.exports = function (config, options) {
return {
name: 'docusaurus-redirect-plugin',
async contentLoaded({ content, actions }) {
async contentLoaded({ actions }) {
const { createData, addRoute } = actions;
const routes = [

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';
export default function Redirect(props) {
const { location, history, redirectConfig = [] } = props;
@ -28,7 +28,7 @@ export default function Redirect(props) {
console.log('未知路由', pathname);
}
}
}, []);
}, [location, history, pathname, redirectConfig]);
return null;
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import clsx from 'clsx';
import storage from '../utils/storage';
import { isIntranet } from '../utils/internal';