@anthusai/papyrus 0.0.0 → 1.0.0-next.10
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2069 -0
- package/amplify/data/resource.ts +2185 -0
- package/amplify/data/schema.js +1882 -0
- package/amplify/data/schema.ts +1990 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +130 -0
- package/amplify/functions/shared/python-bundle.ts +162 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +36 -0
- package/amplify/site-backend.js +615 -0
- package/amplify/site-backend.ts +739 -0
- package/amplify/storage/resource.js +21 -0
- package/amplify/storage/resource.ts +22 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10345 -0
- package/app/layout.tsx +142 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/papyrus-theme.css +11 -0
- package/app/robots.ts +11 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +20 -0
- package/bin/papyrus-infra.mjs +95 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +156 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +116 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +321 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +18005 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +234 -0
- package/infra/amplify-app-shell.ts +284 -0
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +15 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +90 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +43 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/empty-theme.css +1 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +1140 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +44 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +22 -0
- package/lib/site-brand.ts +194 -0
- package/lib/site-env.ts +52 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +103 -0
- package/package.json +318 -1
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +169 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +470 -0
- package/routes.manifest.json +208 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +60 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { ComposableRenderer, RendererProvider } from "@videoml/toolchain/renderer";
|
|
3
|
+
import { dslToScriptData } from "@videoml/toolchain/shared";
|
|
4
|
+
import { executeVomXml } from "@videoml/player";
|
|
5
|
+
import { advanceScriptPreviewTime, computePreviewPlaybackState } from "./preview-timing";
|
|
6
|
+
import { computePreviewStageFit } from "./preview-stage-fit";
|
|
7
|
+
|
|
8
|
+
type PreviewTheme = "dark" | "light";
|
|
9
|
+
type ScriptData = ReturnType<typeof dslToScriptData>;
|
|
10
|
+
type PreviewRetheme = (xml: string, theme: PreviewTheme) => string;
|
|
11
|
+
|
|
12
|
+
type PreviewMountOptions = {
|
|
13
|
+
xml: string;
|
|
14
|
+
theme?: PreviewTheme;
|
|
15
|
+
autoPlay?: boolean;
|
|
16
|
+
audioSrc?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type PreviewPlayerProps = {
|
|
20
|
+
script: ScriptData;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
autoPlay?: boolean;
|
|
24
|
+
audioSrc?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let rethemeXml: PreviewRetheme = (xml) => xml;
|
|
28
|
+
|
|
29
|
+
function registerPreviewRetheme(retheme: PreviewRetheme): void {
|
|
30
|
+
rethemeXml = retheme;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scriptDuration(script: ScriptData): number {
|
|
34
|
+
const scenes = script.scenes ?? [];
|
|
35
|
+
if (!scenes.length) return 10;
|
|
36
|
+
const lastScene = scenes[scenes.length - 1];
|
|
37
|
+
return typeof lastScene.endSec === "number" && lastScene.endSec > 0 ? lastScene.endSec : 10;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function PreviewPlayer({ script, width, height, autoPlay = false, audioSrc }: PreviewPlayerProps) {
|
|
41
|
+
const [isPlaying, setIsPlaying] = useState(autoPlay);
|
|
42
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
43
|
+
const [audioDuration, setAudioDuration] = useState<number | null>(null);
|
|
44
|
+
const [audioBlocked, setAudioBlocked] = useState(false);
|
|
45
|
+
const [audioProgressObserved, setAudioProgressObserved] = useState(false);
|
|
46
|
+
const [viewportSize, setViewportSize] = useState({ width, height });
|
|
47
|
+
const animationFrameRef = useRef<number | undefined>(undefined);
|
|
48
|
+
const lastTimestampRef = useRef<number | undefined>(undefined);
|
|
49
|
+
const currentTimeRef = useRef(0);
|
|
50
|
+
const audioRef = useRef<HTMLAudioElement>(null);
|
|
51
|
+
const lastAudioTimeRef = useRef(0);
|
|
52
|
+
const viewportRef = useRef<HTMLDivElement>(null);
|
|
53
|
+
const usedAudioClockRef = useRef(false);
|
|
54
|
+
|
|
55
|
+
const fps = script.fps ?? 30;
|
|
56
|
+
const scriptDerivedDuration = useMemo(() => scriptDuration(script), [script]);
|
|
57
|
+
const playbackState = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
computePreviewPlaybackState({
|
|
60
|
+
audioBlocked,
|
|
61
|
+
audioDuration,
|
|
62
|
+
audioProgressObserved,
|
|
63
|
+
audioSrc,
|
|
64
|
+
currentTime,
|
|
65
|
+
isPlaying,
|
|
66
|
+
scriptDerivedDuration,
|
|
67
|
+
}),
|
|
68
|
+
[audioBlocked, audioDuration, audioProgressObserved, audioSrc, currentTime, isPlaying, scriptDerivedDuration],
|
|
69
|
+
);
|
|
70
|
+
const { duration, hasAudioSource, useAudioClock, visualTime } = playbackState;
|
|
71
|
+
|
|
72
|
+
const currentFrame = Math.floor(visualTime * fps);
|
|
73
|
+
const sceneBackground = script.scenes?.[0]?.styles?.background;
|
|
74
|
+
const backgroundColor = typeof sceneBackground === "string" ? sceneBackground : "#000";
|
|
75
|
+
const fittedStage = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
computePreviewStageFit({
|
|
78
|
+
containerHeight: viewportSize.height,
|
|
79
|
+
containerWidth: viewportSize.width,
|
|
80
|
+
stageHeight: height,
|
|
81
|
+
stageWidth: width,
|
|
82
|
+
}),
|
|
83
|
+
[height, viewportSize.height, viewportSize.width, width],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const applyTime = useCallback(
|
|
87
|
+
(nextTime: number) => {
|
|
88
|
+
const clamped = Math.max(0, Math.min(nextTime, duration));
|
|
89
|
+
currentTimeRef.current = clamped;
|
|
90
|
+
setCurrentTime(clamped);
|
|
91
|
+
},
|
|
92
|
+
[duration],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const playAudio = useCallback(async () => {
|
|
96
|
+
const audio = audioRef.current;
|
|
97
|
+
if (!audio || !hasAudioSource) return true;
|
|
98
|
+
try {
|
|
99
|
+
await audio.play();
|
|
100
|
+
setAudioBlocked(false);
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
setAudioBlocked(true);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}, [hasAudioSource]);
|
|
107
|
+
|
|
108
|
+
const pauseAudio = useCallback(() => {
|
|
109
|
+
const audio = audioRef.current;
|
|
110
|
+
if (!audio || !hasAudioSource) return;
|
|
111
|
+
audio.pause();
|
|
112
|
+
}, [hasAudioSource]);
|
|
113
|
+
|
|
114
|
+
const seekAudio = useCallback(
|
|
115
|
+
(nextTime: number) => {
|
|
116
|
+
const audio = audioRef.current;
|
|
117
|
+
if (!audio || !hasAudioSource) return;
|
|
118
|
+
try {
|
|
119
|
+
audio.currentTime = nextTime;
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[hasAudioSource],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const audio = audioRef.current;
|
|
129
|
+
setAudioDuration(null);
|
|
130
|
+
setAudioProgressObserved(false);
|
|
131
|
+
setAudioBlocked(false);
|
|
132
|
+
lastAudioTimeRef.current = 0;
|
|
133
|
+
if (!audio || !hasAudioSource) return;
|
|
134
|
+
|
|
135
|
+
const onLoadedMetadata = () => {
|
|
136
|
+
if (Number.isFinite(audio.duration) && audio.duration > 0) {
|
|
137
|
+
setAudioDuration(audio.duration);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const markAudioProgress = () => {
|
|
141
|
+
if (!Number.isFinite(audio.currentTime)) return;
|
|
142
|
+
if (audio.currentTime > 0 || audio.currentTime !== lastAudioTimeRef.current) {
|
|
143
|
+
setAudioProgressObserved(true);
|
|
144
|
+
setAudioBlocked(false);
|
|
145
|
+
}
|
|
146
|
+
lastAudioTimeRef.current = audio.currentTime;
|
|
147
|
+
};
|
|
148
|
+
const onEnded = () => setIsPlaying(false);
|
|
149
|
+
const onError = () => {
|
|
150
|
+
setAudioBlocked(true);
|
|
151
|
+
setAudioProgressObserved(false);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
155
|
+
audio.addEventListener("timeupdate", markAudioProgress);
|
|
156
|
+
audio.addEventListener("seeked", markAudioProgress);
|
|
157
|
+
audio.addEventListener("ended", onEnded);
|
|
158
|
+
audio.addEventListener("error", onError);
|
|
159
|
+
if (audio.readyState >= 1) onLoadedMetadata();
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
163
|
+
audio.removeEventListener("timeupdate", markAudioProgress);
|
|
164
|
+
audio.removeEventListener("seeked", markAudioProgress);
|
|
165
|
+
audio.removeEventListener("ended", onEnded);
|
|
166
|
+
audio.removeEventListener("error", onError);
|
|
167
|
+
};
|
|
168
|
+
}, [audioSrc, hasAudioSource]);
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!autoPlay || !hasAudioSource) return;
|
|
172
|
+
void playAudio();
|
|
173
|
+
}, [audioSrc, autoPlay, hasAudioSource, playAudio]);
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const viewport = viewportRef.current;
|
|
177
|
+
if (!viewport || typeof ResizeObserver === "undefined") return;
|
|
178
|
+
const updateViewport = () => {
|
|
179
|
+
const rect = viewport.getBoundingClientRect();
|
|
180
|
+
const nextWidth = Math.round(rect.width);
|
|
181
|
+
const nextHeight = Math.round(rect.height);
|
|
182
|
+
setViewportSize((current) => {
|
|
183
|
+
if (current.width === nextWidth && current.height === nextHeight) return current;
|
|
184
|
+
return { width: nextWidth > 0 ? nextWidth : width, height: nextHeight > 0 ? nextHeight : height };
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
updateViewport();
|
|
188
|
+
const observer = new ResizeObserver(updateViewport);
|
|
189
|
+
observer.observe(viewport);
|
|
190
|
+
return () => observer.disconnect();
|
|
191
|
+
}, [height, width]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (usedAudioClockRef.current && !useAudioClock) {
|
|
195
|
+
currentTimeRef.current = visualTime;
|
|
196
|
+
setCurrentTime(visualTime);
|
|
197
|
+
}
|
|
198
|
+
usedAudioClockRef.current = useAudioClock;
|
|
199
|
+
}, [useAudioClock, visualTime]);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!isPlaying) {
|
|
203
|
+
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
|
|
204
|
+
pauseAudio();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (hasAudioSource) void playAudio();
|
|
208
|
+
|
|
209
|
+
const tick = (timestamp: number) => {
|
|
210
|
+
if (!isPlaying) return;
|
|
211
|
+
if (useAudioClock) {
|
|
212
|
+
const audio = audioRef.current;
|
|
213
|
+
if (audio && Number.isFinite(audio.currentTime)) {
|
|
214
|
+
let nextTime = audio.currentTime;
|
|
215
|
+
if (nextTime >= duration && duration > 0) {
|
|
216
|
+
nextTime = 0;
|
|
217
|
+
audio.currentTime = 0;
|
|
218
|
+
}
|
|
219
|
+
applyTime(nextTime);
|
|
220
|
+
}
|
|
221
|
+
} else if (lastTimestampRef.current !== undefined) {
|
|
222
|
+
const delta = (timestamp - lastTimestampRef.current) / 1000;
|
|
223
|
+
applyTime(advanceScriptPreviewTime(currentTimeRef.current, delta, duration));
|
|
224
|
+
}
|
|
225
|
+
lastTimestampRef.current = timestamp;
|
|
226
|
+
animationFrameRef.current = requestAnimationFrame(tick);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
lastTimestampRef.current = undefined;
|
|
230
|
+
animationFrameRef.current = requestAnimationFrame(tick);
|
|
231
|
+
return () => {
|
|
232
|
+
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
|
|
233
|
+
};
|
|
234
|
+
}, [applyTime, duration, hasAudioSource, isPlaying, pauseAudio, playAudio, useAudioClock]);
|
|
235
|
+
|
|
236
|
+
const startPlayback = useCallback(async () => {
|
|
237
|
+
if (hasAudioSource) await playAudio();
|
|
238
|
+
setIsPlaying(true);
|
|
239
|
+
lastTimestampRef.current = undefined;
|
|
240
|
+
}, [hasAudioSource, playAudio]);
|
|
241
|
+
|
|
242
|
+
const stopPlayback = useCallback(() => {
|
|
243
|
+
setIsPlaying(false);
|
|
244
|
+
lastTimestampRef.current = undefined;
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
const handleSeek = useCallback(
|
|
248
|
+
(nextTime: number) => {
|
|
249
|
+
applyTime(nextTime);
|
|
250
|
+
seekAudio(nextTime);
|
|
251
|
+
lastTimestampRef.current = undefined;
|
|
252
|
+
},
|
|
253
|
+
[applyTime, seekAudio],
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const lastBroadcastRef = useRef<{ currentTime: number; duration: number } | null>(null);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (typeof window === "undefined" || window.parent === window) return;
|
|
259
|
+
const roundedTime = Math.round(currentTime * 10) / 10;
|
|
260
|
+
const roundedDuration = Math.round(duration * 10) / 10;
|
|
261
|
+
const previous = lastBroadcastRef.current;
|
|
262
|
+
if (previous && previous.currentTime === roundedTime && previous.duration === roundedDuration) return;
|
|
263
|
+
lastBroadcastRef.current = { currentTime: roundedTime, duration: roundedDuration };
|
|
264
|
+
window.parent.postMessage({ kind: "papyrus-video-time", currentTime: roundedTime, duration: roundedDuration }, "*");
|
|
265
|
+
}, [currentTime, duration]);
|
|
266
|
+
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
const onMessage = (event: MessageEvent) => {
|
|
269
|
+
const payload = event.data;
|
|
270
|
+
if (!payload || typeof payload !== "object") return;
|
|
271
|
+
if (payload.kind === "papyrus-video-play") {
|
|
272
|
+
void startPlayback();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (payload.kind === "papyrus-video-pause") {
|
|
276
|
+
stopPlayback();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (payload.kind === "papyrus-video-seek" && typeof payload.time === "number") {
|
|
280
|
+
handleSeek(payload.time);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
window.addEventListener("message", onMessage);
|
|
284
|
+
return () => window.removeEventListener("message", onMessage);
|
|
285
|
+
}, [handleSeek, startPlayback, stopPlayback]);
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div style={{ background: "#000", height: "100%", overflow: "hidden", position: "relative", width: "100%" }}>
|
|
289
|
+
{hasAudioSource ? <audio ref={audioRef} preload="auto" src={audioSrc} style={{ display: "none" }} /> : null}
|
|
290
|
+
<div
|
|
291
|
+
ref={viewportRef}
|
|
292
|
+
style={{
|
|
293
|
+
alignItems: "center",
|
|
294
|
+
background: backgroundColor,
|
|
295
|
+
display: "flex",
|
|
296
|
+
inset: 0,
|
|
297
|
+
justifyContent: "center",
|
|
298
|
+
overflow: "hidden",
|
|
299
|
+
position: "absolute",
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
<div
|
|
303
|
+
style={{
|
|
304
|
+
height,
|
|
305
|
+
left: "50%",
|
|
306
|
+
position: "absolute",
|
|
307
|
+
top: "50%",
|
|
308
|
+
transform: `translate(-50%, -50%) scale(${fittedStage.scale})`,
|
|
309
|
+
transformOrigin: "center center",
|
|
310
|
+
width,
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
<RendererProvider
|
|
314
|
+
frame={currentFrame}
|
|
315
|
+
config={{ fps, width, height, durationFrames: Math.max(1, Math.floor(duration * fps)) }}
|
|
316
|
+
>
|
|
317
|
+
<ComposableRenderer script={script} />
|
|
318
|
+
</RendererProvider>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parsePreviewScript(xml: string): ScriptData {
|
|
326
|
+
const result = executeVomXml(xml);
|
|
327
|
+
const composition = Array.isArray(result?.compositions) && result.compositions[0] ? result.compositions[0] : result;
|
|
328
|
+
const script = dslToScriptData(composition, { type: "cue-count", secondsPerCue: 3 });
|
|
329
|
+
|
|
330
|
+
const timeline: unknown[] = [];
|
|
331
|
+
const scenes = script.scenes ?? [];
|
|
332
|
+
let sceneIndex = 0;
|
|
333
|
+
|
|
334
|
+
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
|
335
|
+
for (const element of Array.from(doc.documentElement.children)) {
|
|
336
|
+
if (element.tagName === "scene") {
|
|
337
|
+
const scene = scenes[sceneIndex];
|
|
338
|
+
if (scene) {
|
|
339
|
+
timeline.push(scene);
|
|
340
|
+
sceneIndex += 1;
|
|
341
|
+
}
|
|
342
|
+
} else if (element.tagName === "transition") {
|
|
343
|
+
const previousScene = sceneIndex > 0 ? scenes[sceneIndex - 1] : null;
|
|
344
|
+
const nextScene = sceneIndex < scenes.length ? scenes[sceneIndex] : null;
|
|
345
|
+
const durationAttribute = element.getAttribute("duration");
|
|
346
|
+
const durationFrames = durationAttribute ? parseFloat(durationAttribute.replace("f", "")) : 16;
|
|
347
|
+
const startSec = nextScene ? (nextScene.startSec ?? 0) : 0;
|
|
348
|
+
const propsAttribute = element.getAttribute("props");
|
|
349
|
+
timeline.push({
|
|
350
|
+
kind: "transition",
|
|
351
|
+
id: element.getAttribute("id") || `trans-${sceneIndex}`,
|
|
352
|
+
effect: element.getAttribute("effect") || "push",
|
|
353
|
+
durationFrames,
|
|
354
|
+
ease: element.getAttribute("ease"),
|
|
355
|
+
props: propsAttribute ? JSON.parse(propsAttribute) : undefined,
|
|
356
|
+
startSec,
|
|
357
|
+
endSec: startSec + durationFrames / (script.fps ?? 30),
|
|
358
|
+
fromSceneId: previousScene?.id,
|
|
359
|
+
toSceneId: nextScene?.id,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
(script as { timeline?: unknown[] }).timeline = timeline;
|
|
365
|
+
return script;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function mountPreview(container: HTMLElement, options: PreviewMountOptions): () => void {
|
|
369
|
+
const theme = options.theme ?? "light";
|
|
370
|
+
const script = parsePreviewScript(rethemeXml(options.xml, theme));
|
|
371
|
+
const width = script.meta?.width && script.meta.width > 0 ? script.meta.width : 1280;
|
|
372
|
+
const height = script.meta?.height && script.meta.height > 0 ? script.meta.height : 720;
|
|
373
|
+
const root = (window as unknown as { ReactDOM: { createRoot: (el: HTMLElement) => { render: (node: React.ReactNode) => void; unmount: () => void } } }).ReactDOM.createRoot(container);
|
|
374
|
+
root.render(
|
|
375
|
+
<PreviewPlayer audioSrc={options.audioSrc} autoPlay={options.autoPlay ?? false} height={height} script={script} width={width} />,
|
|
376
|
+
);
|
|
377
|
+
return () => root.unmount();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
(window as unknown as Record<string, unknown>).PapyrusVideo = { registerPreviewRetheme };
|
|
381
|
+
(window as unknown as Record<string, unknown>).mountPapyrusVideoPreview = mountPreview;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type PreviewStageFitInput = {
|
|
2
|
+
containerHeight: number;
|
|
3
|
+
containerWidth: number;
|
|
4
|
+
stageHeight: number;
|
|
5
|
+
stageWidth: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type PreviewStageFit = {
|
|
9
|
+
height: number;
|
|
10
|
+
scale: number;
|
|
11
|
+
width: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function computePreviewStageFit(input: PreviewStageFitInput): PreviewStageFit {
|
|
15
|
+
const stageWidth = input.stageWidth > 0 ? input.stageWidth : 1280;
|
|
16
|
+
const stageHeight = input.stageHeight > 0 ? input.stageHeight : 720;
|
|
17
|
+
const containerWidth = input.containerWidth > 0 ? input.containerWidth : stageWidth;
|
|
18
|
+
const containerHeight = input.containerHeight > 0 ? input.containerHeight : stageHeight;
|
|
19
|
+
|
|
20
|
+
const scale = Math.min(containerWidth / stageWidth, containerHeight / stageHeight);
|
|
21
|
+
const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
height: stageHeight * safeScale,
|
|
25
|
+
scale: safeScale,
|
|
26
|
+
width: stageWidth * safeScale,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type PreviewPlaybackStateInput = {
|
|
2
|
+
audioBlocked: boolean;
|
|
3
|
+
audioDuration: number | null;
|
|
4
|
+
audioProgressObserved: boolean;
|
|
5
|
+
audioSrc?: string | null;
|
|
6
|
+
currentTime: number;
|
|
7
|
+
isPlaying: boolean;
|
|
8
|
+
scriptDerivedDuration: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type PreviewPlaybackState = {
|
|
12
|
+
audioReady: boolean;
|
|
13
|
+
controlLabel: string;
|
|
14
|
+
duration: number;
|
|
15
|
+
hasAudioSource: boolean;
|
|
16
|
+
useAudioClock: boolean;
|
|
17
|
+
visualTime: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function computePreviewPlaybackState(input: PreviewPlaybackStateInput): PreviewPlaybackState {
|
|
21
|
+
const hasAudioSource = Boolean(input.audioSrc);
|
|
22
|
+
const audioReady =
|
|
23
|
+
input.audioDuration != null && Number.isFinite(input.audioDuration) && input.audioDuration > 0;
|
|
24
|
+
const useAudioClock = hasAudioSource && audioReady && input.audioProgressObserved && !input.audioBlocked;
|
|
25
|
+
const duration = useAudioClock && input.audioDuration != null ? input.audioDuration : input.scriptDerivedDuration;
|
|
26
|
+
const visualTime =
|
|
27
|
+
useAudioClock && duration > 0 && input.scriptDerivedDuration > 0
|
|
28
|
+
? (input.currentTime / duration) * input.scriptDerivedDuration
|
|
29
|
+
: input.currentTime;
|
|
30
|
+
const controlLabel = input.isPlaying ? "Pause" : hasAudioSource && input.audioBlocked ? "Play (audio)" : "Play";
|
|
31
|
+
|
|
32
|
+
return { audioReady, controlLabel, duration, hasAudioSource, useAudioClock, visualTime };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function advanceScriptPreviewTime(currentTime: number, deltaSeconds: number, duration: number): number {
|
|
36
|
+
if (!(duration > 0)) return 0;
|
|
37
|
+
let nextTime = currentTime + Math.max(0, deltaSeconds);
|
|
38
|
+
if (nextTime >= duration) {
|
|
39
|
+
nextTime = 0;
|
|
40
|
+
}
|
|
41
|
+
return Math.max(0, Math.min(nextTime, duration));
|
|
42
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Video preview</title>
|
|
7
|
+
<style>
|
|
8
|
+
html,
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
background: #000;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#preview-root {
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div id="preview-root"></div>
|
|
25
|
+
<script src="./vendor/react.production.min.js"></script>
|
|
26
|
+
<script src="./vendor/react-dom.production.min.js"></script>
|
|
27
|
+
<script src="./preview-bundle.js"></script>
|
|
28
|
+
<script>
|
|
29
|
+
const root = document.getElementById("preview-root");
|
|
30
|
+
const params = new URLSearchParams(window.location.search);
|
|
31
|
+
let cleanup = null;
|
|
32
|
+
|
|
33
|
+
function showMessage(text) {
|
|
34
|
+
root.textContent = text;
|
|
35
|
+
root.style.cssText = "color:#eee;font:14px/1.4 system-ui,sans-serif;padding:24px;";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readPayload(overrides) {
|
|
39
|
+
const target = (overrides && overrides.target) || params.get("target") || "";
|
|
40
|
+
const theme = (overrides && overrides.theme) || params.get("theme") || "light";
|
|
41
|
+
const audioSrc = (overrides && overrides.audio) || params.get("audio") || "";
|
|
42
|
+
const xml = target ? sessionStorage.getItem("papyrus-video-preview:" + target) : null;
|
|
43
|
+
return { theme, xml, audioSrc };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mountPreview(payload) {
|
|
47
|
+
if (!payload || typeof payload.xml !== "string" || !payload.xml.trim()) {
|
|
48
|
+
showMessage("No VideoML DSL available for preview.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof cleanup === "function") {
|
|
52
|
+
cleanup();
|
|
53
|
+
cleanup = null;
|
|
54
|
+
}
|
|
55
|
+
root.textContent = "";
|
|
56
|
+
root.removeAttribute("style");
|
|
57
|
+
cleanup = window.mountPapyrusVideoPreview(root, {
|
|
58
|
+
xml: payload.xml,
|
|
59
|
+
theme: payload.theme || "light",
|
|
60
|
+
audioSrc: payload.audioSrc || undefined,
|
|
61
|
+
autoPlay: false,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
window.addEventListener("message", (event) => {
|
|
66
|
+
const payload = event.data;
|
|
67
|
+
if (payload && payload.kind === "papyrus-video-refresh") mountPreview(readPayload(payload));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
mountPreview(readPayload());
|
|
71
|
+
|
|
72
|
+
if (window.parent !== window) {
|
|
73
|
+
window.parent.postMessage({ kind: "papyrus-video-ready" }, "*");
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import {
|
|
4
|
+
ComposableRenderer,
|
|
5
|
+
RendererProvider,
|
|
6
|
+
interpolate,
|
|
7
|
+
spring,
|
|
8
|
+
useCurrentFrame,
|
|
9
|
+
useVideoConfig,
|
|
10
|
+
} from "@videoml/toolchain/renderer";
|
|
11
|
+
import { getComponent, listComponents, registerComponent } from "@videoml/toolchain/renderer/components/registry";
|
|
12
|
+
|
|
13
|
+
type RenderFrameRequest = {
|
|
14
|
+
script: unknown;
|
|
15
|
+
frame: number;
|
|
16
|
+
config: { fps: number; width: number; height: number; durationFrames: number };
|
|
17
|
+
inputProps?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const root = document.getElementById("root");
|
|
21
|
+
let reactRoot: ReturnType<typeof createRoot> | null = null;
|
|
22
|
+
|
|
23
|
+
const videoApi = {
|
|
24
|
+
registerComponent,
|
|
25
|
+
getComponent,
|
|
26
|
+
listComponents,
|
|
27
|
+
ComposableRenderer,
|
|
28
|
+
RendererProvider,
|
|
29
|
+
useCurrentFrame,
|
|
30
|
+
useVideoConfig,
|
|
31
|
+
interpolate,
|
|
32
|
+
spring,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
(window as unknown as { Babulus: typeof videoApi }).Babulus = videoApi;
|
|
36
|
+
|
|
37
|
+
(window as unknown as { renderFrame: (request: RenderFrameRequest) => Promise<void> }).renderFrame = (request) => {
|
|
38
|
+
if (!root) throw new Error("VideoML render shell has no #root element.");
|
|
39
|
+
reactRoot ??= createRoot(root);
|
|
40
|
+
reactRoot.render(
|
|
41
|
+
<RendererProvider frame={request.frame} config={request.config}>
|
|
42
|
+
<ComposableRenderer script={request.script as never} {...(request.inputProps ?? {})} />
|
|
43
|
+
</RendererProvider>,
|
|
44
|
+
);
|
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, 100));
|
|
46
|
+
};
|
package/src/backend.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defineSiteBackend } from "../amplify/site-backend.js";
|
package/src/backend.ts
ADDED
package/src/sync.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `papyrus-app sync`: write one-line route shims for Papyrus's routes into the
|
|
7
|
+
* publication repo (SPIKE, PPY-82be6c). Output is generated and gitignored;
|
|
8
|
+
* a site-owned file at the same path (no GENERATED header) always wins.
|
|
9
|
+
*/
|
|
10
|
+
const MARKER = "// GENERATED by `papyrus-app sync` from @anthusai/papyrus. Do not edit; add a site-owned file at this path to override.";
|
|
11
|
+
const NEXT_EXPORTS = new Set([
|
|
12
|
+
"generateMetadata", "metadata", "generateStaticParams", "generateViewport", "viewport",
|
|
13
|
+
"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS",
|
|
14
|
+
]);
|
|
15
|
+
const GITIGNORE_BEGIN = "# >>> papyrus-app sync (generated route shims)";
|
|
16
|
+
const GITIGNORE_END = "# <<< papyrus-app sync";
|
|
17
|
+
|
|
18
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const PKG = "@anthusai/papyrus";
|
|
20
|
+
|
|
21
|
+
function isGenerated(file) {
|
|
22
|
+
try {
|
|
23
|
+
return fs.readFileSync(file, "utf8").startsWith(MARKER);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shimFor(route) {
|
|
30
|
+
const spec = `${PKG}/${route.file.replace(/\.(tsx|ts)$/, "")}`;
|
|
31
|
+
const lines = [MARKER];
|
|
32
|
+
const named = route.exports.filter((n) => NEXT_EXPORTS.has(n));
|
|
33
|
+
if (route.hasDefault) lines.push(`export { default${named.length ? ", " + named.join(", ") : ""} } from "${spec}";`);
|
|
34
|
+
else if (named.length) lines.push(`export { ${named.join(", ")} } from "${spec}";`);
|
|
35
|
+
// Route-segment config must be a literal in the route file itself.
|
|
36
|
+
for (const [name, literal] of Object.entries(route.config)) lines.push(`export const ${name} = ${literal};`);
|
|
37
|
+
return lines.join("\n") + "\n";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function middlewareShim(manifest) {
|
|
41
|
+
const matcher = manifest.middleware.matcher ?? '["/((?!_next/static|_next/image|favicon.ico|icon).*)"]';
|
|
42
|
+
return [
|
|
43
|
+
MARKER,
|
|
44
|
+
`export { middleware } from "${PKG}/middleware";`,
|
|
45
|
+
`export const config = { matcher: ${matcher} };`,
|
|
46
|
+
"",
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function sync({ root = process.cwd(), log = console.log } = {}) {
|
|
51
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pkgRoot, "routes.manifest.json"), "utf8"));
|
|
52
|
+
const generated = [];
|
|
53
|
+
const skipped = [];
|
|
54
|
+
const write = (relPath, content) => {
|
|
55
|
+
const dest = path.join(root, relPath);
|
|
56
|
+
if (fs.existsSync(dest) && !isGenerated(dest)) {
|
|
57
|
+
skipped.push(relPath);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
61
|
+
fs.writeFileSync(dest, content);
|
|
62
|
+
generated.push(relPath);
|
|
63
|
+
};
|
|
64
|
+
for (const route of manifest.routes) write(route.file, shimFor(route));
|
|
65
|
+
write("middleware.ts", middlewareShim(manifest));
|
|
66
|
+
|
|
67
|
+
// Remove shims that are no longer in the manifest (e.g. after an upgrade).
|
|
68
|
+
const stateFile = path.join(root, ".papyrus", "generated.json");
|
|
69
|
+
let previous = [];
|
|
70
|
+
try { previous = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch { /* first run */ }
|
|
71
|
+
for (const rel of previous) {
|
|
72
|
+
if (!generated.includes(rel) && isGenerated(path.join(root, rel))) fs.rmSync(path.join(root, rel));
|
|
73
|
+
}
|
|
74
|
+
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
75
|
+
fs.writeFileSync(stateFile, JSON.stringify(generated, null, 2) + "\n");
|
|
76
|
+
|
|
77
|
+
// Keep generated shims out of git.
|
|
78
|
+
const giFile = path.join(root, ".gitignore");
|
|
79
|
+
const gi = fs.existsSync(giFile) ? fs.readFileSync(giFile, "utf8") : "";
|
|
80
|
+
const block = [GITIGNORE_BEGIN, ...generated.map((g) => "/" + g), "/.papyrus/", GITIGNORE_END].join("\n");
|
|
81
|
+
const re = new RegExp(`${GITIGNORE_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GITIGNORE_END}`);
|
|
82
|
+
fs.writeFileSync(giFile, re.test(gi) ? gi.replace(re, block) : `${gi}${gi.endsWith("\n") || !gi ? "" : "\n"}${block}\n`);
|
|
83
|
+
|
|
84
|
+
log(`papyrus-app sync: ${generated.length} shim(s) written, ${skipped.length} site-owned file(s) kept (${PKG}@${manifest.version})`);
|
|
85
|
+
for (const rel of skipped) log(` site-owned: ${rel}`);
|
|
86
|
+
return { generated, skipped };
|
|
87
|
+
}
|