@notis_ai/cli 0.2.0-beta.20.1 → 0.2.0-beta.32.1
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/README.md +36 -9
- package/package.json +2 -1
- package/src/command-specs/apps.js +780 -42
- package/src/command-specs/helpers.js +28 -3
- package/src/command-specs/index.js +1 -1
- package/src/command-specs/tools.js +33 -6
- package/src/runtime/agent-browser.js +192 -0
- package/src/runtime/app-boundary-validator.js +132 -0
- package/src/runtime/app-dev-server.js +577 -0
- package/src/runtime/app-dev-sessions.js +87 -0
- package/src/runtime/app-platform.js +352 -17
- package/src/runtime/cli-mode.generated.js +4 -0
- package/src/runtime/cli-mode.js +29 -0
- package/src/runtime/output.js +2 -2
- package/src/runtime/ports.js +15 -0
- package/src/runtime/profiles.js +34 -3
- package/src/runtime/transport.js +129 -4
- package/template/.harness/index.html.tmpl +260 -0
- package/template/app/layout.tsx +1 -2
- package/template/notis.config.ts +16 -1
- package/template/package.json +1 -2
- package/template/packages/notis-sdk/src/components/MultiSelectActionBar.tsx +272 -0
- package/template/packages/notis-sdk/src/components/MultiSelectCheckbox.tsx +91 -0
- package/template/packages/notis-sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/notis-sdk/src/config.ts +24 -1
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +2 -1
- package/template/packages/notis-sdk/src/hooks/useMultiSelect.ts +502 -0
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +5 -3
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +1 -1
- package/template/packages/notis-sdk/src/hooks/useTopBarSearch.ts +73 -0
- package/template/packages/notis-sdk/src/index.ts +20 -0
- package/template/packages/notis-sdk/src/provider.tsx +23 -24
- package/template/packages/notis-sdk/src/runtime.ts +43 -26
- package/template/packages/notis-sdk/src/styles.css +6 -91
- package/src/runtime/app-preview-server.js +0 -336
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { createContext, useContext,
|
|
3
|
+
import React, { createContext, useContext, type Context, type ReactNode } from 'react';
|
|
4
4
|
import type { NotisRuntime } from './runtime';
|
|
5
|
-
import { getRuntime } from './runtime';
|
|
6
5
|
|
|
7
|
-
const
|
|
6
|
+
const NOTIS_CONTEXT_SYMBOL = Symbol.for('notis.sdk.runtime_context');
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
type NotisContextGlobal = typeof globalThis & {
|
|
9
|
+
[NOTIS_CONTEXT_SYMBOL]?: Context<NotisRuntime | null>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getNotisContext(): Context<NotisRuntime | null> {
|
|
13
|
+
const scope = globalThis as NotisContextGlobal;
|
|
14
|
+
if (!scope[NOTIS_CONTEXT_SYMBOL]) {
|
|
15
|
+
scope[NOTIS_CONTEXT_SYMBOL] = createContext<NotisRuntime | null>(null);
|
|
16
|
+
}
|
|
17
|
+
return scope[NOTIS_CONTEXT_SYMBOL]!;
|
|
18
|
+
}
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
// If runtime was passed as a prop (portal component rendering), use it directly.
|
|
23
|
-
// Otherwise fall back to window.__NOTIS_RUNTIME__ (local dev / preview).
|
|
24
|
-
if (runtime !== undefined) {
|
|
25
|
-
setResolvedRuntime(runtime);
|
|
26
|
-
} else {
|
|
27
|
-
setResolvedRuntime(getRuntime());
|
|
28
|
-
}
|
|
29
|
-
}, [runtime]);
|
|
20
|
+
const NotisContext = getNotisContext();
|
|
30
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Provides the Notis runtime context to all child components. The portal owns
|
|
24
|
+
* the runtime and injects it into the rendered app tree.
|
|
25
|
+
*/
|
|
26
|
+
export function NotisProvider({ children, runtime }: { children: ReactNode; runtime: NotisRuntime | null }) {
|
|
27
|
+
if (runtime === undefined) {
|
|
28
|
+
throw new Error('NotisProvider requires an explicit runtime prop.');
|
|
29
|
+
}
|
|
31
30
|
return (
|
|
32
|
-
<NotisContext.Provider value={
|
|
31
|
+
<NotisContext.Provider value={runtime}>
|
|
33
32
|
{children}
|
|
34
33
|
</NotisContext.Provider>
|
|
35
34
|
);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NotisRuntime is the bridge between app code running in the browser and the
|
|
3
|
-
* Notis platform.
|
|
4
|
-
*
|
|
5
|
-
* (with real server-backed implementations).
|
|
3
|
+
* Notis platform. The portal owns the runtime and injects it through
|
|
4
|
+
* `NotisProvider` when the app is rendered.
|
|
6
5
|
*
|
|
7
|
-
* App code should never
|
|
8
|
-
*
|
|
9
|
-
* from the NotisProvider context.
|
|
6
|
+
* App code should never reach for globals. Use the hooks from `@notis/sdk`
|
|
7
|
+
* (useDatabase, useTool, etc.) which read from the NotisProvider context.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
// ---------------------------------------------------------------------------
|
|
@@ -33,6 +31,7 @@ export interface DocumentRecord {
|
|
|
33
31
|
title: string;
|
|
34
32
|
properties: Record<string, unknown>;
|
|
35
33
|
icon?: string | null;
|
|
34
|
+
cover?: string | null;
|
|
36
35
|
databaseSlug?: string;
|
|
37
36
|
contentBlocknote?: Array<Record<string, unknown>> | null;
|
|
38
37
|
contentMarkdown?: string | null;
|
|
@@ -60,10 +59,16 @@ export interface RouteDescriptor {
|
|
|
60
59
|
path: string;
|
|
61
60
|
name: string;
|
|
62
61
|
icon?: string | null;
|
|
62
|
+
parentSlug?: string | null;
|
|
63
63
|
default?: boolean;
|
|
64
64
|
collection?: {
|
|
65
65
|
database: string;
|
|
66
66
|
titleProperty: string;
|
|
67
|
+
parentProperty?: string | null;
|
|
68
|
+
sidebar?: {
|
|
69
|
+
mode: 'flat-list' | 'tree';
|
|
70
|
+
allowCreate: boolean;
|
|
71
|
+
} | null;
|
|
67
72
|
} | null;
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -73,6 +78,15 @@ export interface CollectionItem {
|
|
|
73
78
|
icon?: string | null;
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
export interface CollectionItemDetail extends CollectionItem {
|
|
82
|
+
properties: Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CollectionTreeItem extends CollectionItem {
|
|
86
|
+
parent_id: string | null;
|
|
87
|
+
has_children: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
// ---------------------------------------------------------------------------
|
|
77
91
|
// App descriptor
|
|
78
92
|
// ---------------------------------------------------------------------------
|
|
@@ -101,17 +115,34 @@ export interface QueryFilter {
|
|
|
101
115
|
page_size?: number;
|
|
102
116
|
}
|
|
103
117
|
|
|
118
|
+
export interface NotisRuntimeContext {
|
|
119
|
+
collectionItem?: CollectionItemDetail | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
104
122
|
// ---------------------------------------------------------------------------
|
|
105
|
-
// NotisRuntime
|
|
123
|
+
// NotisRuntime
|
|
106
124
|
// ---------------------------------------------------------------------------
|
|
107
125
|
|
|
108
126
|
export interface NotisRuntime {
|
|
109
127
|
app: AppDescriptor;
|
|
110
128
|
route: RouteDescriptor;
|
|
111
129
|
databases: DatabaseDescriptor[];
|
|
130
|
+
context: NotisRuntimeContext;
|
|
112
131
|
|
|
113
132
|
navigate?: (payload: { kind: string; [key: string]: unknown }) => void;
|
|
114
133
|
|
|
134
|
+
registerTopBarSearch?: (
|
|
135
|
+
config:
|
|
136
|
+
| {
|
|
137
|
+
onChange: (value: string) => void;
|
|
138
|
+
placeholder?: string;
|
|
139
|
+
onSubmit?: () => void | Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
| null,
|
|
142
|
+
) => void;
|
|
143
|
+
setTopBarSearchValue?: (value: string) => void;
|
|
144
|
+
setTopBarSearchLoading?: (loading: boolean) => void;
|
|
145
|
+
|
|
115
146
|
listTools(): Promise<ToolDescriptor[]>;
|
|
116
147
|
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
117
148
|
|
|
@@ -138,28 +169,14 @@ export interface NotisRuntime {
|
|
|
138
169
|
pageSize?: number;
|
|
139
170
|
}): Promise<{ items: CollectionItem[] }>;
|
|
140
171
|
|
|
172
|
+
listCollectionTree?(args?: {
|
|
173
|
+
databaseSlug?: string;
|
|
174
|
+
pageSize?: number;
|
|
175
|
+
}): Promise<{ items: CollectionTreeItem[] }>;
|
|
176
|
+
|
|
141
177
|
request(path: string, options?: {
|
|
142
178
|
method?: string;
|
|
143
179
|
headers?: Record<string, string>;
|
|
144
180
|
body?: unknown;
|
|
145
181
|
}): Promise<unknown>;
|
|
146
182
|
}
|
|
147
|
-
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
|
-
// Globals
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
|
|
152
|
-
declare global {
|
|
153
|
-
interface Window {
|
|
154
|
-
__NOTIS_RUNTIME__?: NotisRuntime;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Read the injected runtime from the window. Returns null during SSR or if the
|
|
160
|
-
* runtime hasn't been injected yet (e.g. during `next dev` without preview).
|
|
161
|
-
*/
|
|
162
|
-
export function getRuntime(): NotisRuntime | null {
|
|
163
|
-
if (typeof window === 'undefined') return null;
|
|
164
|
-
return window.__NOTIS_RUNTIME__ ?? null;
|
|
165
|
-
}
|
|
@@ -1,110 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Notis theme CSS variables.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* iframe have identical styling. Import this in your root layout:
|
|
4
|
+
* Import this in your app layout:
|
|
6
5
|
*
|
|
7
6
|
* import '@notis/sdk/styles.css';
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
* variables automatically.
|
|
8
|
+
* The portal injects the live Notis theme variables at render time.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
@tailwind base;
|
|
14
12
|
@tailwind components;
|
|
15
13
|
@tailwind utilities;
|
|
16
14
|
|
|
17
|
-
@layer base {
|
|
18
|
-
:root {
|
|
19
|
-
--background: 0 0% 100%;
|
|
20
|
-
--foreground: 222.2 84% 4.9%;
|
|
21
|
-
--card: 0 0% 100%;
|
|
22
|
-
--card-foreground: 222.2 84% 4.9%;
|
|
23
|
-
--popover: 0 0% 100%;
|
|
24
|
-
--popover-foreground: 222.2 84% 4.9%;
|
|
25
|
-
--primary: 222.2 47.4% 11.2%;
|
|
26
|
-
--primary-foreground: 210 40% 98%;
|
|
27
|
-
--secondary: 210 40% 96.1%;
|
|
28
|
-
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
29
|
-
--muted: 210 40% 96.1%;
|
|
30
|
-
--muted-foreground: 0 0% 46.9%;
|
|
31
|
-
--accent: 210 40% 96.1%;
|
|
32
|
-
--accent-foreground: 222.2 47.4% 11.2%;
|
|
33
|
-
--destructive: 0 84.2% 60.2%;
|
|
34
|
-
--destructive-foreground: 210 40% 98%;
|
|
35
|
-
--border: 214.3 31.8% 91.4%;
|
|
36
|
-
--input: 214.3 31.8% 91.4%;
|
|
37
|
-
--ring: 222.2 84% 4.9%;
|
|
38
|
-
--radius: 0.5rem;
|
|
39
|
-
--sidebar-background: 0 0% 7.5%;
|
|
40
|
-
--sidebar-foreground: 240 4.8% 95.9%;
|
|
41
|
-
--sidebar-primary: 0 0% 7.5%;
|
|
42
|
-
--sidebar-primary-foreground: 240 4.8% 95.9%;
|
|
43
|
-
--sidebar-accent: 240 3.7% 15.9%;
|
|
44
|
-
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
45
|
-
--sidebar-border: 0 0% 23.5%;
|
|
46
|
-
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
47
|
-
--chart-1: 210 100% 49%;
|
|
48
|
-
--chart-2: 173 58% 39%;
|
|
49
|
-
--chart-3: 197 37% 24%;
|
|
50
|
-
--chart-4: 43 74% 66%;
|
|
51
|
-
--chart-5: 27 87% 67%;
|
|
52
|
-
--code-block-background: #f6f6f6;
|
|
53
|
-
--code-block-foreground: #27272a;
|
|
54
|
-
--portal-page-max-width: 1100px;
|
|
55
|
-
--portal-page-gutter-mobile: 1rem;
|
|
56
|
-
--portal-page-gutter-desktop: 1rem;
|
|
57
|
-
--portal-page-top-mobile: 1rem;
|
|
58
|
-
--portal-page-top-desktop: 1.5rem;
|
|
59
|
-
--portal-surface-pad-x-mobile: 1rem;
|
|
60
|
-
--portal-surface-pad-x-desktop: 1.5rem;
|
|
61
|
-
--portal-surface-pad-bottom: 1.5rem;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.dark {
|
|
65
|
-
--background: 0 0% 13%;
|
|
66
|
-
--foreground: 210 40% 98%;
|
|
67
|
-
--card: 0 0% 13%;
|
|
68
|
-
--card-foreground: 210 40% 98%;
|
|
69
|
-
--popover: 0 0% 13%;
|
|
70
|
-
--popover-foreground: 210 40% 98%;
|
|
71
|
-
--primary: 210 40% 98%;
|
|
72
|
-
--primary-foreground: 222.2 47.4% 11.2%;
|
|
73
|
-
--secondary: 0 0% 18%;
|
|
74
|
-
--secondary-foreground: 210 40% 98%;
|
|
75
|
-
--muted: 0 0% 18%;
|
|
76
|
-
--muted-foreground: 0 0% 65.1%;
|
|
77
|
-
--accent: 0 0% 18%;
|
|
78
|
-
--accent-foreground: 210 40% 98%;
|
|
79
|
-
--destructive: 0 84.2% 60.2%;
|
|
80
|
-
--destructive-foreground: 210 40% 98%;
|
|
81
|
-
--border: 0 0% 23.5%;
|
|
82
|
-
--input: 0 0% 23.5%;
|
|
83
|
-
--ring: 212.7 26.8% 83.9%;
|
|
84
|
-
--sidebar-background: 0 0% 7.5%;
|
|
85
|
-
--sidebar-foreground: 240 4.8% 95.9%;
|
|
86
|
-
--sidebar-primary: 224.3 76.3% 48%;
|
|
87
|
-
--sidebar-primary-foreground: 0 0% 100%;
|
|
88
|
-
--sidebar-accent: 240 3.7% 15.9%;
|
|
89
|
-
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
90
|
-
--sidebar-border: 0 0% 23.5%;
|
|
91
|
-
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
92
|
-
--chart-1: 210 100% 49%;
|
|
93
|
-
--chart-2: 160 60% 45%;
|
|
94
|
-
--chart-3: 30 80% 55%;
|
|
95
|
-
--chart-4: 280 65% 60%;
|
|
96
|
-
--chart-5: 340 75% 55%;
|
|
97
|
-
--code-block-background: #1e1e1e;
|
|
98
|
-
--code-block-foreground: #e4e4e7;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
15
|
@layer base {
|
|
103
16
|
* {
|
|
104
17
|
@apply border-border;
|
|
105
18
|
}
|
|
106
|
-
|
|
107
|
-
|
|
19
|
+
[data-notis-app-root] {
|
|
20
|
+
color: hsl(var(--foreground));
|
|
21
|
+
display: block;
|
|
22
|
+
min-height: 100%;
|
|
108
23
|
}
|
|
109
24
|
}
|
|
110
25
|
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local preview server for built Notis app bundles.
|
|
3
|
-
*
|
|
4
|
-
* Serves a single index.html that loads the app bundle via <script type="module">.
|
|
5
|
-
* Injects mock window.__NOTIS_RUNTIME__ before the bundle loads. Route switching
|
|
6
|
-
* uses pathname navigation between route pages.
|
|
7
|
-
*
|
|
8
|
-
* Usage: `notis apps preview [dir]` starts this server on localhost:8787.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createServer } from 'node:http';
|
|
12
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
-
import { extname, resolve, sep } from 'node:path';
|
|
14
|
-
|
|
15
|
-
import { readManifest } from './app-platform.js';
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Static file serving
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
function contentTypeFor(path) {
|
|
22
|
-
const types = {
|
|
23
|
-
'.html': 'text/html; charset=utf-8',
|
|
24
|
-
'.css': 'text/css; charset=utf-8',
|
|
25
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
26
|
-
'.json': 'application/json; charset=utf-8',
|
|
27
|
-
'.svg': 'image/svg+xml',
|
|
28
|
-
'.png': 'image/png',
|
|
29
|
-
'.jpg': 'image/jpeg',
|
|
30
|
-
'.jpeg': 'image/jpeg',
|
|
31
|
-
'.ico': 'image/x-icon',
|
|
32
|
-
'.woff': 'font/woff',
|
|
33
|
-
'.woff2': 'font/woff2',
|
|
34
|
-
};
|
|
35
|
-
return types[extname(path)] || 'application/octet-stream';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Mock runtime generation
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
function buildSeedDocuments(databases) {
|
|
43
|
-
const state = {};
|
|
44
|
-
for (const slug of databases) {
|
|
45
|
-
state[slug] = Array.from({ length: 3 }, (_, i) => {
|
|
46
|
-
const title = `${slug} ${i + 1}`;
|
|
47
|
-
const content = `This is a preview note for ${title}.`;
|
|
48
|
-
return {
|
|
49
|
-
id: `${slug}-seed-${i + 1}`,
|
|
50
|
-
databaseSlug: slug,
|
|
51
|
-
title,
|
|
52
|
-
properties: {},
|
|
53
|
-
icon: null,
|
|
54
|
-
contentBlocknote: [{ type: 'paragraph', content: [{ type: 'text', text: content }] }],
|
|
55
|
-
contentMarkdown: content,
|
|
56
|
-
plainText: content,
|
|
57
|
-
};
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return state;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function escapeHtml(value) {
|
|
64
|
-
return String(value)
|
|
65
|
-
.replaceAll('&', '&')
|
|
66
|
-
.replaceAll('<', '<')
|
|
67
|
-
.replaceAll('>', '>')
|
|
68
|
-
.replaceAll('"', '"')
|
|
69
|
-
.replaceAll("'", ''');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function sanitizeBundlePath(value, fallback) {
|
|
73
|
-
if (typeof value !== 'string') return fallback;
|
|
74
|
-
const normalized = value.trim().replaceAll('\\', '/').replace(/^\/+/, '');
|
|
75
|
-
if (!normalized || normalized.includes('..')) return fallback;
|
|
76
|
-
return /^[A-Za-z0-9._/-]+$/.test(normalized) ? normalized : fallback;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function serializeForInlineScript(value) {
|
|
80
|
-
return JSON.stringify(value).replaceAll('</', '<\\/').replaceAll('<!--', '<\\!--');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildPreviewHtml({ manifest, route, databases }) {
|
|
84
|
-
const app = manifest.app;
|
|
85
|
-
const seedState = buildSeedDocuments(databases);
|
|
86
|
-
const bundleJs = sanitizeBundlePath(manifest.bundle?.js, 'bundle/app.js');
|
|
87
|
-
const bundleCss = sanitizeBundlePath(manifest.bundle?.css, 'bundle/app.css');
|
|
88
|
-
const exportName = typeof route.export_name === 'string' && route.export_name ? route.export_name : 'index';
|
|
89
|
-
const pageTitle = escapeHtml(`${app.name} - ${route.name}`);
|
|
90
|
-
const bundleCssHref = escapeHtml(`/${bundleCss}`);
|
|
91
|
-
const bundleJsPathLiteral = JSON.stringify(`/${bundleJs}`);
|
|
92
|
-
const exportNameLiteral = JSON.stringify(exportName);
|
|
93
|
-
|
|
94
|
-
return `<!DOCTYPE html>
|
|
95
|
-
<html lang="en">
|
|
96
|
-
<head>
|
|
97
|
-
<meta charset="UTF-8" />
|
|
98
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
99
|
-
<title>${pageTitle}</title>
|
|
100
|
-
<link rel="stylesheet" href="${bundleCssHref}" />
|
|
101
|
-
<script>
|
|
102
|
-
(function() {
|
|
103
|
-
var DB = ${serializeForInlineScript(databases.map(function(slug) {
|
|
104
|
-
return { slug: slug, title: slug, properties: [] };
|
|
105
|
-
}))};
|
|
106
|
-
var STATE = ${serializeForInlineScript(seedState)};
|
|
107
|
-
var COLLECTION = ${serializeForInlineScript(route.collection || null)};
|
|
108
|
-
|
|
109
|
-
function titleProp(slug) {
|
|
110
|
-
var db = DB.find(function(d) { return d.slug === slug; });
|
|
111
|
-
return db && db.properties ? (db.properties.find(function(p) { return p.type === 'title'; }) || {}).name || 'title' : 'title';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function dbSlug(v) { return v || (COLLECTION && COLLECTION.database) || null; }
|
|
115
|
-
|
|
116
|
-
window.__NOTIS_RUNTIME__ = {
|
|
117
|
-
app: ${serializeForInlineScript(app)},
|
|
118
|
-
route: ${serializeForInlineScript(route)},
|
|
119
|
-
databases: DB,
|
|
120
|
-
|
|
121
|
-
navigate: function(payload) {
|
|
122
|
-
console.log('[notis-preview] Navigate:', payload);
|
|
123
|
-
if (payload.kind === 'route' && payload.path) {
|
|
124
|
-
window.location.assign(payload.path);
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
listTools: function() {
|
|
129
|
-
return Promise.resolve(${serializeForInlineScript((manifest.tools || []).map((name) => ({ name })))});
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
callTool: function(name, args) {
|
|
133
|
-
console.log('[notis-preview] Tool call:', name, args);
|
|
134
|
-
return Promise.reject(new Error('Tool calls are not available in local preview. Deploy the app to use real tools.'));
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
queryDatabase: function(args) {
|
|
138
|
-
var slug = dbSlug(args && args.databaseSlug);
|
|
139
|
-
var docs = (STATE[slug] || []).slice(args && args.offset || 0);
|
|
140
|
-
return Promise.resolve({ documents: docs });
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
getDocument: function(args) {
|
|
144
|
-
var id = args && args.documentId;
|
|
145
|
-
for (var slug in STATE) {
|
|
146
|
-
var match = STATE[slug].find(function(d) { return d.id === id; });
|
|
147
|
-
if (match) return Promise.resolve(match);
|
|
148
|
-
}
|
|
149
|
-
return Promise.reject(new Error('Document not found in preview.'));
|
|
150
|
-
},
|
|
151
|
-
|
|
152
|
-
upsertDocument: function(args) {
|
|
153
|
-
var slug = dbSlug(args && args.databaseSlug);
|
|
154
|
-
if (!slug) return Promise.reject(new Error('No database for upsert.'));
|
|
155
|
-
var docs = STATE[slug] || [];
|
|
156
|
-
var tp = titleProp(slug);
|
|
157
|
-
var existing = docs.find(function(d) { return d.id === (args && args.documentId); });
|
|
158
|
-
var props = Object.assign({}, existing && existing.properties, args && args.properties);
|
|
159
|
-
if (args && args.title) props[tp] = args.title;
|
|
160
|
-
var blocks = Array.isArray(args && args.contentBlocknote)
|
|
161
|
-
? JSON.parse(JSON.stringify(args.contentBlocknote))
|
|
162
|
-
: existing && existing.contentBlocknote
|
|
163
|
-
? JSON.parse(JSON.stringify(existing.contentBlocknote))
|
|
164
|
-
: [{ type: 'paragraph' }];
|
|
165
|
-
var plainText = blocks
|
|
166
|
-
.map(function(block) {
|
|
167
|
-
if (typeof block.content === 'string') return block.content;
|
|
168
|
-
if (!Array.isArray(block.content)) return '';
|
|
169
|
-
return block.content
|
|
170
|
-
.map(function(inline) { return inline && typeof inline.text === 'string' ? inline.text : ''; })
|
|
171
|
-
.join('');
|
|
172
|
-
})
|
|
173
|
-
.filter(Boolean)
|
|
174
|
-
.join('\\n')
|
|
175
|
-
.trim();
|
|
176
|
-
var doc = {
|
|
177
|
-
id: existing ? existing.id : slug + '-' + Date.now(),
|
|
178
|
-
databaseSlug: slug,
|
|
179
|
-
title: props[tp] || 'Untitled',
|
|
180
|
-
properties: props,
|
|
181
|
-
icon: null,
|
|
182
|
-
contentBlocknote: blocks,
|
|
183
|
-
contentMarkdown: plainText,
|
|
184
|
-
plainText: plainText
|
|
185
|
-
};
|
|
186
|
-
if (existing) {
|
|
187
|
-
var idx = docs.indexOf(existing);
|
|
188
|
-
docs[idx] = doc;
|
|
189
|
-
} else {
|
|
190
|
-
docs.unshift(doc);
|
|
191
|
-
}
|
|
192
|
-
STATE[slug] = docs;
|
|
193
|
-
return Promise.resolve({ status: 'success', document: doc });
|
|
194
|
-
},
|
|
195
|
-
|
|
196
|
-
listCollectionItems: function(args) {
|
|
197
|
-
var slug = dbSlug(args && args.databaseSlug);
|
|
198
|
-
if (!slug) return Promise.resolve({ items: [] });
|
|
199
|
-
var tp = (args && args.titleProperty) || titleProp(slug);
|
|
200
|
-
var docs = (STATE[slug] || []).slice(0, (args && args.pageSize) || 100);
|
|
201
|
-
return Promise.resolve({
|
|
202
|
-
items: docs.map(function(d) {
|
|
203
|
-
return { id: d.id, title: (d.properties && d.properties[tp]) || d.title || 'Untitled', icon: d.icon };
|
|
204
|
-
})
|
|
205
|
-
});
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
request: function() {
|
|
209
|
-
return Promise.reject(new Error('Backend requests are not available in local preview.'));
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
})();
|
|
213
|
-
</script>
|
|
214
|
-
</head>
|
|
215
|
-
<body class="min-h-screen bg-background text-foreground antialiased">
|
|
216
|
-
<div id="notis-app-root"></div>
|
|
217
|
-
<script type="importmap">
|
|
218
|
-
{
|
|
219
|
-
"imports": {
|
|
220
|
-
"react": "https://esm.sh/react@19",
|
|
221
|
-
"react-dom": "https://esm.sh/react-dom@19?external=react",
|
|
222
|
-
"react-dom/client": "https://esm.sh/react-dom@19/client?external=react",
|
|
223
|
-
"react/jsx-runtime": "https://esm.sh/react@19/jsx-runtime"
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
</script>
|
|
227
|
-
<script type="module">
|
|
228
|
-
import React from 'react';
|
|
229
|
-
import { createRoot } from 'react-dom/client';
|
|
230
|
-
import * as AppBundle from ${bundleJsPathLiteral};
|
|
231
|
-
|
|
232
|
-
// Try to render: AppShell wrapping the route component, or just the route component
|
|
233
|
-
const exportName = ${exportNameLiteral};
|
|
234
|
-
const RouteComponent = AppBundle[exportName] || AppBundle['default'];
|
|
235
|
-
const AppShell = AppBundle['__AppShell'];
|
|
236
|
-
|
|
237
|
-
if (RouteComponent) {
|
|
238
|
-
const root = document.getElementById('notis-app-root');
|
|
239
|
-
if (root) {
|
|
240
|
-
const reactRoot = createRoot(root);
|
|
241
|
-
if (AppShell) {
|
|
242
|
-
reactRoot.render(React.createElement(AppShell, null, React.createElement(RouteComponent)));
|
|
243
|
-
} else {
|
|
244
|
-
reactRoot.render(React.createElement(RouteComponent));
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
document.getElementById('notis-app-root').innerHTML =
|
|
249
|
-
'<p style="padding:2rem;color:red;">No component found for export "' + exportName + '". Available exports: ' +
|
|
250
|
-
Object.keys(AppBundle).join(', ') + '</p>';
|
|
251
|
-
}
|
|
252
|
-
</script>
|
|
253
|
-
</body>
|
|
254
|
-
</html>`;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// Server
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
function normalizePathname(pathname) {
|
|
262
|
-
if (!pathname || pathname === '/') return '/';
|
|
263
|
-
return pathname.endsWith('/') ? pathname.slice(0, -1) || '/' : pathname;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function resolveSafePath(baseDir, pathname) {
|
|
267
|
-
const normalizedBaseDir = resolve(baseDir);
|
|
268
|
-
const resolvedPath = resolve(normalizedBaseDir, pathname.replace(/^\/+/, ''));
|
|
269
|
-
if (resolvedPath === normalizedBaseDir || resolvedPath.startsWith(`${normalizedBaseDir}${sep}`)) {
|
|
270
|
-
return resolvedPath;
|
|
271
|
-
}
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export async function startPreviewServer({ projectDir, port }) {
|
|
276
|
-
const manifest = readManifest(projectDir);
|
|
277
|
-
const outputDir = resolve(projectDir, '.notis/output');
|
|
278
|
-
const bundleDir = resolve(projectDir, '.notis/output/bundle');
|
|
279
|
-
const databases = (manifest.databases || []).filter((slug) => typeof slug === 'string' && slug);
|
|
280
|
-
const routesByPath = new Map((manifest.routes || []).map((r) => [normalizePathname(r.path), r]));
|
|
281
|
-
const defaultRoute = (manifest.routes || []).find((r) => r.default) || manifest.routes?.[0];
|
|
282
|
-
|
|
283
|
-
if (!defaultRoute) {
|
|
284
|
-
throw new Error('Manifest contains no routes.');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const server = createServer((request, response) => {
|
|
288
|
-
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
|
|
289
|
-
const pathname = normalizePathname(url.pathname);
|
|
290
|
-
|
|
291
|
-
// Serve bundle files directly
|
|
292
|
-
if (pathname.startsWith('/bundle/')) {
|
|
293
|
-
const filePath = resolveSafePath(outputDir, pathname);
|
|
294
|
-
if (filePath && existsSync(filePath)) {
|
|
295
|
-
response.writeHead(200, { 'Content-Type': contentTypeFor(filePath), 'Cache-Control': 'no-store' });
|
|
296
|
-
response.end(readFileSync(filePath));
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Redirect / to default route if it's not /
|
|
302
|
-
if (pathname === '/' && defaultRoute.path !== '/') {
|
|
303
|
-
response.writeHead(302, { Location: defaultRoute.path });
|
|
304
|
-
response.end();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Serve route HTML with runtime injection
|
|
309
|
-
const route = routesByPath.get(pathname) || (pathname === '/' ? defaultRoute : null);
|
|
310
|
-
if (route) {
|
|
311
|
-
const html = buildPreviewHtml({ manifest, route, databases });
|
|
312
|
-
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
313
|
-
response.end(html);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Serve static files from bundle dir
|
|
318
|
-
const filePath = resolveSafePath(bundleDir, pathname);
|
|
319
|
-
if (filePath && existsSync(filePath)) {
|
|
320
|
-
response.writeHead(200, { 'Content-Type': contentTypeFor(filePath), 'Cache-Control': 'no-store' });
|
|
321
|
-
response.end(readFileSync(filePath));
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
response.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
326
|
-
response.end('Not found');
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
await new Promise((resolveP, rejectP) => {
|
|
330
|
-
server.on('error', rejectP);
|
|
331
|
-
server.listen(port, '127.0.0.1', resolveP);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Keep running until interrupted
|
|
335
|
-
await new Promise(() => {});
|
|
336
|
-
}
|