fimo 0.10.0 → 0.10.1-staging.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/assets/skills/fimo-cli/SKILL.md +3 -1
- package/assets/skills/fimo-cli/references/history.md +40 -0
- package/assets/skills/fimo-cli/references/schedules.md +45 -22
- package/assets/skills/fimo-studio/SKILL.md +4 -2
- package/assets/skills/fimo-studio/references/schedules.md +43 -0
- package/assets/skills/fimo-studio/references/version-history.md +9 -9
- package/dist/build/vite/plugins/translations.js +174 -57
- package/dist/cli/bundle.json +1 -1
- package/dist/cli/index.js +36 -14
- package/package.json +1 -1
- package/release.json +2 -2
- package/templates/react-router/package.json +1 -1
|
@@ -5,7 +5,7 @@ description: >-
|
|
|
5
5
|
Load when the user wants to create a new Fimo project, build content
|
|
6
6
|
(schemas, entries, forms, media, labels/locales), deploy a site, manage
|
|
7
7
|
custom domains, configure outbound webhooks and event-driven integrations,
|
|
8
|
-
check site traffic/analytics, invite collaborators, inspect AI credits,
|
|
8
|
+
check site traffic/analytics, inspect or restore project history, invite collaborators, inspect AI credits,
|
|
9
9
|
report a failing or slow Fimo API, rejected documented argument,
|
|
10
10
|
undocumented workaround, missing capability that blocks work, forces a
|
|
11
11
|
fragile or heavily manual workaround, or would materially accelerate a
|
|
@@ -62,6 +62,7 @@ npx fimo@latest clone # interactively choose an existin
|
|
|
62
62
|
cd my-site # then load skills on demand: npx fimo@latest skills get fimo-code
|
|
63
63
|
pnpm dev # run the app locally (use the project's PM: pnpm/npm run/yarn/bun) — your iteration loop
|
|
64
64
|
npx fimo@latest credits # inspect the owning org's AI credit balance (read-only)
|
|
65
|
+
npx fimo@latest history list # inspect this environment's restore points
|
|
65
66
|
npx fimo@latest validate # run all checks + codegen; fails on missing DB labels
|
|
66
67
|
npx fimo@latest deploy # ON-DEMAND: hosted preview / shareable link (runs validate internally)
|
|
67
68
|
npx fimo@latest deploy --publish # ON-DEMAND: publish live (only when the developer says "ship it")
|
|
@@ -92,6 +93,7 @@ Load the matching reference when the user's request fires its trigger. Recipes t
|
|
|
92
93
|
- "Enable provider tools / integrations / API key connection for a provider / agent integration access" → `references/integrations.md`
|
|
93
94
|
- "Firecrawl / scrape a public website / competitor research / agent web research" → `references/integrations.md` **+ load `references/integrations/firecrawl.md`**
|
|
94
95
|
- "Check credits / credit balance / can I keep using AI / billing credits / running low / how many credits are left" → `references/credits.md`
|
|
96
|
+
- "List history / create a restore point / recover content and code / restore a previous version / snapshot usage or retention" → `references/history.md`
|
|
95
97
|
- "Create a branch / feature environment / new env / switch envs / merge envs / diff two envs / env status / what changed on this branch / sync from parent / cut a release / tag a version / go to prod" → `references/branches.md`
|
|
96
98
|
- "List my projects / clone an existing project locally / check out a project / delete this project / unpublish / rename the project / manage projects across the org" → `references/projects.md`
|
|
97
99
|
- "Migrate an existing Webflow / Framer / Squarespace / Wix / Carrd site from only a published URL" → load the `fimo-migration` skill and start with `fimo migrate <url> [dir]` to prepare the agent-ready migration draft
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Project History
|
|
2
|
+
|
|
3
|
+
> **Run via `npx fimo@latest <command>`** — always the latest CLI, no install needed.
|
|
4
|
+
|
|
5
|
+
History is scoped to the selected project environment. A restore point records the environment's CMS content and the matching Git commit so code and content can be recovered together.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
fimo history list
|
|
9
|
+
fimo history show <version-id>
|
|
10
|
+
fimo history create "Before content migration"
|
|
11
|
+
fimo history restore <version-id>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use the global `--env <name>` option to target a different environment. Use `--project <id>` when there is no local `.fimo.settings.json`.
|
|
15
|
+
|
|
16
|
+
## Pagination and usage
|
|
17
|
+
|
|
18
|
+
`fimo history list` returns 20 restore points by default. Use `--limit <1-100>` to change the page size. When another page exists, the output includes the exact next command and its opaque cursor:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
fimo history list --limit 20 --cursor <cursor>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The list also reports compressed snapshot usage and the current retention period. Restore points expire after 14 days on Free and pay-as-you-go plans, or 90 days on Premium. The expiry is fixed when each point is created.
|
|
25
|
+
|
|
26
|
+
## Restore safety
|
|
27
|
+
|
|
28
|
+
Before replacing code and content, Fimo creates an automatic `restore-safety` point from the environment's current state. Restoring that point is the fast recovery path when the selected result is not what the user expected.
|
|
29
|
+
|
|
30
|
+
Pretty mode asks for confirmation. Agents and other non-interactive callers must pass both `--json` and `--yes`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
fimo --json history restore <version-id> --yes
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If another history create, update, delete, or restore is already running, the command fails with `HISTORY_BUSY`. Retry after the active history operation finishes. Do not loop without a delay.
|
|
37
|
+
|
|
38
|
+
If content or Git restore does not finish cleanly, the command fails with `RESTORE_INCOMPLETE` and names the automatic safety point. Stop active editing, inspect the environment, and retry the restore. Collaborative-editor resync continues in the background after code and content are ready.
|
|
39
|
+
|
|
40
|
+
Normal CMS edits, Git pushes, and unsaved collaborative-editor changes do not take the history-operation lock. Avoid starting a restore while someone is actively editing the same environment.
|
|
@@ -1,48 +1,71 @@
|
|
|
1
1
|
# Scheduled Commands
|
|
2
2
|
|
|
3
|
-
> **Run via `npx fimo@latest <command>`**
|
|
3
|
+
> **Run via `npx fimo@latest <command>`** - always the latest CLI, no install needed.
|
|
4
4
|
|
|
5
|
-
`fimo schedules` manages recurring commands for
|
|
5
|
+
`fimo schedules` manages recurring commands for one project environment. It uses the current checkout environment by default, or the environment passed with `--env <name>`.
|
|
6
|
+
|
|
7
|
+
The command starts in that environment's sandbox. Commands can still target another environment explicitly, for example with `fimo schemas list --env preview`.
|
|
6
8
|
|
|
7
9
|
## Commands
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
|
-
fimo schedules create <name> --cron "0 2 * * *" --do "fimo
|
|
11
|
-
fimo schedules create
|
|
12
|
+
fimo schedules create <name> --cron "0 2 * * *" --do "fimo validate"
|
|
13
|
+
fimo schedules create <name> --cron "0 2 * * *" --do "pnpm sync" --secret CMS_TOKEN --secret SEARCH_KEY
|
|
12
14
|
fimo schedules list
|
|
13
|
-
fimo schedules
|
|
14
|
-
fimo schedules
|
|
15
|
+
fimo schedules show <schedule>
|
|
16
|
+
fimo schedules update <schedule> --cron "0 3 * * *" --secret CMS_TOKEN
|
|
17
|
+
fimo schedules update <schedule> --clear-secrets
|
|
18
|
+
fimo schedules pause <schedule>
|
|
19
|
+
fimo schedules resume <schedule>
|
|
20
|
+
fimo schedules run <schedule>
|
|
21
|
+
fimo schedules run <schedule> --no-wait
|
|
22
|
+
fimo schedules runs <schedule> --limit 25 --status failed
|
|
23
|
+
fimo schedules runs <schedule> --all
|
|
24
|
+
fimo schedules runs show <run-id>
|
|
25
|
+
fimo schedules delete <schedule>
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
|
|
28
|
+
`<schedule>` accepts a schedule ID or an exact schedule name. Name-based commands resolve the name in the active environment and then use the stable schedule ID.
|
|
29
|
+
|
|
30
|
+
## Runtime model
|
|
31
|
+
|
|
32
|
+
Scheduled commands run in the project's environment runtime, not on the user's machine. Fimo prepares the environment and executes the stored command from the project root.
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
`--do` can be any command that works with schedule runtime permissions, such as `fimo validate`, `pnpm test`, or a project script. The runtime can edit project content and publish or unpublish the project, but it cannot manage project access, secrets, schedules, or other project-level settings.
|
|
20
35
|
|
|
21
|
-
|
|
36
|
+
Runs use at-least-once delivery. If Fimo loses a completion response, the same run may execute again. Side-effecting commands should use `FIMO_SCHEDULE_RUN_ID` as their idempotency key.
|
|
22
37
|
|
|
23
38
|
Cron expressions use standard five-field syntax. Schedules must not run more often than every five minutes.
|
|
24
39
|
|
|
25
|
-
##
|
|
40
|
+
## Project secrets
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
Repeat `--secret KEY` to expose selected project secrets to a scheduled command. Creating a schedule without `--secret` exposes no project secrets. Updating with one or more `--secret` flags replaces the selected key names. Use `--clear-secrets` to remove every selected key; it cannot be combined with `--secret`.
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
fimo schedules create nightly --cron "0 2 * * *" --do "fimo publish"
|
|
31
|
-
```
|
|
44
|
+
Schedule and run output shows secret key names and snapshot state only. It never returns secret values.
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
## Runs
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
`fimo schedules run` queues a manual run, prints its run ID, and waits for the run to finish. Use `--no-wait` to return as soon as it is queued.
|
|
49
|
+
|
|
50
|
+
Only one run for a schedule can be active at a time. If a run is already queued or running, the command returns `run_in_progress` with the active run ID.
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
Run history is cursor-paginated:
|
|
40
53
|
|
|
41
54
|
```bash
|
|
42
|
-
fimo schedules
|
|
55
|
+
fimo schedules runs nightly --limit 50
|
|
56
|
+
fimo schedules runs nightly --cursor <cursor>
|
|
57
|
+
fimo schedules runs nightly --status succeeded
|
|
58
|
+
fimo schedules runs nightly --all
|
|
43
59
|
```
|
|
44
60
|
|
|
61
|
+
Run detail includes status, timing, trigger, exit/error information, retained output, output retention metadata, and secret snapshot metadata. Output is capped and redacted before it is returned.
|
|
62
|
+
|
|
63
|
+
## JSON
|
|
64
|
+
|
|
65
|
+
All schedule commands support the CLI's global `--json` mode. List and run-history responses preserve the API page shape, including `items`, `nextCursor`, `usage`, and `policy`.
|
|
66
|
+
|
|
45
67
|
## Notes
|
|
46
68
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
69
|
+
- Every operation is scoped to the active environment unless `--env` is passed.
|
|
70
|
+
- Pausing stops future cron runs. It does not delete history.
|
|
71
|
+
- A failed or skipped manual run exits non-zero with its run ID and an exact `runs show` command for inspecting detail.
|
|
@@ -6,8 +6,8 @@ description: >-
|
|
|
6
6
|
Triggers: "in Fimo Studio", "in the dashboard", "in the web app",
|
|
7
7
|
"how do I X in the UI", or screenshots of the dashboard. Topics
|
|
8
8
|
covered: dashboard content and CMS, publishing from the dashboard,
|
|
9
|
-
dashboard SEO/domain settings,
|
|
10
|
-
history UI, dashboard sharing/billing, and dashboard chat. Never describe
|
|
9
|
+
dashboard SEO/domain settings, schedules and run history, outbound webhooks,
|
|
10
|
+
visual editing, version history UI, dashboard sharing/billing, and dashboard chat. Never describe
|
|
11
11
|
UI locations, buttons, or panels from memory — always read the relevant
|
|
12
12
|
reference file first.
|
|
13
13
|
---
|
|
@@ -37,6 +37,7 @@ Read the relevant reference file when the user's question matches a topic.
|
|
|
37
37
|
| AI Chat | references/ai-chat.md | "What can the AI do?", "How do I give context?" |
|
|
38
38
|
| Code Editor | references/code-editor.md | "How do I edit code?", "Where are my files?" |
|
|
39
39
|
| SEO Settings | references/seo-settings.md | "How do I set up SEO?", "Favicon, title, share image" |
|
|
40
|
+
| Schedules | references/schedules.md | "Recurring commands", "Run history", "Run now" |
|
|
40
41
|
| Webhooks | references/webhooks.md | "React to forms", "Send events to another service" |
|
|
41
42
|
| Preview & Responsive | references/preview-and-responsive.md | "How does it look on mobile?", "How do I test sizes?" |
|
|
42
43
|
| Visual Editing | references/visual-editing.md | "Can I edit visually?", "How do I change colors?" |
|
|
@@ -55,6 +56,7 @@ Read the relevant reference file when the user's question matches a topic.
|
|
|
55
56
|
| Adding images | Media Library, AI Chat, Edit Mode |
|
|
56
57
|
| Content/CMS | AI Chat, Media Library |
|
|
57
58
|
| Webhooks | Project secrets, Forms, Publishing, Agents |
|
|
59
|
+
| Schedules | Project secrets, Environments, Agents |
|
|
58
60
|
| Visual changes | Edit Mode (quick tweaks), AI Chat (bigger changes), Code Editor (full control) |
|
|
59
61
|
| Something went wrong | Version History, AI Chat |
|
|
60
62
|
| Working with others | Sharing, Collaboration |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Schedules
|
|
2
|
+
|
|
3
|
+
Schedules run recurring project commands in one environment. Use the Studio surface to review configuration, pause or resume automatic runs, start work now, and inspect what happened afterward.
|
|
4
|
+
|
|
5
|
+
## Open schedules
|
|
6
|
+
|
|
7
|
+
1. Open the project and choose **Settings**.
|
|
8
|
+
2. Select **Schedules**.
|
|
9
|
+
3. Choose the environment whose schedules you want to manage.
|
|
10
|
+
|
|
11
|
+
The list shows each schedule's name, cron, command, enabled state, and latest status. Usage and retention policy are shown for the selected environment.
|
|
12
|
+
|
|
13
|
+
## Create or edit a schedule
|
|
14
|
+
|
|
15
|
+
Choose **New schedule** to set:
|
|
16
|
+
|
|
17
|
+
- A human-readable name
|
|
18
|
+
- A standard five-field cron expression
|
|
19
|
+
- The command to run from the project root
|
|
20
|
+
- The project secret keys the command is allowed to use
|
|
21
|
+
|
|
22
|
+
A schedule with no selected secret keys receives no project secrets. Studio displays key names only and never reveals secret values.
|
|
23
|
+
|
|
24
|
+
Open a schedule to change its configuration. Use **Pause** to prevent future cron runs without deleting its history, and **Resume** to make the cron active again.
|
|
25
|
+
|
|
26
|
+
## Run now
|
|
27
|
+
|
|
28
|
+
Choose **Run now** from a schedule to queue a manual run. Only one run can be active for a schedule at a time. If one is already queued or running, Studio links to the active run instead of starting a duplicate.
|
|
29
|
+
|
|
30
|
+
Runs use at-least-once delivery. If Fimo loses a completion response, the same run may execute again. Side-effecting commands should use `FIMO_SCHEDULE_RUN_ID` as their idempotency key.
|
|
31
|
+
|
|
32
|
+
## Inspect history
|
|
33
|
+
|
|
34
|
+
Open the **Runs** tab on a schedule to browse cursor-paginated history. Filter by queued, running, succeeded, failed, or skipped status.
|
|
35
|
+
|
|
36
|
+
Select a run to see:
|
|
37
|
+
|
|
38
|
+
- Status, trigger, actor, and queued/start/finish times
|
|
39
|
+
- Exit code, stable error code, and error message
|
|
40
|
+
- Redacted captured output, its size, truncation state, and expiry
|
|
41
|
+
- Secret snapshot state and key metadata
|
|
42
|
+
|
|
43
|
+
Secret snapshot metadata helps explain whether a selected key was resolved, missing, deleted, recreated, or rotated after the run. It never contains a secret value or hash.
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
## Where to Find Version History
|
|
2
2
|
|
|
3
|
-
Click your **project name** in the top
|
|
3
|
+
Click your **project name** in the top bar to open the project dropdown menu, then select **History**. This opens the version history panel showing all your saved versions.
|
|
4
4
|
|
|
5
5
|
## How Versions Are Created
|
|
6
6
|
|
|
7
|
-
Versions are created automatically as you work — you don't create them by hand. Whenever the AI makes meaningful changes to your project, it captures a
|
|
7
|
+
Versions are created automatically as you work — you don't create them by hand. Whenever the AI makes meaningful changes to your project, it captures a restore point for the current environment that you can browse, rename, and restore. A restore point includes the project code and CMS data, including schemas, entries, translations, and forms. There is no "create version" button in the panel; the history list only shows the restore points that already exist.
|
|
8
8
|
|
|
9
9
|
## Browsing Versions
|
|
10
10
|
|
|
11
11
|
The version list shows each version's name and timestamp (the relative time, with the full date on hover).
|
|
12
12
|
|
|
13
|
-
- The most recent version shows a **"Current"** badge.
|
|
14
|
-
|
|
15
13
|
## Restoring a Previous Version
|
|
16
14
|
|
|
17
15
|
1. Find the version you want to restore.
|
|
18
16
|
2. Click the **three-dot menu** next to the version and select **Restore**.
|
|
19
|
-
3. A confirmation dialog
|
|
17
|
+
3. A confirmation dialog explains that this will replace the current environment and save its current state as a safety point first.
|
|
20
18
|
4. Click **Restore** to confirm.
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
The current environment's code and CMS data revert to that restore point. Uploaded assets do not change. If you need to undo the restore, restore the automatically created safety point.
|
|
23
21
|
|
|
24
22
|
## Editing Version Details
|
|
25
23
|
|
|
@@ -28,12 +26,14 @@ Find the version you want to update, click the **three-dot menu**, select **Edit
|
|
|
28
26
|
## Key Concepts
|
|
29
27
|
|
|
30
28
|
- **Automatic versions** -- The AI creates these whenever it makes significant changes to your project. You do not need to do anything for these to appear.
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
29
|
+
- **Studio is browse and restore** -- Studio does not create restore points manually. Developers can use `fimo history create "Name"` from the CLI.
|
|
30
|
+
- **Environment-scoped history** -- Each environment has its own restore points. Restoring one environment does not change another.
|
|
31
|
+
- **Safe restore** -- Fimo records the current code and CMS data as a safety point before replacing them.
|
|
32
|
+
- **Assets stay in place** -- Restoring changes code and CMS data, but does not remove or roll back uploaded assets.
|
|
33
33
|
|
|
34
34
|
## Quick Restore from Chat
|
|
35
35
|
|
|
36
|
-
Each AI message in the chat that made changes to your project shows an **undo button** (↩ icon) on the right side. Click it and a **"Restore version?"** prompt appears; confirm it to restore
|
|
36
|
+
Each AI message in the chat that made changes to your project shows an **undo button** (↩ icon) on the right side. Click it and a **"Restore version?"** prompt appears; confirm it to restore the current environment to the state it was in at that message. Fimo saves a safety point first, so CMS work completed after that message remains recoverable. This is the fastest way to undo a recent change without opening the full version history panel.
|
|
37
37
|
|
|
38
38
|
The undo button is disabled on the most recent change (you can't restore the version you're already on) and is only available to members with permission to manage history.
|
|
39
39
|
|
|
@@ -7,15 +7,24 @@ import { getFimoDefaultLocale, getFimoLocales } from '../../../runtime/i18n/loca
|
|
|
7
7
|
import { getConfigServer } from '../../../runtime/shared/fimo-config.server.js';
|
|
8
8
|
const VIRTUAL_ID = 'virtual:translations';
|
|
9
9
|
const RESOLVED_ID = '\0' + VIRTUAL_ID;
|
|
10
|
+
const EVENT_RECONNECT_DELAY_MS = 2000;
|
|
11
|
+
const FALLBACK_POLL_INTERVAL_MS = 30_000;
|
|
10
12
|
export default function translationsPlugin() {
|
|
11
13
|
let rootDir = process.cwd();
|
|
12
|
-
let bundle = {
|
|
14
|
+
let bundle = {
|
|
15
|
+
dictionaries: { en: {} },
|
|
16
|
+
defaultLocale: 'en',
|
|
17
|
+
locales: ['en'],
|
|
18
|
+
updatedAt: null,
|
|
19
|
+
};
|
|
13
20
|
let command = 'build';
|
|
14
21
|
let devServer = null;
|
|
15
22
|
let eventSockets = [];
|
|
16
23
|
let reconnectTimer;
|
|
17
24
|
let refreshTimer;
|
|
18
25
|
let pollTimer;
|
|
26
|
+
let eventConnectionAttempt = 0;
|
|
27
|
+
let closed = false;
|
|
19
28
|
const refreshLabels = async () => {
|
|
20
29
|
bundle = await loadLabelBundle(rootDir);
|
|
21
30
|
return bundle;
|
|
@@ -51,13 +60,91 @@ export default function translationsPlugin() {
|
|
|
51
60
|
});
|
|
52
61
|
};
|
|
53
62
|
const scheduleRefresh = () => {
|
|
63
|
+
if (closed) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
54
66
|
if (refreshTimer) {
|
|
55
67
|
clearTimeout(refreshTimer);
|
|
56
68
|
}
|
|
57
69
|
refreshTimer = setTimeout(() => {
|
|
70
|
+
refreshTimer = undefined;
|
|
71
|
+
if (closed) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
58
74
|
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
59
75
|
}, 150);
|
|
60
76
|
};
|
|
77
|
+
const startFallbackPolling = () => {
|
|
78
|
+
if (closed || pollTimer) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
pollTimer = setInterval(() => {
|
|
82
|
+
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
83
|
+
}, FALLBACK_POLL_INTERVAL_MS);
|
|
84
|
+
};
|
|
85
|
+
const stopFallbackPolling = () => {
|
|
86
|
+
if (!pollTimer) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
clearInterval(pollTimer);
|
|
90
|
+
pollTimer = undefined;
|
|
91
|
+
};
|
|
92
|
+
const closeEventSockets = () => {
|
|
93
|
+
closeSockets(eventSockets.splice(0));
|
|
94
|
+
};
|
|
95
|
+
const scheduleReconnect = (delayMs = EVENT_RECONNECT_DELAY_MS) => {
|
|
96
|
+
startFallbackPolling();
|
|
97
|
+
if (closed || reconnectTimer) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
reconnectTimer = setTimeout(() => {
|
|
101
|
+
reconnectTimer = undefined;
|
|
102
|
+
closeEventSockets();
|
|
103
|
+
void connectEvents();
|
|
104
|
+
}, delayMs);
|
|
105
|
+
};
|
|
106
|
+
const connectEvents = async () => {
|
|
107
|
+
if (closed) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const attempt = ++eventConnectionAttempt;
|
|
111
|
+
const sockets = [];
|
|
112
|
+
eventSockets = sockets;
|
|
113
|
+
try {
|
|
114
|
+
await connectLabelEvents(rootDir, scheduleRefresh, sockets, {
|
|
115
|
+
isActive: () => !closed && attempt === eventConnectionAttempt,
|
|
116
|
+
onConnected: () => {
|
|
117
|
+
if (closed || attempt !== eventConnectionAttempt) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (reconnectTimer) {
|
|
121
|
+
clearTimeout(reconnectTimer);
|
|
122
|
+
reconnectTimer = undefined;
|
|
123
|
+
}
|
|
124
|
+
stopFallbackPolling();
|
|
125
|
+
},
|
|
126
|
+
onDisconnected: () => {
|
|
127
|
+
if (closed || attempt !== eventConnectionAttempt) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
scheduleReconnect();
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
if (closed || attempt !== eventConnectionAttempt) {
|
|
134
|
+
closeSockets(sockets.splice(0));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (sockets.length === 0) {
|
|
138
|
+
scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
if (!closed && attempt === eventConnectionAttempt) {
|
|
143
|
+
closeEventSockets();
|
|
144
|
+
scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
61
148
|
return {
|
|
62
149
|
name: 'fimo-virtual-translations',
|
|
63
150
|
enforce: 'pre',
|
|
@@ -76,16 +163,11 @@ export default function translationsPlugin() {
|
|
|
76
163
|
},
|
|
77
164
|
configureServer(server) {
|
|
78
165
|
devServer = server;
|
|
166
|
+
closed = false;
|
|
79
167
|
server.watcher.add([join(rootDir, 'fimo-config.json'), join(rootDir, '.env'), join(rootDir, '.env.local')]);
|
|
80
168
|
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
eventSockets = sockets;
|
|
84
|
-
})
|
|
85
|
-
.catch(() => undefined);
|
|
86
|
-
pollTimer = setInterval(() => {
|
|
87
|
-
void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
|
|
88
|
-
}, 2000);
|
|
169
|
+
startFallbackPolling();
|
|
170
|
+
void connectEvents();
|
|
89
171
|
},
|
|
90
172
|
resolveId(id) {
|
|
91
173
|
if (id === VIRTUAL_ID) {
|
|
@@ -128,84 +210,119 @@ export default function translationsPlugin() {
|
|
|
128
210
|
return [];
|
|
129
211
|
},
|
|
130
212
|
closeBundle() {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
213
|
+
closed = true;
|
|
214
|
+
eventConnectionAttempt += 1;
|
|
215
|
+
closeEventSockets();
|
|
216
|
+
devServer = null;
|
|
135
217
|
if (reconnectTimer) {
|
|
136
218
|
clearTimeout(reconnectTimer);
|
|
219
|
+
reconnectTimer = undefined;
|
|
137
220
|
}
|
|
138
221
|
if (refreshTimer) {
|
|
139
222
|
clearTimeout(refreshTimer);
|
|
223
|
+
refreshTimer = undefined;
|
|
140
224
|
}
|
|
141
|
-
|
|
142
|
-
clearInterval(pollTimer);
|
|
143
|
-
}
|
|
225
|
+
stopFallbackPolling();
|
|
144
226
|
},
|
|
145
227
|
};
|
|
146
|
-
async function connectLabelEvents(root, onLabelsChanged) {
|
|
228
|
+
async function connectLabelEvents(root, onLabelsChanged, sockets, callbacks) {
|
|
147
229
|
const env = await readProjectEnv(root);
|
|
230
|
+
if (!callbacks.isActive()) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
148
233
|
const apiUrl = env.VITE_API_URL;
|
|
149
234
|
if (!apiUrl) {
|
|
150
|
-
return
|
|
235
|
+
return;
|
|
151
236
|
}
|
|
152
237
|
const runtimeConfig = await loadRuntimeConfig(apiUrl);
|
|
238
|
+
if (!callbacks.isActive()) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
153
241
|
const eventsUrl = runtimeConfig.eventsServerUrl ?? undefined;
|
|
154
242
|
if (!eventsUrl) {
|
|
155
|
-
return
|
|
243
|
+
return;
|
|
156
244
|
}
|
|
157
245
|
const settings = await readProjectSettings(root);
|
|
246
|
+
if (!callbacks.isActive()) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
158
249
|
if (!settings.projectId) {
|
|
159
|
-
return
|
|
250
|
+
return;
|
|
160
251
|
}
|
|
161
252
|
const WebSocketCtor = globalThis.WebSocket;
|
|
162
253
|
if (!WebSocketCtor) {
|
|
163
|
-
return
|
|
254
|
+
return;
|
|
164
255
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const raw = typeof event.data === 'string' ? event.data : '';
|
|
173
|
-
if (!raw) {
|
|
256
|
+
const rooms = deriveEventsRooms(settings.projectId, apiUrl);
|
|
257
|
+
const openSockets = new Set();
|
|
258
|
+
try {
|
|
259
|
+
for (const room of rooms) {
|
|
260
|
+
const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
|
|
261
|
+
if (!callbacks.isActive()) {
|
|
262
|
+
closeSockets(sockets.splice(0));
|
|
174
263
|
return;
|
|
175
264
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
265
|
+
const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
|
|
266
|
+
wsUrl.searchParams.set('authToken', token);
|
|
267
|
+
const socket = new WebSocketCtor(wsUrl.toString());
|
|
268
|
+
socket.onopen = () => {
|
|
269
|
+
if (!callbacks.isActive()) {
|
|
180
270
|
return;
|
|
181
271
|
}
|
|
182
|
-
|
|
272
|
+
openSockets.add(socket);
|
|
273
|
+
if (openSockets.size === rooms.length) {
|
|
274
|
+
callbacks.onConnected();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
socket.onmessage = (event) => {
|
|
278
|
+
if (!callbacks.isActive()) {
|
|
183
279
|
return;
|
|
184
280
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
|
|
189
|
-
onLabelsChanged();
|
|
281
|
+
const raw = typeof event.data === 'string' ? event.data : '';
|
|
282
|
+
if (!raw) {
|
|
283
|
+
return;
|
|
190
284
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
285
|
+
try {
|
|
286
|
+
const message = JSON.parse(raw);
|
|
287
|
+
if (message.type === 'content:changed') {
|
|
288
|
+
emitContentUpdate(parseContentChangePayload(message.payload));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (message.type !== 'labels:changed') {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const changedLocales = message.payload?.changes
|
|
295
|
+
?.map((change) => change.locale)
|
|
296
|
+
.filter((locale) => typeof locale === 'string') ?? [];
|
|
297
|
+
if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
|
|
298
|
+
onLabelsChanged();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
socket.onclose = () => {
|
|
306
|
+
openSockets.delete(socket);
|
|
307
|
+
callbacks.onDisconnected();
|
|
308
|
+
};
|
|
309
|
+
socket.onerror = callbacks.onDisconnected;
|
|
310
|
+
sockets.push(socket);
|
|
311
|
+
}
|
|
207
312
|
}
|
|
208
|
-
|
|
313
|
+
catch (error) {
|
|
314
|
+
closeSockets(sockets.splice(0));
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function closeSockets(sockets) {
|
|
320
|
+
for (const socket of sockets) {
|
|
321
|
+
socket.onopen = null;
|
|
322
|
+
socket.onmessage = null;
|
|
323
|
+
socket.onclose = null;
|
|
324
|
+
socket.onerror = null;
|
|
325
|
+
socket.close();
|
|
209
326
|
}
|
|
210
327
|
}
|
|
211
328
|
function parseContentChangePayload(payload) {
|