Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/app-authoring-lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"apps": minor
---

Export offline source checks and an Oxlint plugin for app-authoring conventions.
45 changes: 45 additions & 0 deletions apps/docs/content/author-an-app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,51 @@ app. An unavailable or unsupported package fails the build and leaves the active
app unchanged. The host protocol is small and shared; early beta releases do not
promise indefinite support for every past version.

## Check source locally

Before creating or deploying an app, run `executor apps check --files ./hello`.
It reads the directory locally and checks the manifest, source paths, UTF-8 text,
and source/public-file limits. It does not contact a host or change the files.
It requires an exact `dependencies.apps` pin and keeps the version you tested;
it does not compare that pin with the current host release. A host can still
refuse a release it does not support.

Use Oxlint for app-authoring conventions. Generate its config from the `apps`
package:

```js
import { writeFile } from "node:fs/promises";
import { appConfig } from "apps/oxlint";

await writeFile(".oxlintrc.json", JSON.stringify(appConfig(), null, 2));
```

Then run the Oxlint CLI with that config. The rules reject host-only imports,
global HTTP in app code, legacy account access, missing mutation approval, and a
root entry that does not default-export `defineApp`. Browser code under `ui/`
may use browser HTTP, but CommonJS loading through global `require`, global
`module`, or `node:module`/`module` imports is rejected throughout the app.

Framework calls must keep their identity. Import `defineApp`, `mutation`,
`query` and `decodeJson` by name from exactly `apps`, in the module that calls
them. A renamed import such as `import { mutation as write } from "apps"` is
allowed when the imported function is called directly. The app entry must use
`export default defineApp(...)` directly; parentheses and TypeScript expression
wrappers are allowed.
Import `QueryContext` and `MutationContext` types directly from `apps` in the
module that uses them. Do not re-export or alias these types through a helper.
Use ESM imports in helpers and browser code too.

Do not assign these functions to another value, destructure them, pass or return
them, or re-export them. Default, namespace and dynamic imports of `apps` are
rejected. Every app source file is checked, including helpers, so a local barrel
cannot hide a framework function. Ordinary helper modules can use named,
default, namespace or literal dynamic imports. DTO fields named `mutation` or
`write` remain valid.

These checks inspect source structure. They do not type-check the app, evaluate
application code, or prove that a host can build and run it.

`defineApp` declares behavior and does not take a name. Set the package name
in `package.json`, such as `"name": "@team.executor.community/calendar"` when
publishing under the organization `team`. The name
Expand Down
60 changes: 34 additions & 26 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions e2e/test-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8449,7 +8449,7 @@ export const scenarios = plan({
localAppsCliFailures: {
file: "local-apps-cli.spec.ts",
title:
"apps CLI reports the host's error code, status and fields as text and JSON, says when the CLI is out of date, refuses source over the limits or not UTF-8 before uploading, and reads EXECUTOR_HOST",
"apps CLI reports host errors, refuses source over the limits or not UTF-8 before uploading, checks source offline, and reads EXECUTOR_HOST",
targets: {
local: scheduled,
"self-host": na("The CLI's hosted path needs a browser OAuth login; local uses an API key."),
Expand All @@ -8469,7 +8469,7 @@ export const scenarios = plan({
localAppsCliRelease: {
file: "local-apps-cli.spec.ts",
title:
"apps CLI prints the host's apps release, requires --files, and deploys the documented two-file app; the local Executor app pins the same release",
"apps CLI prints the host's apps release, checks app metadata offline, lints app conventions and deploys the documented two-file app",
targets: {
local: scheduled,
"self-host": na("The CLI's hosted path needs a browser OAuth login; local uses an API key."),
Expand Down
Loading