Feat: define routes and ignore route files (#69)

* feat: support define routes

* fix: test

* fix: test

* chore: undefined type

* fix: conflict

* chore: remove pages str from route id

* fix: watch route change

* fix: warn

* fix: test

* fix: test

* chore: example

* chore: add route-gen example

* feat: add integration test

* chore: test

* chore: update config file

* chore: remove pnpm cache

* chore: test

* chore: remove test:ci from ci workflow

* chore: update build-scripts version

* chore: build fixture

* chore: remove devServer test

* chore: buildFixture

* feat: add vitest

* chore: add ts-ignore

* chore: node ci version

* chore: comment bundle analyzer

* chore: add test timeout

* fix: lint

* chore: remove threads

* chore: set maxThreads and minThreads

* chore: add maxConcurrency

* chore: remove coverage

* chore: threads

* chore: set threads to false

* fix: conflict

* fix: comment
This commit is contained in:
luhc228 2022-04-14 17:16:51 +08:00 committed by GitHub
parent 95d49c46d5
commit d19f21e8b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1029 additions and 793 deletions

View File

@ -0,0 +1,14 @@
import { defineConfig } from '@ice/app';
export default defineConfig({
routes: {
ignoreFiles: ['about.tsx', 'products.tsx'],
defineRoutes: (route) => {
route('/about-me', 'about.tsx');
route('/', 'layout.tsx', () => {
route('/product', 'products.tsx');
});
},
},
});

View File

@ -0,0 +1,23 @@
{
"name": "basic-project",
"version": "1.0.0",
"scripts": {
"start": "ice start",
"build": "ice build"
},
"description": "",
"author": "",
"license": "MIT",
"dependencies": {
"@ice/app": "file:../../packages/ice",
"@ice/runtime": "^1.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"browserslist": "^4.19.3",
"regenerator-runtime": "^0.13.9"
}
}

View File

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

View File

@ -0,0 +1,26 @@
/* eslint-disable react/self-closing-comp */
import React from 'react';
import { Meta, Title, Links, Main, Scripts } from 'ice';
function Document(props) {
return (
<html lang="en">
<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>
{props.children}
</Main>
<Scripts />
</body>
</html>
);
}
export default Document;

View File

@ -0,0 +1,6 @@
import * as React from 'react';
import { Link } from 'ice';
export default function About() {
return <><h2>About</h2><Link to="/">home</Link></>;
}

View File

@ -0,0 +1,7 @@
import React from 'react';
export default () => {
return (
<h3>A page</h3>
);
};

View File

@ -0,0 +1,7 @@
import React from 'react';
export default () => {
return (
<h3>B page</h3>
);
};

View File

@ -0,0 +1,7 @@
import React from 'react';
export default () => {
return (
<div>Index</div>
);
};

View File

@ -0,0 +1,15 @@
import * as React from 'react';
import { Outlet, Link } from 'ice';
export default () => {
return (
<div>
<h2>Dashboard</h2>
<ul>
<li><Link to="/dashboard/a">a</Link></li>
<li><Link to="/dashboard/b">b</Link></li>
</ul>
<Outlet />
</div>
);
};

View File

@ -0,0 +1,13 @@
import React from 'react';
import { useParams, Link } from 'ice';
export default function DetailId() {
const params = useParams();
return (
<div>
<h2>Detail id: {params.id}</h2>
<Link to="/detail">Back to Detail</Link>
</div>
);
}

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Link } from 'ice';
export default function Detail() {
return (
<div>
<h2>Detail</h2>
<ul>
<li><Link to="/detail/join">join</Link></li>
<li><Link to="/detail/dashboard">dashboard</Link></li>
</ul>
</div>
);
}

View File

@ -0,0 +1,15 @@
import * as React from 'react';
import { Link } from 'ice';
export default function Home() {
return (
<>
<h2>Home</h2>
<ul>
<li><Link to="/about-me">about</Link></li>
<li><Link to="/detail">detail</Link></li>
<li><Link to="/dashboard">dashboard</Link></li>
</ul>
</>
);
}

View File

@ -0,0 +1,11 @@
import * as React from 'react';
import { Outlet } from 'ice';
export default () => {
return (
<div>
<h1>Layout</h1>
<Outlet />
</div>
);
};

View File

@ -0,0 +1,6 @@
import * as React from 'react';
import { Link } from 'ice';
export default function Products() {
return <><h2>Products Page</h2><Link to="/">home</Link></>;
}

View File

@ -0,0 +1,32 @@
{
"compileOnSave": false,
"buildOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"module": "esnext",
"target": "es6",
"jsx": "react",
"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

@ -55,7 +55,7 @@
"semver": "^7.3.5", "semver": "^7.3.5",
"stylelint": "^14.3.0", "stylelint": "^14.3.0",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"vitest": "^0.8.4" "vitest": "^0.9.2"
}, },
"packageManager": "pnpm" "packageManager": "pnpm"
} }

View File

@ -33,7 +33,7 @@
}, },
"devDependencies": { "devDependencies": {
"@ice/types": "^1.0.0", "@ice/types": "^1.0.0",
"build-scripts": "^2.0.0-15", "build-scripts": "^2.0.0-16",
"webpack": "^5.69.1", "webpack": "^5.69.1",
"webpack-dev-server": "^4.7.4" "webpack-dev-server": "^4.7.4"
} }

View File

@ -41,7 +41,7 @@ function getEntry(rootDir: string) {
} }
return { return {
runtime: ['react', 'react-dom', '@ice/runtime'], runtime: ['react', 'react-dom', '@ice/runtime'],
index: { main: {
import: [entryFile], import: [entryFile],
dependOn: 'runtime', dependOn: 'runtime',
}, },

View File

@ -24,7 +24,7 @@
"@ice/runtime": "^1.0.0", "@ice/runtime": "^1.0.0",
"@ice/webpack-config": "^1.0.0", "@ice/webpack-config": "^1.0.0",
"address": "^1.1.2", "address": "^1.1.2",
"build-scripts": "^2.0.0-15", "build-scripts": "^2.0.0-16",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"commander": "^9.0.0", "commander": "^9.0.0",
"consola": "^2.15.3", "consola": "^2.15.3",

View File

@ -32,16 +32,9 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const templateDir = path.join(__dirname, '../template/'); const templateDir = path.join(__dirname, '../template/');
const configFile = 'ice.config.(mts|mjs|ts|js|cjs|json)'; const configFile = 'ice.config.(mts|mjs|ts|js|cjs|json)';
const dataCache = new Map<string, string>(); const dataCache = new Map<string, string>();
const routesRenderData = generateRoutesInfo(rootDir);
dataCache.set('routes', JSON.stringify(routesRenderData));
const generator = new Generator({ const generator = new Generator({
rootDir, rootDir,
targetDir, targetDir,
defaultRenderData: {
...routesRenderData,
},
// add default template of ice // add default template of ice
templates: [templateDir], templates: [templateDir],
}); });
@ -49,14 +42,6 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
const { addWatchEvent, removeWatchEvent } = createWatch({ const { addWatchEvent, removeWatchEvent } = createWatch({
watchDir: rootDir, watchDir: rootDir,
command, command,
watchEvents: getWatchEvents({
generator,
rootDir,
targetDir,
templateDir,
configFile,
cache: dataCache,
}),
}); });
const generatorAPI = { const generatorAPI = {
@ -72,6 +57,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
addRenderFile: generator.addRenderFile, addRenderFile: generator.addRenderFile,
addRenderTemplate: generator.addTemplateFiles, addRenderTemplate: generator.addTemplateFiles,
}; };
const ctx = new Context<any, ExtendsPluginAPI>({ const ctx = new Context<any, ExtendsPluginAPI>({
rootDir, rootDir,
command, command,
@ -90,16 +76,32 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
}, },
}); });
await ctx.resolveConfig(); await ctx.resolveConfig();
const { userConfig: { routes: routesConfig } } = ctx;
const routesRenderData = generateRoutesInfo(rootDir, routesConfig);
generator.modifyRenderData((renderData) => ({
...renderData,
...routesRenderData,
}));
dataCache.set('routes', JSON.stringify(routesRenderData.routeManifest));
const runtimeModules = getRuntimeModules(ctx.getAllPlugin()); const runtimeModules = getRuntimeModules(ctx.getAllPlugin());
generator.modifyRenderData((renderData) => ({ generator.modifyRenderData((renderData) => ({
...renderData, ...renderData,
runtimeModules, runtimeModules,
})); }));
await ctx.setup(); await ctx.setup();
// render template before webpack compile // render template before webpack compile
const renderStart = new Date().getTime(); const renderStart = new Date().getTime();
generator.render(); generator.render();
addWatchEvent(
...getWatchEvents({ generator, targetDir, templateDir, cache: dataCache, ctx }),
);
consola.debug('template render cost:', new Date().getTime() - renderStart); consola.debug('template render cost:', new Date().getTime() - renderStart);
// define runtime env before get webpack config // define runtime env before get webpack config
defineRuntimeEnv(); defineRuntimeEnv();
const compileIncludes = runtimeModules.map(({ name }) => `${name}/runtime`); const compileIncludes = runtimeModules.map(({ name }) => `${name}/runtime`);

View File

@ -1,32 +1,34 @@
import * as path from 'path'; import * as path from 'path';
import consola from 'consola'; import consola from 'consola';
import type { WatchEvent } from '@ice/types/esm/plugin.js'; import type { WatchEvent } from '@ice/types/esm/plugin.js';
import type { Context } from 'build-scripts';
import type { Config } from '@ice/types';
import { generateRoutesInfo } from './routes.js'; import { generateRoutesInfo } from './routes.js';
import type Generator from './service/runtimeGenerator'; import type Generator from './service/runtimeGenerator';
interface Options { interface Options {
rootDir: string;
targetDir: string; targetDir: string;
templateDir: string; templateDir: string;
configFile: string;
generator: Generator; generator: Generator;
cache: Map<string, string>; cache: Map<string, string>;
ctx: Context<Config>;
} }
const getWatchEvents = (options: Options): WatchEvent[] => { const getWatchEvents = (options: Options): WatchEvent[] => {
const { rootDir, generator, targetDir, templateDir, configFile, cache } = options; const { generator, targetDir, templateDir, cache, ctx } = options;
const { userConfig: { routes: routesConfig }, configFile, rootDir } = ctx;
const watchRoutes: WatchEvent = [ const watchRoutes: WatchEvent = [
/src\/pages\/?[\w*-:.$]+$/, /src\/pages\/?[\w*-:.$]+$/,
(eventName: string) => { (eventName: string) => {
if (eventName === 'add' || eventName === 'unlink') { if (eventName === 'add' || eventName === 'unlink') {
const routesRenderData = generateRoutesInfo(rootDir); const routesRenderData = generateRoutesInfo(rootDir, routesConfig);
const stringifiedData = JSON.stringify(routesRenderData); const stringifiedData = JSON.stringify(routesRenderData);
if (cache.get('routes') !== stringifiedData) { if (cache.get('routes') !== stringifiedData) {
cache.set('routes', stringifiedData); cache.set('routes', stringifiedData);
consola.debug('[event]', `routes data regenerated: ${stringifiedData}`); consola.debug('[event]', `routes data regenerated: ${stringifiedData}`);
generator.renderFile( generator.renderFile(
path.join(templateDir, 'routes.ts.ejs'), path.join(templateDir, 'routes.ts.ejs'),
path.join(rootDir, targetDir, 'route.ts'), path.join(rootDir, targetDir, 'routes.ts'),
routesRenderData, routesRenderData,
); );
generator.renderFile( generator.renderFile(
@ -38,6 +40,7 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
} }
}, },
]; ];
const watchGlobalStyle: WatchEvent = [ const watchGlobalStyle: WatchEvent = [
/src\/global.(scss|less|css)/, /src\/global.(scss|less|css)/,
(event: string, filePath: string) => { (event: string, filePath: string) => {
@ -49,7 +52,7 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
]; ];
const watchConfigFile: WatchEvent = [ const watchConfigFile: WatchEvent = [
new RegExp(configFile), new RegExp((typeof configFile === 'string' ? [configFile] : configFile).join('|')),
(event: string, filePath: string) => { (event: string, filePath: string) => {
if (event === 'change') { if (event === 'change') {
consola.warn(`Found a change in ${path.basename(filePath)}. Restart the dev server to see the changes in effect.`); consola.warn(`Found a change in ${path.basename(filePath)}. Restart the dev server to see the changes in effect.`);

View File

@ -112,6 +112,10 @@ const userConfig = [
} }
}, },
}, },
{
name: 'routes',
validation: 'object',
},
]; ];
const cliOptions = [ const cliOptions = [

View File

@ -1,5 +1,5 @@
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import fse from 'fs-extra';
import type { RouteItem } from '@ice/runtime'; import type { RouteItem } from '@ice/runtime';
interface Options { interface Options {
@ -20,7 +20,7 @@ export default async function generateHTML(options: Options) {
} = options; } = options;
const serverEntry = await import(entry); const serverEntry = await import(entry);
const routes = JSON.parse(fs.readFileSync(routeManifest, 'utf8')); const routes = JSON.parse(fse.readFileSync(routeManifest, 'utf8'));
const paths = getPaths(routes); const paths = getPaths(routes);
for (let i = 0, n = paths.length; i < n; i++) { for (let i = 0, n = paths.length; i < n; i++) {
@ -40,7 +40,9 @@ export default async function generateHTML(options: Options) {
} }
const fileName = routePath === '/' ? 'index.html' : `${routePath}.html`; const fileName = routePath === '/' ? 'index.html' : `${routePath}.html`;
fs.writeFileSync(path.join(outDir, fileName), html); const contentPath = path.join(outDir, fileName);
await fse.ensureFile(contentPath);
await fse.writeFile(contentPath, html);
} }
} }
@ -49,14 +51,14 @@ export default async function generateHTML(options: Options) {
* @param routes * @param routes
* @returns * @returns
*/ */
function getPaths(routes: RouteItem[]): string[] { function getPaths(routes: RouteItem[], parentPath = ''): string[] {
let pathList = []; let pathList = [];
routes.forEach(route => { routes.forEach(route => {
if (route.children) { if (route.children) {
pathList = pathList.concat(getPaths(route.children)); pathList = pathList.concat(getPaths(route.children, route.path));
} else { } else {
pathList.push(route.path || '/'); pathList.push(path.join('/', parentPath, route.path || ''));
} }
}); });

View File

@ -1,9 +1,10 @@
import * as path from 'path'; import * as path from 'path';
import { formatNestedRouteManifest, generateRouteManifest } from '@ice/route-manifest'; import { formatNestedRouteManifest, generateRouteManifest } from '@ice/route-manifest';
import type { NestedRouteManifest } from '@ice/route-manifest'; import type { NestedRouteManifest } from '@ice/route-manifest';
import type { UserConfig } from '@ice/types';
export function generateRoutesInfo(rootDir: string) { export function generateRoutesInfo(rootDir: string, routesConfig: UserConfig['routes'] = {}) {
const routeManifest = generateRouteManifest(rootDir); const routeManifest = generateRouteManifest(rootDir, routesConfig.ignoreFiles, routesConfig.defineRoutes);
const routes = formatNestedRouteManifest(routeManifest); const routes = formatNestedRouteManifest(routeManifest);
const str = generateNestRoutesStr(routes); const str = generateNestRoutesStr(routes);
@ -23,7 +24,7 @@ function generateNestRoutesStr(nestRouteManifest: NestedRouteManifest[]) {
let str = `{ let str = `{
path: '${routePath || ''}', path: '${routePath || ''}',
load: () => import(/* webpackChunkName: "${componentName}" */ '@/${componentFile}'), load: () => import(/* webpackChunkName: "${componentName}" */ '@/pages/${componentFile}'),
componentName: '${componentName}', componentName: '${componentName}',
index: ${index}, index: ${index},
id: '${id}', id: '${id}',

View File

@ -32,7 +32,7 @@ const RENDER_WAIT = 150;
interface Options { interface Options {
rootDir: string; rootDir: string;
targetDir: string; targetDir: string;
defaultRenderData: RenderData; defaultRenderData?: RenderData;
templates?: (string | TemplateOptions)[]; templates?: (string | TemplateOptions)[];
} }
@ -106,7 +106,7 @@ export default class Generator {
private contentTypes: string[]; private contentTypes: string[];
public constructor(options: Options) { public constructor(options: Options) {
const { rootDir, targetDir, defaultRenderData, templates } = options; const { rootDir, targetDir, defaultRenderData = {}, templates } = options;
this.rootDir = rootDir; this.rootDir = rootDir;
this.targetDir = targetDir; this.targetDir = targetDir;
this.renderData = defaultRenderData; this.renderData = defaultRenderData;

View File

@ -29,8 +29,8 @@ function createWatch(options: {
return { return {
watcher, watcher,
addWatchEvent: ([pattern, action, name]: WatchEvent) => { addWatchEvent: (...args: WatchEvent[]) => {
watchEvents.push([pattern, action, name]); watchEvents.push(...args);
}, },
removeWatchEvent: (name: string) => { removeWatchEvent: (name: string) => {
const eventIndex = watchEvents.findIndex(([,,watchName]) => watchName === name); const eventIndex = watchEvents.findIndex(([,,watchName]) => watchName === name);

View File

@ -3,6 +3,8 @@ import {
useAppContext, useAppContext,
Link, Link,
Outlet, Outlet,
useParams,
useSearchParams,
Meta, Meta,
Title, Title,
Links, Links,
@ -16,6 +18,8 @@ export {
useAppContext, useAppContext,
Link, Link,
Outlet, Outlet,
useParams,
useSearchParams,
Meta, Meta,
Title, Title,
Links, Links,

View File

@ -5,9 +5,10 @@ import minimatch from 'minimatch';
import { createRouteId, defineRoutes } from './routes.js'; import { createRouteId, defineRoutes } from './routes.js';
import type { RouteManifest, DefineRouteFunction, NestedRouteManifest } from './routes.js'; import type { RouteManifest, DefineRouteFunction, NestedRouteManifest } from './routes.js';
export { export type {
RouteManifest, RouteManifest,
NestedRouteManifest, NestedRouteManifest,
DefineRouteFunction,
}; };
const validRouteChar = ['-', '\\w', '/', ':', '*']; const validRouteChar = ['-', '\\w', '/', ':', '*'];
@ -26,14 +27,18 @@ export function isRouteModuleFile(filename: string): boolean {
return routeModuleExts.includes(path.extname(filename)); return routeModuleExts.includes(path.extname(filename));
} }
export function generateRouteManifest(rootDir: string) { export function generateRouteManifest(
rootDir: string,
ignoreFiles: string[] = [],
defineExtraRoutes?: (defineRoute: DefineRouteFunction) => void,
) {
const srcDir = path.join(rootDir, 'src'); const srcDir = path.join(rootDir, 'src');
const routeManifest: RouteManifest = {}; const routeManifest: RouteManifest = {};
// 2. find routes in `src/pages` directory // 2. find routes in `src/pages` directory
if (fs.existsSync(path.resolve(srcDir, 'pages'))) { if (fs.existsSync(path.resolve(srcDir, 'pages'))) {
const conventionalRoutes = defineConventionalRoutes( const conventionalRoutes = defineConventionalRoutes(
rootDir, rootDir,
[], // TODO: add ignoredFilePatterns defined in ice.config.js ignoreFiles,
); );
for (const key of Object.keys(conventionalRoutes)) { for (const key of Object.keys(conventionalRoutes)) {
@ -44,7 +49,17 @@ export function generateRouteManifest(rootDir: string) {
}; };
} }
} }
// 3. add extra routes from user config
if (defineExtraRoutes) {
const extraRoutes = defineRoutes(defineExtraRoutes);
for (const key of Object.keys(extraRoutes)) {
const route = extraRoutes[key];
routeManifest[route.id] = {
...route,
parentId: route.parentId || undefined,
};
}
}
return routeManifest; return routeManifest;
} }
@ -66,7 +81,6 @@ function defineConventionalRoutes(
ignoredFilePatterns?: string[], ignoredFilePatterns?: string[],
): RouteManifest { ): RouteManifest {
const files: { [routeId: string]: string } = {}; const files: { [routeId: string]: string } = {};
// 1. find all route components in src/pages // 1. find all route components in src/pages
visitFiles( visitFiles(
path.join(rootDir, 'src', 'pages'), path.join(rootDir, 'src', 'pages'),
@ -78,10 +92,9 @@ function defineConventionalRoutes(
return; return;
} }
const filePath = path.join('pages', file);
if (isRouteModuleFile(file)) { if (isRouteModuleFile(file)) {
let routeId = createRouteId(filePath); let routeId = createRouteId(file);
files[routeId] = filePath; files[routeId] = file;
return; return;
} }
}, },
@ -102,24 +115,25 @@ function defineConventionalRoutes(
}); });
for (let routeId of childRouteIds) { for (let routeId of childRouteIds) {
const parentRoutePath = removeLastLayoutStrFromId(parentId) || '';
const routePath: string | undefined = createRoutePath( const routePath: string | undefined = createRoutePath(
routeId.slice((removeLayoutStrFromId(parentId) || 'pages').length), // parentRoutePath = 'home', routeId = 'home/me', the new routeId is 'me'
// in order to escape the child route path is absolute path
routeId.slice(parentRoutePath.length + (parentRoutePath ? 1 : 0)),
); );
const routeFilePath = path.join('src', 'pages', files[routeId]);
if (RegExp(`[^${validRouteChar.join(',')}]`).test(routePath)) { if (RegExp(`[^${validRouteChar.join(',')}]`).test(routePath)) {
throw new Error(`invalid character in '${routeId}'. Only support char: ${validRouteChar.join(', ')}`); throw new Error(`invalid character in '${routeFilePath}'. Only support char: ${validRouteChar.join(', ')}`);
} }
const isIndexRoute = routeId.endsWith('/index'); const isIndexRoute = routeId === 'index' || routeId.endsWith('/index');
let fullPath = createRoutePath(routeId.slice('pages'.length + 1)); const fullPath = createRoutePath(routeId);
let uniqueRouteId = (fullPath || '') + (isIndexRoute ? '?index' : ''); const uniqueRouteId = (fullPath || '') + (isIndexRoute ? '?index' : '');
if (uniqueRouteId) { if (uniqueRouteId) {
if (uniqueRoutes.has(uniqueRouteId)) { if (uniqueRoutes.has(uniqueRouteId)) {
throw new Error( throw new Error(
`Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify( `Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify(routeFilePath)}
routeId, conflicts with route ${JSON.stringify(uniqueRoutes.get(uniqueRouteId))}`,
)} conflicts with route ${JSON.stringify(
uniqueRoutes.get(uniqueRouteId),
)}`,
); );
} else { } else {
uniqueRoutes.set(uniqueRouteId, routeId); uniqueRoutes.set(uniqueRouteId, routeId);
@ -155,7 +169,7 @@ export function createRoutePath(routeId: string): string | undefined {
let result = ''; let result = '';
let rawSegmentBuffer = ''; let rawSegmentBuffer = '';
const partialRouteId = removeLayoutStrFromId(routeId); const partialRouteId = removeLastLayoutStrFromId(routeId);
for (let i = 0; i < partialRouteId.length; i++) { for (let i = 0; i < partialRouteId.length; i++) {
const char = partialRouteId.charAt(i); const char = partialRouteId.charAt(i);
@ -196,7 +210,7 @@ function findParentRouteId(
return routeIds.find((id) => { return routeIds.find((id) => {
// childRouteId is `pages/about` and id is `pages/layout` will match // childRouteId is `pages/about` and id is `pages/layout` will match
// childRouteId is `pages/about/index` and id is `pages/about/layout` will match // childRouteId is `pages/about/index` and id is `pages/about/layout` will match
return childRouteId !== id && id.endsWith('layout') && childRouteId.startsWith(`${id.slice(0, id.length - '/layout'.length)}`); return childRouteId !== id && id.endsWith('layout') && childRouteId.startsWith(`${id.slice(0, id.length - 'layout'.length)}`);
}); });
} }
@ -224,9 +238,12 @@ function visitFiles(
/** /**
* remove `/layout` str if the routeId has it * remove `/layout` str if the routeId has it
* *
* /About/layout -> /About * 'layout' -> ''
* /About/layout/index -> /About/layout/index * 'About/layout' -> 'About'
* 'About/layout/index' -> 'About/layout/index'
*/ */
function removeLayoutStrFromId(id?: string) { function removeLastLayoutStrFromId(id?: string) {
return id?.endsWith('/layout') ? id.slice(0, id.length - '/layout'.length) : id; const layoutStrs = ['/layout', 'layout'];
const currentLayoutStr = layoutStrs.find(layoutStr => id?.endsWith(layoutStr));
return currentLayoutStr ? id.slice(0, id.length - currentLayoutStr.length) : id;
} }

View File

@ -100,9 +100,10 @@ export function defineRoutes(
// route(path, file, options) // route(path, file, options)
options = optionsOrChildren || {}; options = optionsOrChildren || {};
} }
const id = createRouteId(file); const id = createRouteId(file);
const route: ConfigRoute = { const route: ConfigRoute = {
path: path || undefined, path,
index: options.index ? true : undefined, index: options.index ? true : undefined,
id, id,
parentId: parentId:
@ -144,6 +145,6 @@ function stripFileExtension(file: string) {
function createComponentName(id: string) { function createComponentName(id: string) {
return id.replace('.', '/') // 'pages/home.news' -> pages/home/news return id.replace('.', '/') // 'pages/home.news' -> pages/home/news
.split('/') .split('/')
.map((item: string) => item[0].toUpperCase() + item.slice(1, item.length)) .map((item: string) => item.toLowerCase())
.join(''); .join('-');
} }

View File

@ -3,70 +3,33 @@
exports[`generateRouteManifest function > layout-routes 1`] = ` exports[`generateRouteManifest function > layout-routes 1`] = `
[ [
{ {
"componentName": "PagesBlogIndex", "componentName": "blog-index",
"file": "pages/blog/index.tsx", "file": "blog/index.tsx",
"id": "pages/blog/index", "id": "blog/index",
"index": true, "index": true,
"parentId": undefined, "parentId": undefined,
"path": "/blog", "path": "blog",
}, },
{ {
"componentName": "PagesBlog$id", "componentName": "blog-$id",
"file": "pages/blog/$id.tsx", "file": "blog/$id.tsx",
"id": "pages/blog/$id", "id": "blog/$id",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": "/blog/:id", "path": "blog/:id",
}, },
{ {
"componentName": "PagesAbout", "componentName": "about",
"file": "pages/about.tsx", "file": "about.tsx",
"id": "pages/about", "id": "about",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": "/about", "path": "about",
}, },
{ {
"componentName": "PagesIndex", "componentName": "index",
"file": "pages/index.tsx", "file": "index.tsx",
"id": "pages/index", "id": "index",
"index": true,
"parentId": undefined,
"path": undefined,
},
]
`;
exports[`generateRouteManifest function layout-routes 1`] = `
Array [
Object {
"componentName": "PagesBlogIndex",
"file": "pages/blog/index.tsx",
"id": "pages/blog/index",
"index": true,
"parentId": undefined,
"path": "/blog",
},
Object {
"componentName": "PagesBlog$id",
"file": "pages/blog/$id.tsx",
"id": "pages/blog/$id",
"index": undefined,
"parentId": undefined,
"path": "/blog/:id",
},
Object {
"componentName": "PagesAbout",
"file": "pages/about.tsx",
"id": "pages/about",
"index": undefined,
"parentId": undefined,
"path": "/about",
},
Object {
"componentName": "PagesIndex",
"file": "pages/index.tsx",
"id": "pages/index",
"index": true, "index": true,
"parentId": undefined, "parentId": undefined,
"path": undefined, "path": undefined,

View File

@ -2,42 +2,87 @@
exports[`generateRouteManifest function > basic-routes 1`] = ` exports[`generateRouteManifest function > basic-routes 1`] = `
{ {
"pages/About/index": { "About/index": {
"componentName": "PagesAboutIndex", "componentName": "about-index",
"file": "pages/About/index.tsx", "file": "About/index.tsx",
"id": "pages/About/index", "id": "About/index",
"index": true, "index": true,
"parentId": "pages/layout", "parentId": "layout",
"path": "/About", "path": "About",
}, },
"pages/About/me/index": { "About/me/index": {
"componentName": "PagesAboutMeIndex", "componentName": "about-me-index",
"file": "pages/About/me/index.tsx", "file": "About/me/index.tsx",
"id": "pages/About/me/index", "id": "About/me/index",
"index": true, "index": true,
"parentId": "pages/layout", "parentId": "layout",
"path": "/About/me", "path": "About/me",
}, },
"pages/home": { "home": {
"componentName": "PagesHome", "componentName": "home",
"file": "pages/home.tsx", "file": "home.tsx",
"id": "pages/home", "id": "home",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/home", "path": "home",
}, },
"pages/index": { "index": {
"componentName": "PagesIndex", "componentName": "index",
"file": "pages/index.tsx", "file": "index.tsx",
"id": "pages/index", "id": "index",
"index": true, "index": true,
"parentId": "pages/layout", "parentId": "layout",
"path": undefined, "path": undefined,
}, },
"pages/layout": { "layout": {
"componentName": "PagesLayout", "componentName": "layout",
"file": "pages/layout.tsx", "file": "layout.tsx",
"id": "pages/layout", "id": "layout",
"index": undefined,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function > define-extra-routes 1`] = `
{
"About/index": {
"componentName": "about-index",
"file": "About/index.tsx",
"id": "About/index",
"index": undefined,
"parentId": undefined,
"path": "/about-me",
},
"About/me/index": {
"componentName": "about-me-index",
"file": "About/me/index.tsx",
"id": "About/me/index",
"index": true,
"parentId": "layout",
"path": "About/me",
},
"home": {
"componentName": "home",
"file": "home.tsx",
"id": "home",
"index": undefined,
"parentId": "layout",
"path": "home",
},
"index": {
"componentName": "index",
"file": "index.tsx",
"id": "index",
"index": true,
"parentId": "layout",
"path": undefined,
},
"layout": {
"componentName": "layout",
"file": "layout.tsx",
"id": "layout",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": undefined, "path": undefined,
@ -47,18 +92,18 @@ exports[`generateRouteManifest function > basic-routes 1`] = `
exports[`generateRouteManifest function > doc-delimeters-routes 1`] = ` exports[`generateRouteManifest function > doc-delimeters-routes 1`] = `
{ {
"pages/home.news": { "home.news": {
"componentName": "PagesHomeNews", "componentName": "home-news",
"file": "pages/home.news.tsx", "file": "home.news.tsx",
"id": "pages/home.news", "id": "home.news",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/home/news", "path": "home/news",
}, },
"pages/layout": { "layout": {
"componentName": "PagesLayout", "componentName": "layout",
"file": "pages/layout.tsx", "file": "layout.tsx",
"id": "pages/layout", "id": "layout",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": undefined, "path": undefined,
@ -68,34 +113,34 @@ exports[`generateRouteManifest function > doc-delimeters-routes 1`] = `
exports[`generateRouteManifest function > dynamic-routes 1`] = ` exports[`generateRouteManifest function > dynamic-routes 1`] = `
{ {
"pages/about": { "about": {
"componentName": "PagesAbout", "componentName": "about",
"file": "pages/about.tsx", "file": "about.tsx",
"id": "pages/about", "id": "about",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": "/about", "path": "about",
}, },
"pages/blog/$id": { "blog/$id": {
"componentName": "PagesBlog$id", "componentName": "blog-$id",
"file": "pages/blog/$id.tsx", "file": "blog/$id.tsx",
"id": "pages/blog/$id", "id": "blog/$id",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": "/blog/:id", "path": "blog/:id",
}, },
"pages/blog/index": { "blog/index": {
"componentName": "PagesBlogIndex", "componentName": "blog-index",
"file": "pages/blog/index.tsx", "file": "blog/index.tsx",
"id": "pages/blog/index", "id": "blog/index",
"index": true, "index": true,
"parentId": undefined, "parentId": undefined,
"path": "/blog", "path": "blog",
}, },
"pages/index": { "index": {
"componentName": "PagesIndex", "componentName": "index",
"file": "pages/index.tsx", "file": "index.tsx",
"id": "pages/index", "id": "index",
"index": true, "index": true,
"parentId": undefined, "parentId": undefined,
"path": undefined, "path": undefined,
@ -103,76 +148,113 @@ exports[`generateRouteManifest function > dynamic-routes 1`] = `
} }
`; `;
exports[`generateRouteManifest function > ignore-routes 1`] = `
{
"About/me/index": {
"componentName": "about-me-index",
"file": "About/me/index.tsx",
"id": "About/me/index",
"index": true,
"parentId": "layout",
"path": "About/me",
},
"home": {
"componentName": "home",
"file": "home.tsx",
"id": "home",
"index": undefined,
"parentId": "layout",
"path": "home",
},
"index": {
"componentName": "index",
"file": "index.tsx",
"id": "index",
"index": true,
"parentId": "layout",
"path": undefined,
},
"layout": {
"componentName": "layout",
"file": "layout.tsx",
"id": "layout",
"index": undefined,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function > layout-routes 1`] = ` exports[`generateRouteManifest function > layout-routes 1`] = `
{ {
"pages/about": { "about": {
"componentName": "PagesAbout", "componentName": "about",
"file": "pages/about.tsx", "file": "about.tsx",
"id": "pages/about", "id": "about",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/about", "path": "about",
}, },
"pages/blog/$id": { "blog/$id": {
"componentName": "PagesBlog$id", "componentName": "blog-$id",
"file": "pages/blog/$id.tsx", "file": "blog/$id.tsx",
"id": "pages/blog/$id", "id": "blog/$id",
"index": undefined, "index": undefined,
"parentId": "pages/blog/layout", "parentId": "blog/layout",
"path": "/:id", "path": ":id",
}, },
"pages/blog/index": { "blog/index": {
"componentName": "PagesBlogIndex", "componentName": "blog-index",
"file": "pages/blog/index.tsx", "file": "blog/index.tsx",
"id": "pages/blog/index", "id": "blog/index",
"index": true, "index": true,
"parentId": "pages/blog/layout", "parentId": "blog/layout",
"path": undefined, "path": undefined,
}, },
"pages/blog/layout": { "blog/layout": {
"componentName": "PagesBlogLayout", "componentName": "blog-layout",
"file": "pages/blog/layout.tsx", "file": "blog/layout.tsx",
"id": "pages/blog/layout", "id": "blog/layout",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/blog", "path": "blog",
}, },
"pages/home/index": { "home/index": {
"componentName": "PagesHomeIndex", "componentName": "home-index",
"file": "pages/home/index.tsx", "file": "home/index.tsx",
"id": "pages/home/index", "id": "home/index",
"index": true, "index": true,
"parentId": "pages/home/layout", "parentId": "home/layout",
"path": undefined, "path": undefined,
}, },
"pages/home/layout": { "home/layout": {
"componentName": "PagesHomeLayout", "componentName": "home-layout",
"file": "pages/home/layout.tsx", "file": "home/layout.tsx",
"id": "pages/home/layout", "id": "home/layout",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/home", "path": "home",
}, },
"pages/home/layout/index": { "home/layout/index": {
"componentName": "PagesHomeLayoutIndex", "componentName": "home-layout-index",
"file": "pages/home/layout/index.tsx", "file": "home/layout/index.tsx",
"id": "pages/home/layout/index", "id": "home/layout/index",
"index": true, "index": true,
"parentId": "pages/home/layout", "parentId": "home/layout",
"path": "/layout", "path": "layout",
}, },
"pages/index": { "index": {
"componentName": "PagesIndex", "componentName": "index",
"file": "pages/index.tsx", "file": "index.tsx",
"id": "pages/index", "id": "index",
"index": true, "index": true,
"parentId": "pages/layout", "parentId": "layout",
"path": undefined, "path": undefined,
}, },
"pages/layout": { "layout": {
"componentName": "PagesLayout", "componentName": "layout",
"file": "pages/layout.tsx", "file": "layout.tsx",
"id": "pages/layout", "id": "layout",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": undefined, "path": undefined,
@ -182,235 +264,26 @@ exports[`generateRouteManifest function > layout-routes 1`] = `
exports[`generateRouteManifest function > splat-routes 1`] = ` exports[`generateRouteManifest function > splat-routes 1`] = `
{ {
"pages/$": { "$": {
"componentName": "Pages$", "componentName": "$",
"file": "pages/$.tsx", "file": "$.tsx",
"id": "pages/$", "id": "$",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/*", "path": "*",
}, },
"pages/home": { "home": {
"componentName": "PagesHome", "componentName": "home",
"file": "pages/home.tsx", "file": "home.tsx",
"id": "pages/home", "id": "home",
"index": undefined, "index": undefined,
"parentId": "pages/layout", "parentId": "layout",
"path": "/home", "path": "home",
}, },
"pages/layout": { "layout": {
"componentName": "PagesLayout", "componentName": "layout",
"file": "pages/layout.tsx", "file": "layout.tsx",
"id": "pages/layout", "id": "layout",
"index": undefined,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function basic-routes 1`] = `
Object {
"pages/About/index": Object {
"componentName": "PagesAboutIndex",
"file": "pages/About/index.tsx",
"id": "pages/About/index",
"index": true,
"parentId": "pages/layout",
"path": "/About",
},
"pages/About/me/index": Object {
"componentName": "PagesAboutMeIndex",
"file": "pages/About/me/index.tsx",
"id": "pages/About/me/index",
"index": true,
"parentId": "pages/layout",
"path": "/About/me",
},
"pages/home": Object {
"componentName": "PagesHome",
"file": "pages/home.tsx",
"id": "pages/home",
"index": undefined,
"parentId": "pages/layout",
"path": "/home",
},
"pages/index": Object {
"componentName": "PagesIndex",
"file": "pages/index.tsx",
"id": "pages/index",
"index": true,
"parentId": "pages/layout",
"path": undefined,
},
"pages/layout": Object {
"componentName": "PagesLayout",
"file": "pages/layout.tsx",
"id": "pages/layout",
"index": undefined,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function doc-delimeters-routes 1`] = `
Object {
"pages/home.news": Object {
"componentName": "PagesHomeNews",
"file": "pages/home.news.tsx",
"id": "pages/home.news",
"index": undefined,
"parentId": "pages/layout",
"path": "/home/news",
},
"pages/layout": Object {
"componentName": "PagesLayout",
"file": "pages/layout.tsx",
"id": "pages/layout",
"index": undefined,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function dynamic-routes 1`] = `
Object {
"pages/about": Object {
"componentName": "PagesAbout",
"file": "pages/about.tsx",
"id": "pages/about",
"index": undefined,
"parentId": undefined,
"path": "/about",
},
"pages/blog/$id": Object {
"componentName": "PagesBlog$id",
"file": "pages/blog/$id.tsx",
"id": "pages/blog/$id",
"index": undefined,
"parentId": undefined,
"path": "/blog/:id",
},
"pages/blog/index": Object {
"componentName": "PagesBlogIndex",
"file": "pages/blog/index.tsx",
"id": "pages/blog/index",
"index": true,
"parentId": undefined,
"path": "/blog",
},
"pages/index": Object {
"componentName": "PagesIndex",
"file": "pages/index.tsx",
"id": "pages/index",
"index": true,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function layout-routes 1`] = `
Object {
"pages/about": Object {
"componentName": "PagesAbout",
"file": "pages/about.tsx",
"id": "pages/about",
"index": undefined,
"parentId": "pages/layout",
"path": "/about",
},
"pages/blog/$id": Object {
"componentName": "PagesBlog$id",
"file": "pages/blog/$id.tsx",
"id": "pages/blog/$id",
"index": undefined,
"parentId": "pages/blog/layout",
"path": "/:id",
},
"pages/blog/index": Object {
"componentName": "PagesBlogIndex",
"file": "pages/blog/index.tsx",
"id": "pages/blog/index",
"index": true,
"parentId": "pages/blog/layout",
"path": undefined,
},
"pages/blog/layout": Object {
"componentName": "PagesBlogLayout",
"file": "pages/blog/layout.tsx",
"id": "pages/blog/layout",
"index": undefined,
"parentId": "pages/layout",
"path": "/blog",
},
"pages/home/index": Object {
"componentName": "PagesHomeIndex",
"file": "pages/home/index.tsx",
"id": "pages/home/index",
"index": true,
"parentId": "pages/home/layout",
"path": undefined,
},
"pages/home/layout": Object {
"componentName": "PagesHomeLayout",
"file": "pages/home/layout.tsx",
"id": "pages/home/layout",
"index": undefined,
"parentId": "pages/layout",
"path": "/home",
},
"pages/home/layout/index": Object {
"componentName": "PagesHomeLayoutIndex",
"file": "pages/home/layout/index.tsx",
"id": "pages/home/layout/index",
"index": true,
"parentId": "pages/home/layout",
"path": "/layout",
},
"pages/index": Object {
"componentName": "PagesIndex",
"file": "pages/index.tsx",
"id": "pages/index",
"index": true,
"parentId": "pages/layout",
"path": undefined,
},
"pages/layout": Object {
"componentName": "PagesLayout",
"file": "pages/layout.tsx",
"id": "pages/layout",
"index": undefined,
"parentId": undefined,
"path": undefined,
},
}
`;
exports[`generateRouteManifest function splat-routes 1`] = `
Object {
"pages/$": Object {
"componentName": "Pages$",
"file": "pages/$.tsx",
"id": "pages/$",
"index": undefined,
"parentId": "pages/layout",
"path": "/*",
},
"pages/home": Object {
"componentName": "PagesHome",
"file": "pages/home.tsx",
"id": "pages/home",
"index": undefined,
"parentId": "pages/layout",
"path": "/home",
},
"pages/layout": Object {
"componentName": "PagesLayout",
"file": "pages/layout.tsx",
"id": "pages/layout",
"index": undefined, "index": undefined,
"parentId": undefined, "parentId": undefined,
"path": undefined, "path": undefined,

View File

@ -33,6 +33,22 @@ describe('generateRouteManifest function', () => {
}); });
test('invalid-routes', () => { test('invalid-routes', () => {
expect(() => generateRouteManifest(path.join(fixturesDir, 'invalid-routes'))).toThrow(`invalid character in 'pages/[a.pdf]'. 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', () => {
const routeManifest = generateRouteManifest(path.join(fixturesDir, 'basic-routes'), ['About/index.tsx']);
expect(routeManifest).toMatchSnapshot();
});
test('define-extra-routes', () => {
const routeManifest = generateRouteManifest(
path.join(fixturesDir, 'basic-routes'),
['About/index.tsx'],
(defineRoute) => {
defineRoute('/about-me', 'About/index.tsx');
}
);
expect(routeManifest).toMatchSnapshot();
}); });
}); });

View File

@ -1,6 +1,8 @@
import { import {
Link, Link,
Outlet, Outlet,
useParams,
useSearchParams,
} from 'react-router-dom'; } from 'react-router-dom';
import Runtime from './runtime.js'; import Runtime from './runtime.js';
import App from './App.js'; import App from './App.js';
@ -14,7 +16,7 @@ import {
Scripts, Scripts,
Main, Main,
} from './Document.js'; } from './Document.js';
import { import type {
RuntimePlugin, RuntimePlugin,
AppContext, AppContext,
AppConfig, AppConfig,
@ -32,15 +34,20 @@ export {
runServerApp, runServerApp,
renderDocument, renderDocument,
useAppContext, useAppContext,
Link,
Outlet,
Meta, Meta,
Title, Title,
Links, Links,
Scripts, Scripts,
Main, Main,
defineAppConfig, defineAppConfig,
// types // react-router-dom API
Link,
Outlet,
useParams,
useSearchParams,
};
export type {
RuntimePlugin, RuntimePlugin,
AppContext, AppContext,
AppConfig, AppConfig,

View File

@ -107,8 +107,8 @@ function BrowserEntry({ history, appContext, Document, ...rest }: BrowserEntryPr
useLayoutEffect(() => { useLayoutEffect(() => {
history.listen(({ action, location }) => { history.listen(({ action, location }) => {
const matches = matchRoutes(routes, location); const matches = matchRoutes(routes, location);
if (!matches) { if (!matches.length) {
throw new Error(`Routes not found in location ${location}.`); throw new Error(`Routes not found in location ${location.pathname}.`);
} }
loadNextPage(matches, (pageData) => { loadNextPage(matches, (pageData) => {

View File

@ -26,7 +26,7 @@
"bugs": "https://github.com/ice-lab/ice-next/issues", "bugs": "https://github.com/ice-lab/ice-next/issues",
"homepage": "https://next.ice.work", "homepage": "https://next.ice.work",
"devDependencies": { "devDependencies": {
"build-scripts": "^2.0.0-15", "build-scripts": "^2.0.0-16",
"esbuild": "^0.14.23", "esbuild": "^0.14.23",
"@ice/runtime": "^1.0.0", "@ice/runtime": "^1.0.0",
"@ice/route-manifest": "^1.0.0", "@ice/route-manifest": "^1.0.0",

View File

@ -1,3 +1,4 @@
import type { DefineRouteFunction } from '@ice/route-manifest';
import type { IPluginList } from 'build-scripts'; import type { IPluginList } from 'build-scripts';
import type { Config, ModifyWebpackConfig } from './config'; import type { Config, ModifyWebpackConfig } from './config';
@ -12,5 +13,9 @@ export interface UserConfig {
proxy?: Config['proxy']; proxy?: Config['proxy'];
filename?: string; filename?: string;
webpack?: ModifyWebpackConfig; webpack?: ModifyWebpackConfig;
routes?: {
ignoreFiles?: string[];
defineRoutes?: (defineRoute: DefineRouteFunction) => void;
};
plugins?: IPluginList; plugins?: IPluginList;
} }

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,6 @@ describe(`build ${example}`, () => {
test('open /', async () => { test('open /', async () => {
await buildFixture(example); await buildFixture(example);
const res = await setupBrowser({ example }); const res = await setupBrowser({ example });
page = res.page; page = res.page;
browser = res.browser; browser = res.browser;

View File

@ -0,0 +1,107 @@
import { expect, test, describe, afterAll } from 'vitest';
import { buildFixture, setupBrowser } from '../utils/build';
import { startFixture, setupStartBrowser } from '../utils/start';
import { Page } from '../utils/browser';
const example = 'routes-generate';
describe(`build ${example}`, () => {
let page: Page = null;
let browser = null;
test('open /', async () => {
await buildFixture(example);
const res = await setupBrowser({ example });
page = res.page;
browser = res.browser;
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Home']);
}, 120000);
test('define extra routes', async () => {
let res = await setupBrowser({ example, defaultHtml: 'about-me.html' });
page = res.page;
browser = res.browser;
expect(await page.$$text('h1')).toStrictEqual([]);
expect(await page.$$text('h2')).toStrictEqual(['About']);
res = await setupBrowser({ example, defaultHtml: 'product.html' });
page = res.page;
browser = res.browser;
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Products Page']);
});
test('page layout', async () => {
let res = await setupBrowser({ example, defaultHtml: 'dashboard/a.html' });
page = res.page;
browser = res.browser;
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Dashboard']);
expect(await page.$$text('h3')).toStrictEqual(['A page']);
res = await setupBrowser({ example, defaultHtml: 'dashboard/b.html' });
page = res.page;
browser = res.browser;
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Dashboard']);
expect(await page.$$text('h3')).toStrictEqual(['B page']);
});
// TODO: dynamic-routes test
test.todo('dynamic routes', async () => {});
afterAll(async () => {
await browser.close();
});
});
describe(`start ${example}`, () => {
let page: Page = null;
let browser = null;
test('setup devServer', async () => {
const { devServer, port } = await startFixture(example);
const res = await setupStartBrowser({ server: devServer, port });
page = res.page;
browser = res.browser;
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Home']);
}, 120000);
test('define extra routes', async () => {
await page.push('about-me');
expect(await page.$$text('h1')).toStrictEqual([]);
expect(await page.$$text('h2')).toStrictEqual(['About']);
await page.push('product');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Products Page']);
});
test('page layout', async () => {
await page.push('dashboard/a');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Dashboard']);
expect(await page.$$text('h3')).toStrictEqual(['A page']);
await page.push('dashboard/b');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Dashboard']);
expect(await page.$$text('h3')).toStrictEqual(['B page']);
});
test('dynamic routes layout', async () => {
await page.push('detail/a');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Detail id: a']);
await page.push('detail/b');
expect(await page.$$text('h1')).toStrictEqual(['Layout']);
expect(await page.$$text('h2')).toStrictEqual(['Detail id: b']);
});
afterAll(async () => {
await browser.close();
});
});

View File

@ -11,7 +11,8 @@ export default defineConfig({
alias: { ...moduleNameMapper }, alias: { ...moduleNameMapper },
}, },
test: { test: {
// disable threads to avoid `Segmentation fault (core dumped)` error: https://github.com/vitest-dev/vitest/issues/317 // To avoid error `Segmentation fault (core dumped)` in CI environment, disable threads
// ref: https://github.com/vitest-dev/vitest/issues/317
threads: false, threads: false,
exclude: [ exclude: [
'**/node_modules/**', '**/node_modules/**',