Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Add filesystem-based routing #609

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/demo/public/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function Header() {
<a href="/alias-outside">Alias outside</a>
<a href="/error">Error</a>
<a href="/meta-tags">Meta-Tags</a>
<a href="/fs-routes">FS Routes</a>
</nav>
<label>
URL:
Expand Down
5 changes: 5 additions & 0 deletions examples/demo/public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import Home from './pages/home.js';
// import About from './pages/about/index.js';
import NotFound from './pages/_404.js';
import Header from './header.tsx';
import { PageRoutes } from './pages/page-routes.js';
// import './style.css';
import { routes as pageRoutes } from 'wmr:fs-routes-preact';

const sleep = t => new Promise(r => setTimeout(r, t));

Expand All @@ -27,6 +29,7 @@ function hideLoading() {
}

export function App() {
console.log(pageRoutes);
return (
<LocationProvider>
<div class="app">
Expand All @@ -43,6 +46,8 @@ export function App() {
<JSONView path="/json" />
<MetaTags path="/meta-tags" />
<AliasOutside path="/alias-outside" />
<PageRoutes path="/fs-routes" />
{pageRoutes}
<NotFound default />
</Router>
</ErrorBoundary>
Expand Down
18 changes: 18 additions & 0 deletions examples/demo/public/pages/page-routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { routes } from 'wmr:fs-routes';

export function PageRoutes() {
return (
<ul>
{routes.map(route => {
const url = route.route.replace(/(:\w+)/g, (m, g) => {
return 1;
});
return (
<li key={route.route}>
<a href={url}>{route.route}</a>
</li>
);
})}
</ul>
);
}
3 changes: 3 additions & 0 deletions examples/demo/public/pages2/bar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>bar</p>;
}
6 changes: 6 additions & 0 deletions examples/demo/public/pages2/foo/[id].jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useRoute } from 'preact-iso';

export default function Page() {
const route = useRoute();
return <p>dynamic id: {route.params.id}</p>;
}
6 changes: 6 additions & 0 deletions examples/demo/public/pages2/foo/bar/[id2]/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useRoute } from 'preact-iso';

export default function Page() {
const route = useRoute();
return <p>dynamic id index: {route.params.id2}</p>;
}
3 changes: 3 additions & 0 deletions examples/demo/public/pages2/foo/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>foo</p>;
}
3 changes: 3 additions & 0 deletions examples/demo/public/pages2/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p>index</p>;
}
3 changes: 2 additions & 1 deletion examples/demo/wmr.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default function () {
return {
alias: {
'src/*': 'src'
}
},
routesDir: 'public/pages2'
};
}
1 change: 1 addition & 0 deletions packages/wmr/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function bool(v) {
// global options
prog
.option('--cwd', 'The working directory - equivalent to "(cd FOO && wmr)"')
.option('--routesDir', 'Directory for filesystem-based routes(default: <cwd>/routes)')
// Setting env variables isn't common knowledege for many windows users. Much
// easier to pass a flag to our binary instead.
.option('--debug', 'Print internal debugging messages to the console. Same as setting DEBUG=true');
Expand Down
1 change: 1 addition & 0 deletions packages/wmr/src/lib/normalize-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function normalizeOptions(options, mode, configWatchFiles = []) {
options.middleware = [];
options.features = { preact: true };
options.alias = options.alias || options.aliases || {};
options.routesDir = join(options.cwd, options.routesDir || 'pages');

// `wmr` / `wmr start` is a development command.
// `wmr build` / `wmr serve` are production commands.
Expand Down
4 changes: 4 additions & 0 deletions packages/wmr/src/lib/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import nodeBuiltinsPlugin from '../plugins/node-builtins-plugin.js';
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars';
import visualizer from 'rollup-plugin-visualizer';
import { defaultLoaders } from './default-loaders.js';
import fsRoutesPlugin from '../plugins/fs-routes-plugin.js';
import fsRoutesPreactPlugin from '../plugins/fs-routes-preact-plugin.js';

/**
* @param {import("wmr").Options} options
Expand All @@ -44,6 +46,8 @@ export function getPlugins(options) {
jsonPlugin({ cwd }),
bundlePlugin({ inline: !production, cwd }),
aliasPlugin({ alias, cwd: root }),
fsRoutesPlugin({ routesDir: options.routesDir, cwd, root, publicPath: options.publicPath }),
fsRoutesPreactPlugin(),
sucrasePlugin({
typescript: true,
sourcemap,
Expand Down
77 changes: 77 additions & 0 deletions packages/wmr/src/plugins/fs-routes-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { promises as fs } from 'fs';
import path from 'path';
import { toPosix } from './plugin-utils.js';

/**
* Traverse the pages directory and retrieve all routes.
* @param {string} root Directory to start search from
* @param {string} [dir]
*/
async function readRecursive(root, dir = root) {
const mixed = await fs.readdir(dir);

/** @type {{ route: string, url: string }[]} */
const routes = [];

await Promise.all(
mixed.map(async fileOrDir => {
const absolute = path.join(dir, fileOrDir);
if (/\.[tj]sx?$/.test(fileOrDir)) {
const name = path.basename(fileOrDir, path.extname(fileOrDir));
const routePath = name === 'index' ? path.relative(root, dir) : path.relative(root, path.join(dir, name));
routes.push({
route: '/' + toPosix(routePath.replace(/\[(\w+)\]/g, (m, g) => `:${g}`)),
url: '/' + toPosix(path.relative(root, path.join(dir, fileOrDir)))
});
}

const stats = await fs.lstat(absolute);
if (stats.isDirectory()) {
routes.push(...(await readRecursive(root, absolute)));
}
})
);

return routes;
}

/**
* Convert JSX to HTM
* @param {object} options
* @param {string} options.routesDir Controls whether files are processed to transform JSX.
* @param {string} options.cwd
* @param {string} options.root
* @param {string} options.publicPath
* @returns {import('wmr').Plugin}
*/
export default function fsRoutesPlugin({ routesDir, publicPath, root, cwd }) {
const PUBLIC = 'wmr:fs-routes';
const INTERNAL = '\0wmr:fs-routes';
return {
name: 'fs-routes',
resolveId(id) {
if (id === PUBLIC) {
return INTERNAL;
}
},
async load(id) {
if (id !== INTERNAL) return;

const routes = await readRecursive(routesDir);
const base = toPosix(path.relative(cwd, path.join(root, routesDir)));

const routesStr = routes
.map(route => {
return `{
route: ${JSON.stringify(route.route)},
load: () => import("${publicPath}${base}${route.url}")
}`;
})
.join(', ');

console.log(routesStr);

return `export const routes = [${routesStr}]`;
}
};
}
27 changes: 27 additions & 0 deletions packages/wmr/src/plugins/fs-routes-preact-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Export preconfigured routes for preact-iso
* @returns {import('wmr').Plugin}
*/
export default function fsRoutesPreactPlugin() {
const PUBLIC = 'wmr:fs-routes-preact';
const INTERNAL = '\0wmr:fs-routes-preact';
return {
name: 'fs-routes-preact',
resolveId(id) {
if (id === PUBLIC) {
return INTERNAL;
}
},
async load(id) {
if (id !== INTERNAL) return;

return `import { routes as rawRoutes } from 'wmr:fs-routes';
import { lazy, Route } from 'preact-iso';
import { h } from 'preact';

export const routes = rawRoutes.map(route => {
return h(Route, { path: route.route, component: lazy(route.load) });
});`;
}
};
}
8 changes: 8 additions & 0 deletions packages/wmr/src/plugins/plugin-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import path from 'path';

/**
* Replace path separators with a `/`
* @param {string} file
* @returns {string}
*/
export const toPosix = file => file.split(path.sep).join(path.posix.sep);
10 changes: 9 additions & 1 deletion packages/wmr/src/wmr-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,17 @@ export const TRANSFORMS = {
// const resolved = await NonRollup.resolveId(spec, importer);
let originalSpec = spec;
const resolved = await NonRollup.resolveId(spec, file);
console.log('RESOLVED', resolved, spec);
if (resolved) {
spec = typeof resolved == 'object' ? resolved.id : resolved;
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {

// Absolute paths are resolved relative to root dir.
console.log(' spec', spec, file);
if (/^\//.test(spec)) {
spec = '/' + relative(cwd, spec);
// TODO: Add to module graph
console.log(' >>>', spec, cwd);
} else if (/^(\\|[a-z]:\\)/i.test(spec)) {
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
if (!/^\.?\.?\//.test(spec)) {
spec = './' + spec;
Expand Down
9 changes: 9 additions & 0 deletions packages/wmr/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare module 'wmr' {
host: string;
port: number;
root: string;
routesDir: string;
out: string;
overlayDir: string;
sourcemap: boolean;
Expand Down Expand Up @@ -90,6 +91,14 @@ declare interface NodeModule {
}
declare var module: NodeModule;

// Models exposed by internal wmr plugins
declare module 'wmr:fs-routes' {
export const routes: Array<{ route: string; url: string }>;
}
declare module 'wmr:fs-routes-preact' {
export const routes: any[];
}

/** Maps authored classNames to their CSS Modules -suffixed generated classNames. */
type Mapping = Record<string, string>;
declare module '*.module.css' {
Expand Down