Skip to content

Commit

Permalink
feat: add runtime basename config
Browse files Browse the repository at this point in the history
  • Loading branch information
Repraance committed Dec 19, 2023
1 parent 88517aa commit b9766c1
Show file tree
Hide file tree
Showing 22 changed files with 209 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/platform-shared/src/shared/helper/getAppData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type IData = {

export type IAppData<Data = {}, appState = any> = {
ssr: boolean;
basename?: string;
runtimeConfig?: Record<string, string>;
appState?: appState;
pageData?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export async function renderToHTML({
}): Promise<Response> {
let result: Response;
const renderer = new Renderer({ serverPluginContext });
const {
config: { ssr },
appConfig: {
router: { basename }
}
} = serverPluginContext;
const { serverCreateAppTrace } = req._traces;
const { application } = resources.server;
const app = serverCreateAppTrace
Expand All @@ -23,7 +29,8 @@ export async function renderToHTML({
.traceFn(() =>
application.createApp({
req,
ssr: serverPluginContext.config.ssr
ssr,
basename
})
);

Expand All @@ -37,7 +44,7 @@ export async function renderToHTML({
result = await renderer.renderView({
req,
app: app.getPublicAPI(),
ssr: serverPluginContext.config.ssr
ssr
});
} finally {
await app.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export class SsrRenderer extends BaseRenderer {
...result.appData,
ssr: true,
appState: store.getState(),
pageData
pageData,
basename: router.basename
};
appData.runtimeConfig = getPublicRuntimeConfig() || {};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { matchRoutes } from '@shuvi/router';
import { normalizeBase } from '@shuvi/router/lib/utils';
import resources from '@shuvi/service/lib/resources';
import { ShuviRequestHandler, IServerPluginContext } from '@shuvi/service';
import { DevMiddleware } from '@shuvi/service/lib/server/middlewares/dev';
Expand Down Expand Up @@ -85,8 +86,15 @@ export default class OnDemandRouteManager {
}

async ensureRoutes(pathname: string): Promise<void> {
const {
router: { basename }
} = this._serverPluginContext.appConfig;
const matchedRoutes =
matchRoutes(resources.server.pageRoutes, pathname) || [];
matchRoutes(
resources.server.pageRoutes,
pathname,
normalizeBase(basename)
) || [];

const modulesToActivate = matchedRoutes
.map(({ route: { __componentRawRequest__ } }) => __componentRawRequest__)
Expand Down
4 changes: 4 additions & 0 deletions packages/platform-web/src/node/shuvi-type-extensions-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
export {};

declare module '@shuvi/service/lib/resources' {
interface IGetRoutes {
(routes: IPageRouteRecord[]): IPageRouteRecord[];
}
export interface IResources {
server: {
server: IServerModule;
Expand All @@ -27,6 +30,7 @@ declare module '@shuvi/service/lib/resources' {
createApp: CreateAppServer;
};
view: IViewServer;
getRoutes: IGetRoutes;
};
documentPath: string;
clientManifest: IManifest;
Expand Down
6 changes: 5 additions & 1 deletion packages/platform-web/src/shared/appTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export type InternalApplication = _ApplicationImpl<AppConfig>;
export type Application = _Application<AppConfig>;

export interface CreateAppServer {
(options: { req: ShuviRequest; ssr: boolean }): InternalApplication;
(options: {
req: ShuviRequest;
ssr: boolean;
basename: string;
}): InternalApplication;
}

export interface CreateAppClient {
Expand Down
5 changes: 3 additions & 2 deletions packages/platform-web/src/shuvi-app/app/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const createApp: CreateAppClient = ({
return app;
}

const { appState, ssr } = appData;
const { appState, ssr, basename } = appData;
let history: History;
if (historyMode === 'hash') {
history = createHashHistory();
Expand All @@ -48,7 +48,8 @@ export const createApp: CreateAppClient = ({

const router = createRouter({
history,
routes: getRoutes(routes)
routes: getRoutes(routes),
basename
});

app = application({
Expand Down
5 changes: 3 additions & 2 deletions packages/platform-web/src/shuvi-app/app/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import { serializeServerError } from '../helper/serializeServerError';

const { SHUVI_SERVER_RUN_LOADERS } = SERVER_CREATE_APP.events;
export const createApp: CreateAppServer = options => {
const { req, ssr } = options;
const { req, ssr, basename } = options;
const history = createMemoryHistory({
initialEntries: [(req && req.url) || '/'],
initialIndex: 0
});
const router = createRouter({
history,
routes: getRoutes(routes)
routes: getRoutes(routes),
basename
}) as IRouter;
let app: InternalApplication;
if (ssr) {
Expand Down
4 changes: 4 additions & 0 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class Router<RouteRecord extends IRouteRecord> implements IRouter<RouteRecord> {
return this._history.action;
}

get basename(): string {
return this._basename;
}

init = () => {
const setup = () => this._history.setup();
this._history.transitionTo(this._getCurrent(), {
Expand Down
1 change: 1 addition & 0 deletions packages/router/src/types/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface IRouter<
> {
current: IRoute<RouteRecord>;
action: History['action'];
basename: string;
push(to: PathRecord, state?: any): void;
replace(to: PathRecord, state?: any): void;
go: History['go'];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IServerPluginContext } from '../plugin';
import { ShuviRequestHandler } from '../shuviServerTypes';

export const setupAppConfigMiddleware = (
context: IServerPluginContext
): ShuviRequestHandler => {
return async (req, _res, next) => {
const appConfig = context.serverPluginRunner.getAppConfig({ req });
if (appConfig) {
if (typeof appConfig.router.basename !== 'string') {
throw new Error(
'[ServerPlugin Hook getAppConfig] appConfig.router.basename must be a string'
);
}
context.appConfig = appConfig;
}
next();
};
};
26 changes: 24 additions & 2 deletions packages/service/src/server/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {
createAsyncParallelHook,
createHookManager,
createSyncBailHook,
IPluginInstance,
IPluginHandlers,
HookMap
} from '@shuvi/hook';
import { createPluginCreator } from '@shuvi/shared/plugins';
import { IPluginContext } from '../core';
import { CustomServerPluginHooks } from './pluginTypes';
import { ShuviRequest } from './shuviServerTypes';

export * from './pluginTypes';

export interface IServerPluginContext extends IPluginContext {
serverPluginRunner: PluginManager['runner'];
appConfig: AppConfig;
}

export type PluginManager = ReturnType<typeof getManager>;
Expand All @@ -21,12 +24,25 @@ export type PluginRunner = PluginManager['runner'];

const listen = createAsyncParallelHook<{ port: number; hostname?: string }>();

type AppConfigCtx = {
req: ShuviRequest;
};

type AppConfig = {
router: {
basename: string;
};
};
const getAppConfig = createSyncBailHook<void, AppConfigCtx, AppConfig>();

const internalHooks = {
listen
listen,
getAppConfig
};

export interface BuiltInServerPluginHooks extends HookMap {
listen: typeof listen;
getAppConfig: typeof getAppConfig;
}

export interface ServerPluginHooks
Expand Down Expand Up @@ -63,7 +79,13 @@ export const initServerPlugins = (
): IServerPluginContext => {
const serverContext = Object.assign(
{
serverPluginRunner: manager.runner
serverPluginRunner: manager.runner,
// default appConfig, can be override by setupAppConfigMiddleware
appConfig: {
router: {
basename: ''
}
}
},
pluginContext
);
Expand Down
3 changes: 3 additions & 0 deletions packages/service/src/server/shuviDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { applyHttpProxyMiddleware } from './middlewares/httpProxyMiddleware';
import { getAssetMiddleware } from './middlewares/getAssetMiddleware';
import { ShuviDevServerOptions, ShuviRequestHandler } from './shuviServerTypes';
import { loadDotenvConfig } from '../config/env';
import { setupAppConfigMiddleware } from './middlewares/setupAppConfigMiddleware';

export class ShuviDevServer extends ShuviServer {
private _bundler: Bundler;
Expand Down Expand Up @@ -60,6 +61,8 @@ export class ShuviDevServer extends ShuviServer {
next();
}) as ShuviRequestHandler);

server.use(setupAppConfigMiddleware(context));

if (this._options.getMiddlewaresBeforeDevMiddlewares) {
const serverMiddlewaresBeforeDevMiddleware = [
this._options.getMiddlewaresBeforeDevMiddlewares(devMiddleware, context)
Expand Down
2 changes: 2 additions & 0 deletions packages/service/src/server/shuviProdServer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ShuviServer } from './shuviServer';
import { getAssetMiddleware } from './middlewares/getAssetMiddleware';
import { setupAppConfigMiddleware } from './middlewares/setupAppConfigMiddleware';
export class ShuviProdServer extends ShuviServer {
async init() {
const { _serverContext: context } = this;
this._server.use(getAssetMiddleware(context));
this._server.use(setupAppConfigMiddleware(context));
await this._initMiddlewares();
}
}
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions test/e2e/basename.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AppCtx, Page, devFixture } from '../utils';

jest.setTimeout(5 * 60 * 1000);

describe('Basename Support', () => {
let ctx: AppCtx;
let page: Page;

describe('basename should work', () => {
beforeAll(async () => {
ctx = await devFixture('basename');
});
afterAll(async () => {
await page.close();
await ctx.close();
});

test('basename should work for route matching', async () => {
page = await ctx.browser.page(ctx.url('/base-name'));
expect(await page.$text('#index')).toEqual('Index Page');

page = await ctx.browser.page(ctx.url('/base-name/en'));
expect(await page.$text('#lng')).toEqual('en');
});

test.skip('basename should work when client navigation', async () => {
page = await ctx.browser.page(ctx.url('/base-name'));
expect(await page.$text('#index')).toEqual('Index Page');

await page.shuvi.navigate('/en');
await page.waitForSelector('#lng');
expect(await page.$text('#lng')).toEqual('en');

await page.shuvi.navigate('/');
await page.waitForSelector('#index');
expect(await page.$text('#index')).toEqual('index page');
});
});

test('basename must be a string', async () => {
ctx = await devFixture('basename', {
plugins: [['./plugin', { basename: null }]]
});
page = await ctx.browser.page(ctx.url('/base-name'));
const result = await page.goto(ctx.url('/base-name'));
expect(result?.status()).toBe(500);
expect(await page.$text('body')).toContain(
'appConfig.router.basename must be a string'
);
});
});
6 changes: 6 additions & 0 deletions test/fixtures/basename/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "fixture-basename",
"dependencies": {
"shuvi": "workspace:*"
}
}
15 changes: 15 additions & 0 deletions test/fixtures/basename/plugin/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { createServerPlugin } = require('shuvi');

module.exports = ({ basename = '' }) =>
createServerPlugin({
getAppConfig: ({ req }) => {
global._req_url = req.url;
const appConfig = {
router: {
basename
}
};
global._app_config = appConfig;
return appConfig;
}
});
7 changes: 7 additions & 0 deletions test/fixtures/basename/shuvi.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
ssr: true,
router: {
history: 'browser'
},
plugins: [['./plugin', { basename: '/base-name' }]]
};
11 changes: 11 additions & 0 deletions test/fixtures/basename/src/routes/$lng/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useParams, Link } from '@shuvi/runtime';

export default function Page() {
const { lng } = useParams();
return (
<div>
<div id="lng">{lng}</div>
<Link to="">Go to /</Link>
</div>
);
}
6 changes: 6 additions & 0 deletions test/fixtures/basename/src/routes/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { RouterView, useRouter } from '@shuvi/runtime';
export default function Page() {
const router = useRouter();
console.log('----router', router);
return <RouterView />;
}
Loading

0 comments on commit b9766c1

Please sign in to comment.