Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions docs-site/src/content/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,42 @@ Import the types you need:
import type { PluginContext, ViewDescriptor } from "@appos.space/plugin-types";
```

The package exposes module exports only — it ships no ambient (global)
declarations, so types are always imported by name. `import type` is erased at
The main entry exposes module exports only — importing it declares nothing
global, so these types are always imported by name. `import type` is erased at
compile time, so this adds nothing to your bundle.

### Opt-in globals subpath

There is ONE opt-in exception: `@appos.space/plugin-types/globals` declares
the host-injected `URL` global (a Foundation-bridged constructor, targeted
for host 1.1.0 — typed `URLConstructor | undefined`, so ALWAYS guard before
use: older hosts, menu-bar contexts, and a user kill switch can each leave
it undefined regardless of `minHostVersion`). It applies only to
compilations that reference it. Opt in from your plugin entry file:

```ts
/// <reference types="@appos.space/plugin-types/globals" />

if (typeof URL === "function" && URL.canParse(raw)) {
const host = new URL(raw).hostname;
}
```

(or add `"types": ["@appos.space/plugin-types/globals"]` to your tsconfig's
`compilerOptions`.)

Only reference the subpath from plugin-runtime (JavaScriptCore) tsconfigs,
and that tsconfig MUST use a DOM-free `lib` (e.g. `"lib": ["ES2020"]`).
Never reference it from webview code compiled against `lib.dom` — the
browser already has its own `URL`. Don't count on the compiler to catch
that mistake: the two declarations do conflict, but with `skipLibCheck`
enabled (the default in most scaffolds, including `tsc --init`) TypeScript
suppresses declaration-file conflicts and silently merges the interfaces
instead — browser-only members like `searchParams`, mutable accessors, and
unguarded `new URL(...)` can then type-check even though the JSC runtime
has the narrower optional contract. The DOM-free `lib` is the only
reliable isolation. The subpath's docblock documents the runtime's
Foundation-vs-WHATWG divergences and the v1 subset (`url.searchParams` is
absent and throws at runtime — parse `url.search` manually).

Next: [write your first plugin](/getting-started/first-plugin/).
5 changes: 5 additions & 0 deletions docs-site/tsconfig.typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
],
"exclude": [
"../packages/plugin-types/src/__tests__",
// The opt-in globals subpath is deliberately OUTSIDE the API-reference
// entry point (index.ts), and its `declare global { var URL ... }`
// collides with lib.dom's URL — which this program pulls in via the
// default full lib because plugin-utils needs `globalThis.crypto`.
"../packages/plugin-types/src/globals.ts",
"../packages/view-builders/src/__tests__",
"../packages/plugin-utils/src/__tests__"
]
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
"engines": {
"node": ">=18"
},
"version": "3.0.0"
"version": "3.0.1"
}
56 changes: 54 additions & 2 deletions packages/plugin-types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ npm install --save-dev @appos.space/plugin-types

## Usage

Import the types you need (the package ships module exports only — no
ambient globals):
Import the types you need (the main entry ships module exports only; the
ONE exception is the opt-in globals subpath — see
[Host-injected globals](#host-injected-globals-opt-in) below):

```ts
import type {
Expand All @@ -31,6 +32,57 @@ export async function activate(ctx: PluginContext) {
}
```

## Host-injected globals (opt-in)

AppOS hosts inject a **Foundation-bridged `URL` constructor** into the
JavaScriptCore plugin runtime (targeted for host 1.1.0). The matching
ambient declaration ships as a SEPARATE opt-in subpath,
`@appos.space/plugin-types/globals`, so nothing global leaks into projects
that don't reference it. Opt in from your plugin entry file:

```ts
/// <reference types="@appos.space/plugin-types/globals" />
```

or in `tsconfig.json`:

```json
{ "compilerOptions": { "types": ["@appos.space/plugin-types/globals"] } }
```

The global is typed `URLConstructor | undefined` — older hosts, menu-bar
`JSContext` pools, and the `appos.jsc.urlGlobal.disabled` kill switch all
leave it undefined. ALWAYS guard before use. Pinning your manifest's
`minHostVersion` to an injecting host release removes only the older-host
reason for absence — it does not override the kill switch or the menu-bar
limitation, so unguarded use can still crash at runtime:

```ts
if (typeof URL === "function" && URL.canParse(raw)) {
const u = new URL(raw);
// u.hostname parses identically to the host's own security validators
}
```

Notes:

- **Foundation (RFC 3986) semantics, not a WHATWG polyfill.** The pinned
divergences are documented in the subpath's docblock: default ports
retained in `href`/`port`, empty path stays `""`, out-of-range ports
accepted, double-encode on href round-trip of pre-encoded query values,
`hostname` lowercased with IPv6 unbracketed (`host`/`origin` re-bracket).
- **`url.searchParams` is NOT in the v1 subset** — the type omits it and
the runtime getter throws a `TypeError`; parse `url.search` manually.
`URL.parse` is likewise absent, and all accessors are readonly.
- **Reference the subpath only from a DOM-free tsconfig** (e.g.
`"lib": ["ES2020"]`) — never from webview code compiled against `lib.dom`,
which already has its own `URL`. The two declarations conflict, but don't
rely on that as a safeguard: with `skipLibCheck` enabled (the default in
most scaffolds) TypeScript suppresses declaration-file conflicts and
silently merges the interfaces, so browser-only members (`searchParams`,
mutable accessors, unguarded construction) can type-check against the
narrower JSC runtime. The DOM-free `lib` is the only reliable isolation.

## What's included

- **Core** — `PluginContext`, `PluginManifest`, activation lifecycle
Expand Down
12 changes: 12 additions & 0 deletions packages/plugin-types/fixtures/globals/jsc-types-array/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Fixture: the tsconfig `types` array form of the opt-in —
* `"types": ["@appos.space/plugin-types/globals"]` — with NO triple-slash
* reference line in the source.
*
* MUST COMPILE CLEANLY (same guarded usage as `jsc-with-globals`).
*/
export function hostOf(raw: string): string | null {
if (typeof URL !== "function") return null;
if (!URL.canParse(raw)) return null;
return new URL(raw).hostname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"types": ["@appos.space/plugin-types/globals"]
},
"include": ["main.ts"]
}
68 changes: 68 additions & 0 deletions packages/plugin-types/fixtures/globals/jsc-with-globals/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/// <reference types="@appos.space/plugin-types/globals" />
/**
* Fixture: a JSC-runtime plugin tsconfig (lib ES2020, no DOM) that OPTS IN
* to the globals subpath via the triple-slash reference line above.
*
* MUST COMPILE CLEANLY. Every `@ts-expect-error` line below is
* self-checking: if the expected error stops firing, the compile fails
* with "Unused '@ts-expect-error' directive".
*/
import type { PluginContext } from "@appos.space/plugin-types";

export function hostOf(ctx: PluginContext, raw: string): string | null {
void ctx;
// Canonical guard: typeof narrowing removes `undefined`.
if (typeof URL !== "function") return null;
const ok: boolean = URL.canParse(raw);
if (!ok) return null;
const u = new URL(raw);
// Base form — both string and URL bases are accepted.
const resolved = new URL("/path?q=1#frag", u);
const alsoResolved = new URL("/other", raw);
void alsoResolved;
// The full v1 accessor subset typechecks as strings.
const parts: string[] = [
u.href,
u.protocol,
u.hostname,
u.host,
u.port,
u.pathname,
u.search,
u.hash,
u.origin,
u.username,
u.password,
u.toString(),
u.toJSON(),
resolved.href,
];
void parts;
return u.hostname;
}

export function typeofUndefinedGuard(raw: string): string | null {
// The `typeof URL === "undefined"` early-return form also narrows.
//
// NOTE: `typeof` is the ONLY absence-safe guard. A bare-reference check
// like `if (URL)` (or `URL && ...`, or `URL?.canParse(...)`) typechecks —
// the `| undefined` in the declaration is compile-time only — but throws
// ReferenceError at runtime on hosts where the global binding was never
// installed (older hosts, menu-bar contexts, or the
// `appos.jsc.urlGlobal.disabled` kill switch).
if (typeof URL === "undefined") return null;
return new URL(raw).href;
}

declare const unguardedInput: string;

// @ts-expect-error — URL is optionally typed; unguarded `new URL(...)` must not compile.
export const unguarded = new URL(unguardedInput);

declare const someUrl: URL;

// @ts-expect-error — searchParams is deliberately OUT of the v1 subset (runtime getter throws TypeError).
void someUrl.searchParams;

// @ts-expect-error — accessors are readonly; assignment must not compile.
someUrl.hostname = "example.com";
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"types": []
},
"include": ["main.ts"]
}
14 changes: 14 additions & 0 deletions packages/plugin-types/fixtures/globals/jsc-without-globals/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Fixture: the SAME JSC-runtime tsconfig as `jsc-with-globals`, but WITHOUT
* the globals reference line. It still imports the package MAIN entry —
* proving the main entry drags no ambient `URL` into scope.
*
* MUST FAIL to compile, and every diagnostic must be TS2304
* ("Cannot find name 'URL'").
*/
import type { PluginContext } from "@appos.space/plugin-types";

declare const ctx: PluginContext;
void ctx;

export const leaked = new URL("https://example.com/");
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"types": []
},
"include": ["main.ts"]
}
Loading
Loading