@termix-ssh/plugin-sdk 1.0.11 → 1.0.12
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/CHANGELOG.md +21 -0
- package/PLUGIN-GUIDE.md +95 -3
- package/cli/commands/build.mjs +36 -22
- package/cli/commands/pack.mjs +12 -2
- package/cli/commands/validate.mjs +63 -3
- package/cli/lib/plugin-dir.mjs +11 -0
- package/cli/lib/tmxplug.mjs +9 -0
- package/dist/capabilities.js +2 -0
- package/dist/frontend.d.ts +152 -0
- package/dist/frontend.js +7 -0
- package/dist/host/PluginsTab-EV4MSGY6.js +2933 -0
- package/dist/host/{chunk-RLQBKJRK.js → chunk-AWR6FYDO.js} +8873 -11163
- package/dist/host/{chunk-7RRTYD6B.js → chunk-RUPLD6RF.js} +1497 -15
- package/dist/host/{chunk-LSWB3ZZV.js → chunk-VEWOYAMZ.js} +6 -1
- package/dist/host/chunk-X4QZKO5G.js +3120 -0
- package/dist/host/chunk-XKA3DBB2.js +10375 -0
- package/dist/host/chunk-YGYBM7SQ.js +58 -0
- package/dist/host/core-classes.json +1 -1
- package/dist/host/{linked-server-UCW74T2V.js → linked-server-B4NEG4MA.js} +2 -2
- package/dist/host/{rbac-api-6UYMFIFV.js → rbac-api-OUO3LUOC.js} +2 -2
- package/dist/host/testing-host.js +123 -57
- package/dist/host/types/src/types/ui-types.d.ts +2 -1
- package/dist/host/types/src/ui/api/audit-log-api.d.ts +46 -0
- package/dist/host/types/src/ui/api/host-defaults-api.d.ts +42 -0
- package/dist/host/types/src/ui/api/host-tags-api.d.ts +2 -0
- package/dist/host/types/src/ui/api/look-api.d.ts +40 -0
- package/dist/host/types/src/ui/api/open-tabs-api.d.ts +4 -0
- package/dist/host/types/src/ui/api/plugins-api.d.ts +26 -0
- package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
- package/dist/host/types/src/ui/api/tls-api.d.ts +25 -0
- package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +9 -0
- package/dist/host/types/src/ui/components/command.d.ts +78 -0
- package/dist/host/types/src/ui/components/folder-style.d.ts +16 -0
- package/dist/host/types/src/ui/components/theme-provider.d.ts +2 -1
- package/dist/host/types/src/ui/components/version-badge.d.ts +7 -0
- package/dist/host/types/src/ui/hooks/use-page-visible-interval.d.ts +10 -0
- package/dist/host/types/src/ui/lib/activity-types.d.ts +15 -0
- package/dist/host/types/src/ui/lib/app-version.d.ts +2 -0
- package/dist/host/types/src/ui/lib/apply-ui-preset.d.ts +19 -0
- package/dist/host/types/src/ui/lib/create-id.d.ts +2 -0
- package/dist/host/types/src/ui/lib/database-transfer-url.d.ts +8 -0
- package/dist/host/types/src/ui/lib/desktop-updates.d.ts +29 -0
- package/dist/host/types/src/ui/lib/host-connection-tabs.d.ts +14 -0
- package/dist/host/types/src/ui/lib/host-to-ssh-host.d.ts +4 -0
- package/dist/host/types/src/ui/lib/issue-url.d.ts +21 -0
- package/dist/host/types/src/ui/lib/local-adaptive-preferences.d.ts +9 -0
- package/dist/host/types/src/ui/lib/pane-nav-hotkey.d.ts +7 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +4 -3
- package/dist/host/types/src/ui/lib/role-label.d.ts +3 -0
- package/dist/host/types/src/ui/look/LookSettings.d.ts +8 -0
- package/dist/host/types/src/ui/look/layout-registry.d.ts +34 -0
- package/dist/host/types/src/ui/look/look-store.d.ts +29 -0
- package/dist/host/types/src/ui/look/resolve-look.d.ts +18 -0
- package/dist/host/types/src/ui/look/theme-css.d.ts +7 -0
- package/dist/host/types/src/ui/look/theme-registry.d.ts +41 -0
- package/dist/host/types/src/ui/look/use-look-theme.d.ts +12 -0
- package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
- package/dist/host/types/src/ui/plugin-host/loader.d.ts +2 -0
- package/dist/host/types/src/ui/plugin-host/scope.d.ts +6 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +102 -0
- package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +1 -1
- package/dist/host/types/src/ui/plugins/PluginConsentPrompt.d.ts +20 -0
- package/dist/host/types/src/ui/plugins/PluginDetail.d.ts +7 -0
- package/dist/host/types/src/ui/plugins/PluginReleaseNotes.d.ts +14 -0
- package/dist/host/types/src/ui/plugins/PluginsTab.d.ts +1 -0
- package/dist/host/types/src/ui/plugins/RegistriesSection.d.ts +4 -0
- package/dist/host/types/src/ui/plugins/plugin-bits.d.ts +23 -0
- package/dist/host/types/src/ui/plugins/plugin-model.d.ts +116 -0
- package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +75 -0
- package/dist/host/types/src/ui/settings/appearance/appearance-controls.d.ts +53 -0
- package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +12 -0
- package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +15 -0
- package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +3 -0
- package/dist/host/types/src/ui/shell/controller/ShellControllerProvider.d.ts +20 -0
- package/dist/host/types/src/ui/shell/controller/plugin-shell.d.ts +7 -0
- package/dist/host/types/src/ui/shell/controller/shell-store.d.ts +4 -0
- package/dist/host/types/src/ui/shell/controller/types.d.ts +156 -0
- package/dist/host/types/src/ui/shell/controller/use-hosts.d.ts +10 -0
- package/dist/host/types/src/ui/shell/controller/use-regions.d.ts +5 -0
- package/dist/host/types/src/ui/shell/controller/use-shell-keybindings.d.ts +12 -0
- package/dist/host/types/src/ui/shell/controller/use-shell-lifecycle.d.ts +17 -0
- package/dist/host/types/src/ui/shell/controller/use-split.d.ts +21 -0
- package/dist/host/types/src/ui/shell/controller/use-tabs.d.ts +70 -0
- package/dist/host/types/src/ui/shell/controller/use-workspace-layout.d.ts +4 -0
- package/dist/host/types/src/ui/shell/reconnect-tabs.d.ts +6 -0
- package/dist/host/types/src/ui/shell/shell-layout.d.ts +46 -0
- package/dist/host/types/src/ui/shell/split/EmptyPanePicker.d.ts +32 -0
- package/dist/host/types/src/ui/shell/split/SplitDropOverlay.d.ts +9 -0
- package/dist/host/types/src/ui/shell/split/SplitLayoutMenu.d.ts +16 -0
- package/dist/host/types/src/ui/shell/split/SplitView.d.ts +31 -0
- package/dist/host/types/src/ui/shell/split/split-drag.d.ts +58 -0
- package/dist/host/types/src/ui/shell/split/split-presets.d.ts +28 -0
- package/dist/host/types/src/ui/shell/split/split-tabs.d.ts +95 -0
- package/dist/host/types/src/ui/shell/split/split-targets.d.ts +30 -0
- package/dist/host/types/src/ui/shell/split/split-tree.d.ts +89 -0
- package/dist/host/types/src/ui/shell/stage/TabStage.d.ts +9 -0
- package/dist/host/types/src/ui/shell/stage/tab-stage-store.d.ts +24 -0
- package/dist/host/types/src/ui/shell/tab-close-guards.d.ts +9 -0
- package/dist/host/types/src/ui/shell/tab-handles.d.ts +8 -0
- package/dist/host/types/src/ui/shell/tabUtils.d.ts +15 -0
- package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +5 -0
- package/dist/host/types/src/ui/sidebar/HostManagerData.d.ts +4 -0
- package/dist/host/types/src/ui/sidebar/build-host-tree.d.ts +25 -0
- package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +30 -0
- package/dist/host/ui.js +6236 -4055
- package/dist/host/{use-permissions-GYCBY2AU.js → use-permissions-6VSHHUMB.js} +2 -2
- package/dist/manifest.d.ts +41 -1
- package/dist/manifest.js +89 -1
- package/dist/testing/setup.js +1 -0
- package/dist/theme-css.d.ts +11 -0
- package/dist/theme-css.js +142 -0
- package/dist/vitest-preset.d.ts +18 -0
- package/dist/vitest-preset.js +14 -3
- package/package.json +1 -1
- package/schema/manifest.schema.json +111 -8
- package/tsconfig.plugin.json +1 -0
- package/dist/host/chunk-YC2MAKSP.js +0 -14
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
resetPermissionsCache,
|
|
8
8
|
setPermissionsForTesting,
|
|
9
9
|
usePermissions
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-VEWOYAMZ.js";
|
|
11
|
+
import "./chunk-YGYBM7SQ.js";
|
|
12
12
|
export {
|
|
13
13
|
hasPermission,
|
|
14
14
|
loadPermissionChecker,
|
package/dist/manifest.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* made a manifest look like it declared something it did not.
|
|
11
11
|
*/
|
|
12
12
|
import { type PluginEnvVar } from "./docs.js";
|
|
13
|
+
export { validateThemeCss } from "./theme-css.js";
|
|
13
14
|
export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, type PluginEnvVar, } from "./docs.js";
|
|
14
15
|
/**
|
|
15
16
|
* The plugin API this build implements. A minor bump adds to the API and
|
|
@@ -19,7 +20,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
19
20
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
20
21
|
* version by the server.
|
|
21
22
|
*/
|
|
22
|
-
export declare const PLUGIN_API_VERSION = "1.
|
|
23
|
+
export declare const PLUGIN_API_VERSION = "1.4.0";
|
|
23
24
|
/** The API major, the default engine.api for a new plugin. */
|
|
24
25
|
export declare const SUPPORTED_PLUGIN_API_VERSION: string;
|
|
25
26
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
|
@@ -265,6 +266,41 @@ export interface PluginAuthContribution {
|
|
|
265
266
|
/** Keyboard-interactive handler ids, e.g. "warpgate". */
|
|
266
267
|
keyboardInteractive?: string[];
|
|
267
268
|
}
|
|
269
|
+
/** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
|
|
270
|
+
export declare const LOOK_ID_PATTERN: RegExp;
|
|
271
|
+
/**
|
|
272
|
+
* A whole-app layout the frontend registers with app.registerLayout. Needs
|
|
273
|
+
* the ui:layout capability. Declared here so core can list and choose it
|
|
274
|
+
* before any plugin code runs.
|
|
275
|
+
*/
|
|
276
|
+
export interface PluginLayoutContribution {
|
|
277
|
+
id: string;
|
|
278
|
+
/** i18n key in the plugin's namespace, or plain text. */
|
|
279
|
+
label: string;
|
|
280
|
+
description?: string;
|
|
281
|
+
/** Theme picked with this layout when nobody chose one. */
|
|
282
|
+
defaultTheme?: string;
|
|
283
|
+
/** The layout a default-look plugin starts with. */
|
|
284
|
+
default?: boolean;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* A theme: one CSS file of token rules scoped under `.<id>`, applied as a
|
|
288
|
+
* class on <html>. Needs the ui:theme capability. No code runs for a theme.
|
|
289
|
+
*/
|
|
290
|
+
export interface PluginThemeContribution {
|
|
291
|
+
id: string;
|
|
292
|
+
/** i18n key in the plugin's namespace, or plain text. */
|
|
293
|
+
label: string;
|
|
294
|
+
mode: "light" | "dark";
|
|
295
|
+
/** Path of the CSS file inside the plugin. */
|
|
296
|
+
css: string;
|
|
297
|
+
/** Colors the theme picker previews it with. */
|
|
298
|
+
swatch?: {
|
|
299
|
+
background: string;
|
|
300
|
+
panel: string;
|
|
301
|
+
text: string;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
268
304
|
export interface PluginContributions {
|
|
269
305
|
tabs?: PluginTabContribution[];
|
|
270
306
|
panels?: PluginViewContribution[];
|
|
@@ -323,6 +359,10 @@ export interface PluginContributions {
|
|
|
323
359
|
* ctx.credentials.resolveHostProtocol.
|
|
324
360
|
*/
|
|
325
361
|
protocols?: PluginProtocolContribution[];
|
|
362
|
+
/** Whole-app layouts. Needs the ui:layout capability. */
|
|
363
|
+
layouts?: PluginLayoutContribution[];
|
|
364
|
+
/** Themes. Needs the ui:theme capability. */
|
|
365
|
+
themes?: PluginThemeContribution[];
|
|
326
366
|
}
|
|
327
367
|
/** A field a protocol login carries besides username and password. */
|
|
328
368
|
export interface PluginProtocolCredentialField {
|
package/dist/manifest.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import semver from "semver";
|
|
13
13
|
import { isKnownCapability } from "./capabilities.js";
|
|
14
14
|
import { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsUrl, } from "./docs.js";
|
|
15
|
+
export { validateThemeCss } from "./theme-css.js";
|
|
15
16
|
export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDocsUrl, pluginEnvVars, } from "./docs.js";
|
|
16
17
|
/**
|
|
17
18
|
* The plugin API this build implements. A minor bump adds to the API and
|
|
@@ -21,7 +22,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
21
22
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
22
23
|
* version by the server.
|
|
23
24
|
*/
|
|
24
|
-
export const PLUGIN_API_VERSION = "1.
|
|
25
|
+
export const PLUGIN_API_VERSION = "1.4.0";
|
|
25
26
|
/** The API major, the default engine.api for a new plugin. */
|
|
26
27
|
export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
|
|
27
28
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
|
@@ -168,6 +169,8 @@ export const SETTINGS_FIELD_TYPES = [
|
|
|
168
169
|
"custom",
|
|
169
170
|
];
|
|
170
171
|
export const SETTINGS_SCOPES = ["admin", "user", "host"];
|
|
172
|
+
/** A layout id or theme id: short, lowercase, used as a CSS class for themes. */
|
|
173
|
+
export const LOOK_ID_PATTERN = /^[a-z][a-z0-9-]{1,39}$/;
|
|
171
174
|
/** Protocol ids core keeps for itself. */
|
|
172
175
|
export const RESERVED_PROTOCOL_IDS = ["ssh"];
|
|
173
176
|
/** Login keys a declared credential field may not reuse. */
|
|
@@ -243,6 +246,8 @@ const ALLOWED_CONTRIBUTES = new Set([
|
|
|
243
246
|
"uses",
|
|
244
247
|
"keybindingActions",
|
|
245
248
|
"protocols",
|
|
249
|
+
"layouts",
|
|
250
|
+
"themes",
|
|
246
251
|
]);
|
|
247
252
|
const ALLOWED_SETTINGS_FIELD = [
|
|
248
253
|
"key",
|
|
@@ -734,6 +739,81 @@ function validateContributes(contributes, pluginId, errors) {
|
|
|
734
739
|
validateUiPresets(contributes.uiPresets, errors);
|
|
735
740
|
validateKeybindingActions(contributes.keybindingActions, errors);
|
|
736
741
|
validateProtocols(contributes.protocols, errors);
|
|
742
|
+
validateLayouts(contributes.layouts, errors);
|
|
743
|
+
validateThemes(contributes.themes, errors);
|
|
744
|
+
}
|
|
745
|
+
/** An array of objects with unique look ids; calls `each` for every valid entry. */
|
|
746
|
+
function validateLookList(value, field, allowed, errors, each) {
|
|
747
|
+
if (value === undefined)
|
|
748
|
+
return;
|
|
749
|
+
if (!Array.isArray(value)) {
|
|
750
|
+
errors.push(`Field "contributes.${field}" must be an array`);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const seen = new Set();
|
|
754
|
+
value.forEach((raw, index) => {
|
|
755
|
+
const where = `contributes.${field}[${index}]`;
|
|
756
|
+
if (!isPlainObject(raw)) {
|
|
757
|
+
errors.push(`${where} must be an object`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
rejectUnknown(raw, allowed, where, errors);
|
|
761
|
+
if (typeof raw.id !== "string" || !LOOK_ID_PATTERN.test(raw.id)) {
|
|
762
|
+
errors.push(`${where}.id must match ${LOOK_ID_PATTERN} (lowercase letters, digits, dashes)`);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
if (seen.has(raw.id))
|
|
766
|
+
errors.push(`${where}.id duplicates "${raw.id}"`);
|
|
767
|
+
seen.add(raw.id);
|
|
768
|
+
}
|
|
769
|
+
requireString(raw.label, `${where}.label`, errors);
|
|
770
|
+
each(raw, where);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
function validateLayouts(value, errors) {
|
|
774
|
+
validateLookList(value, "layouts", ["id", "label", "description", "defaultTheme", "default"], errors, (raw, where) => {
|
|
775
|
+
if (raw.description !== undefined)
|
|
776
|
+
requireString(raw.description, `${where}.description`, errors);
|
|
777
|
+
if (raw.defaultTheme !== undefined &&
|
|
778
|
+
(typeof raw.defaultTheme !== "string" ||
|
|
779
|
+
!LOOK_ID_PATTERN.test(raw.defaultTheme))) {
|
|
780
|
+
errors.push(`${where}.defaultTheme must be a theme id`);
|
|
781
|
+
}
|
|
782
|
+
if (raw.default !== undefined && typeof raw.default !== "boolean") {
|
|
783
|
+
errors.push(`${where}.default must be a boolean`);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
if (Array.isArray(value) &&
|
|
787
|
+
value.filter((raw) => isPlainObject(raw) && raw.default === true).length > 1) {
|
|
788
|
+
errors.push('Only one entry in "contributes.layouts" may set default');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
|
|
792
|
+
function validateThemes(value, errors) {
|
|
793
|
+
validateLookList(value, "themes", ["id", "label", "mode", "css", "swatch"], errors, (raw, where) => {
|
|
794
|
+
if (raw.mode !== "light" && raw.mode !== "dark") {
|
|
795
|
+
errors.push(`${where}.mode must be "light" or "dark"`);
|
|
796
|
+
}
|
|
797
|
+
if (requireString(raw.css, `${where}.css`, errors)) {
|
|
798
|
+
requireRelativePath(raw.css, `${where}.css`, errors);
|
|
799
|
+
if (!raw.css.endsWith(".css")) {
|
|
800
|
+
errors.push(`${where}.css must be a .css file`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (raw.swatch !== undefined) {
|
|
804
|
+
if (!isPlainObject(raw.swatch)) {
|
|
805
|
+
errors.push(`${where}.swatch must be an object`);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
rejectUnknown(raw.swatch, ["background", "panel", "text"], `${where}.swatch`, errors);
|
|
809
|
+
for (const key of ["background", "panel", "text"]) {
|
|
810
|
+
const color = raw.swatch[key];
|
|
811
|
+
if (typeof color !== "string" || !HEX_COLOR.test(color)) {
|
|
812
|
+
errors.push(`${where}.swatch.${key} must be a hex color`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
737
817
|
}
|
|
738
818
|
export const PROTOCOL_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
739
819
|
export const PROTOCOL_FIELD_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{0,31}$/;
|
|
@@ -1595,6 +1675,14 @@ export function parseManifest(raw) {
|
|
|
1595
1675
|
if (contributesAuth && !manifest.capabilities.includes("auth:provide")) {
|
|
1596
1676
|
errors.push('contributes.auth needs the "auth:provide" capability in the capabilities array');
|
|
1597
1677
|
}
|
|
1678
|
+
if ((manifest.contributes?.layouts?.length ?? 0) > 0 &&
|
|
1679
|
+
!manifest.capabilities.includes("ui:layout")) {
|
|
1680
|
+
errors.push('contributes.layouts needs the "ui:layout" capability in the capabilities array');
|
|
1681
|
+
}
|
|
1682
|
+
if ((manifest.contributes?.themes?.length ?? 0) > 0 &&
|
|
1683
|
+
!manifest.capabilities.includes("ui:theme")) {
|
|
1684
|
+
errors.push('contributes.themes needs the "ui:theme" capability in the capabilities array');
|
|
1685
|
+
}
|
|
1598
1686
|
for (const secret of manifest.requiresSecret ?? []) {
|
|
1599
1687
|
if (secret.plugin === manifest.id) {
|
|
1600
1688
|
errors.push(`requiresSecret["${secret.key}"] points at this plugin; use ctx.secrets.get instead`);
|
package/dist/testing/setup.js
CHANGED
|
@@ -89,6 +89,7 @@ globalThis.__termixTestPluginHost = {
|
|
|
89
89
|
useHostStatus: () => null,
|
|
90
90
|
useCurrentUser: () => null,
|
|
91
91
|
useTheme: () => ({ theme: "dark" }),
|
|
92
|
+
useShell: () => null,
|
|
92
93
|
toast: { success: noop, error: noop, info: noop, warning: noop },
|
|
93
94
|
getApi: () => ({
|
|
94
95
|
get: offline,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a theme's CSS before core serves it.
|
|
3
|
+
*
|
|
4
|
+
* A theme is colors and styles only, so its file may hold nothing but rules
|
|
5
|
+
* whose every selector starts with the theme's own class (`.nord`,
|
|
6
|
+
* `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
|
|
7
|
+
* Anything that could load from elsewhere or run code is refused: @import,
|
|
8
|
+
* url() to another host, other at-rules, expression() and javascript:.
|
|
9
|
+
*/
|
|
10
|
+
/** Every problem with a theme's CSS, empty when it is safe to serve. */
|
|
11
|
+
export declare function validateThemeCss(css: string, themeId: string): string[];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a theme's CSS before core serves it.
|
|
3
|
+
*
|
|
4
|
+
* A theme is colors and styles only, so its file may hold nothing but rules
|
|
5
|
+
* whose every selector starts with the theme's own class (`.nord`,
|
|
6
|
+
* `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
|
|
7
|
+
* Anything that could load from elsewhere or run code is refused: @import,
|
|
8
|
+
* url() to another host, other at-rules, expression() and javascript:.
|
|
9
|
+
*/
|
|
10
|
+
const ALLOWED_BLOCK_AT_RULES = new Set(["media", "supports"]);
|
|
11
|
+
function stripComments(css) {
|
|
12
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
13
|
+
}
|
|
14
|
+
/** Splits on commas that are not inside parentheses or brackets. */
|
|
15
|
+
function splitSelectors(prelude) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let current = "";
|
|
19
|
+
for (const ch of prelude) {
|
|
20
|
+
if (ch === "(" || ch === "[")
|
|
21
|
+
depth += 1;
|
|
22
|
+
if (ch === ")" || ch === "]")
|
|
23
|
+
depth -= 1;
|
|
24
|
+
if (ch === "," && depth === 0) {
|
|
25
|
+
parts.push(current);
|
|
26
|
+
current = "";
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
current += ch;
|
|
30
|
+
}
|
|
31
|
+
parts.push(current);
|
|
32
|
+
return parts.map((part) => part.trim());
|
|
33
|
+
}
|
|
34
|
+
function withoutStrings(css) {
|
|
35
|
+
return css.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
|
36
|
+
}
|
|
37
|
+
function escapeRegExp(value) {
|
|
38
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
39
|
+
}
|
|
40
|
+
/** Index of the brace closing the one at `open`, skipping strings. */
|
|
41
|
+
function matchingBrace(css, open) {
|
|
42
|
+
let depth = 0;
|
|
43
|
+
let quote = null;
|
|
44
|
+
for (let i = open; i < css.length; i += 1) {
|
|
45
|
+
const ch = css[i];
|
|
46
|
+
if (quote) {
|
|
47
|
+
if (ch === "\\")
|
|
48
|
+
i += 1;
|
|
49
|
+
else if (ch === quote)
|
|
50
|
+
quote = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '"' || ch === "'")
|
|
54
|
+
quote = ch;
|
|
55
|
+
else if (ch === "{")
|
|
56
|
+
depth += 1;
|
|
57
|
+
else if (ch === "}") {
|
|
58
|
+
depth -= 1;
|
|
59
|
+
if (depth === 0)
|
|
60
|
+
return i;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return -1;
|
|
64
|
+
}
|
|
65
|
+
function checkUrls(css, errors) {
|
|
66
|
+
const urls = css.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi);
|
|
67
|
+
for (const match of urls) {
|
|
68
|
+
const target = match[2].trim();
|
|
69
|
+
if (target.startsWith("data:"))
|
|
70
|
+
continue;
|
|
71
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) {
|
|
72
|
+
errors.push(`url(${target}) points outside the plugin`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function checkRules(css, scope, themeId, errors) {
|
|
77
|
+
let index = 0;
|
|
78
|
+
while (index < css.length) {
|
|
79
|
+
const rest = css.slice(index);
|
|
80
|
+
if (rest.trim() === "")
|
|
81
|
+
return;
|
|
82
|
+
const open = css.indexOf("{", index);
|
|
83
|
+
const semicolon = css.indexOf(";", index);
|
|
84
|
+
if (semicolon !== -1 && (open === -1 || semicolon < open)) {
|
|
85
|
+
const statement = css.slice(index, semicolon).trim();
|
|
86
|
+
errors.push(statement.startsWith("@")
|
|
87
|
+
? `${statement.split(/\s/)[0]} is not allowed in a theme`
|
|
88
|
+
: `Unexpected "${statement}" outside a rule`);
|
|
89
|
+
index = semicolon + 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (open === -1) {
|
|
93
|
+
errors.push(`Unexpected "${rest.trim()}" at the end`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const close = matchingBrace(css, open);
|
|
97
|
+
if (close === -1) {
|
|
98
|
+
errors.push("A rule is missing its closing brace");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const prelude = css.slice(index, open).trim();
|
|
102
|
+
const body = css.slice(open + 1, close);
|
|
103
|
+
if (prelude.startsWith("@")) {
|
|
104
|
+
const name = prelude.slice(1).split(/[\s(]/)[0].toLowerCase();
|
|
105
|
+
if (ALLOWED_BLOCK_AT_RULES.has(name)) {
|
|
106
|
+
checkRules(body, scope, themeId, errors);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
errors.push(`@${name} is not allowed in a theme`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const selector of splitSelectors(prelude)) {
|
|
114
|
+
if (!scope.test(selector)) {
|
|
115
|
+
errors.push(`Selector "${selector}" must start with .${themeId}, :root.${themeId} or html.${themeId}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (withoutStrings(body).includes("{")) {
|
|
119
|
+
errors.push(`Nested rules are not allowed in "${prelude}"`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
index = close + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Every problem with a theme's CSS, empty when it is safe to serve. */
|
|
126
|
+
export function validateThemeCss(css, themeId) {
|
|
127
|
+
const errors = [];
|
|
128
|
+
const clean = stripComments(css);
|
|
129
|
+
if (/@import\b/i.test(clean))
|
|
130
|
+
errors.push("@import is not allowed in a theme");
|
|
131
|
+
if (/expression\s*\(/i.test(clean))
|
|
132
|
+
errors.push("expression() is not allowed in a theme");
|
|
133
|
+
if (/javascript:/i.test(clean))
|
|
134
|
+
errors.push("javascript: is not allowed in a theme");
|
|
135
|
+
if (/-moz-binding|behavior\s*:/i.test(clean))
|
|
136
|
+
errors.push("Script bindings are not allowed in a theme");
|
|
137
|
+
checkUrls(clean, errors);
|
|
138
|
+
const id = escapeRegExp(themeId);
|
|
139
|
+
const scope = new RegExp(`^(?::root|html)?\\.${id}(?![\\w-])`);
|
|
140
|
+
checkRules(clean, scope, themeId, errors);
|
|
141
|
+
return [...new Set(errors)];
|
|
142
|
+
}
|
package/dist/vitest-preset.d.ts
CHANGED
|
@@ -27,8 +27,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
27
27
|
enforce: "pre";
|
|
28
28
|
load(id: string): string;
|
|
29
29
|
}[];
|
|
30
|
+
server: {
|
|
31
|
+
fs: {
|
|
32
|
+
allow: string[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
30
35
|
resolve: {
|
|
31
36
|
alias: Record<string, string>;
|
|
37
|
+
dedupe: string[];
|
|
32
38
|
};
|
|
33
39
|
test: {
|
|
34
40
|
globals: boolean;
|
|
@@ -40,8 +46,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
40
46
|
enforce: "pre";
|
|
41
47
|
load(id: string): string;
|
|
42
48
|
}[];
|
|
49
|
+
server: {
|
|
50
|
+
fs: {
|
|
51
|
+
allow: string[];
|
|
52
|
+
};
|
|
53
|
+
};
|
|
43
54
|
resolve: {
|
|
44
55
|
alias: Record<string, string>;
|
|
56
|
+
dedupe: string[];
|
|
45
57
|
};
|
|
46
58
|
test: {
|
|
47
59
|
name: string;
|
|
@@ -59,8 +71,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
|
|
|
59
71
|
enforce: "pre";
|
|
60
72
|
load(id: string): string;
|
|
61
73
|
}[];
|
|
74
|
+
server: {
|
|
75
|
+
fs: {
|
|
76
|
+
allow: string[];
|
|
77
|
+
};
|
|
78
|
+
};
|
|
62
79
|
resolve: {
|
|
63
80
|
alias: Record<string, string>;
|
|
81
|
+
dedupe: string[];
|
|
64
82
|
};
|
|
65
83
|
test: {
|
|
66
84
|
name: string;
|
package/dist/vitest-preset.js
CHANGED
|
@@ -80,9 +80,18 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
80
80
|
// unexpectedly". Threads don't.
|
|
81
81
|
const pool = "threads";
|
|
82
82
|
const plugins = [wasmBinaryPlugin()];
|
|
83
|
+
// A local SDK linked into node_modules sits outside the plugin folder, so
|
|
84
|
+
// Vite must be allowed to read it, and React must come from the plugin.
|
|
85
|
+
const sdkDir = fs.realpathSync(fileURLToPath(new URL("..", import.meta.url)));
|
|
86
|
+
const server = { fs: { allow: [pluginDir, sdkDir] } };
|
|
87
|
+
const resolve = {
|
|
88
|
+
alias,
|
|
89
|
+
dedupe: ["react", "react-dom", "react-i18next", "i18next"],
|
|
90
|
+
};
|
|
83
91
|
return {
|
|
84
92
|
plugins,
|
|
85
|
-
|
|
93
|
+
server,
|
|
94
|
+
resolve,
|
|
86
95
|
test: {
|
|
87
96
|
globals: true,
|
|
88
97
|
setupFiles: [setupFile],
|
|
@@ -90,7 +99,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
90
99
|
projects: [
|
|
91
100
|
{
|
|
92
101
|
plugins,
|
|
93
|
-
|
|
102
|
+
server,
|
|
103
|
+
resolve,
|
|
94
104
|
test: {
|
|
95
105
|
name: `${pluginId}:backend`,
|
|
96
106
|
root: pluginDir,
|
|
@@ -103,7 +113,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
|
|
|
103
113
|
},
|
|
104
114
|
{
|
|
105
115
|
plugins,
|
|
106
|
-
|
|
116
|
+
server,
|
|
117
|
+
resolve,
|
|
107
118
|
test: {
|
|
108
119
|
name: `${pluginId}:frontend`,
|
|
109
120
|
root: pluginDir,
|
package/package.json
CHANGED
|
@@ -82,7 +82,11 @@
|
|
|
82
82
|
"features": {
|
|
83
83
|
"type": "array",
|
|
84
84
|
"maxItems": 20,
|
|
85
|
-
"items": {
|
|
85
|
+
"items": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"minLength": 1,
|
|
88
|
+
"maxLength": 160
|
|
89
|
+
},
|
|
86
90
|
"description": "Short lines listing what the plugin does, shown on its page."
|
|
87
91
|
},
|
|
88
92
|
"docs": {
|
|
@@ -98,11 +102,24 @@
|
|
|
98
102
|
"additionalProperties": false,
|
|
99
103
|
"required": ["name", "description"],
|
|
100
104
|
"properties": {
|
|
101
|
-
"name": {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
"
|
|
105
|
+
"name": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"pattern": "^[A-Z][A-Z0-9_]{0,63}$"
|
|
108
|
+
},
|
|
109
|
+
"description": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"minLength": 1,
|
|
112
|
+
"maxLength": 300
|
|
113
|
+
},
|
|
114
|
+
"default": {
|
|
115
|
+
"type": "string"
|
|
116
|
+
},
|
|
117
|
+
"required": {
|
|
118
|
+
"type": "boolean"
|
|
119
|
+
},
|
|
120
|
+
"secret": {
|
|
121
|
+
"type": "boolean"
|
|
122
|
+
}
|
|
106
123
|
}
|
|
107
124
|
}
|
|
108
125
|
},
|
|
@@ -155,7 +172,11 @@
|
|
|
155
172
|
"files:own",
|
|
156
173
|
"secrets:own",
|
|
157
174
|
"settings:read-core",
|
|
158
|
-
"ui:surface"
|
|
175
|
+
"ui:surface",
|
|
176
|
+
"notify:hub",
|
|
177
|
+
"plugins:read",
|
|
178
|
+
"ui:layout",
|
|
179
|
+
"ui:theme"
|
|
159
180
|
]
|
|
160
181
|
}
|
|
161
182
|
},
|
|
@@ -944,7 +965,9 @@
|
|
|
944
965
|
"defaultLevels": {
|
|
945
966
|
"type": "array",
|
|
946
967
|
"minItems": 1,
|
|
947
|
-
"items": {
|
|
968
|
+
"items": {
|
|
969
|
+
"enum": ["admin", "user", "folder"]
|
|
970
|
+
},
|
|
948
971
|
"description": "Levels that may set a default for the field. All three by default."
|
|
949
972
|
},
|
|
950
973
|
"personal": {
|
|
@@ -1127,6 +1150,86 @@
|
|
|
1127
1150
|
"type": "string",
|
|
1128
1151
|
"pattern": "^[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$"
|
|
1129
1152
|
}
|
|
1153
|
+
},
|
|
1154
|
+
"layouts": {
|
|
1155
|
+
"type": "array",
|
|
1156
|
+
"description": "Whole-app layouts the frontend registers with app.registerLayout. Needs the ui:layout capability.",
|
|
1157
|
+
"items": {
|
|
1158
|
+
"type": "object",
|
|
1159
|
+
"additionalProperties": false,
|
|
1160
|
+
"required": ["id", "label"],
|
|
1161
|
+
"properties": {
|
|
1162
|
+
"id": {
|
|
1163
|
+
"type": "string",
|
|
1164
|
+
"pattern": "^[a-z][a-z0-9-]{1,39}$"
|
|
1165
|
+
},
|
|
1166
|
+
"label": {
|
|
1167
|
+
"type": "string",
|
|
1168
|
+
"minLength": 1,
|
|
1169
|
+
"description": "i18n key in the plugin namespace, or plain text."
|
|
1170
|
+
},
|
|
1171
|
+
"description": {
|
|
1172
|
+
"type": "string",
|
|
1173
|
+
"minLength": 1
|
|
1174
|
+
},
|
|
1175
|
+
"defaultTheme": {
|
|
1176
|
+
"type": "string",
|
|
1177
|
+
"pattern": "^[a-z][a-z0-9-]{1,39}$",
|
|
1178
|
+
"description": "Theme picked with this layout when nobody chose one."
|
|
1179
|
+
},
|
|
1180
|
+
"default": {
|
|
1181
|
+
"type": "boolean",
|
|
1182
|
+
"description": "The layout a default-look plugin starts with. At most one."
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
"themes": {
|
|
1188
|
+
"type": "array",
|
|
1189
|
+
"description": "Themes: one CSS file each with token rules scoped under .<id>. Needs the ui:theme capability.",
|
|
1190
|
+
"items": {
|
|
1191
|
+
"type": "object",
|
|
1192
|
+
"additionalProperties": false,
|
|
1193
|
+
"required": ["id", "label", "mode", "css"],
|
|
1194
|
+
"properties": {
|
|
1195
|
+
"id": {
|
|
1196
|
+
"type": "string",
|
|
1197
|
+
"pattern": "^[a-z][a-z0-9-]{1,39}$"
|
|
1198
|
+
},
|
|
1199
|
+
"label": {
|
|
1200
|
+
"type": "string",
|
|
1201
|
+
"minLength": 1
|
|
1202
|
+
},
|
|
1203
|
+
"mode": {
|
|
1204
|
+
"type": "string",
|
|
1205
|
+
"enum": ["light", "dark"]
|
|
1206
|
+
},
|
|
1207
|
+
"css": {
|
|
1208
|
+
"type": "string",
|
|
1209
|
+
"pattern": ".css$",
|
|
1210
|
+
"description": "Path of the CSS file inside the plugin."
|
|
1211
|
+
},
|
|
1212
|
+
"swatch": {
|
|
1213
|
+
"type": "object",
|
|
1214
|
+
"additionalProperties": false,
|
|
1215
|
+
"required": ["background", "panel", "text"],
|
|
1216
|
+
"properties": {
|
|
1217
|
+
"background": {
|
|
1218
|
+
"type": "string",
|
|
1219
|
+
"pattern": "^#[0-9a-fA-F]{3,8}$"
|
|
1220
|
+
},
|
|
1221
|
+
"panel": {
|
|
1222
|
+
"type": "string",
|
|
1223
|
+
"pattern": "^#[0-9a-fA-F]{3,8}$"
|
|
1224
|
+
},
|
|
1225
|
+
"text": {
|
|
1226
|
+
"type": "string",
|
|
1227
|
+
"pattern": "^#[0-9a-fA-F]{3,8}$"
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1130
1233
|
}
|
|
1131
1234
|
}
|
|
1132
1235
|
},
|
package/tsconfig.plugin.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const __termixNoGlob = () => ({});
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __export = (target, all) => {
|
|
4
|
-
for (var name in all)
|
|
5
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
// <define:import.meta.env>
|
|
9
|
-
var define_import_meta_env_default = { DEV: false, PROD: true, MODE: "production", SSR: false, BASE_URL: "/" };
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
__export,
|
|
13
|
-
define_import_meta_env_default
|
|
14
|
-
};
|