mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
9a4816b101
* feat(plugins): site-wide plugin detail pages, share-to-site links, landing deploy trigger Why: a merged plugin PR didn't redeploy the landing site (plugins/** was missing from the deploy paths), and the desktop Share menu copied a local/404 link instead of the public marketplace URL. The landing plugin routing left by the detail-page rework also 404'd: the locale listing's cards used a multi-segment href while detail pages were single-segment, and only 388 bundled _official plugins had pages. What changed: - Deploy: landing-page deploy/ci trigger on plugins/**, and skip the slow previews step on an exact cache hit (cache key aligned across both workflows so a PR-built cache is reused by main). - Share URL: packages/contracts/plugin-url.ts owns the single-segment plugin URL scheme; the web Share menu and the landing site both derive links from it. Web links now point at https://open-design.ai/plugins/<slug>/. - Full detail coverage: detail pages now cover all 403 local plugins (_official incl. atoms + community), each rendered from its local manifest. Fixes the locale-listing 404s and the community manifest-name/catalog-id (- vs /) mismatch. - Self-host: daemon exposes OD_SITE_ORIGIN via /api/app-config; web falls back to the canonical origin until the daemon answers. Validation: pnpm guard, pnpm typecheck (all packages), contracts + web tests green, and a full build E2E confirming all 403 catalog ids and locale-listing cards resolve to built detail pages (0 missing). * chore: retrigger CI * ci(landing): carry plugins/** trigger + previews cache-hit into #2994 split workflows Merged origin/main, which split landing deploy into staging + manual production (#2994). git auto-migrated my landing-page-deploy.yml changes into landing-page-staging.yml via rename detection (plugins/** path, fallback-preview-card.ts cache key, cache-hit skip all carried). The new manual landing-page-production.yml didn't have them, so add the previews cache-key alignment + cache-hit skip there too (plugins/** path is N/A — production is workflow_dispatch only). * fix(ci): wrangler-action uses pnpm so it tolerates landing's workspace dep This PR added @open-design/contracts (workspace:*) to apps/landing-page/package.json so the landing site can share the plugin-url slug rules. But the landing deploy/preview steps run cloudflare/wrangler-action with packageManager: npm in workingDirectory apps/landing-page, and 'npm i wrangler' chokes on the workspace: protocol (EUNSUPPORTEDPROTOCOL), failing 'Validate landing page'. Switch all three landing wrangler-action steps (staging / ci preview / production) to packageManager: pnpm, which is workspace-aware. * test(e2e): bundled plugins now offer the README badge After this branch, buildPluginShareUrl returns a public open-design.ai link for bundled plugins (not just official-marketplace ones), so the home-starter share menu now shows 'Copy README badge'. Update the assertion from toHaveCount(0) to toBeVisible(). * fix(landing): drop @open-design/contracts dep, use a landing-local slug helper Per review on #2999: the marketing site must not import @open-design/contracts (AGENTS.md boundary — it's the web/daemon product-runtime contract layer). Move the slug/path helpers into landing-local app/_lib/plugin-slug.ts; the web client keeps contracts' plugin-url. The two derive the same scheme and are verified in lockstep by the e2e route check (403 share URLs -> 403 detail pages, 0 missing). landing no longer has a workspace dep, so revert the wrangler-action packageManager back to npm. * fix(landing): include plugins/_official in previews cache key Per review on #2999: generate-previews.ts builds bundled-plugin preview jobs from plugins/_official/**/open-design.json and renders fallback cards from manifest fields (title/description/mode/scenario/tags). With plugins/** now triggering the workflow but the cache key not hashing plugin inputs, a plugin-only PR/merge could exact-hit an old cache and skip the preview regen, shipping with a stale or missing /previews/plugins/<manifest-id>.png. Add plugins/_official/** to the cache key in all three landing workflows (ci, staging, production). community is not currently covered by generate-previews so its glob is omitted. * fix(plugins): include community marketplace installs in share gate hasPublicPage now covers sourceMarketplaceId === 'community' so the README badge and public detail link surface for community installs. Community manifest names carry a community- prefix that diverges from the landing-page route slug, so URL derivation uses sourceMarketplaceEntryName (community/<folder>) instead — pluginDetailSlug takes the last segment, matching the /plugins/<folder>/ route the landing page emits. Adds component tests for buildPluginShareUrl, badge copy, and the Open-in-marketplace link for a community/registry-starter record. Generated-By: looper 0.9.2 (runner=fixer, agent=claude-code) --------- Co-authored-by: mrcfps <mrc@powerformer.com>
67 lines
2.6 KiB
TypeScript
67 lines
2.6 KiB
TypeScript
// plugin-url — single source of truth for the public marketplace URL scheme.
|
|
// Web (PluginShareMenu) and the landing site both derive links here; this
|
|
// pins the slug rules so the two surfaces can't drift into a 404 against the
|
|
// SSG site. Detail/share routes are SINGLE-segment (last id segment); the
|
|
// preview route keeps the namespace.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
OPEN_DESIGN_SITE_ORIGIN,
|
|
pluginSlug,
|
|
pluginSlugSegment,
|
|
pluginDetailSlug,
|
|
pluginDetailPath,
|
|
pluginPreviewPath,
|
|
pluginShareUrl,
|
|
} from '../src/plugins/plugin-url.js';
|
|
|
|
describe('pluginSlugSegment', () => {
|
|
it('lowercases, collapses unsafe runs, trims dashes', () => {
|
|
expect(pluginSlugSegment('Hero Deck')).toBe('hero-deck');
|
|
expect(pluginSlugSegment(' Wild!!Name ')).toBe('wild-name');
|
|
});
|
|
it('keeps url-safe punctuation and falls back to "plugin"', () => {
|
|
expect(pluginSlugSegment('keep.dots_and-dashes')).toBe('keep.dots_and-dashes');
|
|
expect(pluginSlugSegment('!!!')).toBe('plugin');
|
|
});
|
|
});
|
|
|
|
describe('pluginDetailSlug (single segment = last id segment)', () => {
|
|
it('takes the slugified last segment, dropping any namespace', () => {
|
|
expect(pluginDetailSlug('open-design/Hero Deck')).toBe('hero-deck');
|
|
expect(pluginDetailSlug('community/registry-starter')).toBe('registry-starter');
|
|
expect(pluginDetailSlug('live-dashboard')).toBe('live-dashboard');
|
|
});
|
|
});
|
|
|
|
describe('pluginSlug (multi-segment, namespace preserved)', () => {
|
|
it('slugifies each segment and keeps / as a separator', () => {
|
|
expect(pluginSlug('open-design/Hero Deck')).toBe('open-design/hero-deck');
|
|
});
|
|
});
|
|
|
|
describe('pluginDetailPath / pluginPreviewPath', () => {
|
|
it('detail path is single-segment with trailing slash', () => {
|
|
expect(pluginDetailPath('open-design/Hero Deck')).toBe('/plugins/hero-deck/');
|
|
expect(pluginDetailPath('live-dashboard')).toBe('/plugins/live-dashboard/');
|
|
});
|
|
it('preview path keeps the namespace', () => {
|
|
expect(pluginPreviewPath('open-design/Hero Deck')).toBe(
|
|
'/plugins/previews/open-design/hero-deck/',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('pluginShareUrl', () => {
|
|
it('defaults to the public open-design.ai origin, single-segment path', () => {
|
|
expect(OPEN_DESIGN_SITE_ORIGIN).toBe('https://open-design.ai');
|
|
expect(pluginShareUrl('open-design/live-dashboard')).toBe(
|
|
'https://open-design.ai/plugins/live-dashboard/',
|
|
);
|
|
});
|
|
it('honours an explicit origin and trims a trailing slash on it', () => {
|
|
expect(pluginShareUrl('x', 'https://self.host')).toBe('https://self.host/plugins/x/');
|
|
expect(pluginShareUrl('x', 'https://self.host/')).toBe('https://self.host/plugins/x/');
|
|
});
|
|
});
|