@samebase/convex-hosting 0.0.0-stage → 0.3.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/CHANGELOG.md +133 -0
- package/INTEGRATION.md +399 -0
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +471 -2
- package/dist/cli/args.d.ts +28 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +148 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/argumentChunks.d.ts +3 -0
- package/dist/cli/argumentChunks.d.ts.map +1 -0
- package/dist/cli/argumentChunks.js +29 -0
- package/dist/cli/argumentChunks.js.map +1 -0
- package/dist/cli/collectFiles.d.ts +10 -0
- package/dist/cli/collectFiles.d.ts.map +1 -0
- package/dist/cli/collectFiles.js +53 -0
- package/dist/cli/collectFiles.js.map +1 -0
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +56 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/componentName.d.ts +32 -0
- package/dist/cli/componentName.d.ts.map +1 -0
- package/dist/cli/componentName.js +43 -0
- package/dist/cli/componentName.js.map +1 -0
- package/dist/cli/deploy.d.ts +16 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +283 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/deployEnvironment.d.ts +10 -0
- package/dist/cli/deployEnvironment.d.ts.map +1 -0
- package/dist/cli/deployEnvironment.js +12 -0
- package/dist/cli/deployEnvironment.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +60 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/upload.d.ts +22 -0
- package/dist/cli/upload.d.ts.map +1 -0
- package/dist/cli/upload.js +630 -0
- package/dist/cli/upload.js.map +1 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +272 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +38 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +43 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/http.d.ts +4 -0
- package/dist/component/http.d.ts.map +1 -0
- package/dist/component/http.js +125 -0
- package/dist/component/http.js.map +1 -0
- package/dist/component/lib.d.ts +164 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +768 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +84 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +69 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/serving.d.ts +17 -0
- package/dist/component/serving.d.ts.map +1 -0
- package/dist/component/serving.js +150 -0
- package/dist/component/serving.js.map +1 -0
- package/dist/react/index.d.ts +59 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +137 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +78 -3
- package/src/cli/args.test.ts +79 -0
- package/src/cli/args.ts +153 -0
- package/src/cli/argumentChunks.test.ts +27 -0
- package/src/cli/argumentChunks.ts +40 -0
- package/src/cli/collectFiles.test.ts +26 -0
- package/src/cli/collectFiles.ts +59 -0
- package/src/cli/commands.ts +68 -0
- package/src/cli/componentName.test.ts +41 -0
- package/src/cli/componentName.ts +46 -0
- package/src/cli/deploy.ts +336 -0
- package/src/cli/deployEnvironment.test.ts +27 -0
- package/src/cli/deployEnvironment.ts +17 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/upload.ts +837 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.test.ts +510 -0
- package/src/client/index.ts +355 -0
- package/src/client/setup.test.ts +22 -0
- package/src/component/_generated/api.ts +54 -0
- package/src/component/_generated/component.ts +55 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/http.test.ts +223 -0
- package/src/component/http.ts +144 -0
- package/src/component/lib.test.ts +1007 -0
- package/src/component/lib.ts +862 -0
- package/src/component/schema.ts +75 -0
- package/src/component/serving.ts +158 -0
- package/src/component/setup.test.ts +11 -0
- package/src/react/index.tsx +197 -0
- package/src/test.ts +23 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
export default defineSchema({
|
|
5
|
+
staticAssets: defineTable({
|
|
6
|
+
path: v.string(), // URL path, e.g., "/index.html", "/assets/main-abc123.js"
|
|
7
|
+
storageId: v.optional(v.id("_storage")), // Reference to Convex file storage (used for HTML + non-CDN assets)
|
|
8
|
+
blobId: v.optional(v.string()), // convex-fs blob ID (used for CDN-served assets)
|
|
9
|
+
contentType: v.string(), // MIME type, e.g., "text/html; charset=utf-8"
|
|
10
|
+
deploymentId: v.string(), // UUID for garbage collection
|
|
11
|
+
})
|
|
12
|
+
.index("by_path", ["path"])
|
|
13
|
+
.index("by_deploymentId", ["deploymentId"])
|
|
14
|
+
.index("by_storageId", ["storageId"]),
|
|
15
|
+
|
|
16
|
+
// Publication moves replaced storage files here. Missing non-HTML paths with
|
|
17
|
+
// an extension can still resolve to these files. A later upload removes them
|
|
18
|
+
// after expiresAt, at least seven days after replacement.
|
|
19
|
+
retiredAssets: defineTable({
|
|
20
|
+
path: v.string(),
|
|
21
|
+
storageId: v.id("_storage"),
|
|
22
|
+
contentType: v.string(),
|
|
23
|
+
deploymentId: v.string(),
|
|
24
|
+
expiresAt: v.number(),
|
|
25
|
+
})
|
|
26
|
+
.index("by_path", ["path"])
|
|
27
|
+
.index("by_storageId", ["storageId"])
|
|
28
|
+
.index("by_expiresAt", ["expiresAt"]),
|
|
29
|
+
|
|
30
|
+
// Uploads stage their manifest in small chunks because the Convex CLI takes
|
|
31
|
+
// function arguments on the command line. Only publishDeployment copies a
|
|
32
|
+
// complete staged manifest into staticAssets, so partial uploads are never
|
|
33
|
+
// visible to HTTP requests.
|
|
34
|
+
stagedAssets: defineTable({
|
|
35
|
+
path: v.string(),
|
|
36
|
+
storageId: v.optional(v.id("_storage")),
|
|
37
|
+
blobId: v.optional(v.string()),
|
|
38
|
+
contentType: v.string(),
|
|
39
|
+
deploymentId: v.string(),
|
|
40
|
+
})
|
|
41
|
+
.index("by_deploymentId", ["deploymentId"])
|
|
42
|
+
.index("by_storageId", ["storageId"]),
|
|
43
|
+
|
|
44
|
+
// Old ConvexFS blobs are app-owned, so the component cannot delete them.
|
|
45
|
+
// Persist the IDs until the CLI's app-level delete function succeeds. This
|
|
46
|
+
// also makes cleanup recoverable when a publish response is lost.
|
|
47
|
+
pendingBlobCleanup: defineTable({
|
|
48
|
+
blobId: v.string(),
|
|
49
|
+
}).index("by_blobId", ["blobId"]),
|
|
50
|
+
|
|
51
|
+
// Storage IDs from the previous live manifest are queued by the atomic
|
|
52
|
+
// publish, then deleted in bounded follow-up mutations. This avoids both a
|
|
53
|
+
// large publish transaction and races with unrelated concurrent uploads.
|
|
54
|
+
pendingStorageCleanup: defineTable({
|
|
55
|
+
storageId: v.id("_storage"),
|
|
56
|
+
}).index("by_storageId", ["storageId"]),
|
|
57
|
+
|
|
58
|
+
// Cleanup may be queued before the first successful deployment exists, so
|
|
59
|
+
// its count cannot live only on deploymentInfo.
|
|
60
|
+
cleanupState: defineTable({
|
|
61
|
+
pendingBlobCleanupCount: v.number(),
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
// Singleton table to track the current deployment
|
|
65
|
+
// Clients subscribe to this to know when to reload
|
|
66
|
+
deploymentInfo: defineTable({
|
|
67
|
+
currentDeploymentId: v.string(),
|
|
68
|
+
deployedAt: v.number(), // timestamp
|
|
69
|
+
// SPA fallback config for the current deployment. Set at upload time
|
|
70
|
+
// (`--no-spa` turns it off). Absent means enabled. Travels with the
|
|
71
|
+
// deploy so the serving behavior matches the code that was shipped.
|
|
72
|
+
spaFallback: v.optional(v.boolean()),
|
|
73
|
+
pendingBlobCleanupCount: v.optional(v.number()),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Pure, dependency-free helpers shared by the HTTP handler (http.ts) and the
|
|
2
|
+
// asset-resolution query (lib.ts). Kept separate so lib.ts can reuse them
|
|
3
|
+
// without importing the HTTP router.
|
|
4
|
+
|
|
5
|
+
const MIME_TYPES: Record<string, string> = {
|
|
6
|
+
".html": "text/html; charset=utf-8",
|
|
7
|
+
".js": "application/javascript; charset=utf-8",
|
|
8
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
9
|
+
".css": "text/css; charset=utf-8",
|
|
10
|
+
".json": "application/json; charset=utf-8",
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".jpg": "image/jpeg",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".gif": "image/gif",
|
|
15
|
+
".svg": "image/svg+xml",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
".webp": "image/webp",
|
|
18
|
+
".woff": "font/woff",
|
|
19
|
+
".woff2": "font/woff2",
|
|
20
|
+
".ttf": "font/ttf",
|
|
21
|
+
".txt": "text/plain; charset=utf-8",
|
|
22
|
+
".map": "application/json",
|
|
23
|
+
".webmanifest": "application/manifest+json",
|
|
24
|
+
".xml": "application/xml",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function getSetupHtml(): string {
|
|
28
|
+
return `<!DOCTYPE html>
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head>
|
|
31
|
+
<meta charset="UTF-8">
|
|
32
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
33
|
+
<title>Convex Hosting</title>
|
|
34
|
+
<style>
|
|
35
|
+
* { box-sizing: border-box; }
|
|
36
|
+
body {
|
|
37
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
38
|
+
max-width: 640px;
|
|
39
|
+
margin: 0 auto;
|
|
40
|
+
padding: 40px 20px;
|
|
41
|
+
background: #fafafa;
|
|
42
|
+
color: #333;
|
|
43
|
+
line-height: 1.6;
|
|
44
|
+
}
|
|
45
|
+
h1 { color: #111; margin-bottom: 8px; }
|
|
46
|
+
.subtitle { color: #666; margin-bottom: 32px; }
|
|
47
|
+
code {
|
|
48
|
+
background: #e8e8e8;
|
|
49
|
+
padding: 2px 6px;
|
|
50
|
+
border-radius: 4px;
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
}
|
|
53
|
+
pre {
|
|
54
|
+
background: #1a1a1a;
|
|
55
|
+
color: #f0f0f0;
|
|
56
|
+
padding: 16px;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
overflow-x: auto;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
}
|
|
61
|
+
.step { margin-bottom: 24px; }
|
|
62
|
+
.step-num {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
background: #333;
|
|
65
|
+
color: white;
|
|
66
|
+
width: 24px;
|
|
67
|
+
height: 24px;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
text-align: center;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
line-height: 24px;
|
|
72
|
+
margin-right: 8px;
|
|
73
|
+
}
|
|
74
|
+
a { color: #0070f3; }
|
|
75
|
+
</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
<h1>Almost there!</h1>
|
|
79
|
+
<p class="subtitle">Your Convex backend is running, but no static files have been deployed yet.</p>
|
|
80
|
+
|
|
81
|
+
<div class="step">
|
|
82
|
+
<span class="step-num">1</span>
|
|
83
|
+
<strong>Build your frontend</strong>
|
|
84
|
+
<pre>npm run build</pre>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="step">
|
|
88
|
+
<span class="step-num">2</span>
|
|
89
|
+
<strong>Deploy your static files</strong>
|
|
90
|
+
<pre>npx @samebase/convex-hosting deploy</pre>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<p>Or deploy everything in one command:</p>
|
|
94
|
+
<pre>npm run deploy</pre>
|
|
95
|
+
|
|
96
|
+
<p style="margin-top: 32px; color: #666; font-size: 14px;">
|
|
97
|
+
Learn more at <a href="https://github.com/samebase/convex-hosting">github.com/samebase/convex-hosting</a>
|
|
98
|
+
</p>
|
|
99
|
+
</body>
|
|
100
|
+
</html>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getMimeType(path: string): string {
|
|
104
|
+
const ext = path.substring(path.lastIndexOf(".")).toLowerCase();
|
|
105
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function hasFileExtension(path: string): boolean {
|
|
109
|
+
const lastSegment = path.split("/").pop() || "";
|
|
110
|
+
return lastSegment.includes(".") && !lastSegment.startsWith(".");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Bundler content-hash suffix: e.g. `index-lj_vq_aF.js`,
|
|
114
|
+
// `index-D3LP-ukt.js`, or `style-B71cUw87.css`. Requiring at least one
|
|
115
|
+
// non-letter avoids treating ordinary names such as `privacy-policy.html` as
|
|
116
|
+
// content-addressed assets.
|
|
117
|
+
export function isHashedAsset(path: string): boolean {
|
|
118
|
+
const match = path.match(/[-.]([\dA-Za-z_-]{6,32})\.[A-Za-z\d]+$/);
|
|
119
|
+
return match !== null && /[\d_-]/.test(match[1]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isHtmlContentType(contentType: string): boolean {
|
|
123
|
+
return contentType.startsWith("text/html");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function cacheControlFor(path: string): string {
|
|
127
|
+
return !path.toLowerCase().endsWith(".html") && isHashedAsset(path)
|
|
128
|
+
? "public, max-age=31536000, immutable"
|
|
129
|
+
: "public, max-age=0, must-revalidate";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decode the URL pathname before looking it up in the asset table. A malformed
|
|
134
|
+
* escape sequence is a bad request, not a missing static asset.
|
|
135
|
+
*/
|
|
136
|
+
export function decodeRequestPath(pathname: string): string | null {
|
|
137
|
+
try {
|
|
138
|
+
return decodeURIComponent(pathname);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* If-None-Match uses weak comparison for GET and HEAD requests. Browsers and
|
|
146
|
+
* proxies may send a weak validator or a comma-separated list of validators.
|
|
147
|
+
*/
|
|
148
|
+
export function etagMatches(ifNoneMatch: string | null, currentEtag: string): boolean {
|
|
149
|
+
if (!ifNoneMatch) return false;
|
|
150
|
+
|
|
151
|
+
const normalize = (value: string) => value.trim().replace(/^W\//, "").trim();
|
|
152
|
+
const normalizedCurrent = normalize(currentEtag);
|
|
153
|
+
|
|
154
|
+
return ifNoneMatch.split(",").some((candidate) => {
|
|
155
|
+
const normalizedCandidate = normalize(candidate);
|
|
156
|
+
return normalizedCandidate === "*" || normalizedCandidate === normalizedCurrent;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { test } from "vite-plus/test";
|
|
3
|
+
import schema from "./schema.js";
|
|
4
|
+
import { convexTest } from "convex-test";
|
|
5
|
+
export const modules = import.meta.glob("./**/*.*s");
|
|
6
|
+
|
|
7
|
+
export function initConvexTest() {
|
|
8
|
+
const t = convexTest(schema, modules);
|
|
9
|
+
return t;
|
|
10
|
+
}
|
|
11
|
+
test("setup", () => {});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQuery_experimental } from "convex/react";
|
|
4
|
+
import { useState, useMemo, type CSSProperties, type JSX } from "react";
|
|
5
|
+
import { makeFunctionReference, type FunctionReference } from "convex/server";
|
|
6
|
+
|
|
7
|
+
type DeploymentInfo = {
|
|
8
|
+
_id: string;
|
|
9
|
+
_creationTime: number;
|
|
10
|
+
currentDeploymentId: string;
|
|
11
|
+
deployedAt: number;
|
|
12
|
+
spaFallback?: boolean;
|
|
13
|
+
} | null;
|
|
14
|
+
|
|
15
|
+
type DeploymentQueryRef = FunctionReference<
|
|
16
|
+
"query",
|
|
17
|
+
"public",
|
|
18
|
+
Record<string, never>,
|
|
19
|
+
DeploymentInfo
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
const DEFAULT_QUERY_REF = makeFunctionReference<"query", Record<string, never>, DeploymentInfo>(
|
|
23
|
+
"hosting:getCurrentDeployment",
|
|
24
|
+
) as DeploymentQueryRef;
|
|
25
|
+
|
|
26
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook to detect when a new deployment is available.
|
|
30
|
+
*
|
|
31
|
+
* @param getCurrentDeployment - Optional query reference. Defaults to
|
|
32
|
+
* `api.hosting.getCurrentDeployment` (resolved by path). If you've
|
|
33
|
+
* re-exported the deployment query under a different module name, pass the
|
|
34
|
+
* reference explicitly.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* import { useDeploymentUpdates } from "@samebase/convex-hosting/react";
|
|
39
|
+
*
|
|
40
|
+
* function App() {
|
|
41
|
+
* const { updateAvailable, reload } = useDeploymentUpdates();
|
|
42
|
+
* return updateAvailable ? <button onClick={reload}>Reload</button> : null;
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useDeploymentUpdates(getCurrentDeployment?: DeploymentQueryRef) {
|
|
47
|
+
const result = useQuery_experimental({
|
|
48
|
+
query: getCurrentDeployment ?? DEFAULT_QUERY_REF,
|
|
49
|
+
args: {},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const deployment = result.status === "success" ? result.data : null;
|
|
53
|
+
const setupError =
|
|
54
|
+
result.status === "error" ? deploymentQuerySetupHelp(getCurrentDeployment) : null;
|
|
55
|
+
|
|
56
|
+
const [initialDeploymentId, setInitialDeploymentId] = useState<string | null>(null);
|
|
57
|
+
const [dismissedDeploymentId, setDismissedDeploymentId] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
if (deployment && initialDeploymentId === null) {
|
|
60
|
+
setInitialDeploymentId(deployment.currentDeploymentId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const updateAvailable = useMemo(() => {
|
|
64
|
+
if (!deployment || initialDeploymentId === null) return false;
|
|
65
|
+
const hasNew = deployment.currentDeploymentId !== initialDeploymentId;
|
|
66
|
+
const isDismissed = deployment.currentDeploymentId === dismissedDeploymentId;
|
|
67
|
+
return hasNew && !isDismissed;
|
|
68
|
+
}, [deployment, initialDeploymentId, dismissedDeploymentId]);
|
|
69
|
+
|
|
70
|
+
const reload = () => {
|
|
71
|
+
window.location.reload();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const dismiss = () => {
|
|
75
|
+
if (deployment) {
|
|
76
|
+
setDismissedDeploymentId(deployment.currentDeploymentId);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
updateAvailable,
|
|
82
|
+
reload,
|
|
83
|
+
dismiss,
|
|
84
|
+
deployment,
|
|
85
|
+
/**
|
|
86
|
+
* When set, the deployment query isn't exported from the app; the banner
|
|
87
|
+
* won't work until the user wires it up. The string is suitable to render
|
|
88
|
+
* during local development so the missing setup is obvious.
|
|
89
|
+
*/
|
|
90
|
+
setupError,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function deploymentQuerySetupHelp(ref: DeploymentQueryRef | undefined): string {
|
|
95
|
+
if (ref) {
|
|
96
|
+
return "Deployment query failed. Verify the function you passed to useDeploymentUpdates is deployed.";
|
|
97
|
+
}
|
|
98
|
+
return [
|
|
99
|
+
"@samebase/convex-hosting: UpdateBanner requires you to expose the deployment query.",
|
|
100
|
+
"Create convex/hosting.ts:",
|
|
101
|
+
"",
|
|
102
|
+
' import { exposeDeploymentQuery } from "@samebase/convex-hosting";',
|
|
103
|
+
' import { components } from "./_generated/api";',
|
|
104
|
+
" export const { getCurrentDeployment } = exposeDeploymentQuery(",
|
|
105
|
+
" components.hosting,",
|
|
106
|
+
" );",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Ready-to-use banner shown when a new deployment is available.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```tsx
|
|
115
|
+
* import { UpdateBanner } from "@samebase/convex-hosting/react";
|
|
116
|
+
* <UpdateBanner />
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function UpdateBanner({
|
|
120
|
+
getCurrentDeployment,
|
|
121
|
+
message = "A new version is available!",
|
|
122
|
+
buttonText = "Reload",
|
|
123
|
+
dismissable = true,
|
|
124
|
+
className,
|
|
125
|
+
style,
|
|
126
|
+
}: {
|
|
127
|
+
getCurrentDeployment?: DeploymentQueryRef;
|
|
128
|
+
message?: string;
|
|
129
|
+
buttonText?: string;
|
|
130
|
+
dismissable?: boolean;
|
|
131
|
+
className?: string;
|
|
132
|
+
style?: CSSProperties;
|
|
133
|
+
} = {}): JSX.Element | null {
|
|
134
|
+
const { updateAvailable, reload, dismiss, setupError } =
|
|
135
|
+
useDeploymentUpdates(getCurrentDeployment);
|
|
136
|
+
|
|
137
|
+
if (setupError) {
|
|
138
|
+
if (typeof console !== "undefined") {
|
|
139
|
+
console.warn(setupError);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!updateAvailable) return null;
|
|
145
|
+
|
|
146
|
+
const defaultStyle: CSSProperties = {
|
|
147
|
+
position: "fixed",
|
|
148
|
+
bottom: "1rem",
|
|
149
|
+
right: "1rem",
|
|
150
|
+
backgroundColor: "#1a1a2e",
|
|
151
|
+
color: "#fff",
|
|
152
|
+
padding: "1rem 1.5rem",
|
|
153
|
+
borderRadius: "8px",
|
|
154
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
|
155
|
+
display: "flex",
|
|
156
|
+
alignItems: "center",
|
|
157
|
+
gap: "1rem",
|
|
158
|
+
zIndex: 9999,
|
|
159
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
160
|
+
fontSize: "14px",
|
|
161
|
+
...style,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const buttonStyle: CSSProperties = {
|
|
165
|
+
backgroundColor: "#4f46e5",
|
|
166
|
+
color: "#fff",
|
|
167
|
+
border: "none",
|
|
168
|
+
padding: "0.5rem 1rem",
|
|
169
|
+
borderRadius: "4px",
|
|
170
|
+
cursor: "pointer",
|
|
171
|
+
fontWeight: 500,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const dismissStyle: CSSProperties = {
|
|
175
|
+
background: "none",
|
|
176
|
+
border: "none",
|
|
177
|
+
color: "#888",
|
|
178
|
+
cursor: "pointer",
|
|
179
|
+
padding: "0.25rem",
|
|
180
|
+
fontSize: "18px",
|
|
181
|
+
lineHeight: 1,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className={className} style={defaultStyle}>
|
|
186
|
+
<span>{message}</span>
|
|
187
|
+
<button onClick={reload} style={buttonStyle}>
|
|
188
|
+
{buttonText}
|
|
189
|
+
</button>
|
|
190
|
+
{dismissable && (
|
|
191
|
+
<button onClick={dismiss} style={dismissStyle} aria-label="Dismiss">
|
|
192
|
+
×
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import type { TestConvex } from "convex-test";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import { componentsGeneric } from "convex/server";
|
|
5
|
+
import type { ComponentApi } from "./component/_generated/component.js";
|
|
6
|
+
import schema from "./component/schema.js";
|
|
7
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register the component with the test convex instance.
|
|
11
|
+
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
12
|
+
* @param name - The name of the component, as registered in convex.config.ts.
|
|
13
|
+
* @returns a component api to test via ctx.runMutation or for thick client
|
|
14
|
+
* usage. Also provides types for convex-test's defineTestApp.
|
|
15
|
+
*/
|
|
16
|
+
export function register(
|
|
17
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
18
|
+
name: string = "hosting",
|
|
19
|
+
) {
|
|
20
|
+
t.registerComponent(name, schema, modules);
|
|
21
|
+
return componentsGeneric()[name] as unknown as ComponentApi;
|
|
22
|
+
}
|
|
23
|
+
export default { register, schema, modules };
|