flare-plugin-sdk 0.1.0-beta.6 → 0.1.0-beta.8
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 +53 -20
- package/bin/cli.mjs +7 -3
- package/package.json +2 -1
- package/skill/flare-plugin/README.md +8 -5
- package/skill/flare-plugin/SKILL.md +7 -2
- package/skill/flare-plugin/assets/github-template/README.md +35 -18
- package/skill/flare-plugin/assets/github-template/_gitignore +5 -0
- package/skill/flare-plugin/assets/github-template/build.mjs +76 -0
- package/skill/flare-plugin/assets/github-template/package.json +16 -0
- package/skill/flare-plugin/assets/github-template/plugin.js +109 -103
- package/skill/flare-plugin/assets/github-template/src/plugin.ts +118 -0
- package/skill/flare-plugin/assets/github-template/tsconfig.json +14 -0
- package/skill/flare-plugin/references/publishing.md +17 -3
- package/skill/flare-plugin/references/testing.md +17 -10
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ With Node 22 or newer, run:
|
|
|
13
13
|
```sh
|
|
14
14
|
npx flare-plugin-sdk@next init my-plugin
|
|
15
15
|
cd my-plugin
|
|
16
|
+
npm install
|
|
16
17
|
```
|
|
17
18
|
|
|
18
19
|
This copies the GitHub template bundled with that SDK version into `my-plugin`:
|
|
@@ -22,6 +23,11 @@ my-plugin/
|
|
|
22
23
|
manifest.json
|
|
23
24
|
plugin.js
|
|
24
25
|
assets/icon.png
|
|
26
|
+
src/plugin.ts
|
|
27
|
+
package.json
|
|
28
|
+
tsconfig.json
|
|
29
|
+
build.mjs
|
|
30
|
+
.gitignore
|
|
25
31
|
README.md
|
|
26
32
|
```
|
|
27
33
|
|
|
@@ -38,11 +44,15 @@ used to create it. Open it in VS Code or another JSON Schema editor for property
|
|
|
38
44
|
completion, descriptions, enum choices, and static validation. The complete schema
|
|
39
45
|
is bundled at `skill/flare-plugin/manifest.schema.json` inside the SDK.
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
The root already contains an installable, prebuilt `plugin.js`. Install development
|
|
48
|
+
dependencies to edit and rebuild the TypeScript source. The private project pins
|
|
49
|
+
the SDK to the version used by `init`; TypeScript and esbuild are development tools.
|
|
50
|
+
|
|
51
|
+
Edit `src/plugin.ts` to use your website's real authentication, endpoints, response
|
|
42
52
|
mapping, and pagination. The starter's `/api/me` and `/api/posts` are illustrative;
|
|
43
53
|
no demonstration server is included. Update the description, login methods and
|
|
44
|
-
capabilities in `manifest.json`, and replace the placeholder icon.
|
|
45
|
-
|
|
54
|
+
capabilities in `manifest.json`, and replace the placeholder icon. Keep `plugin.js`
|
|
55
|
+
as generated output; edit the TS source and rebuild instead of editing both copies.
|
|
46
56
|
|
|
47
57
|
For manual template copies, replace the example IDs and names before use. For
|
|
48
58
|
`npx` projects, keep the generated IDs. Read the
|
|
@@ -51,13 +61,16 @@ for authentication, Host APIs, capabilities, and Wire data mapping.
|
|
|
51
61
|
|
|
52
62
|
## Develop and debug in Flare
|
|
53
63
|
|
|
54
|
-
1. From `my-plugin`,
|
|
64
|
+
1. From `my-plugin`, run:
|
|
55
65
|
|
|
56
66
|
```sh
|
|
57
|
-
|
|
67
|
+
npm run check
|
|
68
|
+
npm run build
|
|
58
69
|
node -e 'JSON.parse(require("node:fs").readFileSync("manifest.json", "utf8")); console.log("Manifest JSON parses")'
|
|
59
70
|
```
|
|
60
71
|
|
|
72
|
+
`check` uses the SDK's ambient types with strict TypeScript checking. `build`
|
|
73
|
+
checks types and bundles local TS modules into one ES2020 IIFE at root.
|
|
61
74
|
Actual execution and manifest/registration validation happen in Flare, which
|
|
62
75
|
provides `definePlugin` and `flare`.
|
|
63
76
|
2. In Compose Desktop or native macOS, open **Settings → Plugins → More → Plugin
|
|
@@ -67,8 +80,11 @@ for authentication, Host APIs, capabilities, and Wire data mapping.
|
|
|
67
80
|
website's HTTPS API origin, and import a real access token using the starter's
|
|
68
81
|
login form. Adapt this step if you change the login method. Test login,
|
|
69
82
|
timeline refresh, older pages, end-of-list behavior, and website link clicks.
|
|
70
|
-
4.
|
|
71
|
-
|
|
83
|
+
4. Run `npm run dev` in a terminal, then edit `src/plugin.ts` or its local modules.
|
|
84
|
+
Watch mode rebuilds `plugin.js`; syntax/build errors preserve the previous
|
|
85
|
+
script. It transpiles without type checking, so use editor diagnostics or run
|
|
86
|
+
`npm run check` separately. Flare automatically validates runtime-file changes;
|
|
87
|
+
click **Reload plugin** to apply them. Invalid edits keep the last working
|
|
72
88
|
version running. The development panel shows validation errors and recent-call
|
|
73
89
|
diagnostics; recent calls contain method names and outcomes, without HTTP
|
|
74
90
|
request/response payloads.
|
|
@@ -78,8 +94,8 @@ for authentication, Host APIs, capabilities, and Wire data mapping.
|
|
|
78
94
|
Development uses real accounts and data, and remote writes are not rolled back.
|
|
79
95
|
|
|
80
96
|
Finish active writes before reloading; credential-schema changes may require
|
|
81
|
-
logging in again.
|
|
82
|
-
|
|
97
|
+
logging in again. Keep Flare pointed at the project root, where the build writes
|
|
98
|
+
`plugin.js`. Flare loads that classic script and does not execute TS or build tools.
|
|
83
99
|
See [development and testing details](https://github.com/DimensionDev/Flare/blob/feature/plugin/feature/plugin/sdk/skill/flare-plugin/references/testing.md#develop-locally-in-desktop).
|
|
84
100
|
|
|
85
101
|
## Publish and install through GitHub (recommended)
|
|
@@ -89,12 +105,15 @@ See [development and testing details](https://github.com/DimensionDev/Flare/blob
|
|
|
89
105
|
and optional `locales/*.json`. Update the project README with the actual website,
|
|
90
106
|
login instructions, supported features, and verification results. Flare reads
|
|
91
107
|
these files directly; commit any compiled output at the root before tagging.
|
|
92
|
-
2.
|
|
108
|
+
2. Run `npm run build`, then commit both source and the resulting root `plugin.js`.
|
|
109
|
+
The generated `.gitignore` excludes dependencies, temporary files and `.fpp`
|
|
110
|
+
archives, while retaining `plugin.js`. Commit the `package-lock.json` created
|
|
111
|
+
by `npm install`; use `npm ci` on later clean checkouts. For a new project and an empty GitHub
|
|
93
112
|
repository, replace `OWNER/REPO` and run from `my-plugin`:
|
|
94
113
|
|
|
95
114
|
```sh
|
|
96
115
|
git init
|
|
97
|
-
git add manifest.json plugin.js assets README.md
|
|
116
|
+
git add manifest.json plugin.js assets src package.json package-lock.json tsconfig.json build.mjs .gitignore README.md
|
|
98
117
|
# If used, also stage translations with: git add locales
|
|
99
118
|
git commit -m "Add Flare plugin"
|
|
100
119
|
git branch -M main
|
|
@@ -130,23 +149,33 @@ See [GitHub publication and update rules](https://github.com/DimensionDev/Flare/
|
|
|
130
149
|
## Optional: package and install a local file
|
|
131
150
|
|
|
132
151
|
Use `.fpp` for offline transfer or local testing when GitHub distribution is not
|
|
133
|
-
needed. From
|
|
134
|
-
`manifest.json`, `plugin.js`, one `assets/icon.<extension>`, and any `locales/*.json`
|
|
135
|
-
into it, preserving those paths. Keep source, `.git`, `package.json`, `node_modules`,
|
|
136
|
-
and previous archives outside `dist`. Then run from the project root:
|
|
152
|
+
needed. From the generated project root:
|
|
137
153
|
|
|
138
154
|
```sh
|
|
139
|
-
|
|
155
|
+
npm run package
|
|
140
156
|
```
|
|
141
157
|
|
|
158
|
+
This checks types, rebuilds `plugin.js`, stages only manifest/script/icon/locale
|
|
159
|
+
files in a temporary directory, reuses the SDK packer, and writes `plugin.fpp` in
|
|
160
|
+
the project root. Temporary staging is cleaned up. Source and development files
|
|
161
|
+
are excluded. The standalone `flare-plugin-pack` command still requires a clean
|
|
162
|
+
runtime directory; passing the entire TS project is not supported.
|
|
163
|
+
|
|
142
164
|
Stop development for this plugin. In **Settings → Plugins → Add**, choose file
|
|
143
165
|
installation, select the `.fpp`, review, and confirm. Installation is immediate.
|
|
144
166
|
Packaging checks archive structure; Flare still validates the manifest,
|
|
145
167
|
registration, permissions, and behavior. File installs have no GitHub update tracking.
|
|
146
168
|
|
|
147
|
-
##
|
|
169
|
+
## SDK types and editor support
|
|
170
|
+
|
|
171
|
+
The generated `tsconfig.json` already sets `types: ["flare-plugin-sdk"]`, strict
|
|
172
|
+
checking, and an ES2020 library without browser or Node globals. `definePlugin`
|
|
173
|
+
contextually types its handlers; `PostV1`, `ProfileV1`, and other SDK types are
|
|
174
|
+
available directly. The SDK is a development dependency, not a runtime import.
|
|
175
|
+
Local TS imports are bundled into `plugin.js`; imported code must also work
|
|
176
|
+
without Node/browser APIs. `build` and `package` stop on type errors.
|
|
148
177
|
|
|
149
|
-
For JSON Schema use without network access, install the
|
|
178
|
+
For JSON Schema use without network access, install the project's dependencies and set the
|
|
150
179
|
manifest's `$schema` to `./node_modules/flare-plugin-sdk/skill/flare-plugin/manifest.schema.json`.
|
|
151
180
|
Keep these development files outside the optional `.fpp` staging directory.
|
|
152
181
|
Flare treats `$schema` as editor metadata and never downloads it. With an older
|
|
@@ -155,7 +184,7 @@ association in workspace settings instead. See the
|
|
|
155
184
|
[manifest editor guide](https://github.com/DimensionDev/Flare/blob/feature/plugin/feature/plugin/sdk/skill/flare-plugin/references/manifest.md#json-schema-and-editor-support)
|
|
156
185
|
and [VS Code schema settings](https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings).
|
|
157
186
|
|
|
158
|
-
|
|
187
|
+
For an existing project, install editor types:
|
|
159
188
|
|
|
160
189
|
```sh
|
|
161
190
|
npm install --save-dev flare-plugin-sdk@next
|
|
@@ -191,7 +220,11 @@ All documentation is in English. The complete Pixelfed adapter is bundled at
|
|
|
191
220
|
From `feature/plugin/sdk`, run `npm ci` and `npm test` with Node 22 or newer.
|
|
192
221
|
The tests build an npm tarball, check its complete resource set and license,
|
|
193
222
|
install it offline in an empty project, invoke the actual npx initializer,
|
|
194
|
-
type-check a consumer, and package
|
|
223
|
+
type-check a consumer, and install/build/package the generated TS project. The
|
|
224
|
+
SDK tarball is installed offline; development tools use cached packages where
|
|
225
|
+
available and fetch missing npm metadata or packages. Checks compare the built and
|
|
226
|
+
prebuilt script, exercise local-module bundling, watch errors/recovery and missing
|
|
227
|
+
Node APIs, and verify the package contains only runtime files.
|
|
195
228
|
Initializer tests cover valid unique IDs, empty/nested/Unicode paths, invalid
|
|
196
229
|
arguments, and preserving existing directories, files, and symlinks. They do not
|
|
197
230
|
contact a website or publish to npm. Flare's Host and platform acceptance checks are
|
package/bin/cli.mjs
CHANGED
|
@@ -39,20 +39,24 @@ async function main() {
|
|
|
39
39
|
manifest.name = name;
|
|
40
40
|
manifest.platform.id = `Plugin${identity}`;
|
|
41
41
|
manifest.platform.name = name;
|
|
42
|
+
const project = JSON.parse(await readFile(join(template, "package.json"), "utf8"));
|
|
43
|
+
project.name = manifest.id;
|
|
44
|
+
project.devDependencies["flare-plugin-sdk"] = version;
|
|
42
45
|
|
|
43
46
|
await mkdir(target, { recursive: true });
|
|
44
47
|
for (const entry of await readdir(template)) {
|
|
45
|
-
if (
|
|
46
|
-
await cp(join(template, entry), join(target, entry), {
|
|
48
|
+
if (["manifest.json", "package.json"].includes(entry)) continue;
|
|
49
|
+
await cp(join(template, entry), join(target, entry === "_gitignore" ? ".gitignore" : entry), {
|
|
47
50
|
recursive: true,
|
|
48
51
|
force: false,
|
|
49
52
|
errorOnExist: true,
|
|
50
53
|
});
|
|
51
54
|
}
|
|
52
55
|
await writeFile(join(target, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
|
|
56
|
+
await writeFile(join(target, "package.json"), `${JSON.stringify(project, null, 2)}\n`, { flag: "wx" });
|
|
53
57
|
|
|
54
58
|
console.log(`Created ${target}\nPlugin ID: ${manifest.id}\nPlatform ID: ${manifest.platform.id}`);
|
|
55
|
-
console.log("
|
|
59
|
+
console.log("Run npm install, edit src/plugin.ts, and use npm run dev to update plugin.js. Load this directory in Flare's Plugin development.");
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flare-plugin-sdk",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.8",
|
|
4
4
|
"description": "Types, tooling, templates and authoring references for Flare plugins",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://github.com/DimensionDev/Flare#plugins-beta",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"ajv": "8.20.0",
|
|
36
|
+
"esbuild": "0.28.2",
|
|
36
37
|
"typescript": "6.0.3"
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -168,10 +168,12 @@ cases matter. It should identify gaps rather than invent website endpoints.
|
|
|
168
168
|
cd my-plugin
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
It generates unique plugin/platform IDs
|
|
172
|
-
|
|
171
|
+
It generates a TS project with SDK types and unique plugin/platform IDs. A
|
|
172
|
+
prebuilt root `plugin.js` makes it installable before dependencies are installed.
|
|
173
|
+
Run `npm install`, edit `src/plugin.ts`, and use `npm run dev` while developing;
|
|
174
|
+
`npm run build` checks types before rebuilding the root script. For manual copies of
|
|
173
175
|
[assets/github-template](assets/github-template/README.md), replace its example
|
|
174
|
-
IDs and names first
|
|
176
|
+
IDs and names first and rename `_gitignore` to `.gitignore`. The example API needs adaptation; no demonstration server
|
|
175
177
|
is provided.
|
|
176
178
|
2. Keep the generated IDs stable, replace the placeholder icon, and adapt
|
|
177
179
|
authentication and data mapping to your website. See [manifest](references/manifest.md)
|
|
@@ -183,8 +185,9 @@ cases matter. It should identify gaps rather than invent website endpoints.
|
|
|
183
185
|
to start the local directory, test an account, and apply saved edits with
|
|
184
186
|
**Reload plugin** after automatic validation. Follow the syntax, behavior and
|
|
185
187
|
platform checks. Stop development before installing/updating the same plugin.
|
|
186
|
-
5. Prefer GitHub distribution:
|
|
187
|
-
|
|
188
|
+
5. Prefer GitHub distribution: run `npm run build`, commit the TS source, build
|
|
189
|
+
configuration, lockfile and root `plugin.js`, then push a stable tag matching
|
|
190
|
+
`manifest.version`. In Flare's plugin
|
|
188
191
|
management, enter `https://github.com/OWNER/REPO` and click **Install**. Review
|
|
189
192
|
source and permissions, then confirm to use the plugin immediately. Follow the
|
|
190
193
|
plugin's README to select the platform, origin, and login method.
|
|
@@ -29,8 +29,13 @@ to the user's plugin repository.
|
|
|
29
29
|
|
|
30
30
|
## Implement the requested adapter
|
|
31
31
|
|
|
32
|
-
Start
|
|
33
|
-
|
|
32
|
+
Start with `npx flare-plugin-sdk@next init my-plugin` and follow the
|
|
33
|
+
[project workflow](assets/github-template/README.md). For manual copies, put the
|
|
34
|
+
**contents** of `assets/github-template/` at root, replace example IDs and rename
|
|
35
|
+
`_gitignore` to `.gitignore`. Install development dependencies, edit `src/plugin.ts`
|
|
36
|
+
using the ambient SDK types, and run `npm run build`. Commit the resulting root
|
|
37
|
+
`plugin.js` with the TS source and lockfile. `npm run dev` watches without type
|
|
38
|
+
checking; `npm run check` and the type-checked build are the release gates.
|
|
34
39
|
Its `/api/me` and `/api/posts` contract is illustrative, not a live service. Adapt
|
|
35
40
|
it from verified website evidence before presenting it as an integration.
|
|
36
41
|
Use [Pixelfed](examples/pixelfed/README.md) as a complete real-service example,
|
|
@@ -12,6 +12,7 @@ If you created this project with `npx flare-plugin-sdk@next init my-plugin`, wor
|
|
|
12
12
|
inside `my-plugin` and keep its generated plugin/platform IDs. If you copied the
|
|
13
13
|
template manually, first copy its contents to your repository root and replace
|
|
14
14
|
the example `manifest.id`, `platform.id`, and names. Keep IDs stable for updates.
|
|
15
|
+
For manual copies, also rename `_gitignore` to `.gitignore`; `init` does this for you.
|
|
15
16
|
|
|
16
17
|
The generated manifest's `$schema` is pinned to the SDK version that created it.
|
|
17
18
|
Open `manifest.json` in VS Code or another JSON Schema editor for completion,
|
|
@@ -22,7 +23,7 @@ for offline use and older Flare builds that reject `$schema`.
|
|
|
22
23
|
|
|
23
24
|
1. Update the description, login methods, and declared capabilities in
|
|
24
25
|
`manifest.json`. Replace the one-pixel placeholder PNG with your own icon.
|
|
25
|
-
2. Adapt `plugin.
|
|
26
|
+
2. Adapt `src/plugin.ts` using the
|
|
26
27
|
[developer guide](https://github.com/DimensionDev/Flare/tree/feature/plugin/feature/plugin/sdk/skill/flare-plugin)
|
|
27
28
|
and your website's API evidence.
|
|
28
29
|
It currently verifies an imported bearer token with `/api/me`, then reads
|
|
@@ -37,14 +38,23 @@ authenticated profile returned at login supplies the saved account identity.
|
|
|
37
38
|
|
|
38
39
|
## 2. Develop and debug locally
|
|
39
40
|
|
|
40
|
-
The
|
|
41
|
-
|
|
41
|
+
The root contains a prebuilt, installable `plugin.js`, alongside TS source and
|
|
42
|
+
development tools. With Node 22 or newer, run from this project directory:
|
|
42
43
|
|
|
43
44
|
```sh
|
|
44
|
-
|
|
45
|
+
npm install
|
|
46
|
+
npm run check
|
|
47
|
+
npm run build
|
|
45
48
|
node -e 'JSON.parse(require("node:fs").readFileSync("manifest.json", "utf8")); console.log("Manifest JSON parses")'
|
|
46
49
|
```
|
|
47
50
|
|
|
51
|
+
`npm install` creates a lockfile; commit it and use `npm ci` on later checkouts.
|
|
52
|
+
The private package uses the SDK for ambient types, TypeScript for strict checking,
|
|
53
|
+
and esbuild to bundle local TS modules into one ES2020 IIFE at root. The generated
|
|
54
|
+
SDK dependency is version-pinned. Types such as `PostV1`, `ProfileV1`, and
|
|
55
|
+
`PluginErrorCodeV1` are available directly, and `definePlugin` types its handlers.
|
|
56
|
+
The SDK is not imported at runtime; plugin code has no Node or browser globals.
|
|
57
|
+
|
|
48
58
|
Flare supplies the `definePlugin` and `flare` globals and performs the actual
|
|
49
59
|
manifest, registration, and runtime checks. Use a Flare build with Plugins in Settings.
|
|
50
60
|
|
|
@@ -54,9 +64,12 @@ manifest, registration, and runtime checks. Use a Flare build with Plugins in Se
|
|
|
54
64
|
2. Add an account, explicitly select this plugin platform, and enter your website's
|
|
55
65
|
HTTPS API origin. Import a real access token. After adapting the endpoints,
|
|
56
66
|
verify the account identity, Home refresh, older/empty/end pages, and link clicks.
|
|
57
|
-
3.
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
3. Run `npm run dev` and edit `src/plugin.ts` or its local modules. Watch mode
|
|
68
|
+
rebuilds `plugin.js`, preserving the previous script on syntax/build errors.
|
|
69
|
+
It does not check types; use editor diagnostics or `npm run check` separately.
|
|
70
|
+
Flare automatically validates the built script, manifest, icon and translations;
|
|
71
|
+
click **Reload plugin** to apply the validated version. Invalid changes leave
|
|
72
|
+
the previous working version running.
|
|
60
73
|
4. Check validation errors and recent-call diagnostics in the development panel.
|
|
61
74
|
Recent calls show method names and outcomes, without HTTP request/response
|
|
62
75
|
payloads. Reproduce authentication, HTTP, response-mapping, and paging failures
|
|
@@ -66,9 +79,9 @@ manifest, registration, and runtime checks. Use a Flare build with Plugins in Se
|
|
|
66
79
|
automatically mount this directory. Development uses real accounts and data,
|
|
67
80
|
and remote writes are not rolled back.
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
Keep the project root selected in Flare. Edit TS source, not the generated
|
|
83
|
+
`plugin.js`; `npm run build` checks types before rebuilding it. Commit that built
|
|
84
|
+
script at root so GitHub installation works without running npm on the device.
|
|
72
85
|
See [development and testing details](https://github.com/DimensionDev/Flare/blob/feature/plugin/feature/plugin/sdk/skill/flare-plugin/references/testing.md#develop-locally-in-desktop)
|
|
73
86
|
for reload restrictions, credential changes, and platform acceptance checks.
|
|
74
87
|
|
|
@@ -87,13 +100,16 @@ locales/en.json # optional; other locale catalogs as needed
|
|
|
87
100
|
The icon can also be JPEG, WebP, GIF, or ICO; keep exactly one supported icon.
|
|
88
101
|
Development files may coexist in the repository, but keep credentials,
|
|
89
102
|
`node_modules`, build caches, and private fixtures out of Git. Flare reads the
|
|
90
|
-
committed plugin files directly
|
|
103
|
+
committed plugin files directly. Run `npm run build` before committing/tagging;
|
|
104
|
+
it must pass and the latest `plugin.js` must be committed. Keep the TS source,
|
|
105
|
+
build configuration and lockfile in Git too. `.gitignore` excludes dependencies
|
|
106
|
+
and local archives while retaining the runtime script.
|
|
91
107
|
|
|
92
108
|
For a new project and an empty GitHub repository, replace `OWNER/REPO` and run:
|
|
93
109
|
|
|
94
110
|
```sh
|
|
95
111
|
git init
|
|
96
|
-
git add manifest.json plugin.js assets README.md
|
|
112
|
+
git add manifest.json plugin.js assets src package.json package-lock.json tsconfig.json build.mjs .gitignore README.md
|
|
97
113
|
# If used, also stage translations with: git add locales
|
|
98
114
|
git commit -m "Add Flare plugin"
|
|
99
115
|
git branch -M main
|
|
@@ -125,16 +141,17 @@ review/confirm the new version. See the
|
|
|
125
141
|
|
|
126
142
|
## Alternative: package a local file
|
|
127
143
|
|
|
128
|
-
Use `.fpp` for offline transfer or local testing.
|
|
129
|
-
directory and copy only `manifest.json`, `plugin.js`, one supported
|
|
130
|
-
`assets/icon.<extension>`, and any `locales/*.json` into it, keeping those paths.
|
|
131
|
-
Keep `.git`, `package.json`, `node_modules`, tests, and old archives outside `dist`.
|
|
132
|
-
From this project directory, run:
|
|
144
|
+
Use `.fpp` for offline transfer or local testing. From this project directory:
|
|
133
145
|
|
|
134
146
|
```sh
|
|
135
|
-
|
|
147
|
+
npm run package
|
|
136
148
|
```
|
|
137
149
|
|
|
150
|
+
This checks types, rebuilds the root script, copies only manifest/script/icon/locale
|
|
151
|
+
files to temporary staging, and uses the SDK packer to write `plugin.fpp` at root.
|
|
152
|
+
Staging is cleaned up and development files are excluded. The standalone packer
|
|
153
|
+
still accepts only a clean runtime directory, not this whole project.
|
|
154
|
+
|
|
138
155
|
Stop development for this plugin, then choose file installation in **Settings →
|
|
139
156
|
Plugins → Add**, select the `.fpp`, review, and confirm. It becomes available
|
|
140
157
|
immediately. The packer checks archive structure; Flare still validates the
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cp, mkdir, mkdtemp, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { build, context } from "esbuild";
|
|
7
|
+
|
|
8
|
+
const root = fileURLToPath(new URL(".", import.meta.url));
|
|
9
|
+
const mode = process.argv[2];
|
|
10
|
+
if (process.argv.length > 3 || (mode && !["--watch", "--package"].includes(mode))) {
|
|
11
|
+
throw new Error("Usage: node build.mjs [--watch|--package]");
|
|
12
|
+
}
|
|
13
|
+
const options = {
|
|
14
|
+
absWorkingDir: root,
|
|
15
|
+
entryPoints: ["src/plugin.ts"],
|
|
16
|
+
outfile: "plugin.js",
|
|
17
|
+
bundle: true,
|
|
18
|
+
format: "iife",
|
|
19
|
+
platform: "neutral",
|
|
20
|
+
mainFields: ["module", "main"],
|
|
21
|
+
target: "es2020",
|
|
22
|
+
logLevel: "info",
|
|
23
|
+
write: false,
|
|
24
|
+
plugins: [{
|
|
25
|
+
name: "plugin-output",
|
|
26
|
+
setup(builder) {
|
|
27
|
+
builder.onEnd(async (result) => {
|
|
28
|
+
if (result.errors.length) return;
|
|
29
|
+
if (result.outputFiles.length !== 1) throw new Error("A plugin build must emit one JavaScript file");
|
|
30
|
+
const temporary = await mkdtemp(join(root, ".plugin-build-"));
|
|
31
|
+
try {
|
|
32
|
+
// Preserve the installable script while edits are invalid or a build is interrupted.
|
|
33
|
+
await writeFile(join(temporary, "plugin.js"), result.outputFiles[0].contents, { flag: "wx" });
|
|
34
|
+
await rename(join(temporary, "plugin.js"), join(root, "plugin.js"));
|
|
35
|
+
} finally {
|
|
36
|
+
await rm(temporary, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
}],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (mode === "--watch") {
|
|
44
|
+
const watcher = await context(options);
|
|
45
|
+
await watcher.watch();
|
|
46
|
+
} else {
|
|
47
|
+
await build(options);
|
|
48
|
+
if (mode === "--package") {
|
|
49
|
+
const staging = await mkdtemp(join(tmpdir(), "flare-plugin-package-"));
|
|
50
|
+
try {
|
|
51
|
+
for (const name of ["manifest.json", "plugin.js"]) {
|
|
52
|
+
await cp(join(root, name), join(staging, name));
|
|
53
|
+
}
|
|
54
|
+
await mkdir(join(staging, "assets"));
|
|
55
|
+
for (const name of await readdir(join(root, "assets"))) {
|
|
56
|
+
if (/^icon\.(?:png|jpe?g|webp|gif|ico)$/.test(name)) {
|
|
57
|
+
await cp(join(root, "assets", name), join(staging, "assets", name));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const catalogs = await readdir(join(root, "locales")).catch((error) => {
|
|
61
|
+
if (error.code !== "ENOENT") throw error;
|
|
62
|
+
return [];
|
|
63
|
+
});
|
|
64
|
+
if (catalogs.length) {
|
|
65
|
+
await mkdir(join(staging, "locales"));
|
|
66
|
+
for (const name of catalogs.filter((name) => name.endsWith(".json"))) {
|
|
67
|
+
await cp(join(root, "locales", name), join(staging, "locales", name));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const packer = fileURLToPath(import.meta.resolve("flare-plugin-sdk/skill/flare-plugin/pack.mjs"));
|
|
71
|
+
execFileSync(process.execPath, [packer, staging, join(root, "plugin.fpp")], { stdio: "inherit" });
|
|
72
|
+
} finally {
|
|
73
|
+
await rm(staging, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flare-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"check": "tsc --noEmit",
|
|
7
|
+
"build": "npm run check && node build.mjs",
|
|
8
|
+
"dev": "node build.mjs --watch",
|
|
9
|
+
"package": "npm run check && node build.mjs --package"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"esbuild": "0.28.2",
|
|
13
|
+
"flare-plugin-sdk": "next",
|
|
14
|
+
"typescript": "6.0.3"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,108 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
capabilities: {
|
|
23
|
-
timeline: {
|
|
24
|
-
async page(request, context) {
|
|
25
|
-
if (request.timelineId !== "home") fail("Unsupported", "Unknown timeline");
|
|
26
|
-
const credential = await flare.credential.read();
|
|
27
|
-
if (!credential || typeof credential !== "object" || Array.isArray(credential) ||
|
|
28
|
-
typeof credential.accessToken !== "string" || !credential.accessToken) {
|
|
29
|
-
fail("AuthenticationRequired", "Import your access token again");
|
|
30
|
-
}
|
|
31
|
-
let url = context.origin + "/api/posts?limit=" + request.page.limit;
|
|
32
|
-
if (request.page.direction !== "refresh" && request.page.cursor) {
|
|
33
|
-
url += "&cursor=" + encodeURIComponent(request.page.cursor);
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// src/plugin.ts
|
|
4
|
+
definePlugin({
|
|
5
|
+
login: {
|
|
6
|
+
token: {
|
|
7
|
+
async begin(request, context) {
|
|
8
|
+
const token = request.values.token?.trim();
|
|
9
|
+
if (!token) fail("Validation", "Enter an access token");
|
|
10
|
+
const profile = mapProfile(await requestJson(context.origin + "/api/me", token), context);
|
|
11
|
+
return {
|
|
12
|
+
type: "success",
|
|
13
|
+
value: {
|
|
14
|
+
accountId: profile.key.id,
|
|
15
|
+
origin: context.origin,
|
|
16
|
+
credential: { accessToken: token },
|
|
17
|
+
profile,
|
|
18
|
+
capabilities: { "flare.datasource.timeline/v1": ["page"] }
|
|
19
|
+
}
|
|
20
|
+
};
|
|
34
21
|
}
|
|
35
|
-
|
|
36
|
-
if (!Array.isArray(data.items)) fail("InvalidResponse", "Expected a post array");
|
|
37
|
-
if (data.next_cursor != null && (typeof data.next_cursor !== "string" || !data.next_cursor)) {
|
|
38
|
-
fail("InvalidResponse", "Expected a non-empty cursor or null");
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
items: data.items.map(post => ({
|
|
42
|
-
key: { id: requiredString(post && post.id), host: accountHost(context) },
|
|
43
|
-
author: mapProfile(post.author, context),
|
|
44
|
-
createdAt: requiredString(post.created_at),
|
|
45
|
-
content: { format: "plain", value: requiredString(post.text) },
|
|
46
|
-
detailType: "link",
|
|
47
|
-
url: requiredString(post.url),
|
|
48
|
-
})),
|
|
49
|
-
olderCursor: data.next_cursor,
|
|
50
|
-
endReached: data.next_cursor == null,
|
|
51
|
-
};
|
|
52
|
-
},
|
|
22
|
+
}
|
|
53
23
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
24
|
+
capabilities: {
|
|
25
|
+
timeline: {
|
|
26
|
+
async page(request, context) {
|
|
27
|
+
if (request.timelineId !== "home") fail("Unsupported", "Unknown timeline");
|
|
28
|
+
const credential = await flare.credential.read();
|
|
29
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential) || typeof credential.accessToken !== "string" || !credential.accessToken) {
|
|
30
|
+
fail("AuthenticationRequired", "Import your access token again");
|
|
31
|
+
}
|
|
32
|
+
let url = context.origin + "/api/posts?limit=" + request.page.limit;
|
|
33
|
+
if (request.page.direction !== "refresh" && request.page.cursor) {
|
|
34
|
+
url += "&cursor=" + encodeURIComponent(request.page.cursor);
|
|
35
|
+
}
|
|
36
|
+
const data = await requestJson(url, credential.accessToken);
|
|
37
|
+
if (!Array.isArray(data.items)) fail("InvalidResponse", "Expected a post array");
|
|
38
|
+
const cursor = data.next_cursor;
|
|
39
|
+
if (cursor != null && (typeof cursor !== "string" || !cursor)) {
|
|
40
|
+
fail("InvalidResponse", "Expected a non-empty cursor or null");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
items: data.items.map((value) => {
|
|
44
|
+
const post = requiredObject(value);
|
|
45
|
+
return {
|
|
46
|
+
key: { id: requiredString(post.id), host: accountHost(context) },
|
|
47
|
+
author: mapProfile(post.author, context),
|
|
48
|
+
createdAt: requiredString(post.created_at),
|
|
49
|
+
content: { format: "plain", value: requiredString(post.text) },
|
|
50
|
+
detailType: "link",
|
|
51
|
+
url: requiredString(post.url)
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
olderCursor: cursor ?? void 0,
|
|
55
|
+
endReached: cursor == null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
function accountHost(context) {
|
|
62
|
+
return context.origin.slice("https://".length);
|
|
63
|
+
}
|
|
64
|
+
function mapProfile(value, context) {
|
|
65
|
+
const profile = requiredObject(value);
|
|
66
|
+
return {
|
|
67
|
+
key: { id: requiredString(profile.id), host: accountHost(context) },
|
|
68
|
+
handle: requiredString(profile.username),
|
|
69
|
+
displayName: requiredString(profile.name)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function requiredString(value) {
|
|
73
|
+
if (typeof value !== "string" || !value.trim()) fail("InvalidResponse", "Missing required text field");
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function requiredObject(value) {
|
|
77
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("InvalidResponse", "Expected a JSON object");
|
|
78
|
+
return value;
|
|
81
79
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
80
|
+
async function requestJson(url, token) {
|
|
81
|
+
let response;
|
|
82
|
+
try {
|
|
83
|
+
response = await flare.http.request({ url, authorization: { type: "bearer", token } });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error && typeof error === "object" && "code" in error && error.code === "host.unavailable") {
|
|
86
|
+
fail("Network", "The request could not be completed");
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
if (response.status === 401) fail("AuthenticationRequired", "The access token was rejected");
|
|
91
|
+
if (response.status === 404) fail("NotFound", "The requested resource was not found");
|
|
92
|
+
if (response.status === 429) {
|
|
93
|
+
const entry = Object.entries(response.headers).find(([name]) => name.toLowerCase() === "retry-after");
|
|
94
|
+
const value = entry?.[1][0];
|
|
95
|
+
const seconds = value && /^\d+$/.test(value) ? Number(value) : void 0;
|
|
96
|
+
throw flare.error({
|
|
97
|
+
code: "RateLimited",
|
|
98
|
+
message: { value: "Try again later" },
|
|
99
|
+
retryAfterSeconds: typeof seconds === "number" && Number.isInteger(seconds) && seconds <= 604800 ? seconds : void 0
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (response.status < 200 || response.status >= 300) fail("Remote", "The website returned an error");
|
|
103
|
+
let data;
|
|
104
|
+
try {
|
|
105
|
+
data = JSON.parse(response.body);
|
|
106
|
+
} catch {
|
|
107
|
+
fail("InvalidResponse", "The website did not return JSON");
|
|
108
|
+
}
|
|
109
|
+
return requiredObject(data);
|
|
93
110
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
data = JSON.parse(response.body);
|
|
98
|
-
} catch (_) {
|
|
99
|
-
fail("InvalidResponse", "The website did not return JSON");
|
|
111
|
+
function fail(code, message) {
|
|
112
|
+
throw flare.error({ code, message: { value: message } });
|
|
100
113
|
}
|
|
101
|
-
|
|
102
|
-
return data;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** @param {PluginErrorCodeV1} code @param {string} message @returns {never} */
|
|
106
|
-
function fail(code, message) {
|
|
107
|
-
throw flare.error({ code, message: { value: message } });
|
|
108
|
-
}
|
|
114
|
+
})();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Adapt the illustrative website contract in README.md before publication.
|
|
2
|
+
definePlugin({
|
|
3
|
+
login: {
|
|
4
|
+
token: {
|
|
5
|
+
async begin(request, context) {
|
|
6
|
+
const token = request.values.token?.trim();
|
|
7
|
+
if (!token) fail("Validation", "Enter an access token");
|
|
8
|
+
const profile = mapProfile(await requestJson(context.origin + "/api/me", token), context);
|
|
9
|
+
return {
|
|
10
|
+
type: "success",
|
|
11
|
+
value: {
|
|
12
|
+
accountId: profile.key.id,
|
|
13
|
+
origin: context.origin,
|
|
14
|
+
credential: { accessToken: token },
|
|
15
|
+
profile,
|
|
16
|
+
capabilities: { "flare.datasource.timeline/v1": ["page"] },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
capabilities: {
|
|
23
|
+
timeline: {
|
|
24
|
+
async page(request, context) {
|
|
25
|
+
if (request.timelineId !== "home") fail("Unsupported", "Unknown timeline");
|
|
26
|
+
const credential = await flare.credential.read();
|
|
27
|
+
if (!credential || typeof credential !== "object" || Array.isArray(credential) ||
|
|
28
|
+
typeof credential.accessToken !== "string" || !credential.accessToken) {
|
|
29
|
+
fail("AuthenticationRequired", "Import your access token again");
|
|
30
|
+
}
|
|
31
|
+
let url = context.origin + "/api/posts?limit=" + request.page.limit;
|
|
32
|
+
if (request.page.direction !== "refresh" && request.page.cursor) {
|
|
33
|
+
url += "&cursor=" + encodeURIComponent(request.page.cursor);
|
|
34
|
+
}
|
|
35
|
+
const data = await requestJson(url, credential.accessToken);
|
|
36
|
+
if (!Array.isArray(data.items)) fail("InvalidResponse", "Expected a post array");
|
|
37
|
+
const cursor = data.next_cursor;
|
|
38
|
+
if (cursor != null && (typeof cursor !== "string" || !cursor)) {
|
|
39
|
+
fail("InvalidResponse", "Expected a non-empty cursor or null");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
items: data.items.map((value: unknown): PostV1 => {
|
|
43
|
+
const post = requiredObject(value);
|
|
44
|
+
return {
|
|
45
|
+
key: { id: requiredString(post.id), host: accountHost(context) },
|
|
46
|
+
author: mapProfile(post.author, context),
|
|
47
|
+
createdAt: requiredString(post.created_at),
|
|
48
|
+
content: { format: "plain", value: requiredString(post.text) },
|
|
49
|
+
detailType: "link",
|
|
50
|
+
url: requiredString(post.url),
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
53
|
+
olderCursor: cursor ?? undefined,
|
|
54
|
+
endReached: cursor == null,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function accountHost(context: PluginInvocationContextV1): string {
|
|
62
|
+
return context.origin.slice("https://".length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mapProfile(value: unknown, context: PluginInvocationContextV1): ProfileV1 {
|
|
66
|
+
const profile = requiredObject(value);
|
|
67
|
+
return {
|
|
68
|
+
key: { id: requiredString(profile.id), host: accountHost(context) },
|
|
69
|
+
handle: requiredString(profile.username),
|
|
70
|
+
displayName: requiredString(profile.name),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requiredString(value: unknown): string {
|
|
75
|
+
if (typeof value !== "string" || !value.trim()) fail("InvalidResponse", "Missing required text field");
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requiredObject(value: unknown): Record<string, unknown> {
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) fail("InvalidResponse", "Expected a JSON object");
|
|
81
|
+
return value as Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function requestJson(url: string, token: string): Promise<Record<string, unknown>> {
|
|
85
|
+
let response: HttpResponseV1;
|
|
86
|
+
try {
|
|
87
|
+
response = await flare.http.request({ url, authorization: { type: "bearer", token } });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error && typeof error === "object" && "code" in error && error.code === "host.unavailable") {
|
|
90
|
+
fail("Network", "The request could not be completed");
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
if (response.status === 401) fail("AuthenticationRequired", "The access token was rejected");
|
|
95
|
+
if (response.status === 404) fail("NotFound", "The requested resource was not found");
|
|
96
|
+
if (response.status === 429) {
|
|
97
|
+
const entry = Object.entries(response.headers).find(([name]) => name.toLowerCase() === "retry-after");
|
|
98
|
+
const value = entry?.[1][0];
|
|
99
|
+
const seconds = value && /^\d+$/.test(value) ? Number(value) : undefined;
|
|
100
|
+
throw flare.error({
|
|
101
|
+
code: "RateLimited",
|
|
102
|
+
message: { value: "Try again later" },
|
|
103
|
+
retryAfterSeconds: typeof seconds === "number" && Number.isInteger(seconds) && seconds <= 604800 ? seconds : undefined,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (response.status < 200 || response.status >= 300) fail("Remote", "The website returned an error");
|
|
107
|
+
let data: unknown;
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(response.body);
|
|
110
|
+
} catch {
|
|
111
|
+
fail("InvalidResponse", "The website did not return JSON");
|
|
112
|
+
}
|
|
113
|
+
return requiredObject(data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fail(code: PluginErrorCodeV1, message: string): never {
|
|
117
|
+
throw flare.error({ code, message: { value: message } });
|
|
118
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"target": "ES2020",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"lib": ["ES2020"],
|
|
10
|
+
"types": ["flare-plugin-sdk"],
|
|
11
|
+
"isolatedModules": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|
|
@@ -31,6 +31,9 @@ copy that example's contents into a repository you own first.
|
|
|
31
31
|
by `npx flare-plugin-sdk@next init my-plugin` stable.
|
|
32
32
|
Keep real tokens, Cookies, passwords, and private fixtures out of Git history.
|
|
33
33
|
2. Put the required files at root and check that a clean checkout contains them.
|
|
34
|
+
For the TS starter, run `npm run build` and commit `src/`, the build files,
|
|
35
|
+
`package-lock.json`, and the newly built root `plugin.js`. Keep `plugin.js`
|
|
36
|
+
tracked; the generated `.gitignore` excludes dependencies and local artifacts.
|
|
34
37
|
Include README instructions for supported website/instances, token acquisition
|
|
35
38
|
if relevant, features, permissions, and tests actually performed.
|
|
36
39
|
3. Set a stable SemVer manifest version, for example `0.1.0`. Publish a tag
|
|
@@ -101,8 +104,19 @@ plugin authors do not control it.
|
|
|
101
104
|
|
|
102
105
|
## Optional local installation
|
|
103
106
|
|
|
104
|
-
Use `.fpp` as an alternative for offline transfer or local testing.
|
|
105
|
-
|
|
107
|
+
Use `.fpp` as an alternative for offline transfer or local testing. From the
|
|
108
|
+
generated TS project, run:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm run package
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It checks types, rebuilds the root script, stages only runtime files in a fresh
|
|
115
|
+
temporary directory and invokes the SDK packer. The result is `plugin.fpp` at
|
|
116
|
+
project root; temporary staging is cleaned up. GitHub remains the recommended
|
|
117
|
+
distribution path and this file has no GitHub update tracking.
|
|
118
|
+
|
|
119
|
+
For other project layouts, create a new, empty `dist` directory. Copy only `manifest.json`, `plugin.js`,
|
|
106
120
|
one supported `assets/icon.<extension>`, and optional `locales/*.json` into it,
|
|
107
121
|
preserving those paths. From the project root:
|
|
108
122
|
|
|
@@ -114,7 +128,7 @@ The complete skill also includes the standard-library-only packer for use withou
|
|
|
114
128
|
npm. For example, from this skill directory:
|
|
115
129
|
|
|
116
130
|
```sh
|
|
117
|
-
node pack.mjs
|
|
131
|
+
node pack.mjs /path/to/runtime-directory /tmp/example-feed-0.1.0.fpp
|
|
118
132
|
```
|
|
119
133
|
|
|
120
134
|
The packer ignores only a root README.md; unlike GitHub import it rejects other
|
|
@@ -7,15 +7,19 @@ No demonstration backend is required for authoring this SDK.
|
|
|
7
7
|
|
|
8
8
|
## Checks in a generated repository
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The generated TS project includes a prebuilt root `plugin.js`. From its root:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
+
npm install
|
|
14
|
+
npm run check
|
|
15
|
+
npm run build
|
|
13
16
|
node --check plugin.js
|
|
14
17
|
node -e 'JSON.parse(require("node:fs").readFileSync("manifest.json", "utf8")); console.log("Manifest JSON parses")'
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
The SDK types help check source code; building emits one self-contained ES2020
|
|
21
|
+
IIFE. CI checks that it matches the committed prebuilt script. Type/syntax checks
|
|
22
|
+
do not replace Host validation, and parsing JSON does not validate the manifest. Also inspect:
|
|
19
23
|
|
|
20
24
|
- Required root files and a valid supported icon; stable IDs and version/tag consistency.
|
|
21
25
|
- Every declared known operation has its required registration and full method
|
|
@@ -27,9 +31,8 @@ provide, and parsing JSON does not validate the manifest. Also inspect:
|
|
|
27
31
|
- Auth/cursor/action data is not accidentally inserted into public content or
|
|
28
32
|
media URLs. No real secrets enter checked-in fixtures.
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
bundler to emit one classic script. Keep manifest/Host validation even when the
|
|
34
|
+
The generated tsconfig loads the SDK's ambient types with strict checking and no
|
|
35
|
+
browser/Node globals. Keep manifest/Host validation even when the
|
|
33
36
|
type checker passes: TypeScript optional service properties do not express all
|
|
34
37
|
capability requirements, and optionality does not make every null value valid.
|
|
35
38
|
|
|
@@ -39,11 +42,15 @@ In Settings > Plugins, open the overflow menu and choose **Plugin development**.
|
|
|
39
42
|
Choose the directory containing `manifest.json`, `plugin.js`, and the plugin assets.
|
|
40
43
|
Click **Start development** and review access to your existing accounts. Both Compose Desktop and native macOS support this flow.
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
For the TS starter, run `npm run dev` and edit `src/plugin.ts` or its local modules.
|
|
46
|
+
The watcher rebuilds the root `plugin.js`; syntax/build failures preserve the last
|
|
47
|
+
script through atomic replacement. Watch mode does not type-check; use editor
|
|
48
|
+
diagnostics or `npm run check` separately. Changes to the built script,
|
|
49
|
+
`manifest.json`, `assets/icon.<extension>`, or `locales/*.json` are automatically
|
|
50
|
+
validated by Flare; click **Reload plugin** to apply the validated
|
|
44
51
|
snapshot to this plugin's accounts and open pages. An invalid edit keeps the last
|
|
45
|
-
working version running.
|
|
46
|
-
|
|
52
|
+
working version running. Keep the starter project root selected. Other build
|
|
53
|
+
layouts must select their runtime output directory; Flare does not execute build scripts.
|
|
47
54
|
|
|
48
55
|
Only one development directory can be active. Reload is refused while publishing,
|
|
49
56
|
uploading, deleting, or sending is in progress; retry after the operation finishes.
|