gantry-web 0.3.0 → 0.3.3
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/package.json +1 -1
- package/src/app.tsx +59 -13
- package/src/index.ts +1 -1
- package/src/router.tsx +10 -4
- package/src/vite/index.d.ts +11 -0
- package/src/vite/index.js +23 -5
- package/types/index.d.ts +4 -21
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
/// <reference path="../types/index.d.ts" />
|
|
2
2
|
import { StrictMode, useEffect, type FC, type ReactNode } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
|
-
import { pages, components as pairedComponents, layouts, appConfig, type GantryPage } from "virtual:gantry-app";
|
|
5
4
|
import { TitleBar, type TitleBarProps } from "./TitleBar";
|
|
6
5
|
import { ResizeFrame } from "./ResizeFrame";
|
|
7
6
|
import { installZoomGuard } from "./zoom";
|
|
8
7
|
import { connect, ready } from "./socket";
|
|
9
8
|
import { useRoute, redirect } from "./router";
|
|
10
9
|
import { setRegistry, type ComponentRegistry, type TeaComponentProps } from "./tea/Runtime";
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
/** The shape of a page module (a pages/<name>/<name>.tsx file). */
|
|
12
|
+
export interface GantryPageModule {
|
|
13
|
+
default: FC;
|
|
14
|
+
/** export const chrome = false to hide the TitleBar on this page. */
|
|
15
|
+
chrome?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Which layouts/ wrap this page: a name ("compact"), several nested
|
|
18
|
+
* outermost-first (["main", "compact"]), false for none, true to
|
|
19
|
+
* force "main" even on a chromeless page. Default: "main" if it
|
|
20
|
+
* exists (chromeless pages default to none).
|
|
21
|
+
*/
|
|
22
|
+
layout?: boolean | string | string[];
|
|
23
|
+
/** export const route = "/custom" to override the derived route. */
|
|
24
|
+
route?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GantryPage {
|
|
28
|
+
key: string;
|
|
29
|
+
route: string;
|
|
30
|
+
mod: GantryPageModule;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The generated virtual:gantry-app module: the page/component/layout
|
|
35
|
+
* registry the Vite plugin derives from the app's folders. The
|
|
36
|
+
* synthesized .gantry/main.tsx imports it and hands it to createApp.
|
|
37
|
+
* gantry-web itself must never import the virtual module: esbuild
|
|
38
|
+
* cannot resolve virtual ids, so that import would force the package
|
|
39
|
+
* out of dep prebundling - and serving it as raw node_modules source
|
|
40
|
+
* lets Vite double-instantiate modules (?v=hash vs bare URLs), which
|
|
41
|
+
* split the router's subscriber set and broke navigation.
|
|
42
|
+
*/
|
|
43
|
+
export interface GantryAppModule {
|
|
44
|
+
pages: GantryPage[];
|
|
45
|
+
components: Record<string, { default: FC }>;
|
|
46
|
+
/** Layouts by short name: layouts/main/main.tsx -> "main". */
|
|
47
|
+
layouts: Record<string, { default: FC<{ children?: ReactNode }> }>;
|
|
48
|
+
/** The root app.tsx module (default export: CreateAppOptions), or null. */
|
|
49
|
+
appConfig: { default: CreateAppOptions } | null;
|
|
50
|
+
singlePage?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The registry handed to createApp; module-level so match/layoutsFor
|
|
54
|
+
// read it without threading props.
|
|
55
|
+
let reg: GantryAppModule;
|
|
12
56
|
|
|
13
57
|
export interface CreateAppOptions {
|
|
14
58
|
/** Title shown in the TitleBar (default: none). */
|
|
@@ -40,7 +84,7 @@ function routeOf(p: GantryPage): string {
|
|
|
40
84
|
|
|
41
85
|
function match(path: string): GantryPage | undefined {
|
|
42
86
|
const clean = path !== "/" && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
43
|
-
return pages.find((p) => routeOf(p) === clean) ?? pages.find((p) => routeOf(p) === "/");
|
|
87
|
+
return reg.pages.find((p) => routeOf(p) === clean) ?? reg.pages.find((p) => routeOf(p) === "/");
|
|
44
88
|
}
|
|
45
89
|
|
|
46
90
|
function PageHost({ page }: { page: GantryPage }) {
|
|
@@ -72,19 +116,19 @@ function layoutsFor(page: GantryPage, chrome: boolean): FC<{ children?: ReactNod
|
|
|
72
116
|
} else if (Array.isArray(sel)) {
|
|
73
117
|
names = sel;
|
|
74
118
|
} else if (sel === true) {
|
|
75
|
-
names = layouts.main ? ["main"] : [];
|
|
119
|
+
names = reg.layouts.main ? ["main"] : [];
|
|
76
120
|
} else {
|
|
77
121
|
// Default: the "main" layout on normal pages; chromeless pages
|
|
78
122
|
// (widgets, popups) are their own surfaces and skip it.
|
|
79
|
-
names = chrome && layouts.main ? ["main"] : [];
|
|
123
|
+
names = chrome && reg.layouts.main ? ["main"] : [];
|
|
80
124
|
}
|
|
81
125
|
const out: FC<{ children?: ReactNode }>[] = [];
|
|
82
126
|
for (const n of names) {
|
|
83
|
-
const mod = layouts[n];
|
|
127
|
+
const mod = reg.layouts[n];
|
|
84
128
|
if (mod?.default) {
|
|
85
129
|
out.push(mod.default);
|
|
86
130
|
} else {
|
|
87
|
-
console.warn(`gantry: page ${page.key} wants unknown layout "${n}" (have: ${Object.keys(layouts).join(", ") || "none"})`);
|
|
131
|
+
console.warn(`gantry: page ${page.key} wants unknown layout "${n}" (have: ${Object.keys(reg.layouts).join(", ") || "none"})`);
|
|
88
132
|
}
|
|
89
133
|
}
|
|
90
134
|
return out;
|
|
@@ -124,20 +168,22 @@ function AppRoot({ options }: { options: CreateAppOptions }) {
|
|
|
124
168
|
* createApp boots a Gantry frontend: zoom guard, websocket, component
|
|
125
169
|
* registry, TitleBar, the root layout (layout.tsx at the app root, if
|
|
126
170
|
* present), and the page router (single-page mode when only pages/index
|
|
127
|
-
* exists). The synthesized .gantry/main.tsx calls this
|
|
128
|
-
*
|
|
171
|
+
* exists). The synthesized .gantry/main.tsx calls this with the
|
|
172
|
+
* virtual:gantry-app module (`import * as app from "virtual:gantry-app"`);
|
|
173
|
+
* an app only touches it to pass options.
|
|
129
174
|
*/
|
|
130
|
-
export function createApp(options: CreateAppOptions = {}): void {
|
|
175
|
+
export function createApp(app: GantryAppModule, options: CreateAppOptions = {}): void {
|
|
176
|
+
reg = app;
|
|
131
177
|
// The optional root app.tsx wins over the synthesized defaults, so
|
|
132
178
|
// apps customize everything without owning the entry file.
|
|
133
|
-
if (appConfig?.default) {
|
|
134
|
-
options = { ...options, ...appConfig.default };
|
|
179
|
+
if (reg.appConfig?.default) {
|
|
180
|
+
options = { ...options, ...reg.appConfig.default };
|
|
135
181
|
}
|
|
136
182
|
installZoomGuard();
|
|
137
183
|
connect(options.socketURL);
|
|
138
184
|
|
|
139
185
|
const registry: ComponentRegistry = {};
|
|
140
|
-
for (const [key, mod] of Object.entries(
|
|
186
|
+
for (const [key, mod] of Object.entries(reg.components)) {
|
|
141
187
|
if (typeof mod.default === "function") {
|
|
142
188
|
registry[key] = mod.default as FC<TeaComponentProps>;
|
|
143
189
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { DragStrip } from "./DragStrip";
|
|
|
8
8
|
export type { DragStripProps } from "./DragStrip";
|
|
9
9
|
export { ResizeFrame } from "./ResizeFrame";
|
|
10
10
|
export { createApp } from "./app";
|
|
11
|
-
export type { CreateAppOptions } from "./app";
|
|
11
|
+
export type { CreateAppOptions, GantryAppModule, GantryPage, GantryPageModule } from "./app";
|
|
12
12
|
export { navigate, redirect, goBack, goForward, useRoute, isActive, Link, ExternalLink } from "./router";
|
|
13
13
|
export type { LinkProps, ExternalLinkProps } from "./router";
|
|
14
14
|
export { usePaired } from "./paired";
|
package/src/router.tsx
CHANGED
|
@@ -9,10 +9,16 @@ import {
|
|
|
9
9
|
} from "react";
|
|
10
10
|
import { getShell } from "./bridge";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Subscriptions go through a window-scoped event, NOT a module-local
|
|
13
|
+
// set: if bundling ever instantiates this module twice (it happened -
|
|
14
|
+
// Vite served excluded node_modules source under ?v=hash and bare URLs
|
|
15
|
+
// at once), a local set splits the subscribers and navigation silently
|
|
16
|
+
// stops re-rendering half the app. The window survives any number of
|
|
17
|
+
// module copies; the route itself already lives in location.pathname.
|
|
18
|
+
const NAV_EVENT = "gantry:navigate";
|
|
13
19
|
|
|
14
20
|
function notify() {
|
|
15
|
-
|
|
21
|
+
window.dispatchEvent(new Event(NAV_EVENT));
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
if (typeof window !== "undefined") {
|
|
@@ -47,8 +53,8 @@ export function goForward(): void {
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
function subscribe(fn: () => void): () => void {
|
|
50
|
-
|
|
51
|
-
return () =>
|
|
56
|
+
window.addEventListener(NAV_EVENT, fn);
|
|
57
|
+
return () => window.removeEventListener(NAV_EVENT, fn);
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/** useRoute returns the current pathname; re-renders on navigation. */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
export interface GantryPluginOptions {
|
|
4
|
+
/** App root relative to the Vite root (default ".."). */
|
|
5
|
+
appRoot?: string;
|
|
6
|
+
/** Go server port for the /api and /gantry/ws proxies (default: gantry.json "port", else 8330). */
|
|
7
|
+
goPort?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export declare function gantry(opts?: GantryPluginOptions): Plugin;
|
|
11
|
+
export default gantry;
|
package/src/vite/index.js
CHANGED
|
@@ -160,11 +160,29 @@ export function gantry(opts = {}) {
|
|
|
160
160
|
const target = "http://127.0.0.1:" + goPort;
|
|
161
161
|
|
|
162
162
|
return {
|
|
163
|
-
// gantry-web
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
163
|
+
// gantry-web MUST stay prebundlable (which is why it never
|
|
164
|
+
// imports virtual:gantry-app - esbuild cannot resolve virtual
|
|
165
|
+
// ids). A registry install is bundled into one optimized module;
|
|
166
|
+
// do NOT add it to optimizeDeps.exclude: served as raw
|
|
167
|
+
// node_modules source, Vite can reference the same file under
|
|
168
|
+
// two URLs (?v=hash vs bare) and instantiate the package twice,
|
|
169
|
+
// splitting the router's subscribers so navigation stops
|
|
170
|
+
// rendering. An npm file: link (framework development) resolves
|
|
171
|
+
// outside node_modules and is served as source automatically -
|
|
172
|
+
// there the force-included deps below matter: the CJS ones
|
|
173
|
+
// (react-dom/client) otherwise get served raw and lose their
|
|
174
|
+
// named exports, and lucide-react would waterfall one request
|
|
175
|
+
// per icon module.
|
|
176
|
+
optimizeDeps: {
|
|
177
|
+
include: [
|
|
178
|
+
"react",
|
|
179
|
+
"react-dom",
|
|
180
|
+
"react-dom/client",
|
|
181
|
+
"react/jsx-runtime",
|
|
182
|
+
"react/jsx-dev-runtime",
|
|
183
|
+
"lucide-react",
|
|
184
|
+
],
|
|
185
|
+
},
|
|
168
186
|
// dedupe makes imports inside the symlinked gantry-web source
|
|
169
187
|
// resolve from the APP's node_modules (the package itself has
|
|
170
188
|
// none) and guarantees a single React instance.
|
package/types/index.d.ts
CHANGED
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
// Ambient types for the module the Gantry Vite plugin generates. This
|
|
2
2
|
// file keeps editors happy without the synthesized .gantry/ folder
|
|
3
3
|
// existing - include it via tsconfig "types": ["gantry-web/types"].
|
|
4
|
+
// The module's shape is GantryAppModule (exported by gantry-web); the
|
|
5
|
+
// synthesized .gantry/main.tsx passes the whole module to createApp.
|
|
4
6
|
|
|
5
7
|
declare module "virtual:gantry-app" {
|
|
6
8
|
import type { FC, ReactNode } from "react";
|
|
7
9
|
|
|
8
|
-
export
|
|
9
|
-
default: FC;
|
|
10
|
-
/** export const chrome = false to hide the TitleBar on this page. */
|
|
11
|
-
chrome?: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Which layouts/ wrap this page: a name ("compact"), several nested
|
|
14
|
-
* outermost-first (["main", "compact"]), false for none, true to
|
|
15
|
-
* force "main" even on a chromeless page. Default: "main" if it
|
|
16
|
-
* exists (chromeless pages default to none).
|
|
17
|
-
*/
|
|
18
|
-
layout?: boolean | string | string[];
|
|
19
|
-
/** export const route = "/custom" to override the derived route. */
|
|
20
|
-
route?: string;
|
|
21
|
-
}
|
|
10
|
+
export type { GantryPage, GantryPageModule } from "gantry-web";
|
|
22
11
|
|
|
23
|
-
export
|
|
24
|
-
key: string;
|
|
25
|
-
route: string;
|
|
26
|
-
mod: GantryPageModule;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const pages: GantryPage[];
|
|
12
|
+
export const pages: import("gantry-web").GantryPage[];
|
|
30
13
|
export const components: Record<string, { default: FC }>;
|
|
31
14
|
/** Layouts by short name: layouts/main/main.tsx -> "main". */
|
|
32
15
|
export const layouts: Record<string, { default: FC<{ children?: ReactNode }> }>;
|