Skip to content

Commit

Permalink
feat: make router-api work with mp
Browse files Browse the repository at this point in the history
  • Loading branch information
wangjinyang authored Aug 27, 2021
1 parent 97a27b2 commit 45aeacd
Show file tree
Hide file tree
Showing 27 changed files with 736 additions and 68 deletions.
170 changes: 143 additions & 27 deletions packages/platform-mp/src/lib/bundler/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path';
import { IApi, APIHooks } from '@shuvi/types';
import { IApi, APIHooks, Runtime } from '@shuvi/types';
import { BUNDLER_TARGET_SERVER } from '@shuvi/shared/lib/constants';
import { rankRouteBranches } from '@shuvi/router';
import { PACKAGE_NAME } from '../constants';
import fs from 'fs';
import BuildAssetsPlugin from './plugins/build-assets-plugin';
Expand All @@ -9,6 +10,7 @@ import DomEnvPlugin from './plugins/dom-env-plugin';
import modifyStyle from './modifyStyle';
import {
resolveAppFile,
resolveRouterFile,
resolveDep,
resolveLib,
PACKAGE_RESOLVED
Expand Down Expand Up @@ -46,42 +48,26 @@ function withExts(file: string, extensions: string[]): string[] {
}

export interface AppConfig extends TaroAppConfig {
entryPagePath?: string;
darkMode: boolean;
}

export interface PageConfigs {
[name: string]: Config;
}

export function installPlatform(api: IApi) {
const appConfigFile = api.helpers.fileSnippets.findFirstExistedFile([
...withExts(api.resolveUserFile('app.config'), moduleFileExtensions)
]);
const appConfig: AppConfig = appConfigFile ? readConfig(appConfigFile) : {};

if (isEmptyObject(appConfig)) {
throw new Error('缺少 app 全局配置文件,请检查!');
}
const appPages = appConfig.pages;
if (!appPages || !appPages.length) {
throw new Error('全局配置缺少 pages 字段,请检查!');
}

let appPages: string[] = [];
const pageConfigs: PageConfigs = {};
(appConfig.pages || []).forEach(page => {
const pageFile = api.resolveUserFile(`${page}`);
const pageConfigFile = api.helpers.fileSnippets.findFirstExistedFile(
withExts(api.resolveUserFile(`${page}.config`), moduleFileExtensions)
);
const pageConfig = pageConfigFile ? readConfig(pageConfigFile) : {};
pageConfigs[page] = pageConfig;
api.addAppFile({
name: `${page}.js`,
content: () => `
import { createPageConfig } from '@tarojs/runtime';
import pageComponent from '${pageFile}'
const pageConfig = ${JSON.stringify(pageConfig)}
const inst = Page(createPageConfig(pageComponent, '${page}', {root:{cn:[]}}, pageConfig || {}))
`
});
});

const { themeLocation, darkmode: darkMode } = appConfig;
let themeFilePath: string = '';
Expand All @@ -94,24 +80,154 @@ export function installPlatform(api: IApi) {
api.addAppPolyfill(resolveDep('react-app-polyfill/ie11'));
api.addAppPolyfill(resolveDep('react-app-polyfill/stable'));
api.addAppExport(resolveAppFile('App'), '{ default as App }');
api.addAppExport(resolveAppFile('head/head'), '{default as Head}');
api.addAppExport(resolveAppFile('dynamic'), '{default as dynamic}');
// api.addAppExport(resolveAppFile('head/head'), '{default as Head}');
// api.addAppExport(resolveAppFile('dynamic'), '{default as dynamic}');
api.addAppExport(
resolveLib('@shuvi/router-react'),
'{ useParams, useRouter, useCurrentRoute, Link, RouterView, withRouter }'
'{ useParams, useRouter, useCurrentRoute, RouterView, withRouter }'
);
api.addEntryCode(`require('${PACKAGE_NAME}/lib/runtime')`);

api.addAppService(resolveRouterFile('lib', 'index'), '*', 'router-mp.js');

let pageFiles: any[];

let mpPathToRoutesDone: any;
const PromiseRoutes: Promise<any> = new Promise(resolve => {
mpPathToRoutesDone = resolve;
});
// this hooks works before webpack bundler
// orders can make sure appConfig.page and pageConfigs has correctly value
api.tap<APIHooks.IHookAppRoutes>('app:routes', {
name: 'mpPathToRoutes',
fn: async routes => {
type IUserRouteHandlerWithoutChildren = Omit<
Runtime.IUserRouteConfig,
'children'
>;
// map url to component
let routesMap: [string, string][] = [];
const routesName = new Set<string>();
// flatten routes remove children
function flattenRoutes(
apiRoutes: Runtime.IUserRouteConfig[],
branches: IUserRouteHandlerWithoutChildren[] = [],
parentPath = ''
): IUserRouteHandlerWithoutChildren[] {
apiRoutes.forEach(route => {
const { children, component } = route;
let tempPath = path.join(parentPath, route.path);

if (children) {
flattenRoutes(children, branches, tempPath);
}
if (component) {
branches.push({
path: tempPath,
component
});
}
});
return branches;
}

function removeConfigPathAddMpPath(
routes: IUserRouteHandlerWithoutChildren[]
) {
for (let i = routes.length - 1; i >= 0; i--) {
const route = routes[i];
const { component } = route;
if (component) {
// remove config path, eg: miniprogram/src/pages/index/index.config.js
if (/.*\.config\.\w+$/.test(component)) {
routes.splice(i, 1);
} else {
let tempMpPath = component;
if (tempMpPath.startsWith(api.paths.pagesDir)) {
// ensure path relate to pagesDir
tempMpPath = path.relative(api.paths.pagesDir, tempMpPath);
// Remove the file extension from the end
tempMpPath = tempMpPath.replace(/\.\w+$/, '');
}
// ensure path starts with pages
if (!tempMpPath.startsWith('pages')) {
tempMpPath = path.join('pages', tempMpPath);
}
if (route.path !== tempMpPath) {
// generate routesMap
routesMap.push([route.path, tempMpPath]);
route.path = tempMpPath;
}
routesName.add(route.path);
}
}
}
}
routes = flattenRoutes(routes);
removeConfigPathAddMpPath(routes);
let rankRoutes = routesMap.map(r => [r[0], r] as [string, typeof r]);
rankRoutes = rankRouteBranches(rankRoutes);
routesMap = rankRoutes.map(apiRoute => apiRoute[1]);
await api.addAppFile({
name: 'routesMap.js',
content: () => `export default ${JSON.stringify(routesMap)}`
});

// make sure entryPagePath first postion on appPages
const entryPagePath = appConfig.entryPagePath;
if (entryPagePath && routesName.has(entryPagePath)) {
routesName.delete(entryPagePath);
routesName.add(entryPagePath);
}
appPages = [...routesName].reverse();
appConfig.pages = appPages;
if (!appPages || !appPages.length) {
throw new Error('shuvi config routes property pages config error');
}
for (const page of appPages) {
const pageFile = api.resolveUserFile(`${page}`);
const pageConfigFile = api.helpers.fileSnippets.findFirstExistedFile(
withExts(api.resolveUserFile(`${page}.config`), moduleFileExtensions)
);
const pageConfig = pageConfigFile ? readConfig(pageConfigFile) : {};
pageConfigs[page] = pageConfig;
await api.addAppFile({
name: `${page}.js`,
content: () => `
import * as React from 'react';
import { createPageConfig } from '@tarojs/runtime';
import { addGlobalRoutes, getGlobalRoutes, MpRouter } from '@shuvi/services/router-mp';
import pageComponent from '${pageFile}';
const pageConfig = ${JSON.stringify(pageConfig)};
const pageName = '${page}';
addGlobalRoutes(pageName, pageComponent);
function MpRouterWrapper(){
return (
<MpRouter
initialEntries={['/' + pageName]}
routes={getGlobalRoutes()}
>
</MpRouter>
)
};
const component = MpRouterWrapper;
const inst = Page(createPageConfig(component, pageName, {root:{cn:[]}}, pageConfig || {}))
`
});
}
mpPathToRoutesDone();
return []; // routes file no use, remove it
}
});

api.tap<APIHooks.IHookBundlerConfig>('bundler:configTarget', {
name: 'platform-mp',
fn: (config, { name }) => {
fn: async (config, { name }) => {
await PromiseRoutes;
if (name === BUNDLER_TARGET_SERVER) {
config.set('entry', '@shuvi/util/lib/noop');
return config;
}

if (!pageFiles) {
pageFiles = getAllFiles(api.resolveAppFile('files', 'pages'));
}
Expand Down
1 change: 1 addition & 0 deletions packages/platform-mp/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const PACKAGE_NAME = '@shuvi/platform-mp';
export const PACKAGE_ROUTER = '@shuvi/router-mp';
5 changes: 4 additions & 1 deletion packages/platform-mp/src/lib/paths.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { resolve, dirname, join } from 'path';
import { PACKAGE_NAME } from './constants';
import { PACKAGE_NAME, PACKAGE_ROUTER } from './constants';

export const resolveDep = (module: string) => require.resolve(module);

Expand All @@ -10,3 +10,6 @@ export const PACKAGE_RESOLVED = resolveLib(PACKAGE_NAME);

export const resolveAppFile = (...paths: string[]) =>
`${resolve(PACKAGE_RESOLVED, 'shuvi-app', ...paths)}`;

export const resolveRouterFile = (...paths: string[]) =>
`${resolve(resolveLib(PACKAGE_ROUTER), ...paths)}`;
37 changes: 37 additions & 0 deletions packages/router-mp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@shuvi/router-mp",
"version": "0.0.1-rc.32",
"repository": {
"type": "git",
"url": "git+https://github.com/shuvijs/shuvi.git",
"directory": "packages/router-mp"
},
"author": "liximomo",
"license": "MIT",
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts",
"files": [
"lib"
],
"scripts": {
"dev": "run-p watch:*",
"watch:esm": "tsc -p tsconfig.build.esm.json -w",
"watch:cjs": "tsc -p tsconfig.build.cjs.json -w",
"prebuild": "rimraf lib esm",
"build": "run-p build:*",
"build:esm": "tsc -p tsconfig.build.esm.json",
"build:cjs": "tsc -p tsconfig.build.cjs.json"
},
"engines": {
"node": ">= 12.0.0"
},
"dependencies": {
"@shuvi/router-react": "^0.0.1-rc.32",
"@shuvi/router": "^0.0.1-rc.32",
"@shuvi/types": "^0.0.1-rc.32"
},
"devDependencies": {
"@types/react": "^16.9.43"
}
}
77 changes: 77 additions & 0 deletions packages/router-mp/src/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {
useResolvedPath,
useCurrentRoute,
useNavigate
} from '@shuvi/router-react';
import { pathToString, State, PathRecord } from '@shuvi/router';
import { View } from '@tarojs/components';
import { __DEV__ } from './constants';

function isModifiedEvent(event: React.MouseEvent) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}

/**
* The public API for rendering a history-aware <a>.
*/
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
function LinkWithRef(
{ onClick, replace: replaceProp = false, state, target, to, ...rest },
ref
) {
let navigate = useNavigate();
const location = useCurrentRoute();
let path = useResolvedPath(to);

function handleClick(event: React.MouseEvent<HTMLAnchorElement>) {
if (onClick) onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
!isModifiedEvent(event) // Ignore clicks with modifier keys
) {
event.preventDefault();

// If the URL hasn't changed, a regular <a> will do a replace instead of
// a push, so do the same here.
let replace =
!!replaceProp ||
pathToString(location) === pathToString(path) ||
!target ||
target === '_self';

navigate(to, { replace, state });
}
}
return (
// @ts-ignore
<View {...rest} ref={ref} onClick={handleClick} />
);
}
);

export interface LinkProps
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
replace?: boolean;
state?: State;
to: PathRecord;
}

if (__DEV__) {
Link.displayName = 'MpLink';
Link.propTypes = {
onClick: PropTypes.func,
replace: PropTypes.bool,
state: PropTypes.object,
target: PropTypes.string,
to: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
hash: PropTypes.string
})
]).isRequired
};
}
1 change: 1 addition & 0 deletions packages/router-mp/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const __DEV__ = process.env.NODE_ENV !== 'production';
37 changes: 37 additions & 0 deletions packages/router-mp/src/globalRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { IRouteRecord } from '@shuvi/router';

const _globalRoutes: IRouteRecord[] = [];

export function getGlobalRoutes() {
return _globalRoutes;
}

export function addGlobalRoutes(
routePath: string,
routeComponent: React.ReactElement
) {
for (let i = 0; i < _globalRoutes.length; i++) {
const { path, component } = _globalRoutes[i];
if (path === routePath && component === routeComponent) {
return _globalRoutes;
}
}
_globalRoutes.push({
path: routePath,
component: routeComponent
});
return _globalRoutes;
}

export function delGlobalRoutes(
routePath: string,
routeComponent: React.ReactElement
) {
for (let i = _globalRoutes.length - 1; i <= 0; i--) {
const { path, component } = _globalRoutes[i];
if (path === routePath && component === routeComponent) {
_globalRoutes.splice(i, 1);
}
}
return _globalRoutes;
}
Loading

0 comments on commit 45aeacd

Please sign in to comment.