@@ -43,6 +46,8 @@ export function App() {
+
+ {pageRoutes}
diff --git a/examples/demo/public/pages/page-routes.js b/examples/demo/public/pages/page-routes.js
new file mode 100644
index 000000000..33312ea18
--- /dev/null
+++ b/examples/demo/public/pages/page-routes.js
@@ -0,0 +1,18 @@
+import { routes } from 'wmr:fs-routes';
+
+export function PageRoutes() {
+ return (
+
+ {routes.map(route => {
+ const url = route.route.replace(/(:\w+)/g, (m, g) => {
+ return 1;
+ });
+ return (
+ -
+ {route.route}
+
+ );
+ })}
+
+ );
+}
diff --git a/examples/demo/public/pages2/bar.jsx b/examples/demo/public/pages2/bar.jsx
new file mode 100644
index 000000000..ebfad70ea
--- /dev/null
+++ b/examples/demo/public/pages2/bar.jsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
bar
;
+}
diff --git a/examples/demo/public/pages2/foo/[id].jsx b/examples/demo/public/pages2/foo/[id].jsx
new file mode 100644
index 000000000..cd7a9d767
--- /dev/null
+++ b/examples/demo/public/pages2/foo/[id].jsx
@@ -0,0 +1,6 @@
+import { useRoute } from 'preact-iso';
+
+export default function Page() {
+ const route = useRoute();
+ return
dynamic id: {route.params.id}
;
+}
diff --git a/examples/demo/public/pages2/foo/bar/[id2]/index.jsx b/examples/demo/public/pages2/foo/bar/[id2]/index.jsx
new file mode 100644
index 000000000..9cd7a6db8
--- /dev/null
+++ b/examples/demo/public/pages2/foo/bar/[id2]/index.jsx
@@ -0,0 +1,6 @@
+import { useRoute } from 'preact-iso';
+
+export default function Page() {
+ const route = useRoute();
+ return
dynamic id index: {route.params.id2}
;
+}
diff --git a/examples/demo/public/pages2/foo/index.jsx b/examples/demo/public/pages2/foo/index.jsx
new file mode 100644
index 000000000..7e56d0164
--- /dev/null
+++ b/examples/demo/public/pages2/foo/index.jsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
foo
;
+}
diff --git a/examples/demo/public/pages2/index.jsx b/examples/demo/public/pages2/index.jsx
new file mode 100644
index 000000000..b804dba49
--- /dev/null
+++ b/examples/demo/public/pages2/index.jsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
index
;
+}
diff --git a/examples/demo/wmr.config.ts b/examples/demo/wmr.config.ts
index e8dc73afa..01b2cbadf 100644
--- a/examples/demo/wmr.config.ts
+++ b/examples/demo/wmr.config.ts
@@ -2,6 +2,7 @@ export default function () {
return {
alias: {
'src/*': 'src'
- }
+ },
+ routesDir: 'public/pages2'
};
}
diff --git a/packages/wmr/src/cli.js b/packages/wmr/src/cli.js
index 356f9677b..6897db1ef 100644
--- a/packages/wmr/src/cli.js
+++ b/packages/wmr/src/cli.js
@@ -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:
/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');
diff --git a/packages/wmr/src/lib/normalize-options.js b/packages/wmr/src/lib/normalize-options.js
index b6e24da5c..dfb18392a 100644
--- a/packages/wmr/src/lib/normalize-options.js
+++ b/packages/wmr/src/lib/normalize-options.js
@@ -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.
diff --git a/packages/wmr/src/lib/plugins.js b/packages/wmr/src/lib/plugins.js
index 04848bda9..c499b4140 100644
--- a/packages/wmr/src/lib/plugins.js
+++ b/packages/wmr/src/lib/plugins.js
@@ -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
@@ -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,
diff --git a/packages/wmr/src/plugins/fs-routes-plugin.js b/packages/wmr/src/plugins/fs-routes-plugin.js
new file mode 100644
index 000000000..1a6139955
--- /dev/null
+++ b/packages/wmr/src/plugins/fs-routes-plugin.js
@@ -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}]`;
+ }
+ };
+}
diff --git a/packages/wmr/src/plugins/fs-routes-preact-plugin.js b/packages/wmr/src/plugins/fs-routes-preact-plugin.js
new file mode 100644
index 000000000..52e5aee9d
--- /dev/null
+++ b/packages/wmr/src/plugins/fs-routes-preact-plugin.js
@@ -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) });
+});`;
+ }
+ };
+}
diff --git a/packages/wmr/src/plugins/plugin-utils.js b/packages/wmr/src/plugins/plugin-utils.js
new file mode 100644
index 000000000..92acb4cc2
--- /dev/null
+++ b/packages/wmr/src/plugins/plugin-utils.js
@@ -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);
diff --git a/packages/wmr/src/wmr-middleware.js b/packages/wmr/src/wmr-middleware.js
index 7903048d6..c270ab1af 100644
--- a/packages/wmr/src/wmr-middleware.js
+++ b/packages/wmr/src/wmr-middleware.js
@@ -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;
diff --git a/packages/wmr/types.d.ts b/packages/wmr/types.d.ts
index 493ddb2e3..a1efcf699 100644
--- a/packages/wmr/types.d.ts
+++ b/packages/wmr/types.d.ts
@@ -35,6 +35,7 @@ declare module 'wmr' {
host: string;
port: number;
root: string;
+ routesDir: string;
out: string;
overlayDir: string;
sourcemap: boolean;
@@ -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;
declare module '*.module.css' {