proto-plugin 0.1.8 → 0.1.9
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 +24 -12
- package/package.json +3 -1
- package/scripts/ensure-component-library-page.mjs +89 -0
- package/scripts/link-source.sh +2 -0
- package/src/app.ts +1 -0
- package/src/components/shell/prototype-plugin-update-popup.tsx +22 -26
- package/src/config/create-prototype-host-app.tsx +12 -1
- package/src/config/prototype-site-layout-client.tsx +5 -4
- package/src/lib/prototypes/upgrade-proto-plugin-prompt.ts +6 -0
- package/src/lib/prototypes/use-proto-plugin-version-check.ts +38 -2
- package/src/server/proto-plugin-version-route.ts +7 -20
package/README.md
CHANGED
|
@@ -56,7 +56,17 @@ export default page.default;
|
|
|
56
56
|
export const generateStaticParams = page.generateStaticParams;
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
4. **
|
|
59
|
+
4. **Component library** — `src/app/component-library/page.tsx` (created automatically on `pnpm install` if missing; or run `pnpm --dir node_modules/proto-plugin setup-host`):
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { createPrototypeComponentLibraryPage } from "proto-plugin";
|
|
63
|
+
|
|
64
|
+
export default createPrototypeComponentLibraryPage();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Pass `children` once you've synced UI from source. Until then, the page shows an empty state with a copyable agent prompt.
|
|
68
|
+
|
|
69
|
+
5. **Next.js config** — transpile the package (use a relative import so CSS is not loaded during config evaluation):
|
|
60
70
|
|
|
61
71
|
```ts
|
|
62
72
|
import { withPrototype } from "./packages/prototype/src/config/with-prototype";
|
|
@@ -64,7 +74,7 @@ import { withPrototype } from "./packages/prototype/src/config/with-prototype";
|
|
|
64
74
|
export default withPrototype({ nextConfig: { /* … */ } });
|
|
65
75
|
```
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
6. **Tailwind** — scan prototype package components from the host `globals.css`. Register semantic `--color-*` tokens as self-referential vars so utilities like `text-muted-foreground` resolve from `[data-prototype-root]` at runtime — **do not** map `--color-foreground` / `--color-background` to product `:root` tokens (that breaks gallery contrast):
|
|
68
78
|
|
|
69
79
|
```css
|
|
70
80
|
@import "tailwindcss";
|
|
@@ -79,7 +89,7 @@ export default withPrototype({ nextConfig: { /* … */ } });
|
|
|
79
89
|
|
|
80
90
|
Product tokens use separate names (e.g. `--product-background`) scoped to `[data-prototype-screenshot]`.
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
7. **Root layout + tool themes** — wrap routes with `createPrototypeSiteLayout()` and import package chrome CSS (dark/light themes live here, not in host `globals.css`):
|
|
83
93
|
|
|
84
94
|
```tsx
|
|
85
95
|
import { createPrototypeSiteLayout } from "proto-plugin";
|
|
@@ -89,7 +99,7 @@ import "./globals.css";
|
|
|
89
99
|
const PrototypeSiteLayout = createPrototypeSiteLayout();
|
|
90
100
|
```
|
|
91
101
|
|
|
92
|
-
|
|
102
|
+
8. **tsconfig paths** (optional, for tool internal `@prototype/*` alias when developing the package):
|
|
93
103
|
|
|
94
104
|
```json
|
|
95
105
|
{
|
|
@@ -102,7 +112,7 @@ const PrototypeSiteLayout = createPrototypeSiteLayout();
|
|
|
102
112
|
}
|
|
103
113
|
```
|
|
104
114
|
|
|
105
|
-
|
|
115
|
+
9. **Dev server port** — run the host on port **1985** so it stays separate from a typical source app on 3000. In the host `package.json`:
|
|
106
116
|
|
|
107
117
|
```json
|
|
108
118
|
{
|
|
@@ -120,6 +130,7 @@ Open [http://localhost:1985](http://localhost:1985) when developing.
|
|
|
120
130
|
| --- | --- |
|
|
121
131
|
| `definePrototypeConfig` | Register prototypes (sets global config as a side effect) |
|
|
122
132
|
| `createPrototypePage` | Factory for `/prototypes/[slug]` page + `generateStaticParams` |
|
|
133
|
+
| `createPrototypeComponentLibraryPage` | Factory for `/component-library` (empty state until host passes `children`) |
|
|
123
134
|
| `createPrototypeRegistry` | Server helpers: `getAllPrototypes`, `getPrototype`, … |
|
|
124
135
|
| `PrototypeComponent`, `PrototypeControl` | Comment-anchorable UI wrappers |
|
|
125
136
|
| `PrototypeShell` | Review chrome (comments sidebar, screenshot capture) |
|
|
@@ -261,16 +272,17 @@ Follow the package README (`node_modules/proto-plugin/README.md`) and implement
|
|
|
261
272
|
1. **`prototype.config.ts`** at the repo root — start with an empty `prototypes: []` array using `definePrototypeConfig` from `proto-plugin`.
|
|
262
273
|
2. **Gallery home** — `src/app/page.tsx` via `createPrototypeGalleryPage(prototypeConfig)`.
|
|
263
274
|
3. **Prototype route** — `src/app/prototypes/[slug]/page.tsx` via `createPrototypePage(prototypeConfig)` (export `default` + `generateStaticParams`).
|
|
264
|
-
4. **
|
|
275
|
+
4. **Component library** — `src/app/component-library/page.tsx` via `createPrototypeComponentLibraryPage()` (auto-scaffolded on `pnpm install` if missing; shows empty state + copyable populate prompt until synced UI is wired as `children`).
|
|
276
|
+
5. **Root layout** — wrap children with `createPrototypeSiteLayout()` from `proto-plugin` in `src/app/layout.tsx`, and import the tool chrome theme:
|
|
265
277
|
|
|
266
278
|
```tsx
|
|
267
279
|
import { createPrototypeSiteLayout } from "proto-plugin";
|
|
268
280
|
import "proto-plugin/styles/globals.css";
|
|
269
281
|
import "./globals.css";
|
|
270
282
|
```
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
283
|
+
6. **API catch-all** — `src/app/api/[...path]/route.ts` using `createPrototypeApiRoute(prototypeConfig)` from `proto-plugin/server`; export `GET`, `POST`, and `PUT` handlers that call `dispatch`.
|
|
284
|
+
7. **`next.config.ts`** — wrap with `withPrototype` from `proto-plugin/with-prototype` and ensure `proto-plugin` is transpiled.
|
|
285
|
+
8. **`src/app/globals.css`** — Tailwind v4 + product tokens synced from source. **Do not** put tool chrome dark/light mode here (no `prefers-color-scheme`, no hardcoded gallery colors). Scan package components:
|
|
274
286
|
|
|
275
287
|
```css
|
|
276
288
|
@import "tailwindcss";
|
|
@@ -279,7 +291,7 @@ Follow the package README (`node_modules/proto-plugin/README.md`) and implement
|
|
|
279
291
|
|
|
280
292
|
Tool themes live in `proto-plugin/styles/globals.css` (import in layout). The package toggles dark/light on `[data-prototype-root]` via `data-prototype-comment-theme`.
|
|
281
293
|
|
|
282
|
-
|
|
294
|
+
9. **`package.json` scripts** — set dev to port 1985 and forward CLI commands to the installed package:
|
|
283
295
|
|
|
284
296
|
```json
|
|
285
297
|
{
|
|
@@ -292,9 +304,9 @@ Follow the package README (`node_modules/proto-plugin/README.md`) and implement
|
|
|
292
304
|
}
|
|
293
305
|
```
|
|
294
306
|
|
|
295
|
-
|
|
307
|
+
10. **`.env.example` and `.env.local`** — document and set `SOURCE_PATH` to the source app path I provided. Add comment/screenshot storage vars only if I asked for them in step 5 — the app should run without them, but comments won't persist.
|
|
296
308
|
|
|
297
|
-
Run `pnpm dev` and confirm [http://localhost:1985](http://localhost:1985) shows the empty gallery and the app builds cleanly before continuing.
|
|
309
|
+
Run `pnpm dev` and confirm [http://localhost:1985](http://localhost:1985) shows the empty gallery, `/component-library` renders (empty state is fine), and the app builds cleanly before continuing.
|
|
298
310
|
|
|
299
311
|
## 3. Link the source app
|
|
300
312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "proto-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org"
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"./styles/globals.css": "./src/styles/globals.css"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
+
"postinstall": "node scripts/ensure-component-library-page.mjs",
|
|
35
|
+
"setup-host": "node scripts/ensure-component-library-page.mjs",
|
|
34
36
|
"verify:prototype-ids": "node scripts/verify-prototype-component-ids.mjs",
|
|
35
37
|
"verify:prototype-preview-states": "node scripts/verify-prototype-preview-states.mjs",
|
|
36
38
|
"link-source": "./scripts/link-source.sh",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { resolveHostRoot } from "./lib/resolve-host-root.mjs";
|
|
6
|
+
|
|
7
|
+
const PAGE_CONTENT = `import { createPrototypeComponentLibraryPage } from "proto-plugin";
|
|
8
|
+
|
|
9
|
+
// Scaffolded by proto-plugin — pass \`children\` once you've synced UI from source.
|
|
10
|
+
export default createPrototypeComponentLibraryPage();
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
function resolveAppDir(hostRoot) {
|
|
14
|
+
const srcApp = path.join(hostRoot, "src", "app");
|
|
15
|
+
if (fs.existsSync(srcApp)) {
|
|
16
|
+
return srcApp;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const app = path.join(hostRoot, "app");
|
|
20
|
+
if (fs.existsSync(app)) {
|
|
21
|
+
return app;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPrototypeHost(hostRoot) {
|
|
28
|
+
if (fs.existsSync(path.join(hostRoot, "prototype.config.ts"))) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const packageJsonPath = path.join(hostRoot, "package.json");
|
|
33
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
39
|
+
const deps = {
|
|
40
|
+
...packageJson.dependencies,
|
|
41
|
+
...packageJson.devDependencies,
|
|
42
|
+
};
|
|
43
|
+
return Boolean(deps["proto-plugin"]);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function ensureComponentLibraryPage(hostRoot) {
|
|
50
|
+
if (!isPrototypeHost(hostRoot)) {
|
|
51
|
+
return { created: false, reason: "not-a-prototype-host" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const appDir = resolveAppDir(hostRoot);
|
|
55
|
+
if (!appDir) {
|
|
56
|
+
return { created: false, reason: "missing-app-dir" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pageDir = path.join(appDir, "component-library");
|
|
60
|
+
const pagePath = path.join(pageDir, "page.tsx");
|
|
61
|
+
|
|
62
|
+
if (fs.existsSync(pagePath)) {
|
|
63
|
+
return { created: false, reason: "already-exists", pagePath };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fs.mkdirSync(pageDir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(pagePath, PAGE_CONTENT, "utf8");
|
|
68
|
+
|
|
69
|
+
return { created: true, pagePath };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function main() {
|
|
73
|
+
const toolRoot = path.resolve(
|
|
74
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
75
|
+
"..",
|
|
76
|
+
);
|
|
77
|
+
const hostRoot = resolveHostRoot(toolRoot);
|
|
78
|
+
const result = ensureComponentLibraryPage(hostRoot);
|
|
79
|
+
|
|
80
|
+
if (result.created) {
|
|
81
|
+
console.log(
|
|
82
|
+
`proto-plugin: created component library route at ${path.relative(hostRoot, result.pagePath)}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (import.meta.url === new URL(process.argv[1], "file:").href) {
|
|
88
|
+
main();
|
|
89
|
+
}
|
package/scripts/link-source.sh
CHANGED
package/src/app.ts
CHANGED
|
@@ -2,3 +2,4 @@ import "server-only";
|
|
|
2
2
|
|
|
3
3
|
export { createPrototypeGalleryPage } from "./config/create-prototype-gallery-page";
|
|
4
4
|
export { createPrototypeCatchAllPage } from "./config/create-prototype-host-app";
|
|
5
|
+
export { createPrototypeComponentLibraryPage } from "./config/create-prototype-component-library-page";
|
|
@@ -16,17 +16,17 @@ export function PrototypePluginUpdatePopup() {
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const handleCopyPrompt = async () => {
|
|
20
20
|
try {
|
|
21
21
|
await copy(
|
|
22
22
|
buildUpgradeProtoPluginCopyText({
|
|
23
|
-
installed: status.installed
|
|
24
|
-
latest: status.latest
|
|
23
|
+
installed: status.installed,
|
|
24
|
+
latest: status.latest,
|
|
25
25
|
}),
|
|
26
26
|
);
|
|
27
27
|
} catch {
|
|
28
28
|
toast.error("Copy error", {
|
|
29
|
-
description: "Failed to copy
|
|
29
|
+
description: "Failed to copy update prompt",
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
};
|
|
@@ -36,37 +36,33 @@ export function PrototypePluginUpdatePopup() {
|
|
|
36
36
|
role="status"
|
|
37
37
|
aria-live="polite"
|
|
38
38
|
className={cn(
|
|
39
|
-
"
|
|
39
|
+
"pointer-events-auto fixed bottom-4 left-4 z-[1050] relative isolate flex w-fit max-w-[min(18rem,calc(100vw-2rem))] flex-col gap-2 rounded-lg bg-[var(--tool-chrome-surface)] p-2.5 pr-7 shadow-[var(--tool-review-card-shadow)] ring-[0.5px] ring-[var(--tool-chrome-border)]",
|
|
40
40
|
)}
|
|
41
41
|
>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
>
|
|
55
|
-
<X size={14} />
|
|
56
|
-
</button>
|
|
57
|
-
</div>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
aria-label="Dismiss update notification"
|
|
45
|
+
className="absolute right-1.5 top-1.5 rounded p-0.5 text-[var(--tool-chrome-text-muted)] transition-colors duration-200 ease hover:text-[var(--tool-chrome-text-heading)]"
|
|
46
|
+
onClick={dismiss}
|
|
47
|
+
>
|
|
48
|
+
<X size={12} />
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<p className="text-xs leading-relaxed text-[var(--tool-chrome-text-muted)]">
|
|
52
|
+
A new version of Proto is available, copy the prompt below into your agent to update
|
|
53
|
+
</p>
|
|
58
54
|
|
|
59
55
|
<Button
|
|
60
56
|
type="button"
|
|
61
|
-
variant="
|
|
57
|
+
variant="chrome"
|
|
62
58
|
size="sm"
|
|
63
|
-
className="h-
|
|
59
|
+
className="h-7 w-fit cursor-pointer gap-1 px-2 text-[11px]"
|
|
64
60
|
onClick={() => {
|
|
65
|
-
void
|
|
61
|
+
void handleCopyPrompt();
|
|
66
62
|
}}
|
|
67
|
-
aria-label="Copy
|
|
63
|
+
aria-label="Copy update prompt"
|
|
68
64
|
>
|
|
69
|
-
{isCopied ? "Copied prompt" : "Copy prompt"}
|
|
65
|
+
{isCopied ? "Copied update prompt" : "Copy update prompt"}
|
|
70
66
|
{icon}
|
|
71
67
|
</Button>
|
|
72
68
|
</div>
|
|
@@ -10,9 +10,12 @@ import type {
|
|
|
10
10
|
PrototypeExtraRoute,
|
|
11
11
|
} from "@prototype/lib/prototypes/prototype-config-types";
|
|
12
12
|
|
|
13
|
+
import { createPrototypeComponentLibraryPage } from "./create-prototype-component-library-page";
|
|
13
14
|
import { createPrototypePage } from "./create-prototype-page";
|
|
14
15
|
import { createPrototypeStateMapPage } from "./create-prototype-state-map-page";
|
|
15
16
|
|
|
17
|
+
const ComponentLibraryPage = createPrototypeComponentLibraryPage();
|
|
18
|
+
|
|
16
19
|
function matchExtraRoute(
|
|
17
20
|
path: string[],
|
|
18
21
|
pattern: string[],
|
|
@@ -61,7 +64,11 @@ export function createPrototypeCatchAllPage(config: PrototypeConfig) {
|
|
|
61
64
|
path: ["prototypes", slug, "states"],
|
|
62
65
|
}));
|
|
63
66
|
|
|
64
|
-
return [
|
|
67
|
+
return [
|
|
68
|
+
{ path: ["component-library"] },
|
|
69
|
+
...prototypePaths,
|
|
70
|
+
...stateMapPaths,
|
|
71
|
+
];
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
async function CatchAllPage({
|
|
@@ -71,6 +78,10 @@ export function createPrototypeCatchAllPage(config: PrototypeConfig) {
|
|
|
71
78
|
}) {
|
|
72
79
|
const { path = [] } = await params;
|
|
73
80
|
|
|
81
|
+
if (path.length === 1 && path[0] === "component-library") {
|
|
82
|
+
return <ComponentLibraryPage />;
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
if (path.length === 2 && path[0] === "prototypes") {
|
|
75
86
|
const slug = path[1]!;
|
|
76
87
|
return prototypePage.default({ params: Promise.resolve({ slug }) });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { PrototypeProvider } from "@prototype/components/prototype-provider";
|
|
3
4
|
import { PrototypePluginUpdatePopup } from "@prototype/components/shell/prototype-plugin-update-popup";
|
|
4
5
|
import { usePathname } from "next/navigation";
|
|
5
6
|
import type { ComponentType, ReactNode } from "react";
|
|
@@ -29,7 +30,7 @@ export function PrototypeSiteLayoutClient({
|
|
|
29
30
|
|
|
30
31
|
if (isGalleryRoute) {
|
|
31
32
|
return (
|
|
32
|
-
<
|
|
33
|
+
<PrototypeProvider className="bg-[var(--bg-main)] flex h-svh min-h-svh w-full flex-col overflow-hidden">
|
|
33
34
|
<GalleryShell
|
|
34
35
|
sourceDirectoryName={sourceDirectoryName}
|
|
35
36
|
sourcePath={sourcePath}
|
|
@@ -38,15 +39,15 @@ export function PrototypeSiteLayoutClient({
|
|
|
38
39
|
</GalleryShell>
|
|
39
40
|
<PrototypePluginUpdatePopup />
|
|
40
41
|
{toaster}
|
|
41
|
-
</
|
|
42
|
+
</PrototypeProvider>
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
|
-
<
|
|
47
|
+
<PrototypeProvider className="bg-[var(--bg-ground)] flex h-svh overflow-hidden">
|
|
47
48
|
{children}
|
|
48
49
|
<PrototypePluginUpdatePopup />
|
|
49
50
|
{toaster}
|
|
50
|
-
</
|
|
51
|
+
</PrototypeProvider>
|
|
51
52
|
);
|
|
52
53
|
}
|
|
@@ -38,3 +38,9 @@ export function buildUpgradeProtoPluginCopyText(
|
|
|
38
38
|
): string {
|
|
39
39
|
return buildUpgradeProtoPluginPrompt(options);
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
export function buildUpgradeProtoPluginCommand({
|
|
43
|
+
latest,
|
|
44
|
+
}: Pick<UpgradeProtoPluginPromptOptions, "latest">): string {
|
|
45
|
+
return `pnpm add proto-plugin@${latest}`;
|
|
46
|
+
}
|
|
@@ -8,12 +8,20 @@ import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
9
9
|
|
|
10
10
|
const DISMISSED_VERSION_KEY = "prototype-review:global:dismissed-plugin-version";
|
|
11
|
+
const PREVIEW_PLUGIN_UPDATE_PARAM = "previewPluginUpdate";
|
|
12
|
+
|
|
13
|
+
function readPreviewPluginUpdateParam(): boolean {
|
|
14
|
+
if (process.env.NODE_ENV !== "development") return false;
|
|
15
|
+
if (typeof window === "undefined") return false;
|
|
16
|
+
return new URLSearchParams(window.location.search).has(PREVIEW_PLUGIN_UPDATE_PARAM);
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
export function useProtoPluginVersionCheck() {
|
|
13
20
|
const [status, setStatus] = useState<ProtoPluginVersionStatus | null>(null);
|
|
14
21
|
const [isLoading, setIsLoading] = useState(true);
|
|
15
22
|
const { value: dismissedVersion, updateValue: setDismissedVersion } =
|
|
16
23
|
usePersistedLocalString(DISMISSED_VERSION_KEY, "");
|
|
24
|
+
const forcePreview = readPreviewPluginUpdateParam();
|
|
17
25
|
|
|
18
26
|
useEffect(() => {
|
|
19
27
|
let cancelled = false;
|
|
@@ -46,7 +54,35 @@ export function useProtoPluginVersionCheck() {
|
|
|
46
54
|
}
|
|
47
55
|
}, [setDismissedVersion, status?.latest]);
|
|
48
56
|
|
|
57
|
+
const previewStatus = useMemo<ProtoPluginVersionStatus | null>(() => {
|
|
58
|
+
if (!forcePreview) return null;
|
|
59
|
+
|
|
60
|
+
const installed = status?.installed ?? "0.1.7";
|
|
61
|
+
let latest = status?.latest ?? "0.1.9";
|
|
62
|
+
|
|
63
|
+
if (latest === installed) {
|
|
64
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(installed);
|
|
65
|
+
latest = match
|
|
66
|
+
? `${match[1]}.${match[2]}.${Number(match[3]) + 1}`
|
|
67
|
+
: "0.1.9";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
installed,
|
|
72
|
+
latest,
|
|
73
|
+
updateAvailable: true,
|
|
74
|
+
isWorkspaceLink: false,
|
|
75
|
+
repositoryUrl: status?.repositoryUrl,
|
|
76
|
+
};
|
|
77
|
+
}, [forcePreview, status]);
|
|
78
|
+
|
|
79
|
+
const effectiveStatus = previewStatus ?? status;
|
|
80
|
+
|
|
49
81
|
const shouldShowPopup = useMemo(() => {
|
|
82
|
+
if (forcePreview) {
|
|
83
|
+
return Boolean(effectiveStatus?.installed && effectiveStatus.latest);
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
if (isLoading || !status?.updateAvailable || !status.latest || !status.installed) {
|
|
51
87
|
return false;
|
|
52
88
|
}
|
|
@@ -56,10 +92,10 @@ export function useProtoPluginVersionCheck() {
|
|
|
56
92
|
}
|
|
57
93
|
|
|
58
94
|
return dismissedVersion !== status.latest;
|
|
59
|
-
}, [dismissedVersion, isLoading, status]);
|
|
95
|
+
}, [dismissedVersion, effectiveStatus, forcePreview, isLoading, status]);
|
|
60
96
|
|
|
61
97
|
return {
|
|
62
|
-
status,
|
|
98
|
+
status: effectiveStatus,
|
|
63
99
|
shouldShowPopup,
|
|
64
100
|
dismiss,
|
|
65
101
|
isLoading,
|
|
@@ -5,7 +5,6 @@ import { NextResponse } from "next/server";
|
|
|
5
5
|
|
|
6
6
|
const PACKAGE_NAME = "proto-plugin";
|
|
7
7
|
const NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
8
|
-
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
9
8
|
|
|
10
9
|
export type ProtoPluginVersionStatus = {
|
|
11
10
|
installed: string | null;
|
|
@@ -20,9 +19,6 @@ type NpmLatestResponse = {
|
|
|
20
19
|
repository?: { url?: string };
|
|
21
20
|
};
|
|
22
21
|
|
|
23
|
-
let cachedLatest: { version: string; repositoryUrl?: string; fetchedAt: number } | null =
|
|
24
|
-
null;
|
|
25
|
-
|
|
26
22
|
function parseSemver(version: string): [number, number, number] | null {
|
|
27
23
|
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
28
24
|
if (!match) return null;
|
|
@@ -76,18 +72,9 @@ async function fetchLatestFromNpm(): Promise<{
|
|
|
76
72
|
version: string | null;
|
|
77
73
|
repositoryUrl?: string;
|
|
78
74
|
}> {
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
|
|
81
|
-
if (cachedLatest && now - cachedLatest.fetchedAt < CACHE_TTL_MS) {
|
|
82
|
-
return {
|
|
83
|
-
version: cachedLatest.version,
|
|
84
|
-
repositoryUrl: cachedLatest.repositoryUrl,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
75
|
try {
|
|
89
76
|
const response = await fetch(NPM_LATEST_URL, {
|
|
90
|
-
|
|
77
|
+
cache: "no-store",
|
|
91
78
|
});
|
|
92
79
|
|
|
93
80
|
if (!response.ok) {
|
|
@@ -98,10 +85,6 @@ async function fetchLatestFromNpm(): Promise<{
|
|
|
98
85
|
const version = data.version ?? null;
|
|
99
86
|
const repositoryUrl = data.repository?.url?.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
100
87
|
|
|
101
|
-
if (version) {
|
|
102
|
-
cachedLatest = { version, repositoryUrl, fetchedAt: now };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
88
|
return { version, repositoryUrl };
|
|
106
89
|
} catch {
|
|
107
90
|
return { version: null };
|
|
@@ -119,7 +102,9 @@ export async function GET() {
|
|
|
119
102
|
updateAvailable: false,
|
|
120
103
|
isWorkspaceLink: true,
|
|
121
104
|
};
|
|
122
|
-
return NextResponse.json(status
|
|
105
|
+
return NextResponse.json(status, {
|
|
106
|
+
headers: { "Cache-Control": "no-store, max-age=0" },
|
|
107
|
+
});
|
|
123
108
|
}
|
|
124
109
|
|
|
125
110
|
const { version: latest, repositoryUrl } = await fetchLatestFromNpm();
|
|
@@ -135,5 +120,7 @@ export async function GET() {
|
|
|
135
120
|
repositoryUrl,
|
|
136
121
|
};
|
|
137
122
|
|
|
138
|
-
return NextResponse.json(status
|
|
123
|
+
return NextResponse.json(status, {
|
|
124
|
+
headers: { "Cache-Control": "no-store, max-age=0" },
|
|
125
|
+
});
|
|
139
126
|
}
|