mirror of https://github.com/alibaba/ice.git
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:
parent
95d49c46d5
commit
d19f21e8b0
|
|
@ -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');
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { defineAppConfig } from 'ice';
|
||||
|
||||
export default defineAppConfig({});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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></>;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<h3>A page</h3>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<h3>B page</h3>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<div>Index</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import { Outlet } from 'ice';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Layout</h1>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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></>;
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
"semver": "^7.3.5",
|
||||
"stylelint": "^14.3.0",
|
||||
"typescript": "^4.5.5",
|
||||
"vitest": "^0.8.4"
|
||||
"vitest": "^0.9.2"
|
||||
},
|
||||
"packageManager": "pnpm"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@ice/types": "^1.0.0",
|
||||
"build-scripts": "^2.0.0-15",
|
||||
"build-scripts": "^2.0.0-16",
|
||||
"webpack": "^5.69.1",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function getEntry(rootDir: string) {
|
|||
}
|
||||
return {
|
||||
runtime: ['react', 'react-dom', '@ice/runtime'],
|
||||
index: {
|
||||
main: {
|
||||
import: [entryFile],
|
||||
dependOn: 'runtime',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"@ice/runtime": "^1.0.0",
|
||||
"@ice/webpack-config": "^1.0.0",
|
||||
"address": "^1.1.2",
|
||||
"build-scripts": "^2.0.0-15",
|
||||
"build-scripts": "^2.0.0-16",
|
||||
"chalk": "^4.0.0",
|
||||
"commander": "^9.0.0",
|
||||
"consola": "^2.15.3",
|
||||
|
|
|
|||
|
|
@ -32,16 +32,9 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
|
|||
const templateDir = path.join(__dirname, '../template/');
|
||||
const configFile = 'ice.config.(mts|mjs|ts|js|cjs|json)';
|
||||
const dataCache = new Map<string, string>();
|
||||
|
||||
const routesRenderData = generateRoutesInfo(rootDir);
|
||||
dataCache.set('routes', JSON.stringify(routesRenderData));
|
||||
|
||||
const generator = new Generator({
|
||||
rootDir,
|
||||
targetDir,
|
||||
defaultRenderData: {
|
||||
...routesRenderData,
|
||||
},
|
||||
// add default template of ice
|
||||
templates: [templateDir],
|
||||
});
|
||||
|
|
@ -49,14 +42,6 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
|
|||
const { addWatchEvent, removeWatchEvent } = createWatch({
|
||||
watchDir: rootDir,
|
||||
command,
|
||||
watchEvents: getWatchEvents({
|
||||
generator,
|
||||
rootDir,
|
||||
targetDir,
|
||||
templateDir,
|
||||
configFile,
|
||||
cache: dataCache,
|
||||
}),
|
||||
});
|
||||
|
||||
const generatorAPI = {
|
||||
|
|
@ -72,6 +57,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
|
|||
addRenderFile: generator.addRenderFile,
|
||||
addRenderTemplate: generator.addTemplateFiles,
|
||||
};
|
||||
|
||||
const ctx = new Context<any, ExtendsPluginAPI>({
|
||||
rootDir,
|
||||
command,
|
||||
|
|
@ -90,16 +76,32 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
|
|||
},
|
||||
});
|
||||
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());
|
||||
generator.modifyRenderData((renderData) => ({
|
||||
...renderData,
|
||||
runtimeModules,
|
||||
}));
|
||||
await ctx.setup();
|
||||
|
||||
// render template before webpack compile
|
||||
const renderStart = new Date().getTime();
|
||||
|
||||
generator.render();
|
||||
|
||||
addWatchEvent(
|
||||
...getWatchEvents({ generator, targetDir, templateDir, cache: dataCache, ctx }),
|
||||
);
|
||||
|
||||
consola.debug('template render cost:', new Date().getTime() - renderStart);
|
||||
|
||||
// define runtime env before get webpack config
|
||||
defineRuntimeEnv();
|
||||
const compileIncludes = runtimeModules.map(({ name }) => `${name}/runtime`);
|
||||
|
|
|
|||
|
|
@ -1,32 +1,34 @@
|
|||
import * as path from 'path';
|
||||
import consola from 'consola';
|
||||
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 type Generator from './service/runtimeGenerator';
|
||||
|
||||
interface Options {
|
||||
rootDir: string;
|
||||
targetDir: string;
|
||||
templateDir: string;
|
||||
configFile: string;
|
||||
generator: Generator;
|
||||
cache: Map<string, string>;
|
||||
ctx: Context<Config>;
|
||||
}
|
||||
|
||||
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 = [
|
||||
/src\/pages\/?[\w*-:.$]+$/,
|
||||
(eventName: string) => {
|
||||
if (eventName === 'add' || eventName === 'unlink') {
|
||||
const routesRenderData = generateRoutesInfo(rootDir);
|
||||
const routesRenderData = generateRoutesInfo(rootDir, routesConfig);
|
||||
const stringifiedData = JSON.stringify(routesRenderData);
|
||||
if (cache.get('routes') !== stringifiedData) {
|
||||
cache.set('routes', stringifiedData);
|
||||
consola.debug('[event]', `routes data regenerated: ${stringifiedData}`);
|
||||
generator.renderFile(
|
||||
path.join(templateDir, 'routes.ts.ejs'),
|
||||
path.join(rootDir, targetDir, 'route.ts'),
|
||||
path.join(rootDir, targetDir, 'routes.ts'),
|
||||
routesRenderData,
|
||||
);
|
||||
generator.renderFile(
|
||||
|
|
@ -38,6 +40,7 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
|
|||
}
|
||||
},
|
||||
];
|
||||
|
||||
const watchGlobalStyle: WatchEvent = [
|
||||
/src\/global.(scss|less|css)/,
|
||||
(event: string, filePath: string) => {
|
||||
|
|
@ -49,7 +52,7 @@ const getWatchEvents = (options: Options): WatchEvent[] => {
|
|||
];
|
||||
|
||||
const watchConfigFile: WatchEvent = [
|
||||
new RegExp(configFile),
|
||||
new RegExp((typeof configFile === 'string' ? [configFile] : configFile).join('|')),
|
||||
(event: string, filePath: string) => {
|
||||
if (event === 'change') {
|
||||
consola.warn(`Found a change in ${path.basename(filePath)}. Restart the dev server to see the changes in effect.`);
|
||||
|
|
|
|||
|
|
@ -112,6 +112,10 @@ const userConfig = [
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'routes',
|
||||
validation: 'object',
|
||||
},
|
||||
];
|
||||
|
||||
const cliOptions = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import type { RouteItem } from '@ice/runtime';
|
||||
|
||||
interface Options {
|
||||
|
|
@ -20,7 +20,7 @@ export default async function generateHTML(options: Options) {
|
|||
} = options;
|
||||
|
||||
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);
|
||||
|
||||
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`;
|
||||
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
|
||||
* @returns
|
||||
*/
|
||||
function getPaths(routes: RouteItem[]): string[] {
|
||||
function getPaths(routes: RouteItem[], parentPath = ''): string[] {
|
||||
let pathList = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
if (route.children) {
|
||||
pathList = pathList.concat(getPaths(route.children));
|
||||
pathList = pathList.concat(getPaths(route.children, route.path));
|
||||
} else {
|
||||
pathList.push(route.path || '/');
|
||||
pathList.push(path.join('/', parentPath, route.path || ''));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import * as path from 'path';
|
||||
import { formatNestedRouteManifest, generateRouteManifest } from '@ice/route-manifest';
|
||||
import type { NestedRouteManifest } from '@ice/route-manifest';
|
||||
import type { UserConfig } from '@ice/types';
|
||||
|
||||
export function generateRoutesInfo(rootDir: string) {
|
||||
const routeManifest = generateRouteManifest(rootDir);
|
||||
export function generateRoutesInfo(rootDir: string, routesConfig: UserConfig['routes'] = {}) {
|
||||
const routeManifest = generateRouteManifest(rootDir, routesConfig.ignoreFiles, routesConfig.defineRoutes);
|
||||
const routes = formatNestedRouteManifest(routeManifest);
|
||||
const str = generateNestRoutesStr(routes);
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ function generateNestRoutesStr(nestRouteManifest: NestedRouteManifest[]) {
|
|||
|
||||
let str = `{
|
||||
path: '${routePath || ''}',
|
||||
load: () => import(/* webpackChunkName: "${componentName}" */ '@/${componentFile}'),
|
||||
load: () => import(/* webpackChunkName: "${componentName}" */ '@/pages/${componentFile}'),
|
||||
componentName: '${componentName}',
|
||||
index: ${index},
|
||||
id: '${id}',
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const RENDER_WAIT = 150;
|
|||
interface Options {
|
||||
rootDir: string;
|
||||
targetDir: string;
|
||||
defaultRenderData: RenderData;
|
||||
defaultRenderData?: RenderData;
|
||||
templates?: (string | TemplateOptions)[];
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export default class Generator {
|
|||
private contentTypes: string[];
|
||||
|
||||
public constructor(options: Options) {
|
||||
const { rootDir, targetDir, defaultRenderData, templates } = options;
|
||||
const { rootDir, targetDir, defaultRenderData = {}, templates } = options;
|
||||
this.rootDir = rootDir;
|
||||
this.targetDir = targetDir;
|
||||
this.renderData = defaultRenderData;
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ function createWatch(options: {
|
|||
|
||||
return {
|
||||
watcher,
|
||||
addWatchEvent: ([pattern, action, name]: WatchEvent) => {
|
||||
watchEvents.push([pattern, action, name]);
|
||||
addWatchEvent: (...args: WatchEvent[]) => {
|
||||
watchEvents.push(...args);
|
||||
},
|
||||
removeWatchEvent: (name: string) => {
|
||||
const eventIndex = watchEvents.findIndex(([,,watchName]) => watchName === name);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import {
|
|||
useAppContext,
|
||||
Link,
|
||||
Outlet,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
Meta,
|
||||
Title,
|
||||
Links,
|
||||
|
|
@ -16,6 +18,8 @@ export {
|
|||
useAppContext,
|
||||
Link,
|
||||
Outlet,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
Meta,
|
||||
Title,
|
||||
Links,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import minimatch from 'minimatch';
|
|||
import { createRouteId, defineRoutes } from './routes.js';
|
||||
import type { RouteManifest, DefineRouteFunction, NestedRouteManifest } from './routes.js';
|
||||
|
||||
export {
|
||||
export type {
|
||||
RouteManifest,
|
||||
NestedRouteManifest,
|
||||
DefineRouteFunction,
|
||||
};
|
||||
|
||||
const validRouteChar = ['-', '\\w', '/', ':', '*'];
|
||||
|
|
@ -26,14 +27,18 @@ export function isRouteModuleFile(filename: string): boolean {
|
|||
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 routeManifest: RouteManifest = {};
|
||||
// 2. find routes in `src/pages` directory
|
||||
if (fs.existsSync(path.resolve(srcDir, 'pages'))) {
|
||||
const conventionalRoutes = defineConventionalRoutes(
|
||||
rootDir,
|
||||
[], // TODO: add ignoredFilePatterns defined in ice.config.js
|
||||
ignoreFiles,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +81,6 @@ function defineConventionalRoutes(
|
|||
ignoredFilePatterns?: string[],
|
||||
): RouteManifest {
|
||||
const files: { [routeId: string]: string } = {};
|
||||
|
||||
// 1. find all route components in src/pages
|
||||
visitFiles(
|
||||
path.join(rootDir, 'src', 'pages'),
|
||||
|
|
@ -78,10 +92,9 @@ function defineConventionalRoutes(
|
|||
return;
|
||||
}
|
||||
|
||||
const filePath = path.join('pages', file);
|
||||
if (isRouteModuleFile(file)) {
|
||||
let routeId = createRouteId(filePath);
|
||||
files[routeId] = filePath;
|
||||
let routeId = createRouteId(file);
|
||||
files[routeId] = file;
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
|
@ -102,24 +115,25 @@ function defineConventionalRoutes(
|
|||
});
|
||||
|
||||
for (let routeId of childRouteIds) {
|
||||
const parentRoutePath = removeLastLayoutStrFromId(parentId) || '';
|
||||
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)) {
|
||||
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');
|
||||
let fullPath = createRoutePath(routeId.slice('pages'.length + 1));
|
||||
let uniqueRouteId = (fullPath || '') + (isIndexRoute ? '?index' : '');
|
||||
const isIndexRoute = routeId === 'index' || routeId.endsWith('/index');
|
||||
const fullPath = createRoutePath(routeId);
|
||||
const uniqueRouteId = (fullPath || '') + (isIndexRoute ? '?index' : '');
|
||||
|
||||
if (uniqueRouteId) {
|
||||
if (uniqueRoutes.has(uniqueRouteId)) {
|
||||
throw new Error(
|
||||
`Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify(
|
||||
routeId,
|
||||
)} conflicts with route ${JSON.stringify(
|
||||
uniqueRoutes.get(uniqueRouteId),
|
||||
)}`,
|
||||
`Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify(routeFilePath)}
|
||||
conflicts with route ${JSON.stringify(uniqueRoutes.get(uniqueRouteId))}`,
|
||||
);
|
||||
} else {
|
||||
uniqueRoutes.set(uniqueRouteId, routeId);
|
||||
|
|
@ -155,7 +169,7 @@ export function createRoutePath(routeId: string): string | undefined {
|
|||
let result = '';
|
||||
let rawSegmentBuffer = '';
|
||||
|
||||
const partialRouteId = removeLayoutStrFromId(routeId);
|
||||
const partialRouteId = removeLastLayoutStrFromId(routeId);
|
||||
|
||||
for (let i = 0; i < partialRouteId.length; i++) {
|
||||
const char = partialRouteId.charAt(i);
|
||||
|
|
@ -196,7 +210,7 @@ function findParentRouteId(
|
|||
return routeIds.find((id) => {
|
||||
// childRouteId is `pages/about` and id is `pages/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
|
||||
*
|
||||
* /About/layout -> /About
|
||||
* /About/layout/index -> /About/layout/index
|
||||
* 'layout' -> ''
|
||||
* 'About/layout' -> 'About'
|
||||
* 'About/layout/index' -> 'About/layout/index'
|
||||
*/
|
||||
function removeLayoutStrFromId(id?: string) {
|
||||
return id?.endsWith('/layout') ? id.slice(0, id.length - '/layout'.length) : id;
|
||||
function removeLastLayoutStrFromId(id?: string) {
|
||||
const layoutStrs = ['/layout', 'layout'];
|
||||
const currentLayoutStr = layoutStrs.find(layoutStr => id?.endsWith(layoutStr));
|
||||
return currentLayoutStr ? id.slice(0, id.length - currentLayoutStr.length) : id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,9 +100,10 @@ export function defineRoutes(
|
|||
// route(path, file, options)
|
||||
options = optionsOrChildren || {};
|
||||
}
|
||||
|
||||
const id = createRouteId(file);
|
||||
const route: ConfigRoute = {
|
||||
path: path || undefined,
|
||||
path,
|
||||
index: options.index ? true : undefined,
|
||||
id,
|
||||
parentId:
|
||||
|
|
@ -144,6 +145,6 @@ function stripFileExtension(file: string) {
|
|||
function createComponentName(id: string) {
|
||||
return id.replace('.', '/') // 'pages/home.news' -> pages/home/news
|
||||
.split('/')
|
||||
.map((item: string) => item[0].toUpperCase() + item.slice(1, item.length))
|
||||
.join('');
|
||||
.map((item: string) => item.toLowerCase())
|
||||
.join('-');
|
||||
}
|
||||
|
|
@ -3,70 +3,33 @@
|
|||
exports[`generateRouteManifest function > layout-routes 1`] = `
|
||||
[
|
||||
{
|
||||
"componentName": "PagesBlogIndex",
|
||||
"file": "pages/blog/index.tsx",
|
||||
"id": "pages/blog/index",
|
||||
"componentName": "blog-index",
|
||||
"file": "blog/index.tsx",
|
||||
"id": "blog/index",
|
||||
"index": true,
|
||||
"parentId": undefined,
|
||||
"path": "/blog",
|
||||
"path": "blog",
|
||||
},
|
||||
{
|
||||
"componentName": "PagesBlog$id",
|
||||
"file": "pages/blog/$id.tsx",
|
||||
"id": "pages/blog/$id",
|
||||
"componentName": "blog-$id",
|
||||
"file": "blog/$id.tsx",
|
||||
"id": "blog/$id",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": "/blog/:id",
|
||||
"path": "blog/:id",
|
||||
},
|
||||
{
|
||||
"componentName": "PagesAbout",
|
||||
"file": "pages/about.tsx",
|
||||
"id": "pages/about",
|
||||
"componentName": "about",
|
||||
"file": "about.tsx",
|
||||
"id": "about",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": "/about",
|
||||
"path": "about",
|
||||
},
|
||||
{
|
||||
"componentName": "PagesIndex",
|
||||
"file": "pages/index.tsx",
|
||||
"id": "pages/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",
|
||||
"componentName": "index",
|
||||
"file": "index.tsx",
|
||||
"id": "index",
|
||||
"index": true,
|
||||
"parentId": undefined,
|
||||
"path": undefined,
|
||||
|
|
|
|||
|
|
@ -2,42 +2,87 @@
|
|||
|
||||
exports[`generateRouteManifest function > basic-routes 1`] = `
|
||||
{
|
||||
"pages/About/index": {
|
||||
"componentName": "PagesAboutIndex",
|
||||
"file": "pages/About/index.tsx",
|
||||
"id": "pages/About/index",
|
||||
"About/index": {
|
||||
"componentName": "about-index",
|
||||
"file": "About/index.tsx",
|
||||
"id": "About/index",
|
||||
"index": true,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/About",
|
||||
"parentId": "layout",
|
||||
"path": "About",
|
||||
},
|
||||
"pages/About/me/index": {
|
||||
"componentName": "PagesAboutMeIndex",
|
||||
"file": "pages/About/me/index.tsx",
|
||||
"id": "pages/About/me/index",
|
||||
"About/me/index": {
|
||||
"componentName": "about-me-index",
|
||||
"file": "About/me/index.tsx",
|
||||
"id": "About/me/index",
|
||||
"index": true,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/About/me",
|
||||
"parentId": "layout",
|
||||
"path": "About/me",
|
||||
},
|
||||
"pages/home": {
|
||||
"componentName": "PagesHome",
|
||||
"file": "pages/home.tsx",
|
||||
"id": "pages/home",
|
||||
"home": {
|
||||
"componentName": "home",
|
||||
"file": "home.tsx",
|
||||
"id": "home",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/home",
|
||||
"parentId": "layout",
|
||||
"path": "home",
|
||||
},
|
||||
"pages/index": {
|
||||
"componentName": "PagesIndex",
|
||||
"file": "pages/index.tsx",
|
||||
"id": "pages/index",
|
||||
"index": {
|
||||
"componentName": "index",
|
||||
"file": "index.tsx",
|
||||
"id": "index",
|
||||
"index": true,
|
||||
"parentId": "pages/layout",
|
||||
"parentId": "layout",
|
||||
"path": undefined,
|
||||
},
|
||||
"pages/layout": {
|
||||
"componentName": "PagesLayout",
|
||||
"file": "pages/layout.tsx",
|
||||
"id": "pages/layout",
|
||||
"layout": {
|
||||
"componentName": "layout",
|
||||
"file": "layout.tsx",
|
||||
"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,
|
||||
"parentId": undefined,
|
||||
"path": undefined,
|
||||
|
|
@ -47,18 +92,18 @@ exports[`generateRouteManifest function > basic-routes 1`] = `
|
|||
|
||||
exports[`generateRouteManifest function > doc-delimeters-routes 1`] = `
|
||||
{
|
||||
"pages/home.news": {
|
||||
"componentName": "PagesHomeNews",
|
||||
"file": "pages/home.news.tsx",
|
||||
"id": "pages/home.news",
|
||||
"home.news": {
|
||||
"componentName": "home-news",
|
||||
"file": "home.news.tsx",
|
||||
"id": "home.news",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/home/news",
|
||||
"parentId": "layout",
|
||||
"path": "home/news",
|
||||
},
|
||||
"pages/layout": {
|
||||
"componentName": "PagesLayout",
|
||||
"file": "pages/layout.tsx",
|
||||
"id": "pages/layout",
|
||||
"layout": {
|
||||
"componentName": "layout",
|
||||
"file": "layout.tsx",
|
||||
"id": "layout",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": undefined,
|
||||
|
|
@ -68,34 +113,34 @@ exports[`generateRouteManifest function > doc-delimeters-routes 1`] = `
|
|||
|
||||
exports[`generateRouteManifest function > dynamic-routes 1`] = `
|
||||
{
|
||||
"pages/about": {
|
||||
"componentName": "PagesAbout",
|
||||
"file": "pages/about.tsx",
|
||||
"id": "pages/about",
|
||||
"about": {
|
||||
"componentName": "about",
|
||||
"file": "about.tsx",
|
||||
"id": "about",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": "/about",
|
||||
"path": "about",
|
||||
},
|
||||
"pages/blog/$id": {
|
||||
"componentName": "PagesBlog$id",
|
||||
"file": "pages/blog/$id.tsx",
|
||||
"id": "pages/blog/$id",
|
||||
"blog/$id": {
|
||||
"componentName": "blog-$id",
|
||||
"file": "blog/$id.tsx",
|
||||
"id": "blog/$id",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": "/blog/:id",
|
||||
"path": "blog/:id",
|
||||
},
|
||||
"pages/blog/index": {
|
||||
"componentName": "PagesBlogIndex",
|
||||
"file": "pages/blog/index.tsx",
|
||||
"id": "pages/blog/index",
|
||||
"blog/index": {
|
||||
"componentName": "blog-index",
|
||||
"file": "blog/index.tsx",
|
||||
"id": "blog/index",
|
||||
"index": true,
|
||||
"parentId": undefined,
|
||||
"path": "/blog",
|
||||
"path": "blog",
|
||||
},
|
||||
"pages/index": {
|
||||
"componentName": "PagesIndex",
|
||||
"file": "pages/index.tsx",
|
||||
"id": "pages/index",
|
||||
"index": {
|
||||
"componentName": "index",
|
||||
"file": "index.tsx",
|
||||
"id": "index",
|
||||
"index": true,
|
||||
"parentId": 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`] = `
|
||||
{
|
||||
"pages/about": {
|
||||
"componentName": "PagesAbout",
|
||||
"file": "pages/about.tsx",
|
||||
"id": "pages/about",
|
||||
"about": {
|
||||
"componentName": "about",
|
||||
"file": "about.tsx",
|
||||
"id": "about",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/about",
|
||||
"parentId": "layout",
|
||||
"path": "about",
|
||||
},
|
||||
"pages/blog/$id": {
|
||||
"componentName": "PagesBlog$id",
|
||||
"file": "pages/blog/$id.tsx",
|
||||
"id": "pages/blog/$id",
|
||||
"blog/$id": {
|
||||
"componentName": "blog-$id",
|
||||
"file": "blog/$id.tsx",
|
||||
"id": "blog/$id",
|
||||
"index": undefined,
|
||||
"parentId": "pages/blog/layout",
|
||||
"path": "/:id",
|
||||
"parentId": "blog/layout",
|
||||
"path": ":id",
|
||||
},
|
||||
"pages/blog/index": {
|
||||
"componentName": "PagesBlogIndex",
|
||||
"file": "pages/blog/index.tsx",
|
||||
"id": "pages/blog/index",
|
||||
"blog/index": {
|
||||
"componentName": "blog-index",
|
||||
"file": "blog/index.tsx",
|
||||
"id": "blog/index",
|
||||
"index": true,
|
||||
"parentId": "pages/blog/layout",
|
||||
"parentId": "blog/layout",
|
||||
"path": undefined,
|
||||
},
|
||||
"pages/blog/layout": {
|
||||
"componentName": "PagesBlogLayout",
|
||||
"file": "pages/blog/layout.tsx",
|
||||
"id": "pages/blog/layout",
|
||||
"blog/layout": {
|
||||
"componentName": "blog-layout",
|
||||
"file": "blog/layout.tsx",
|
||||
"id": "blog/layout",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/blog",
|
||||
"parentId": "layout",
|
||||
"path": "blog",
|
||||
},
|
||||
"pages/home/index": {
|
||||
"componentName": "PagesHomeIndex",
|
||||
"file": "pages/home/index.tsx",
|
||||
"id": "pages/home/index",
|
||||
"home/index": {
|
||||
"componentName": "home-index",
|
||||
"file": "home/index.tsx",
|
||||
"id": "home/index",
|
||||
"index": true,
|
||||
"parentId": "pages/home/layout",
|
||||
"parentId": "home/layout",
|
||||
"path": undefined,
|
||||
},
|
||||
"pages/home/layout": {
|
||||
"componentName": "PagesHomeLayout",
|
||||
"file": "pages/home/layout.tsx",
|
||||
"id": "pages/home/layout",
|
||||
"home/layout": {
|
||||
"componentName": "home-layout",
|
||||
"file": "home/layout.tsx",
|
||||
"id": "home/layout",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/home",
|
||||
"parentId": "layout",
|
||||
"path": "home",
|
||||
},
|
||||
"pages/home/layout/index": {
|
||||
"componentName": "PagesHomeLayoutIndex",
|
||||
"file": "pages/home/layout/index.tsx",
|
||||
"id": "pages/home/layout/index",
|
||||
"home/layout/index": {
|
||||
"componentName": "home-layout-index",
|
||||
"file": "home/layout/index.tsx",
|
||||
"id": "home/layout/index",
|
||||
"index": true,
|
||||
"parentId": "pages/home/layout",
|
||||
"path": "/layout",
|
||||
"parentId": "home/layout",
|
||||
"path": "layout",
|
||||
},
|
||||
"pages/index": {
|
||||
"componentName": "PagesIndex",
|
||||
"file": "pages/index.tsx",
|
||||
"id": "pages/index",
|
||||
"index": {
|
||||
"componentName": "index",
|
||||
"file": "index.tsx",
|
||||
"id": "index",
|
||||
"index": true,
|
||||
"parentId": "pages/layout",
|
||||
"parentId": "layout",
|
||||
"path": undefined,
|
||||
},
|
||||
"pages/layout": {
|
||||
"componentName": "PagesLayout",
|
||||
"file": "pages/layout.tsx",
|
||||
"id": "pages/layout",
|
||||
"layout": {
|
||||
"componentName": "layout",
|
||||
"file": "layout.tsx",
|
||||
"id": "layout",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": undefined,
|
||||
|
|
@ -182,235 +264,26 @@ exports[`generateRouteManifest function > layout-routes 1`] = `
|
|||
|
||||
exports[`generateRouteManifest function > splat-routes 1`] = `
|
||||
{
|
||||
"pages/$": {
|
||||
"componentName": "Pages$",
|
||||
"file": "pages/$.tsx",
|
||||
"id": "pages/$",
|
||||
"$": {
|
||||
"componentName": "$",
|
||||
"file": "$.tsx",
|
||||
"id": "$",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/*",
|
||||
"parentId": "layout",
|
||||
"path": "*",
|
||||
},
|
||||
"pages/home": {
|
||||
"componentName": "PagesHome",
|
||||
"file": "pages/home.tsx",
|
||||
"id": "pages/home",
|
||||
"home": {
|
||||
"componentName": "home",
|
||||
"file": "home.tsx",
|
||||
"id": "home",
|
||||
"index": undefined,
|
||||
"parentId": "pages/layout",
|
||||
"path": "/home",
|
||||
"parentId": "layout",
|
||||
"path": "home",
|
||||
},
|
||||
"pages/layout": {
|
||||
"componentName": "PagesLayout",
|
||||
"file": "pages/layout.tsx",
|
||||
"id": "pages/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",
|
||||
"layout": {
|
||||
"componentName": "layout",
|
||||
"file": "layout.tsx",
|
||||
"id": "layout",
|
||||
"index": undefined,
|
||||
"parentId": undefined,
|
||||
"path": undefined,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,22 @@ describe('generateRouteManifest function', () => {
|
|||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
Link,
|
||||
Outlet,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import Runtime from './runtime.js';
|
||||
import App from './App.js';
|
||||
|
|
@ -14,7 +16,7 @@ import {
|
|||
Scripts,
|
||||
Main,
|
||||
} from './Document.js';
|
||||
import {
|
||||
import type {
|
||||
RuntimePlugin,
|
||||
AppContext,
|
||||
AppConfig,
|
||||
|
|
@ -32,15 +34,20 @@ export {
|
|||
runServerApp,
|
||||
renderDocument,
|
||||
useAppContext,
|
||||
Link,
|
||||
Outlet,
|
||||
Meta,
|
||||
Title,
|
||||
Links,
|
||||
Scripts,
|
||||
Main,
|
||||
defineAppConfig,
|
||||
// types
|
||||
// react-router-dom API
|
||||
Link,
|
||||
Outlet,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
};
|
||||
|
||||
export type {
|
||||
RuntimePlugin,
|
||||
AppContext,
|
||||
AppConfig,
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ function BrowserEntry({ history, appContext, Document, ...rest }: BrowserEntryPr
|
|||
useLayoutEffect(() => {
|
||||
history.listen(({ action, location }) => {
|
||||
const matches = matchRoutes(routes, location);
|
||||
if (!matches) {
|
||||
throw new Error(`Routes not found in location ${location}.`);
|
||||
if (!matches.length) {
|
||||
throw new Error(`Routes not found in location ${location.pathname}.`);
|
||||
}
|
||||
|
||||
loadNextPage(matches, (pageData) => {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"bugs": "https://github.com/ice-lab/ice-next/issues",
|
||||
"homepage": "https://next.ice.work",
|
||||
"devDependencies": {
|
||||
"build-scripts": "^2.0.0-15",
|
||||
"build-scripts": "^2.0.0-16",
|
||||
"esbuild": "^0.14.23",
|
||||
"@ice/runtime": "^1.0.0",
|
||||
"@ice/route-manifest": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { DefineRouteFunction } from '@ice/route-manifest';
|
||||
import type { IPluginList } from 'build-scripts';
|
||||
import type { Config, ModifyWebpackConfig } from './config';
|
||||
|
||||
|
|
@ -12,5 +13,9 @@ export interface UserConfig {
|
|||
proxy?: Config['proxy'];
|
||||
filename?: string;
|
||||
webpack?: ModifyWebpackConfig;
|
||||
routes?: {
|
||||
ignoreFiles?: string[];
|
||||
defineRoutes?: (defineRoute: DefineRouteFunction) => void;
|
||||
};
|
||||
plugins?: IPluginList;
|
||||
}
|
||||
|
|
|
|||
702
pnpm-lock.yaml
702
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -16,7 +16,6 @@ describe(`build ${example}`, () => {
|
|||
|
||||
test('open /', async () => {
|
||||
await buildFixture(example);
|
||||
|
||||
const res = await setupBrowser({ example });
|
||||
page = res.page;
|
||||
browser = res.browser;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,8 @@ export default defineConfig({
|
|||
alias: { ...moduleNameMapper },
|
||||
},
|
||||
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,
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
|
|
@ -19,4 +20,4 @@ export default defineConfig({
|
|||
'**/tests/fixtures/**',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue