diff --git a/apps/cloud/drizzle/0022_productive_anthem.sql b/apps/cloud/drizzle/0022_productive_anthem.sql new file mode 100644 index 0000000000..8d551fb746 --- /dev/null +++ b/apps/cloud/drizzle/0022_productive_anthem.sql @@ -0,0 +1,3 @@ +ALTER TABLE "artifact" ADD COLUMN "compiled_fingerprint" text;--> statement-breakpoint +ALTER TABLE "artifact" ADD COLUMN "compiled_js" text;--> statement-breakpoint +ALTER TABLE "artifact" ADD COLUMN "compiled_css" text; \ No newline at end of file diff --git a/apps/cloud/drizzle/meta/0022_snapshot.json b/apps/cloud/drizzle/meta/0022_snapshot.json new file mode 100644 index 0000000000..ba7d8e1d3f --- /dev/null +++ b/apps/cloud/drizzle/meta/0022_snapshot.json @@ -0,0 +1,1821 @@ +{ + "id": "18cfbe2f-8cc3-4c9f-8501-680178204093", + "prevId": "003096c7-6702-4d05-905a-8fcc608faa3a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workos_updated_at": { + "name": "workos_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sign_in_at": { + "name": "last_sign_in_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_email_lower_idx": { + "name": "accounts_email_lower_idx", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.membership_tombstones": { + "name": "membership_tombstones", + "schema": "", + "columns": { + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "membership_tombstones_organization_id_idx": { + "name": "membership_tombstones_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "membership_tombstones_account_id_accounts_id_fk": { + "name": "membership_tombstones_account_id_accounts_id_fk", + "tableFrom": "membership_tombstones", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "membership_tombstones_organization_id_organizations_id_fk": { + "name": "membership_tombstones_organization_id_organizations_id_fk", + "tableFrom": "membership_tombstones", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "workos_updated_at": { + "name": "workos_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_membership_id_unique": { + "name": "memberships_membership_id_unique", + "columns": [ + { + "expression": "membership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_organization_id_idx": { + "name": "memberships_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_account_id_organization_id_pk": { + "name": "memberships_account_id_organization_id_pk", + "columns": [ + "account_id", + "organization_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backfilled_at": { + "name": "backfilled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "workos_updated_at": { + "name": "workos_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workos_sync": { + "name": "workos_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "range_start": { + "name": "range_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "backfill_completed_at": { + "name": "backfill_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "drained_at": { + "name": "drained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.artifact": { + "name": "artifact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bindings": { + "name": "bindings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "preview": { + "name": "preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compiled_fingerprint": { + "name": "compiled_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compiled_js": { + "name": "compiled_js", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "compiled_css": { + "name": "compiled_css", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "artifact_uidx": { + "name": "artifact_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob": { + "name": "blob", + "schema": "", + "columns": { + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "blob_id_uidx": { + "name": "blob_id_uidx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection": { + "name": "connection", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_ids": { + "name": "item_ids", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "credential_write": { + "name": "credential_write", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_health": { + "name": "last_health", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "tools_synced_at": { + "name": "tools_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tools_sync_started_at": { + "name": "tools_sync_started_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "oauth_client": { + "name": "oauth_client", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_owner": { + "name": "oauth_client_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_item_id": { + "name": "refresh_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "refresh_lease_until": { + "name": "refresh_lease_until", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "refresh_lease_holder": { + "name": "refresh_lease_holder", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_refreshed_at": { + "name": "oauth_refreshed_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "oauth_scope": { + "name": "oauth_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_token_url": { + "name": "oauth_token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_state": { + "name": "provider_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_uidx": { + "name": "connection_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.definition": { + "name": "definition", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "definition_uidx": { + "name": "definition_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "health_check": { + "name": "health_check", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "config_revised_at": { + "name": "config_revised_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "can_remove": { + "name": "can_remove", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "integration_uidx": { + "name": "integration_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant": { + "name": "grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_item_id": { + "name": "client_secret_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_write": { + "name": "credential_write", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_integration": { + "name": "origin_integration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_issuer": { + "name": "origin_issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_redirect_uri": { + "name": "origin_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_client_uidx": { + "name": "oauth_client_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_session": { + "name": "oauth_session", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "client_slug": { + "name": "client_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pkce_verifier": { + "name": "pkce_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_session_uidx": { + "name": "oauth_session_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "collection": { + "name": "collection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "plugin_storage_uidx": { + "name": "plugin_storage_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "collection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.private_executor_cloud_settings": { + "name": "private_executor_cloud_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subject": { + "name": "subject", + "schema": "", + "columns": { + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "subject_uidx": { + "name": "subject_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_schema": { + "name": "input_schema", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_uidx": { + "name": "tool_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policy": { + "name": "tool_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_policy_uidx": { + "name": "tool_policy_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json index d6fc154426..07930eac04 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1790359874309, "tag": "0021_connection_refresh_lease", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1792307915126, + "tag": "0022_productive_anthem", + "breakpoints": true } ] } diff --git a/apps/cloud/src/db/executor-schema.ts b/apps/cloud/src/db/executor-schema.ts index 63ef229cf5..2721636357 100644 --- a/apps/cloud/src/db/executor-schema.ts +++ b/apps/cloud/src/db/executor-schema.ts @@ -242,6 +242,9 @@ export const artifact = pgTable( code: text("code").notNull(), bindings: json("bindings"), preview: text("preview"), + compiled_fingerprint: text("compiled_fingerprint"), + compiled_js: text("compiled_js"), + compiled_css: text("compiled_css"), created_at: timestamp("created_at").notNull(), updated_at: timestamp("updated_at").notNull(), row_id: varchar("row_id", { length: 255 }) diff --git a/apps/host-cloudflare/src/db/d1.test.ts b/apps/host-cloudflare/src/db/d1.test.ts index 871f78c01f..83f9788090 100644 --- a/apps/host-cloudflare/src/db/d1.test.ts +++ b/apps/host-cloudflare/src/db/d1.test.ts @@ -110,6 +110,32 @@ describe("createD1ExecutorDb", () => { await sqlite.close(); }); + it("adds the compiled artifact columns to a database that predates them", async () => { + const sqlite = await createSqliteTestFumaDb({ tables: collectTables() }); + const { db } = makeRecordingD1(sqlite.client); + const first = await createD1ExecutorDb(db, undefined); + await first.close(); + for (const column of ["compiled_fingerprint", "compiled_js", "compiled_css"]) { + await sqlite.client.execute(`ALTER TABLE "artifact" DROP COLUMN "${column}"`); + } + await sqlite.client.execute( + `UPDATE private_executor_cloudflare_schema_fingerprint + SET fingerprint = 'stale' + WHERE id = 'runtime-schema'`, + ); + + const second = await createD1ExecutorDb(db, undefined); + await second.close(); + + const columns = await sqlite.client.execute(`PRAGMA table_info("artifact")`); + const names = columns.rows.map((row) => String(row.name)); + expect(names).toEqual( + expect.arrayContaining(["compiled_fingerprint", "compiled_js", "compiled_css"]), + ); + + await sqlite.close(); + }); + it("converges safely when concurrent opens see the same stale fingerprint", async () => { const sqlite = await createSqliteTestFumaDb({ tables: collectTables() }); const { db } = makeRecordingD1(sqlite.client); diff --git a/apps/host-cloudflare/src/execution.ts b/apps/host-cloudflare/src/execution.ts index 44de35104b..a63afea50b 100644 --- a/apps/host-cloudflare/src/execution.ts +++ b/apps/host-cloudflare/src/execution.ts @@ -10,6 +10,7 @@ import { PluginsProvider, type ExecutorDbHandle, } from "@executor-js/api/server"; +import { ARTIFACT_COMPILE_FINGERPRINT } from "@executor-js/mcp-apps-shell/compile-fingerprint"; import { makeDynamicWorkerExecutor } from "@executor-js/runtime-dynamic-worker"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import { env } from "cloudflare:workers"; @@ -62,6 +63,15 @@ export const makeCloudflareHostConfig = (config: CloudflareConfig): Layer.Layer< // memory. Rebuild stale catalogs one at a time so many of them cannot take // the session down together. toolsSyncConcurrency: 1, + // Tailwind and Sucrase load on the first save or render read, not at + // Worker startup. + artifactCompiler: { + fingerprint: ARTIFACT_COMPILE_FINGERPRINT, + compile: (code) => + Effect.promise(() => + import("@executor-js/mcp-apps-shell/precompile").then((m) => m.precompileArtifact(code)), + ), + }, // Absent unless both gateway ids are set, which leaves search lexical. ...(config.jevGateway === undefined ? {} : { jevGateway: config.jevGateway }), }); diff --git a/bun.lock b/bun.lock index b2431ad8d7..cbf476dde0 100644 --- a/bun.lock +++ b/bun.lock @@ -730,13 +730,14 @@ "effect": "catalog:", "esbuild": "^0.27.7", "sucrase": "^3.35.1", + "tailwindcss": "catalog:", }, "devDependencies": { "@effect/vitest": "catalog:", "@executor-js/execution": "workspace:*", "@executor-js/host-mcp": "workspace:*", "@executor-js/sdk": "workspace:*", - "@tailwindcss/browser": "^4.2.2", + "@tailwindcss/browser": "catalog:", "@tailwindcss/vite": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", @@ -748,7 +749,6 @@ "react": "catalog:", "react-dom": "catalog:", "recharts": "3.8.0", - "tailwindcss": "catalog:", "typescript": "catalog:", "vite": "catalog:", "vite-plugin-singlefile": "^2.3.2", @@ -1287,6 +1287,7 @@ "@jitl/quickjs-wasmfile-release-sync": "0.31.0", "@libsql/client": "^0.17.3", "@libsql/kysely-libsql": "^0.4.1", + "@tailwindcss/browser": "4.2.2", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-router": "^1.170.41", "@tanstack/react-start": "^1.168.60", @@ -1308,7 +1309,7 @@ "quickjs-emscripten": "^0.31.0", "react": "^19.1.0", "react-dom": "^19.1.0", - "tailwindcss": "^4.2.2", + "tailwindcss": "4.2.2", "tsup": "^8.5.0", "typescript": "^5.9.3", "vite": "^8.0.0", @@ -2967,7 +2968,7 @@ "@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="], - "@tailwindcss/browser": ["@tailwindcss/browser@4.3.3", "", {}, "sha512-fFaqX18SiyiukLoEI7/cVc0B65mkTslDNo2TxazSDZ9rKs5umOEoujmEkpy8z6GrMdoVzBDbWD8j5DTKLyAWWw=="], + "@tailwindcss/browser": ["@tailwindcss/browser@4.2.2", "", {}, "sha512-JVdJBWWtvKxliBI9CtpOrqxgk8aijUWGsn0STxcOcXAYsUFAbOxN5PwawOJ+v0PoQfPcOK9eZoj9LTwvBV0Vmw=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="], diff --git a/e2e/cloudflare/artifact-precompiled-styles.test.ts b/e2e/cloudflare/artifact-precompiled-styles.test.ts new file mode 100644 index 0000000000..37f2188bf9 --- /dev/null +++ b/e2e/cloudflare/artifact-precompiled-styles.test.ts @@ -0,0 +1,91 @@ +// Cloudflare self-host: an artifact saved through the API is precompiled by the +// host, and the page renders it with the model's own classes styled from the +// stored CSS. The assertion is on COMPUTED styles two frames down, because a +// class present in the DOM was always true even when it did nothing. +import { randomBytes } from "node:crypto"; + +import { expect } from "@effect/vitest"; +import { Effect } from "effect"; +import { composePluginApi } from "@executor-js/api/server"; +import { AccountHttpApi } from "@executor-js/api"; + +import { scenario } from "../src/scenario"; +import { Api, Browser, Target } from "../src/services"; +import { visit } from "../src/surfaces/browser"; + +const api = composePluginApi([] as const); + +// Classes that exist nowhere in the shell's own sources: they only become CSS +// through the artifact's own styles. +const CODE = `function App() { + return ( +
+
24
+
+ abcd +
+
label
+
+ ); +}`; + +scenario( + "Cloudflare · an artifact created through the API renders with its model-written classes styled", + { timeout: 180_000 }, + Effect.gen(function* () { + const target = yield* Target; + const browser = yield* Browser; + const { client: apiClient } = yield* Api; + + const identity = yield* target.newIdentity(); + const client = yield* apiClient(api, identity); + const accountClient = yield* apiClient(AccountHttpApi, identity); + const orgSlug = (yield* accountClient.account.me()).organization?.slug; + + const title = `Styled classes ${randomBytes(4).toString("hex")}`; + const saved = yield* client.artifacts.save({ payload: { title, code: CODE } }); + + yield* Effect.gen(function* () { + const fetched = yield* client.artifacts.get({ params: { artifactId: saved.id } }); + expect(fetched.compiled?.js, "the host stored the compiled JS").toContain("createElement"); + expect(fetched.compiled?.css, "the host stored the artifact's CSS").toContain(".text-2xl"); + + yield* browser.session(identity, async ({ page, step }) => { + await step("Open the artifact saved through the API", async () => { + const path = orgSlug ? `/${orgSlug}/artifacts/${saved.id}` : `/artifacts/${saved.id}`; + await visit(page, new URL(path, target.baseUrl).toString()); + await page + .frameLocator('[data-testid="artifact-shell-frame"]') + .frameLocator("iframe") + .locator("#probe") + .waitFor({ timeout: 30_000 }); + }); + + await step("The model's own classes are real styles", async () => { + const computed = await page + .frameLocator('[data-testid="artifact-shell-frame"]') + .frameLocator("iframe") + .locator("#probe") + .evaluate(() => { + const styleOf = (id: string) => getComputedStyle(document.getElementById(id)!); + return { + fontSize: styleOf("size").fontSize, + gridColumns: styleOf("grid").gridTemplateColumns.split(" ").length, + labelSize: styleOf("arbitrary").fontSize, + }; + }); + expect(computed.fontSize, "text-2xl resolves to its real size").toBe("24px"); + expect(computed.gridColumns, "md:grid-cols-4 produces four columns").toBe(4); + expect(computed.labelSize, "an arbitrary text size compiles").toBe("11px"); + }); + }); + }).pipe( + Effect.ensuring( + client.artifacts.remove({ params: { artifactId: saved.id } }).pipe( + // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: cleanup must fail the scenario if the API cannot remove the fixture + Effect.orDie, + ), + ), + ); + }), +); diff --git a/package.json b/package.json index 4ef32460ed..2276087655 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,8 @@ "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "typescript": "^5.9.3", - "tailwindcss": "^4.2.2", + "tailwindcss": "4.2.2", + "@tailwindcss/browser": "4.2.2", "quickjs-emscripten": "^0.31.0", "@jitl/quickjs-wasmfile-release-sync": "0.31.0", "tsup": "^8.5.0", diff --git a/packages/core/api/src/artifacts/api.ts b/packages/core/api/src/artifacts/api.ts index f83f50ee0f..47ac60c89f 100644 --- a/packages/core/api/src/artifacts/api.ts +++ b/packages/core/api/src/artifacts/api.ts @@ -47,9 +47,18 @@ const ArtifactSummaryResponse = Schema.Struct({ updatedAt: Schema.Number, }); +/** The server-side precompile of `code`. Only `get` carries it, and only when + * the host has a compiler. A null half means the frame does that step itself. */ +const ArtifactCompiledResponse = Schema.Struct({ + fingerprint: Schema.String, + js: Schema.NullOr(Schema.String), + css: Schema.NullOr(Schema.String), +}); + const ArtifactResponse = Schema.Struct({ ...ArtifactSummaryResponse.fields, code: Schema.String, + compiled: Schema.optional(Schema.NullOr(ArtifactCompiledResponse)), /** Null for artifacts saved before the binding contract, whose code still * carries full connection addresses. */ bindings: Schema.NullOr(ArtifactBindings), diff --git a/packages/core/api/src/artifacts/artifacts-compiled.test.ts b/packages/core/api/src/artifacts/artifacts-compiled.test.ts new file mode 100644 index 0000000000..60e65aa646 --- /dev/null +++ b/packages/core/api/src/artifacts/artifacts-compiled.test.ts @@ -0,0 +1,99 @@ +import { HttpApiBuilder } from "effect/unstable/httpapi"; +import { HttpRouter, HttpServer } from "effect/unstable/http"; +import { describe, expect, it } from "@effect/vitest"; +import { Context, Effect, Layer } from "effect"; + +import { createExecutor, type ArtifactCompiler, type Executor } from "@executor-js/sdk"; +import { makeTestConfig } from "@executor-js/sdk/testing"; + +import { ExecutorApi } from "../api"; +import { observabilityMiddleware } from "../observability"; +import { CoreHandlers, ExecutionEngineService, ExecutorService } from "../server"; + +const compiler: ArtifactCompiler = { + fingerprint: "fp-1", + compile: (code) => + Effect.succeed({ fingerprint: "fp-1", js: `js(${code})`, css: `css(${code})` }), +}; + +const webHandlerFor = (executor: Executor) => + Effect.acquireRelease( + Effect.sync(() => + HttpRouter.toWebHandler( + HttpApiBuilder.layer(ExecutorApi).pipe( + Layer.provide(CoreHandlers), + Layer.provide(observabilityMiddleware(ExecutorApi)), + Layer.provide(Layer.succeed(ExecutorService)(executor)), + Layer.provide( + Layer.succeed(ExecutionEngineService)({} as ExecutionEngineService["Service"]), + ), + Layer.provideMerge(HttpServer.layerServices), + Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), + ), + { disableLogger: true }, + ), + ), + (web) => Effect.promise(() => web.dispose()), + ); + +const handlerContextFor = (executor: Executor) => + Context.make(ExecutorService, executor).pipe( + Context.add(ExecutionEngineService, {} as ExecutionEngineService["Service"]), + ); + +interface ArtifactBody { + readonly id: string; + readonly code: string; + readonly compiled?: { readonly fingerprint: string; readonly js: string | null } | null; +} + +describe("artifact compiled output over HTTP", () => { + it.effect("get returns the precompiled output", () => + Effect.gen(function* () { + const executor = yield* createExecutor(makeTestConfig({ artifactCompiler: compiler })); + const saved = yield* executor.artifacts.save({ title: "A", code: "const a = 1;" }); + const web = yield* webHandlerFor(executor); + + const response = yield* Effect.promise(() => + web.handler( + new Request(`http://localhost/artifacts/${saved.id}`), + handlerContextFor(executor), + ), + ); + expect(response.status).toBe(200); + const body = (yield* Effect.promise(() => response.json())) as ArtifactBody; + expect(body.compiled).toEqual({ + fingerprint: "fp-1", + js: "js(const a = 1;)", + css: "css(const a = 1;)", + }); + }), + ); + + it.effect("a save payload carrying compiled output does not store it", () => + Effect.gen(function* () { + const executor = yield* createExecutor(makeTestConfig({ artifactCompiler: compiler })); + const web = yield* webHandlerFor(executor); + + const response = yield* Effect.promise(() => + web.handler( + new Request("http://localhost/artifacts", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + title: "A", + code: "const a = 1;", + compiled: { fingerprint: "fp-1", js: "evil()", css: "evil" }, + }), + }), + handlerContextFor(executor), + ), + ); + expect(response.status).toBe(200); + const saved = (yield* Effect.promise(() => response.json())) as ArtifactBody; + + const rendered = yield* executor.artifacts.getForRender(saved.id); + expect(rendered.compiled?.js).toBe("js(const a = 1;)"); + }), + ); +}); diff --git a/packages/core/api/src/handlers/artifacts.ts b/packages/core/api/src/handlers/artifacts.ts index 8647f610a6..0b74cba015 100644 --- a/packages/core/api/src/handlers/artifacts.ts +++ b/packages/core/api/src/handlers/artifacts.ts @@ -3,6 +3,7 @@ import { Effect } from "effect"; import { sanitizeArtifactPreviewMarkup, type Artifact, + type ArtifactForRender, type ArtifactSummary, } from "@executor-js/sdk"; @@ -34,6 +35,11 @@ const artifactToResponse = (a: Artifact) => ({ bindings: a.bindings, }); +const artifactForRenderToResponse = (a: ArtifactForRender) => ({ + ...artifactToResponse(a), + compiled: a.compiled, +}); + export const ArtifactsHandlers = HttpApiBuilder.group(ExecutorApi, "artifacts", (handlers) => handlers .handle("list", () => @@ -49,9 +55,9 @@ export const ArtifactsHandlers = HttpApiBuilder.group(ExecutorApi, "artifacts", capture( Effect.gen(function* () { const executor = yield* ExecutorService; - const artifact = yield* executor.artifacts.get(path.artifactId); + const artifact = yield* executor.artifacts.getForRender(path.artifactId); yield* notifyArtifactUsage("viewed"); - return artifactToResponse(artifact); + return artifactForRenderToResponse(artifact); }), ), ) diff --git a/packages/core/api/src/server/scoped-executor.ts b/packages/core/api/src/server/scoped-executor.ts index 9c0caf8ebb..86a65413a4 100644 --- a/packages/core/api/src/server/scoped-executor.ts +++ b/packages/core/api/src/server/scoped-executor.ts @@ -134,6 +134,13 @@ export interface HostConfigShape { * attempted as it was before the gate existed. */ readonly enterpriseManagedRollout?: ExecutorConfig["enterpriseManagedRollout"]; + /** + * Forwarded to `ExecutorConfig.artifactCompiler`: precompiles artifact source + * at save time so the render frame can skip its own compile. Declared here + * because it is a deployment-wide capability both request planes share. Hosts + * that omit it keep compiling in the frame. + */ + readonly artifactCompiler?: ExecutorConfig["artifactCompiler"]; /** * Forwarded verbatim to `ExecutorConfig.toolsSyncTtlMs`: how long a * connection's persisted remote tool catalog stays fresh. Omit to take the @@ -372,6 +379,7 @@ export const makeScopedExecutor = < oauthCallbackStateOrgSlug: orgSlug, firstPartyOAuthClients: config.firstPartyOAuthClients, enterpriseManagedRollout: config.enterpriseManagedRollout, + artifactCompiler: config.artifactCompiler, coreTools: { webBaseUrl, orgSlug, diff --git a/packages/core/execution/src/skills.ts b/packages/core/execution/src/skills.ts index 9bf8f43ef0..0987085c50 100644 --- a/packages/core/execution/src/skills.ts +++ b/packages/core/execution/src/skills.ts @@ -623,6 +623,7 @@ const ARTIFACT_STYLE_SKILL_BODY = [ "- `100vh` / `100vw` sizing. The viewport those refer to is the frame's, not the window's, so they either overflow or collapse. Use `h-full` against a bounded parent.", "- Rounded corners on an element with a border on only one side.", "- Icons larger than 20px inline (16px is the default, `size-4`); 24px is the ceiling for a genuinely decorative one.", + '- Class names built by interpolation or concatenation (`` `bg-${tone}` ``, `"w-" + n`). Write every class as a complete literal, and pick between whole literals (`on ? "bg-muted" : "bg-card"`).', ].join("\n"); export const ARTIFACT_STYLE_SKILL: Skill = { diff --git a/packages/core/sdk/src/artifact-compile.test.ts b/packages/core/sdk/src/artifact-compile.test.ts new file mode 100644 index 0000000000..9d92c643f3 --- /dev/null +++ b/packages/core/sdk/src/artifact-compile.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from "@effect/vitest"; +import { Effect } from "effect"; + +import type { ArtifactCompiler } from "./artifact"; +import { makeTestExecutor } from "./testing"; + +const CODE = "export default function App() { return
hi
; }"; + +// A compiler with literal outputs, so expectations are worked examples rather +// than a recomputation of what a real compiler would emit. `fingerprint` is +// mutable to model a deploy that changes the compiler. +const makeCompiler = (onCompile?: (code: string) => Effect.Effect) => { + const state = { fingerprint: "fp-1", calls: 0 }; + const compiler: ArtifactCompiler = { + get fingerprint() { + return state.fingerprint; + }, + compile: (code) => + Effect.gen(function* () { + state.calls++; + if (onCompile) yield* onCompile(code); + return { fingerprint: state.fingerprint, js: `js(${code})`, css: `css(${code})` }; + }), + }; + return { compiler, state }; +}; + +describe("artifact precompile", () => { + it.effect("save stores compiled output that getForRender returns", () => + Effect.gen(function* () { + const { compiler, state } = makeCompiler(); + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + + const rendered = yield* executor.artifacts.getForRender(saved.id); + expect(rendered.compiled).toEqual({ + fingerprint: "fp-1", + js: `js(${CODE})`, + css: `css(${CODE})`, + }); + expect(state.calls).toBe(1); + }), + ); + + it.effect("overwriting replaces the compiled output with the new source's", () => + Effect.gen(function* () { + const { compiler } = makeCompiler(); + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + yield* executor.artifacts.save({ id: saved.id, title: "A", code: "const x = 1;" }); + + const rendered = yield* executor.artifacts.getForRender(saved.id); + expect(rendered.compiled?.js).toBe("js(const x = 1;)"); + }), + ); + + it.effect("a compiler failure still saves, with no compiled output", () => + Effect.gen(function* () { + const compiler: ArtifactCompiler = { + fingerprint: "fp-1", + compile: () => Effect.die("boom"), + }; + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + + expect(saved.code).toBe(CODE); + const rendered = yield* executor.artifacts.getForRender(saved.id); + expect(rendered.code).toBe(CODE); + expect(rendered.compiled).toBeNull(); + }), + ); + + it.effect("rename keeps the compiled output", () => + Effect.gen(function* () { + const { compiler, state } = makeCompiler(); + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + yield* executor.artifacts.rename({ id: saved.id, title: "B" }); + + const rendered = yield* executor.artifacts.getForRender(saved.id); + expect(rendered.title).toBe("B"); + expect(rendered.compiled?.js).toBe(`js(${CODE})`); + expect(state.calls).toBe(1); + }), + ); + + it.effect("a host without a compiler stores nothing and reports null", () => + Effect.gen(function* () { + const executor = yield* makeTestExecutor(); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + expect((yield* executor.artifacts.getForRender(saved.id)).compiled).toBeNull(); + }), + ); + + it.effect("get carries no compiled output", () => + Effect.gen(function* () { + const { compiler } = makeCompiler(); + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + + expect(yield* executor.artifacts.get(saved.id)).not.toHaveProperty("compiled"); + expect(saved).not.toHaveProperty("compiled"); + }), + ); + + it.effect("getForRender recompiles a stale fingerprint and writes it back in place", () => + Effect.gen(function* () { + const { compiler, state } = makeCompiler(); + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + + state.fingerprint = "fp-2"; + const first = yield* executor.artifacts.getForRender(saved.id); + expect(first.compiled?.fingerprint).toBe("fp-2"); + expect(first.updatedAt).toEqual(saved.updatedAt); + expect(state.calls).toBe(2); + + const second = yield* executor.artifacts.getForRender(saved.id); + expect(second.compiled).toEqual(first.compiled); + expect(state.calls).toBe(2); + }), + ); + + it.effect("a save that lands during the write-back compile is not overwritten", () => + Effect.gen(function* () { + let concurrentSave: Effect.Effect = Effect.void; + const { compiler, state } = makeCompiler((code) => + code === CODE ? concurrentSave : Effect.void, + ); + const executor = yield* makeTestExecutor({ artifactCompiler: compiler }); + const saved = yield* executor.artifacts.save({ title: "A", code: CODE }); + + state.fingerprint = "fp-2"; + concurrentSave = executor.artifacts + .save({ id: saved.id, title: "A", code: "const next = 1;" }) + .pipe(Effect.asVoid, Effect.orDie); + yield* executor.artifacts.getForRender(saved.id); + + const rendered = yield* executor.artifacts.getForRender(saved.id); + expect(rendered.code).toBe("const next = 1;"); + expect(rendered.compiled?.js).toBe("js(const next = 1;)"); + }), + ); +}); diff --git a/packages/core/sdk/src/artifact.ts b/packages/core/sdk/src/artifact.ts index 683ed61b94..af9a8ba068 100644 --- a/packages/core/sdk/src/artifact.ts +++ b/packages/core/sdk/src/artifact.ts @@ -9,9 +9,9 @@ // policy, so org-tier sharing later needs no new machinery. // --------------------------------------------------------------------------- -import { Option, Schema } from "effect"; +import { Option, Schema, type Effect } from "effect"; -import type { ArtifactRow, ArtifactSummaryRow } from "./core-schema"; +import type { ArtifactFullRow, ArtifactRow, ArtifactSummaryRow } from "./core-schema"; import { ArtifactId, ConnectionName, IntegrationSlug, Owner } from "./ids"; /** @@ -136,6 +136,52 @@ export interface Artifact { readonly updatedAt: Date; } +/** + * The result of precompiling an artifact's source, as stored on its row. + * + * `fingerprint` names the compiler that produced it; output whose fingerprint + * differs from the host's current one is stale and is recomputed on the next + * render read. `js` is the JSX transformed to plain JS and `css` the Tailwind + * CSS for the classes the source uses. Each is `null` when the compiler could + * not produce it, and the renderer then does that step itself: no `js` means + * the frame compiles the source, no `css` means the frame runs Tailwind. + */ +export interface ArtifactCompiledOutput { + readonly fingerprint: string; + readonly js: string | null; + readonly css: string | null; +} + +/** + * A host-supplied precompiler for artifact source (`ExecutorConfig.artifactCompiler`). + * + * Optional: a host without one stores no compiled output and every render + * compiles in the frame, exactly as before. It must not fail; a failure is + * logged and the artifact is saved without compiled output. + */ +export interface ArtifactCompiler { + readonly fingerprint: string; + readonly compile: (code: string) => Effect.Effect; +} + +/** An artifact plus its precompiled output, for the renderer. `null` when no + * compiler is configured on this host. */ +export interface ArtifactForRender extends Artifact { + readonly compiled: ArtifactCompiledOutput | null; +} + +/** The compiled output stored on a row, or `null` when it was never compiled. */ +export const compiledFromRow = ( + row: Pick, +): ArtifactCompiledOutput | null => + typeof row.compiled_fingerprint === "string" + ? { + fingerprint: row.compiled_fingerprint, + js: row.compiled_js ?? null, + css: row.compiled_css ?? null, + } + : null; + /** What a list returns: everything except the source and the bindings that * interpret it. Both are only meaningful to a renderer, and both are heavy. * The preview stays, because the list is what draws it. */ @@ -204,7 +250,7 @@ export const rowToArtifactSummary = (row: ArtifactSummaryRow): ArtifactSummary = updatedAt: asDate(row.updated_at), }); -export const rowToArtifact = (row: ArtifactRow): Artifact => ({ +export const rowToArtifact = (row: ArtifactFullRow): Artifact => ({ ...rowToArtifactSummary(row), code: row.code, bindings: bindingsFromColumn(row.bindings), diff --git a/packages/core/sdk/src/core-schema.ts b/packages/core/sdk/src/core-schema.ts index 1abb610912..65aecda3ee 100644 --- a/packages/core/sdk/src/core-schema.ts +++ b/packages/core/sdk/src/core-schema.ts @@ -426,6 +426,15 @@ export const coreTables = defineTables({ // column existed have none, and so does one whose render was too large, // failed open, or has never been opened. preview: nullableTextColumn("preview"), + // The server-side precompile of `code`: the fingerprint of the compiler + // that produced it, the JSX transformed to plain JS, and the Tailwind + // CSS for the classes the source uses. All nullable: never compiled, a + // host without a compiler, source over the size cap, or classes only + // known at runtime (`css` alone). Only `getForRender` reads them, so the + // per-tool-call `get` stays light. See `ArtifactCompiler`. + compiled_fingerprint: nullableTextColumn("compiled_fingerprint"), + compiled_js: nullableTextColumn("compiled_js"), + compiled_css: nullableTextColumn("compiled_css"), created_at: dateColumn("created_at"), updated_at: dateColumn("updated_at"), }, @@ -506,6 +515,18 @@ export const ARTIFACT_SUMMARY_COLUMNS = [ "created_at", "updated_at", ] as const satisfies readonly (keyof ArtifactRow)[]; +/** + * Everything except the compiled output, which can be twice the size of `code`. + * `artifacts.get` runs on every tool call an artifact makes, so only + * `getForRender` selects the compiled columns. + */ +export const ARTIFACT_FULL_COLUMNS = [ + ...ARTIFACT_SUMMARY_COLUMNS, + "code", + "bindings", +] as const satisfies readonly (keyof ArtifactRow)[]; +/** The artifact-row projection {@link ARTIFACT_FULL_COLUMNS} selects. */ +export type ArtifactFullRow = Pick; /** The artifact-row projection {@link ARTIFACT_SUMMARY_COLUMNS} selects. */ export type ArtifactSummaryRow = Pick; export type PluginStorageRow = FumaRow; diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index d4022913ad..ad14303dd3 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -52,6 +52,7 @@ import { } from "./health-check"; import type { HealthCheckCandidate } from "./health-check"; import { + ARTIFACT_FULL_COLUMNS, ARTIFACT_SUMMARY_COLUMNS, coreSchema, isToolPolicyAction, @@ -87,9 +88,13 @@ import { export type { OnElicitation, InvokeOptions } from "./elicitation"; import { + compiledFromRow, rowToArtifact, rowToArtifactSummary, type Artifact, + type ArtifactCompiledOutput, + type ArtifactCompiler, + type ArtifactForRender, type ArtifactSummary, type RemoveArtifactInput, type RenameArtifactInput, @@ -509,6 +514,16 @@ export type Executor = { /** Newest first, without the JSX source — lists stay light. */ readonly list: () => Effect.Effect; readonly get: (id: string) => Effect.Effect; + /** + * `get` plus the precompiled output, for a renderer. Output stored under an + * older compiler fingerprint is recomputed and written back first, so + * artifacts saved before a compiler existed are compiled lazily here. + * `compiled` is `null` when the host has no `artifactCompiler`. Prefer + * `get` anywhere the compiled columns are not needed: they are large. + */ + readonly getForRender: ( + id: string, + ) => Effect.Effect; /** Create, or overwrite an existing artifact in place when `id` is given. */ readonly save: ( input: SaveArtifactInput, @@ -763,6 +778,12 @@ export interface ExecutorConfig nothing is + * precompiled and every render compiles in the frame, as before. + */ + readonly artifactCompiler?: ArtifactCompiler; /** * Host-operated OAuth apps (the deployment's own registered GitHub/Google/… * apps), addressed as `first-party:`. Users connect through them with @@ -6713,11 +6734,66 @@ export const createExecutor = ({ + compiled_fingerprint: compiled?.fingerprint ?? null, + compiled_js: compiled?.js ?? null, + compiled_css: compiled?.css ?? null, + }); + + // Fail-open: a compiler problem must never refuse a save or a render. + // Without compiled output the frame compiles the source itself. + const compileArtifactSource = (code: string): Effect.Effect => { + const compiler = config.artifactCompiler; + if (!compiler) return Effect.succeed(null); + return compiler.compile(code).pipe( + Effect.map((output): ArtifactCompiledOutput | null => output), + Effect.catchCause((cause) => + Effect.logWarning("executor artifact precompile failed", cause).pipe(Effect.as(null)), + ), + ); + }; + + const artifactsGetForRender = ( + id: string, + ): Effect.Effect => + Effect.gen(function* () { + const row = yield* core.findFirst("artifact", { where: artifactById(id) }); + if (!row) return yield* new ArtifactNotFoundError({ id: ArtifactId.make(id) }); + const artifact = rowToArtifact(row); + const compiler = config.artifactCompiler; + if (!compiler) return { ...artifact, compiled: null }; + + const stored = compiledFromRow(row); + if (stored !== null && stored.fingerprint === compiler.fingerprint) { + return { ...artifact, compiled: stored }; + } + + const fresh = yield* compileArtifactSource(row.code); + if (fresh === null) return { ...artifact, compiled: null }; + // Guarded on the source this compiled, so a save that lands meanwhile + // always wins: the write simply matches no row. `updated_at` is no + // guard here, since SQLite stores it at one-second resolution. No + // column other than the compiled ones moves, because being rendered is + // not an edit. + yield* core + .updateMany("artifact", { + where: (b: AnyCb) => b.and(b("id", "=", id), b("code", "=", row.code)), + set: compiledColumns(fresh), + }) + .pipe( + Effect.catchCause((cause) => + Effect.logWarning("executor artifact compiled output write-back failed", cause), + ), + ); + return { ...artifact, compiled: fresh }; + }); + const artifactsSave = ( input: SaveArtifactInput, ): Effect.Effect => @@ -6729,7 +6805,10 @@ export const createExecutor = => Effect.gen(function* () { const where = artifactById(input.id); - const existing = yield* core.findFirst("artifact", { where }); + const existing = yield* core.findFirst("artifact", { + where, + select: ARTIFACT_FULL_COLUMNS, + }); if (!existing) { return yield* new ArtifactNotFoundError({ id: ArtifactId.make(input.id), @@ -7804,6 +7891,7 @@ export const createExecutor = ["onIntegrationChange"]; readonly firstPartyOAuthClients?: ExecutorConfig["firstPartyOAuthClients"]; readonly enterpriseManagedRollout?: ExecutorConfig["enterpriseManagedRollout"]; + readonly artifactCompiler?: ExecutorConfig["artifactCompiler"]; /** Workspace-settings permission for the test binding (see * `ExecutorConfig.orgWrites`). Defaults to allowed, like production hosts * with no role model. */ @@ -183,6 +184,7 @@ export const makeTestConfig = { + let dir = path.dirname(require.resolve(name)); + while (!fs.existsSync(path.join(dir, "package.json"))) dir = path.dirname(dir); + const manifest: { readonly version: string } = JSON.parse( + fs.readFileSync(path.join(dir, "package.json"), "utf8"), + ); + return manifest.version; +}; + +const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json")); +const readTailwindCss = (file: string) => fs.readFileSync(path.join(tailwindDir, file), "utf8"); +const read = (relative: string) => fs.readFileSync(path.join(packageRoot, relative), "utf8"); + +export const buildPrecompileAssets = (): { + readonly assets: string; + readonly fingerprint: string; +} => { + const themeSource = read("src/shell/theme.css"); + const tailwindSources = { + index: readTailwindCss("index.css"), + preflight: readTailwindCss("preflight.css"), + theme: readTailwindCss("theme.css"), + utilities: readTailwindCss("utilities.css"), + }; + + const hash = createHash("sha256"); + hash.update( + JSON.stringify({ + sucrase: readPackageVersion("sucrase"), + tailwindcss: readPackageVersion("tailwindcss"), + jsx: ARTIFACT_JSX_TRANSFORM_OPTIONS, + themeSource, + tailwindSources, + compileJsx: read("src/shell/compile-jsx.ts"), + extractCandidates: read("src/precompile/extract-candidates.ts"), + }), + ); + const fingerprint = hash.digest("hex"); + + const assets = `/** + * GENERATED by \`scripts/gen-precompile-assets.ts\`. Do not hand-edit: + * \`compile-fingerprint.pin.test.ts\` fails if this drifts. Regenerate with + * \`bun run gen:precompile\`. + */ + +// oxfmt-ignore +export const THEME_SOURCE: string = ${JSON.stringify(themeSource)}; + +// oxfmt-ignore +export const TAILWIND_SOURCES = ${JSON.stringify(tailwindSources, null, 2)} as const; +`; + + return { assets, fingerprint }; +}; + +if (import.meta.main) { + const { assets, fingerprint } = buildPrecompileAssets(); + const dir = path.join(packageRoot, "src/precompile"); + fs.writeFileSync(path.join(dir, "assets.gen.ts"), assets); + fs.writeFileSync( + path.join(dir, "compile-fingerprint.gen.ts"), + `/** + * GENERATED by \`scripts/gen-precompile-assets.ts\`. Do not hand-edit: + * \`compile-fingerprint.pin.test.ts\` fails if this drifts. Regenerate with + * \`bun run gen:precompile\`. + */ + +// oxfmt-ignore +export const ARTIFACT_COMPILE_FINGERPRINT = ${JSON.stringify(fingerprint)}; +`, + ); +} diff --git a/packages/hosts/mcp-apps-shell/src/precompile/assets.gen.ts b/packages/hosts/mcp-apps-shell/src/precompile/assets.gen.ts new file mode 100644 index 0000000000..7e200ef0ad --- /dev/null +++ b/packages/hosts/mcp-apps-shell/src/precompile/assets.gen.ts @@ -0,0 +1,16 @@ +/** + * GENERATED by `scripts/gen-precompile-assets.ts`. Do not hand-edit: + * `compile-fingerprint.pin.test.ts` fails if this drifts. Regenerate with + * `bun run gen:precompile`. + */ + +// oxfmt-ignore +export const THEME_SOURCE: string = "/*\n * Executor's design tokens, as Tailwind theme SOURCE.\n *\n * Split out of `globals.css` because this file has two consumers:\n *\n * 1. The shell's own build, which `@import`s it and compiles it ahead of time.\n * 2. The sandboxed inner frame, which embeds it VERBATIM in a\n * ` + ` + : ``; + const frameCompiler = + plan.js === null ? `\n ` : ""; return ` @@ -242,12 +284,11 @@ const buildRendererSrcDoc = (token: string): string => { - - + ${artifactStyles}
- + ${frameCompiler} `; }; @@ -557,6 +598,7 @@ export function McpAppsShell({ postToRenderer({ type: "executor.render", code: current.code, + ...(current.compiledJs === null ? {} : { compiledJs: current.compiledJs }), theme: hostContext?.theme, // The inner frame is the last link in the height chain, and it cannot // see the host context — so the mode is told to it, the same way the @@ -701,13 +743,15 @@ export function McpAppsShell({ }, [fill, postToRenderer, renderer]); /** Render a JSX code string in the sandboxed inner iframe. */ - const renderCode = useCallback((code: string) => { + const renderCode = useCallback((code: string, compiled?: unknown) => { try { const token = createRendererToken(); + const plan = readRenderPlan(compiled); const nextRenderer = { token, code, - srcDoc: buildRendererSrcDoc(token), + compiledJs: plan.js, + srcDoc: buildRendererSrcDoc(token, plan), config: {}, height: 240, }; @@ -740,7 +784,7 @@ export function McpAppsShell({ if (typeof artifactId === "string") artifactIdRef.current = artifactId; const code = params.arguments?.code; if (code && typeof code === "string") { - renderCode(code); + renderCode(code, params.arguments?.compiled); } }; @@ -753,7 +797,7 @@ export function McpAppsShell({ if (typeof artifactId === "string") artifactIdRef.current = artifactId; if (code && typeof code === "string") { - renderCode(code); + renderCode(code, structured?.compiled); return; } diff --git a/packages/hosts/mcp-apps-shell/src/shell/smoke-harness.tsx b/packages/hosts/mcp-apps-shell/src/shell/smoke-harness.tsx index df15e0a523..fe354f803c 100644 --- a/packages/hosts/mcp-apps-shell/src/shell/smoke-harness.tsx +++ b/packages/hosts/mcp-apps-shell/src/shell/smoke-harness.tsx @@ -29,7 +29,8 @@ import { StrictMode, createElement } from "react"; import { renderToReadableStream } from "react-dom/server.browser"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { compileJsx, evaluateComponent } from "./component-runtime"; +import { compileJsx } from "./compile-jsx"; +import { evaluateComponent } from "./component-runtime"; import * as Components from "./components"; import { createToolsProxy } from "./tools-proxy"; import type { SmokeRenderResult } from "./smoke-render-result"; diff --git a/packages/hosts/mcp-apps-shell/src/vite.test.ts b/packages/hosts/mcp-apps-shell/src/vite.test.ts index 6dcbea0069..556f5efc58 100644 --- a/packages/hosts/mcp-apps-shell/src/vite.test.ts +++ b/packages/hosts/mcp-apps-shell/src/vite.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@effect/vitest"; -import { mcpAppsShellAsset } from "./vite"; +import { innerRendererPlugin, mcpAppsShellAsset } from "./vite"; describe("mcpAppsShellAsset", () => { it("resolves the dev asset URL without building the shell during server startup", async () => { @@ -26,3 +26,19 @@ describe("mcpAppsShellAsset", () => { ); }); }); + +describe("innerRendererPlugin", () => { + it("serves the JSX compiler as its own program, apart from the renderer", async () => { + const plugin = innerRendererPlugin(); + const compilerId = plugin.resolveId("virtual:executor-inner-compiler"); + const rendererId = plugin.resolveId("virtual:executor-inner-renderer"); + expect(compilerId).toBeDefined(); + expect(rendererId).toBeDefined(); + + const compiler = await plugin.load(compilerId ?? ""); + const renderer = await plugin.load(rendererId ?? ""); + expect(compiler).toContain("__executorCompileJsx"); + expect(compiler).toContain("jsxRuntime"); + expect(renderer).not.toContain("jsxRuntime"); + }); +}); diff --git a/packages/hosts/mcp-apps-shell/src/vite.ts b/packages/hosts/mcp-apps-shell/src/vite.ts index d67f31fbd8..8b34086605 100644 --- a/packages/hosts/mcp-apps-shell/src/vite.ts +++ b/packages/hosts/mcp-apps-shell/src/vite.ts @@ -7,6 +7,10 @@ * by esbuild — it cannot be a normal import, because it must exist as *source * text* to be embedded in the sandboxed frame. * + * `virtual:executor-inner-compiler` is the Sucrase half of that frame, bundled + * the same way but kept out of the renderer: a frame whose artifact was + * precompiled on the server never loads it. + * * `mcpAppsShellAsset` supplies `virtual:executor-mcp-apps-shell-html`: the URL * of the BUILT, self-contained shell document, emitted into the app's client * assets. The console's artifact page loads that URL into a sandboxed iframe @@ -41,22 +45,18 @@ export interface InnerRendererVitePlugin { const VIRTUAL_ID = "virtual:executor-inner-renderer"; const RESOLVED_ID = `\0${VIRTUAL_ID}`; +const COMPILER_VIRTUAL_ID = "virtual:executor-inner-compiler"; +const COMPILER_RESOLVED_ID = `\0${COMPILER_VIRTUAL_ID}`; const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export const innerRendererEntry = (): string => path.resolve(packageRoot, "src/shell/inner-renderer.tsx"); -/** - * Bundle the inner renderer to a self-contained IIFE string. - * - * Exported on its own so a build script or test can produce the same bytes the - * plugin serves without standing up Vite. - */ -export const bundleInnerRenderer = async (): Promise => { +const bundleInnerProgram = async (entry: string, failure: string): Promise => { const { build } = await import("esbuild"); const result = await build({ - entryPoints: [innerRendererEntry()], + entryPoints: [entry], absWorkingDir: packageRoot, bundle: true, write: false, @@ -75,11 +75,27 @@ export const bundleInnerRenderer = async (): Promise => { const js = result.outputFiles[0]; if (!js) { // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: Vite plugin hooks report build failures by throwing - throw new Error("Failed to bundle the MCP-Apps inner renderer."); + throw new Error(failure); } return js.text; }; +/** + * Bundle the inner renderer to a self-contained IIFE string. + * + * Exported on its own so a build script or test can produce the same bytes the + * plugin serves without standing up Vite. + */ +export const bundleInnerRenderer = (): Promise => + bundleInnerProgram(innerRendererEntry(), "Failed to bundle the MCP-Apps inner renderer."); + +export const innerCompilerEntry = (): string => + path.resolve(packageRoot, "src/shell/inner-compiler.ts"); + +/** Bundle the in-frame JSX compiler to a self-contained IIFE string. */ +export const bundleInnerCompiler = (): Promise => + bundleInnerProgram(innerCompilerEntry(), "Failed to bundle the MCP-Apps inner compiler."); + export const smokeHarnessEntry = (): string => path.resolve(packageRoot, "src/shell/smoke-harness.tsx"); @@ -132,10 +148,17 @@ export const innerRendererPlugin = (): InnerRendererVitePlugin => ({ name: "executor-inner-renderer-source", // `pre` so the virtual id resolves before any generic resolver claims it. enforce: "pre", - resolveId: (id) => (id === VIRTUAL_ID ? RESOLVED_ID : undefined), + resolveId: (id) => { + if (id === VIRTUAL_ID) return RESOLVED_ID; + if (id === COMPILER_VIRTUAL_ID) return COMPILER_RESOLVED_ID; + return undefined; + }, load: async (id) => { - if (id !== RESOLVED_ID) return undefined; - return `export default ${JSON.stringify(await bundleInnerRenderer())};`; + if (id === RESOLVED_ID) return `export default ${JSON.stringify(await bundleInnerRenderer())};`; + if (id === COMPILER_RESOLVED_ID) { + return `export default ${JSON.stringify(await bundleInnerCompiler())};`; + } + return undefined; }, }); diff --git a/packages/react/src/api/artifact-renderer.tsx b/packages/react/src/api/artifact-renderer.tsx index c5da707d52..d0ef2e74ba 100644 --- a/packages/react/src/api/artifact-renderer.tsx +++ b/packages/react/src/api/artifact-renderer.tsx @@ -17,9 +17,9 @@ import * as React from "react"; // for any host-specific capability. // // The seam carries a LOADER, not a component. The shell is browser-only by -// nature: it imports `@tailwindcss/browser`, which builds a `