@mismo/next 0.0.0-stage → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +32 -2
- package/dist/config.d.ts +19 -0
- package/dist/config.js +33 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/output.d.ts +14 -0
- package/dist/output.js +86 -0
- package/dist/runtime.d.ts +28 -0
- package/dist/runtime.js +79 -0
- package/package.json +54 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix Bernal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mismo/next
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
mismo for Next.js (App Router). [mismo](https://mismo.dev) shares screens, routes and navigation between a React Native app and a web app, each on its own router.
|
|
4
|
+
|
|
5
|
+
**Docs: [mismo.dev/docs/nextjs](https://mismo.dev/docs/nextjs)**
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i @mismo/core @mismo/next
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Wrap the app's Next.js config. It writes the shared routes into `app/` and runs the shared screens' navigation on Next.js:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// next.config.ts
|
|
19
|
+
import { withMismo } from "@mismo/next/config";
|
|
20
|
+
|
|
21
|
+
export default withMismo(
|
|
22
|
+
{ transpilePackages: ["@acme/app", "@mismo/core", "@mismo/next"] },
|
|
23
|
+
{ config: "../../packages/app/routes.config.ts", outDir: "app", types: "app/routes.gen.ts" },
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`@acme/app` is your shared package, which Next.js compiles along with mismo. Supports Next.js `>=16 <17`.
|
|
28
|
+
|
|
29
|
+
See [Getting started](https://mismo.dev/docs/getting-started) for the whole setup: the shared routes package, and each app.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface WithMismoOptions {
|
|
2
|
+
/** The shared `routes.config.ts`, relative to the app. */
|
|
3
|
+
config: string;
|
|
4
|
+
/** The App Router's folder, relative to the app: `app`, or `src/app`. */
|
|
5
|
+
outDir: string;
|
|
6
|
+
/** The app's types file, relative to the app. */
|
|
7
|
+
types: string;
|
|
8
|
+
/** The app's folder. Next.js loads its config there, so it is `process.cwd()` by default. */
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Next.js's config for an app on mismo: writes the app's routes into the App Router's folder when
|
|
13
|
+
* Next.js loads its config (before it reads the folder, for `next dev` and `next build` alike), and
|
|
14
|
+
* again in dev as route files come and go; and points `@mismo/core` at Next.js's runtime, for Turbopack
|
|
15
|
+
* and for webpack.
|
|
16
|
+
*
|
|
17
|
+
* export default withMismo({ … }, { config: "../shared/routes.config.ts", outDir: "app", types: "app/routes.gen.ts" });
|
|
18
|
+
*/
|
|
19
|
+
export declare function withMismo<T extends object>(nextConfig: T, { config, outDir, types, cwd }: WithMismoOptions): T;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { watchAppRoutes } from "@mismo/core/generate";
|
|
4
|
+
import { output } from "./output.js";
|
|
5
|
+
/**
|
|
6
|
+
* Next.js's config for an app on mismo: writes the app's routes into the App Router's folder when
|
|
7
|
+
* Next.js loads its config (before it reads the folder, for `next dev` and `next build` alike), and
|
|
8
|
+
* again in dev as route files come and go; and points `@mismo/core` at Next.js's runtime, for Turbopack
|
|
9
|
+
* and for webpack.
|
|
10
|
+
*
|
|
11
|
+
* export default withMismo({ … }, { config: "../shared/routes.config.ts", outDir: "app", types: "app/routes.gen.ts" });
|
|
12
|
+
*/
|
|
13
|
+
export function withMismo(nextConfig, { config, outDir, types, cwd = process.cwd() }) {
|
|
14
|
+
const next = nextConfig;
|
|
15
|
+
watchAppRoutes({ config, output: output({ outDir, types }), cwd });
|
|
16
|
+
// From the app, which depends on this adapter: the shared package importing `@mismo/core` may not.
|
|
17
|
+
const runtime = createRequire(path.join(cwd, "package.json")).resolve("@mismo/next/runtime");
|
|
18
|
+
// Turbopack takes a path relative to the app, webpack an absolute one.
|
|
19
|
+
const relative = `./${path.relative(cwd, runtime).split(path.sep).join("/")}`;
|
|
20
|
+
const previous = next.webpack;
|
|
21
|
+
const changed = {
|
|
22
|
+
turbopack: {
|
|
23
|
+
...next.turbopack,
|
|
24
|
+
resolveAlias: { ...next.turbopack?.resolveAlias, "@mismo/core": relative },
|
|
25
|
+
},
|
|
26
|
+
webpack: (webpackConfig, context) => {
|
|
27
|
+
const config = previous ? previous(webpackConfig, context) : webpackConfig;
|
|
28
|
+
config.resolve = { ...config.resolve, alias: { ...config.resolve?.alias, "@mismo/core$": runtime } };
|
|
29
|
+
return config;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return { ...next, ...changed };
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RouteOutput, type RouteRules } from "@mismo/core/adapter";
|
|
2
|
+
/** What Next.js's App Router cannot express, from its folder conventions. */
|
|
3
|
+
export declare const rules: RouteRules;
|
|
4
|
+
/**
|
|
5
|
+
* Next.js's files, for a web app on the App Router: the routes folder's layout in `app/`, a
|
|
6
|
+
* `page.tsx` per page and a `layout.tsx` per layout, next to the app's own (its root `layout.tsx`
|
|
7
|
+
* among them). The shared screens use hooks, so each file is a client component. A page renders in
|
|
8
|
+
* a Suspense boundary, which Next.js needs around `useSearchParams` to prerender it; a layout hands
|
|
9
|
+
* the page Next.js gives it (`children`) to `Slot`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function output({ outDir, types }: {
|
|
12
|
+
outDir: string;
|
|
13
|
+
types: string;
|
|
14
|
+
}): RouteOutput;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { GENERATED, importPath, isGroup, isParam, oneUrlPerPage, placesOf } from "@mismo/core/adapter";
|
|
3
|
+
/** What Next.js's App Router cannot express, from its folder conventions. */
|
|
4
|
+
export const rules = {
|
|
5
|
+
router: "Next.js",
|
|
6
|
+
target: "web",
|
|
7
|
+
check(routes) {
|
|
8
|
+
// It refuses two pages that resolve to one URL, such as `(a)/about` and `(b)/about`.
|
|
9
|
+
const problems = oneUrlPerPage(routes, "Next.js");
|
|
10
|
+
// Folder names it reads as something other than a route, without an error.
|
|
11
|
+
for (const { route } of placesOf(routes)) {
|
|
12
|
+
for (const segment of route.segments) {
|
|
13
|
+
if (isParam(segment))
|
|
14
|
+
continue;
|
|
15
|
+
if (segment.startsWith("_"))
|
|
16
|
+
problems.push(`${route.id}: "${segment}" starts with "_", a private folder to Next.js, left out of its routes.`);
|
|
17
|
+
if (segment.startsWith("@"))
|
|
18
|
+
problems.push(`${route.id}: "${segment}" starts with "@", a parallel route's slot to Next.js.`);
|
|
19
|
+
if (isGroup(segment) && segment.startsWith("(."))
|
|
20
|
+
problems.push(`${route.id}: "${segment}" is an intercepting route to Next.js.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return problems;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Next.js's files, for a web app on the App Router: the routes folder's layout in `app/`, a
|
|
28
|
+
* `page.tsx` per page and a `layout.tsx` per layout, next to the app's own (its root `layout.tsx`
|
|
29
|
+
* among them). The shared screens use hooks, so each file is a client component. A page renders in
|
|
30
|
+
* a Suspense boundary, which Next.js needs around `useSearchParams` to prerender it; a layout hands
|
|
31
|
+
* the page Next.js gives it (`children`) to `Slot`.
|
|
32
|
+
*/
|
|
33
|
+
export function output({ outDir, types }) {
|
|
34
|
+
return {
|
|
35
|
+
name: "next",
|
|
36
|
+
target: "web",
|
|
37
|
+
outDir,
|
|
38
|
+
types,
|
|
39
|
+
rules,
|
|
40
|
+
runtime: "@mismo/next/runtime",
|
|
41
|
+
emit(routes, context) {
|
|
42
|
+
const files = new Map();
|
|
43
|
+
for (const { route, folder } of placesOf(routes)) {
|
|
44
|
+
const segments = [...folder, ...route.segments].map(toNext);
|
|
45
|
+
const dirs = route.layout || segments.at(-1) !== "index" ? segments : segments.slice(0, -1);
|
|
46
|
+
const file = [...dirs, route.layout ? "layout.tsx" : "page.tsx"].join("/");
|
|
47
|
+
const from = importPath(path.join(context.outDir, file), route.file);
|
|
48
|
+
files.set(file, route.layout ? layoutFile(from) : pageFile(from));
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** A segment as Next.js spells it: `:id` is `[id]`. */
|
|
55
|
+
const toNext = (segment) => (isParam(segment) ? `[${segment.slice(1)}]` : segment);
|
|
56
|
+
const pageFile = (from) => `${GENERATED}"use client";
|
|
57
|
+
|
|
58
|
+
import { Suspense } from "react";
|
|
59
|
+
|
|
60
|
+
import Page from "${from}";
|
|
61
|
+
|
|
62
|
+
// Next.js prerenders a page only with a boundary around \`useSearchParams\`.
|
|
63
|
+
export default function MismoPage() {
|
|
64
|
+
return (
|
|
65
|
+
<Suspense>
|
|
66
|
+
<Page />
|
|
67
|
+
</Suspense>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
const layoutFile = (from) => `${GENERATED}"use client";
|
|
72
|
+
|
|
73
|
+
import { SlotChildren } from "@mismo/next/runtime";
|
|
74
|
+
import type { ReactNode } from "react";
|
|
75
|
+
|
|
76
|
+
import Layout from "${from}";
|
|
77
|
+
|
|
78
|
+
// Next.js gives a layout its page as \`children\`: the shared layout renders it with \`Slot\`.
|
|
79
|
+
export default function MismoLayout({ children }: { children: ReactNode }) {
|
|
80
|
+
return (
|
|
81
|
+
<SlotChildren value={children}>
|
|
82
|
+
<Layout />
|
|
83
|
+
</SlotChildren>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type BackLinkProps, type Href, type LinkProps, type Nav, type Routes, type SearchParams, type To } from "@mismo/core/contract";
|
|
3
|
+
export { pathOf, type BackLinkProps, type Href, type LinkProps, type Nav, type Routes, type SearchParams, type To, } from "@mismo/core/contract";
|
|
4
|
+
/** For the generated `layout.tsx`: the page below the shared layout it renders. */
|
|
5
|
+
export declare function SlotChildren({ value, children }: {
|
|
6
|
+
value: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
9
|
+
/** Where a layout renders its child route. */
|
|
10
|
+
export declare function Slot(): import("react").JSX.Element;
|
|
11
|
+
/** A real link, so it can be opened in a new tab. Next.js loads its page ahead as it shows. */
|
|
12
|
+
export declare function Link({ to, replace, label, children }: LinkProps): import("react").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* Goes back when there is somewhere to go back to in this app, and to `fallback` otherwise. A real
|
|
15
|
+
* link to `fallback`, so a modified click still opens it in a new tab.
|
|
16
|
+
*/
|
|
17
|
+
export declare function BackLink({ fallback, label, children }: BackLinkProps): import("react").JSX.Element;
|
|
18
|
+
/** Navigates as soon as it renders, replacing the current entry. */
|
|
19
|
+
export declare function Redirect({ to }: {
|
|
20
|
+
to: To;
|
|
21
|
+
}): null;
|
|
22
|
+
export declare function useNav(): Nav;
|
|
23
|
+
/** The current path, such as `/orgs/acme`. */
|
|
24
|
+
export declare function usePathname(): string;
|
|
25
|
+
/** The current route's params. `href` is the route the screen is for, which types them. */
|
|
26
|
+
export declare function useParams<H extends Href>(_href: H): Routes[H];
|
|
27
|
+
/** The query of the current URL. A key given twice is its first value, as on native. */
|
|
28
|
+
export declare function useSearchParams(): SearchParams;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import NextLink from "next/link";
|
|
4
|
+
import { usePathname as useNextPathname, useParams as useNextParams, useRouter, useSearchParams as useNextSearchParams, } from "next/navigation";
|
|
5
|
+
import { createContext, useContext, useEffect, useMemo } from "react";
|
|
6
|
+
import { pathOf } from "@mismo/core/contract";
|
|
7
|
+
export { pathOf, } from "@mismo/core/contract";
|
|
8
|
+
/**
|
|
9
|
+
* The page a Next.js layout was given. Next.js passes it as `children`, where other routers render
|
|
10
|
+
* an outlet: the generated `layout.tsx` provides it, and `Slot` renders it, so a shared layout is
|
|
11
|
+
* the same on every router.
|
|
12
|
+
*/
|
|
13
|
+
const Children = createContext(null);
|
|
14
|
+
/** For the generated `layout.tsx`: the page below the shared layout it renders. */
|
|
15
|
+
export function SlotChildren({ value, children }) {
|
|
16
|
+
return _jsx(Children.Provider, { value: value, children: children });
|
|
17
|
+
}
|
|
18
|
+
/** Where a layout renders its child route. */
|
|
19
|
+
export function Slot() {
|
|
20
|
+
return _jsx(_Fragment, { children: useContext(Children) });
|
|
21
|
+
}
|
|
22
|
+
/** A real link, so it can be opened in a new tab. Next.js loads its page ahead as it shows. */
|
|
23
|
+
export function Link({ to, replace = false, label, children }) {
|
|
24
|
+
return (_jsx(NextLink, { href: pathOf(to), replace: replace, "aria-label": label, children: children }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether there is an earlier entry in this site's history to go back to. The Navigation API says
|
|
28
|
+
* so exactly; without it, any earlier entry counts.
|
|
29
|
+
*/
|
|
30
|
+
function canGoBack() {
|
|
31
|
+
const browser = globalThis;
|
|
32
|
+
if (!browser.history)
|
|
33
|
+
return false;
|
|
34
|
+
return browser.navigation?.canGoBack ?? browser.history.length > 1;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Goes back when there is somewhere to go back to in this app, and to `fallback` otherwise. A real
|
|
38
|
+
* link to `fallback`, so a modified click still opens it in a new tab.
|
|
39
|
+
*/
|
|
40
|
+
export function BackLink({ fallback, label, children }) {
|
|
41
|
+
const router = useRouter();
|
|
42
|
+
const onClick = (event) => {
|
|
43
|
+
if (event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || !canGoBack())
|
|
44
|
+
return;
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
router.back();
|
|
47
|
+
};
|
|
48
|
+
return (_jsx(NextLink, { href: pathOf(fallback), "aria-label": label, onClick: onClick, children: children }));
|
|
49
|
+
}
|
|
50
|
+
/** Navigates as soon as it renders, replacing the current entry. */
|
|
51
|
+
export function Redirect({ to }) {
|
|
52
|
+
const router = useRouter();
|
|
53
|
+
const path = pathOf(to);
|
|
54
|
+
useEffect(() => router.replace(path), [router, path]);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
export function useNav() {
|
|
58
|
+
const router = useRouter();
|
|
59
|
+
return useMemo(() => ({
|
|
60
|
+
go: (to) => router.push(pathOf(to)),
|
|
61
|
+
replace: (to) => router.replace(pathOf(to)),
|
|
62
|
+
back: () => router.back(),
|
|
63
|
+
canGoBack,
|
|
64
|
+
preload: async (to) => router.prefetch(pathOf(to)),
|
|
65
|
+
}), [router]);
|
|
66
|
+
}
|
|
67
|
+
/** The current path, such as `/orgs/acme`. */
|
|
68
|
+
export function usePathname() {
|
|
69
|
+
return useNextPathname();
|
|
70
|
+
}
|
|
71
|
+
/** The current route's params. `href` is the route the screen is for, which types them. */
|
|
72
|
+
export function useParams(_href) {
|
|
73
|
+
return useNextParams();
|
|
74
|
+
}
|
|
75
|
+
/** The query of the current URL. A key given twice is its first value, as on native. */
|
|
76
|
+
export function useSearchParams() {
|
|
77
|
+
const search = useNextSearchParams();
|
|
78
|
+
return useMemo(() => Object.fromEntries([...search.keys()].map((key) => [key, search.get(key) ?? undefined])), [search]);
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mismo/next",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "mismo for Next.js (App Router): run shared screens and routes in a Next.js app.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mismo",
|
|
7
|
+
"next",
|
|
8
|
+
"nextjs",
|
|
9
|
+
"app-router",
|
|
10
|
+
"routes"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://mismo.dev",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./runtime": {
|
|
21
|
+
"types": "./dist/runtime.d.ts",
|
|
22
|
+
"default": "./dist/runtime.js"
|
|
23
|
+
},
|
|
24
|
+
"./config": {
|
|
25
|
+
"types": "./dist/config.d.ts",
|
|
26
|
+
"default": "./dist/config.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.18"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@mismo/core": "^0.1.0-beta.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"next": ">=16 <17",
|
|
42
|
+
"react": ">=19"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19.3.0",
|
|
46
|
+
"next": "^16.4.3",
|
|
47
|
+
"react": "19.3.0",
|
|
48
|
+
"react-dom": "19.3.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
55
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../scripts/fix-declarations.mjs dist"
|
|
56
|
+
}
|
|
6
57
|
}
|