steedos-webapps

Installation
SKILL.md

Steedos Webapps | Steedos 软件包自定义 React 应用

Overview | 概述

Steedos packages can contain React + Vite sub-projects in the webapps/ directory. Each webapp is an independent Vite application that can be developed standalone and compiled as an IIFE script that self-registers as an amis Renderer component.

Steedos 软件包通过 webapps/ 目录管理 React 子项目。每个子项目是独立的 Vite 应用,可独立开发调试,也可编译为 amis 自注册组件。

Directory Structure | 目录结构

my-package/                              # Steedos package root
├── webapps/                             # React sub-projects
│   ├── designer/                        # webapp 1
│   │   ├── src/
│   │   │   ├── components/              # React components
│   │   │   ├── amis-entry.ts            # amis registration entry
│   │   │   ├── amis-jsx-shim.ts         # JSX Runtime bridge
│   │   │   └── amis-renderer.css
│   │   ├── dist/amis-renderer/          # Build output
│   │   ├── vite.config.ts               # Standard dev config
│   │   ├── vite.amis.config.ts          # amis IIFE build config
│   │   ├── package.json
│   │   └── tailwind.config.js
│   │
│   └── dashboard/                       # webapp 2
│       ├── src/
│       │   ├── components/
│       │   ├── amis-entry.ts
│       │   └── amis-jsx-shim.ts
│       ├── vite.amis.config.ts
│       └── package.json
├── main/default/
│   ├── client/                          # Client loader files
│   │   ├── designer.client.js           # Loads designer amis renderer
│   │   └── dashboard.client.js          # Loads dashboard amis renderer
│   └── routes/                          # Express SPA routers
│       ├── designer.router.js           # SPA access for designer
│       └── dashboard.router.js          # SPA access for dashboard
├── public/                              # Deployed output (copied from each webapp)
│   ├── designer/
│   │   ├── amis-renderer.js
│   │   └── amis-renderer.css
│   └── dashboard/
│       ├── amis-renderer.js
│       └── amis-renderer.css
├── package.json                         # Package root package.json
└── package.service.js                   # Moleculer service definition

Key Concepts:

  • Each webapps/ subdirectory is an independent React + Vite project
  • Two build modes: standard Vite build (dev) + IIFE build (amis component)
  • IIFE output is copied to public/<webapp-name>/, served as static files by Steedos
  • main/default/client/<webapp-name>.client.js triggers loading of the IIFE into the Steedos frontend
  • Each webapp is isolated — can use different dependencies and versions

Scaffold Selection | 脚手架选择

Before creating a webapp, choose a UI scaffold. This determines which component library and styling approach to use. You can use any React-compatible UI library — below are two recommended options.

创建 webapp 前,先选择 UI 脚手架,决定使用哪个组件库和样式方案。支持任意 React 兼容的 UI 库,以下是两个推荐选项。

Scaffold Description When to Use
antd (default) Ant Design component library. Reuses host page's antd via external, no extra bundle size. Default choice — enterprise forms, tables, standard UI. Recommended when unsure.
shadcn/ui Tailwind CSS + Radix UI primitives. Components are copied into project (not a dependency). Custom/modern UI, full design control, lightweight output.
Other Any React UI library (MUI, Chakra, Mantine, headless, etc.). Follow the same IIFE build pattern. Specific design system requirements or team preference.

Key Differences | 关键区别

antd shadcn/ui
Styling CSS-in-JS (host page antd) Tailwind CSS utility classes
Bundle antd marked as external — zero bundle cost Components copied into src/, bundled into IIFE
Tailwind Optional Required
waitForThing target window.antd window.antd (still needed — amis SDK depends on antd)
PostCSS plugins postcss-prefix-selector postcss-prefix-selector + Tailwind v4 workarounds (removeAtProperty, unwrapTwSupports, removeAtLayer)

antd Scaffold Setup | antd 脚手架

cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
npm install antd
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser

In vite.amis.config.ts, mark antd as external:

rollupOptions: {
  external: ['react', 'react-dom', 'antd'],
  output: {
    globals: {
      react: 'amisRequire("react")',
      'react-dom': 'amisRequire("react-dom")',
      'antd': 'antd',
    },
  },
},

shadcn/ui Scaffold Setup | shadcn/ui 脚手架

cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install
npx shadcn@latest init
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser
npm install -D tailwindcss

In vite.amis.config.ts, do NOT externalize antd (shadcn/ui doesn't use it):

rollupOptions: {
  external: ['react', 'react-dom'],
  output: {
    globals: {
      react: 'amisRequire("react")',
      'react-dom': 'amisRequire("react-dom")',
    },
  },
},

⚠️ shadcn/ui uses Tailwind v4 — you MUST add the 3 PostCSS workaround plugins (see Tailwind CSS v4 Workarounds section).

Other Scaffolds | 其他脚手架

Any React-compatible UI library works. The core requirements are the same:

  1. Mark react and react-dom as external in rollup (use amisRequire)
  2. If the library is already on the host page (like antd), mark it as external too
  3. Use postcss-prefix-selector for CSS isolation
  4. If using Tailwind v4, add the 3 PostCSS workaround plugins

Creating a New Webapp | 创建新 webapp

Step 1: Initialize Vite Project | 初始化

cd my-package/webapps
npm create vite@latest my-widget -- --template react-ts
cd my-widget
npm install

Step 2: Add Build Dependencies | 添加构建依赖

# Common dependencies (both scaffolds)
npm install -D @tailwindcss/postcss autoprefixer postcss-prefix-selector terser

# antd scaffold
npm install antd

# shadcn/ui scaffold
npx shadcn@latest init
npm install -D tailwindcss

Step 3: Create amis Integration Files | 创建 amis 集成文件

Each webapp needs 3 amis-specific files:

webapps/my-widget/src/
├── amis-entry.ts          # Registration entry
├── amis-jsx-shim.ts       # JSX bridge (copy from other webapp)
└── amis-renderer.css      # Style entry (imported by amis-entry.ts)

Step 4: Configure Build Scripts | 配置构建脚本

In the webapp's package.json:

{
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "build:amis": "vite build --config vite.amis.config.ts && rm -rf ../../public/my-widget && cp -R dist/amis-renderer ../../public/my-widget",
    "build:all": "tsc -b && vite build && vite build --config vite.amis.config.ts && rm -rf ../../public/my-widget && cp -R dist/amis-renderer ../../public/my-widget"
  }
}

Step 5: Create SPA Router | 创建 SPA 路由

Create main/default/routes/<webapp-name>.router.js to serve the webapp as a standalone SPA:

创建 main/default/routes/<webapp-name>.router.js,让 webapp 可以作为独立 SPA 访问:

// main/default/routes/my-widget.router.js
'use strict';
const express = require('express');
const router = express.Router();
const { requireAuthentication } = require("@steedos/auth");
const path = require('path');

const packageRoot = path.dirname(require.resolve('@steedos-labs/my-package/package.json'));
const webappDistPath = path.join(packageRoot, 'webapps', 'my-widget', 'dist');
const amisRendererDistPath = path.join(webappDistPath, 'amis-renderer');

// Main page entry (requires auth)
router.get('/api/my-package/my-widget', requireAuthentication, async (req, res) => {
  try {
    if (process.env.NODE_ENV === 'development') {
      return res.redirect('http://localhost:5173');
    }
    res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
  } catch (e) {
    res.status(500).send({ errors: [{ errorMessage: e.message }] });
  }
});

// Static assets
router.use('/api/my-package/my-widget', express.static(webappDistPath));

// SPA fallback (frontend routing support)
router.use('/api/my-package/my-widget', (req, res, next) => {
  if (req.method !== 'GET') return next();
  if (path.extname(req.path)) return next();
  if (req.path === '/' || req.path === '') return next();
  requireAuthentication(req, res, () => {
    try {
      if (process.env.NODE_ENV === 'development') {
        return res.redirect('http://localhost:5173');
      }
      res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
    } catch (e) {
      res.status(500).send({ errors: [{ errorMessage: e.message }] });
    }
  });
});

// amis renderer static assets
router.use('/api/my-package/my-widget-amis', express.static(amisRendererDistPath));

exports.default = router;

Also update the webapp's vite.config.ts to set base matching the router path:

同时更新 webapp 的 vite.config.ts,设置 base 与路由路径一致:

// webapps/my-widget/vite.config.ts
export default defineConfig(({ command }) => ({
  base: command === 'build' ? '/api/my-package/my-widget/' : '/',
  // ... other config
}))

Step 6: Register in Package Root | 在软件包根目录注册

{
  "name": "@steedos-labs/my-package",
  "files": [
    "main/default/client",
    "main/default/routes",
    "webapps/my-widget/dist",
    "public/my-widget",
    "package.service.js"
  ],
  "scripts": {
    "build:my-widget": "cd webapps/my-widget && npm run build:all",
    "build:webapps": "npm run build:my-widget"
  }
}

Key Files | 关键文件

vite.amis.config.ts — IIFE Build Config

Compiles React components into self-executing IIFE scripts loadable by amis.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/postcss'
import autoprefixer from 'autoprefixer'
import path from 'path'

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const prefixSelector = require('postcss-prefix-selector')

// CSS scope prefix — all styles scoped under this selector
const SCOPE = '.my-widget'

export default defineConfig({
  plugins: [react({ jsxRuntime: 'classic' })],

  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      'react/jsx-runtime': path.resolve(__dirname, './src/amis-jsx-shim.ts'),
      'react/jsx-dev-runtime': path.resolve(__dirname, './src/amis-jsx-shim.ts'),
    },
  },

  define: {
    'process.env.NODE_ENV': JSON.stringify('production'),
    'process.env': JSON.stringify({}),
  },

  build: {
    outDir: 'dist/amis-renderer',
    emptyOutDir: true,
    lib: {
      entry: path.resolve(__dirname, 'src/amis-entry.ts'),
      name: 'MyWidget',
      formats: ['iife'],
      fileName: () => 'amis-renderer.js',
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'antd'],
      output: {
        globals: {
          react: 'amisRequire("react")',
          'react-dom': 'amisRequire("react-dom")',
          'antd': 'antd',
        },
        assetFileNames: 'amis-renderer.[ext]',
      },
    },
    assetsInlineLimit: 8192,
    cssCodeSplit: false,
    minify: 'terser',
  },

  css: {
    postcss: {
      plugins: [
        tailwindcss(),
        autoprefixer(),
        prefixSelector({
          prefix: SCOPE,
          transform(prefix, selector, prefixedSelector, _filePath, rule) {
            if (selector === ':root') return selector;
            if (/^(html|body)(\s|,|$)/.test(selector)) return selector;
            const parent = rule.parent;
            if (parent?.type === 'atrule' && /^keyframes/.test(parent.name)) return selector;
            return prefixedSelector;
          },
        }),
      ],
    },
  },
})

Critical configuration points:

Config Purpose
formats: ['iife'] Self-executing script, registers on load
external: ['react', 'react-dom'] React NOT bundled — reuse amis SDK's React
react/jsx-runtime alias Prevent 3rd-party libs from bundling their own jsx-runtime
postcss-prefix-selector Scope all CSS under prefix to prevent style leaks
jsxRuntime: 'classic' Use React.createElement instead of new JSX Transform

amis-jsx-shim.ts — JSX Runtime Bridge

Rollup external can only externalize react, not the react/jsx-runtime sub-path. Third-party dependencies (e.g. @tiptap/react) import from react/jsx-runtime, which would bundle an incompatible React copy. This shim delegates jsx()/jsxs() to the externalized React.createElement().

This file is identical across all webapps — copy directly without modification.

import React from 'react';

export function jsx(
  type: React.ElementType,
  props: Record<string, unknown>,
  key?: string,
): React.ReactElement {
  const { children, ...rest } = props;
  if (key !== undefined) (rest as Record<string, unknown>).key = key;
  return children !== undefined
    ? React.createElement(type, rest as React.Attributes, children as React.ReactNode)
    : React.createElement(type, rest as React.Attributes);
}

export function jsxs(
  type: React.ElementType,
  props: Record<string, unknown>,
  key?: string,
): React.ReactElement {
  const { children, ...rest } = props;
  if (key !== undefined) (rest as Record<string, unknown>).key = key;
  if (Array.isArray(children)) {
    return React.createElement(type, rest as React.Attributes, ...children);
  }
  return children !== undefined
    ? React.createElement(type, rest as React.Attributes, children as React.ReactNode)
    : React.createElement(type, rest as React.Attributes);
}

export const jsxDEV = jsx;
export const Fragment = React.Fragment;

amis-entry.ts — Registration Entry

IIFE build entry point. Imports styles, defines a bridge component, registers via amisLib.Renderer().

import './amis-renderer.css';
import { MyReactComponent } from './components/MyReactComponent';

declare global {
  function amisRequire(mod: string): any;
}

function register() {
  if (typeof amisRequire === 'undefined') {
    console.error('[my-widget] amisRequire is not defined. Load amis SDK first.');
    return;
  }

  const React = amisRequire('react');
  const amisLib = amisRequire('amis');

  if (!amisLib?.Renderer) {
    console.error('[my-widget] amis.Renderer not found.');
    return;
  }

  // Bridge component: amis props → React component props
  function MyWidget(props: any) {
    const { $schema, data, dispatchEvent } = props;

    // Register to amis ScopedContext (makes getComponentById work)
    const ScopedContext = amisLib.ScopedContext;
    const scoped = React.useContext(ScopedContext);
    const compRef = React.useRef(null);
    const scopedRef = React.useRef(null);

    const componentMethods = {
      getValue: () => compRef.current?.getValue(),
      validate: async () => compRef.current?.validate(),
    };

    if (scopedRef.current === null) {
      scopedRef.current = { ...componentMethods };
    } else {
      Object.assign(scopedRef.current, componentMethods);
    }

    Object.defineProperty(scopedRef.current, 'props', {
      get: () => props,
      configurable: true,
    });

    React.useEffect(() => {
      if (!scoped || !($schema.id || props.id)) return;
      scoped.registerComponent(scopedRef.current);
      return () => scoped.unRegisterComponent(scopedRef.current);
    }, [$schema.id || props.id]);

    // Read custom properties from $schema (configured in amis JSON schema)
    const title = $schema.title || '';
    const config = $schema.config || {};

    // Read from amis data scope
    const contextValue = data?.someKey || '';

    // Event callback — dispatch events to amis
    const handleChange = (val: any) => {
      dispatchEvent?.('change', { value: val });
    };

    // Render the actual React component
    return React.createElement(MyReactComponent, {
      ref: compRef,
      title,
      config,
      value: contextValue,
      onChange: handleChange,
      className: 'my-widget',  // MUST match CSS scope prefix
    });
  }

  // Register as amis Renderer
  amisLib.Renderer({
    type: 'my-widget',    // type name used in amis JSON schema
    autoVar: true,
  })(MyWidget);

  console.log('[my-widget] amis Renderer registered.');
}

register();

amis Props Reference | amis 传入的 Props

Prop Description
$schema Full JSON schema node — includes all custom properties you defined
data amis current data scope (context variables)
dispatchEvent Dispatch events to amis (change, submit, etc.)
onBulkChange Batch-write values back to amis data scope
env amis environment config (fetcher, notify, etc.)

Using in Amis Schema | 在 amis Schema 中使用

After registration, reference via type in amis JSON schema:

{
  "type": "my-widget",
  "id": "widget1",
  "title": "Hello World",
  "config": { "theme": "dark" },
  "onEvent": {
    "change": {
      "actions": [
        {
          "actionType": "setValue",
          "args": { "value": "${event.data.value}" }
        }
      ]
    }
  }
}

API v6 Response Structures | API v6 响应数据结构

When calling Steedos API v6 endpoints from webapp code (fetch/axios), use the correct response format:

Endpoint Response Format
GET /api/v6/data/:obj?skip=0&top=20 (list) { "data": [...], "totalCount": 42 }
GET /api/v6/data/:obj/:id (single) { "_id": "...", "name": "...", ... } — Raw document, NOT wrapped
POST /api/v6/data/:obj (create) { "_id": "...", ... } — Raw created document, NOT wrapped
PATCH /api/v6/data/:obj/:id (update) { "_id": "...", ... } — Raw updated document, NOT wrapped
DELETE /api/v6/data/:obj/:id (delete) { "deleted": true, "_id": "..." }
POST /api/v6/functions/:obj/:fn (function) Whatever the function returns — NO wrapping, raw return value
// List records — response has { data, totalCount }
const res = await fetch('/api/v6/data/orders?skip=0&top=20');
const { data: orders, totalCount } = await res.json();

// Single record — response IS the record
const res = await fetch(`/api/v6/data/orders/${id}`);
const order = await res.json(); // { _id, name, status, ... }

// Create record — response IS the created record
const res = await fetch('/api/v6/data/orders', { method: 'POST', body: JSON.stringify(record) });
const created = await res.json(); // { _id, name, created, ... }

// Call function — response IS whatever the function returns
const res = await fetch('/api/v6/functions/orders/approve', {
  method: 'POST',
  body: JSON.stringify({ id: orderId })
});
const result = await res.json(); // e.g. { message: "Approved", success: true }

⚠️ skip and top are REQUIRED for all list endpoints (/api/v6/data/, /api/v6/tables/, /api/v6/direct/).

📖 For complete API v6 documentation (all endpoints, filter operators, complex filters, authentication), load the steedos-server-api skill.

📖 如需 API v6 完整文档(所有端点、筛选运算符、复合筛选、认证方式),请加载 steedos-server-api 技能

Express Router for SPA Access | 通过 Router 提供 SPA 访问

Note: The SPA router is created by default in Step 5 of "Creating a New Webapp". This section provides detailed reference for the router implementation.

注意: SPA 路由已在「创建新 webapp」Step 5 中默认创建。本节提供路由实现的详细参考。

Webapps serve as standalone SPA applications via Express routes in main/default/routes/:

'use strict';
const express = require('express');
const router = express.Router();
const { requireAuthentication } = require("@steedos/auth");
const path = require('path');

const packageRoot = path.dirname(require.resolve('@steedos-labs/my-package/package.json'));
const webappDistPath = path.join(packageRoot, 'webapps', 'my-widget', 'dist');
const amisRendererDistPath = path.join(webappDistPath, 'amis-renderer');

// Main page entry (requires auth)
router.get('/api/my-package/my-widget', requireAuthentication, async (req, res) => {
  try {
    if (process.env.NODE_ENV === 'development') {
      return res.redirect('http://localhost:5173');
    }
    res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
  } catch (e) {
    res.status(500).send({ errors: [{ errorMessage: e.message }] });
  }
});

// Static assets
router.use('/api/my-package/my-widget', express.static(webappDistPath));

// SPA fallback (frontend routing support)
router.use('/api/my-package/my-widget', (req, res, next) => {
  if (req.method !== 'GET') return next();
  if (path.extname(req.path)) return next();
  if (req.path === '/' || req.path === '') return next();
  requireAuthentication(req, res, () => {
    try {
      if (process.env.NODE_ENV === 'development') {
        return res.redirect('http://localhost:5173');
      }
      res.sendFile(path.join(webappDistPath, 'index.html'), { dotfiles: 'allow' });
    } catch (e) {
      res.status(500).send({ errors: [{ errorMessage: e.message }] });
    }
  });
});

// amis renderer static assets
router.use('/api/my-package/my-widget-amis', express.static(amisRendererDistPath));

exports.default = router;

Important: The webapp's vite.config.ts base must match the router path:

base: command === 'build' ? '/api/my-package/my-widget/' : '/',

Client Loader File | 客户端加载文件

⚠️ Required: Each webapp MUST have a client loader file at main/default/client/<webapp-name>.client.js. This file tells Steedos to load the compiled amis renderer JS and CSS into the frontend. Without it, the amis component will NOT be available in pages.

⚠️ 必须:每个 webapp 都必须有 main/default/client/<webapp-name>.client.js 加载文件。没有此文件,amis 自定义组件不会被加载到前端。

File Naming | 命名规则

The file name MUST match the webapp/public folder name:

Webapp Directory Public Output Client Loader File
webapps/designer/ public/designer/ main/default/client/designer.client.js
webapps/dashboard/ public/dashboard/ main/default/client/dashboard.client.js
webapps/workstation/ public/workstation/ main/default/client/workstation.client.js

File Content | 文件内容

// main/default/client/my-widget.client.js
waitForThing(window, 'antd').then(function(){
    loadJs('/my-widget/amis-renderer.js');
    loadCss('/my-widget/amis-renderer.css')
})

How it works:

  1. waitForThing(window, 'antd') — Waits until window.antd is available. The IIFE uses amisRequire("react") and amisRequire("amis") which depend on antd being loaded first.
  2. loadJs('/my-widget/amis-renderer.js') — Loads the compiled IIFE script from public/my-widget/. The script self-executes and registers the amis Renderer.
  3. loadCss('/my-widget/amis-renderer.css') — Loads the scoped CSS from public/my-widget/.

The paths (/my-widget/...) correspond to the public/my-widget/ directory, which Steedos serves as static files.

waitForThingloadJsloadCss 是 Steedos 前端内置的全局工具函数,无需额外引入。

CSS Isolation | CSS 隔离

Scope Prefix | 作用域前缀

postcss-prefix-selector adds a scope class to all CSS rules:

/* Before */
.btn { color: red; }

/* After */
.my-widget .btn { color: red; }

The component root element MUST have the matching class name (passed via className in amis-entry.ts).

Tailwind CSS v4 Workarounds

Tailwind v4 introduces @property, @supports, @layer — global CSS features that conflict with the host page. Add these 3 PostCSS plugins after prefixSelector in vite.amis.config.ts:

// 1. Remove @property — global, pollutes host page CSS property registry
function removeAtProperty() {
  return {
    postcssPlugin: 'remove-at-property',
    AtRule: { property(rule) { rule.remove() } },
  }
}
removeAtProperty.postcss = true

// 2. Unwrap @supports fallbacks — @property removed, so variable defaults must apply unconditionally
function unwrapTwSupports() {
  return {
    postcssPlugin: 'unwrap-tw-supports',
    AtRule: {
      supports(atRule) {
        if (atRule.params.includes('-webkit-hyphens') || atRule.params.includes('-moz-orient')) {
          atRule.nodes?.length ? atRule.replaceWith(atRule.nodes) : atRule.remove()
        }
      },
    },
  }
}
unwrapTwSupports.postcss = true

// 3. Remove @layer — host page CSS not in layers, layer styles can never override
function removeAtLayer() {
  return {
    postcssPlugin: 'remove-at-layer',
    AtRule: {
      layer(atRule) {
        atRule.nodes?.length ? atRule.replaceWith(atRule.nodes) : atRule.remove()
      },
    },
  }
}
removeAtLayer.postcss = true

Multi-Webapp Management | 多 webapp 管理

Package Root Configuration

{
  "name": "@steedos-labs/my-package",
  "files": [
    "main/default/client",
    "main/default/routes",
    "webapps/designer/dist",
    "webapps/dashboard/dist",
    "public/designer",
    "public/dashboard",
    "package.service.js"
  ],
  "scripts": {
    "build:designer": "cd webapps/designer && npm run build:all",
    "build:dashboard": "cd webapps/dashboard && npm run build:all",
    "build:webapps": "npm run build:designer && npm run build:dashboard",
    "release": "npm run build:webapps && npm publish"
  }
}

Webapp Independence | webapp 独立性

  • Each webapp is a fully independent Vite project with its own node_modules
  • Different webapps can use different dependency versions
  • CSS scope prefixes are unique per webapp — no style conflicts
  • amis type names must be globally unique (e.g. workflow-form-v2, dashboard-chart)
  • amis-jsx-shim.ts is identical across all webapps — copy directly

Development Workflow | 开发工作流

# ---- Development ----
cd webapps/my-widget && npm install
npm run dev                    # http://localhost:5173

# ---- Build ----
npm run build:amis             # IIFE only → dist/amis-renderer/ → public/my-widget/
npm run build:all              # Standard + IIFE

# ---- Test in Steedos ----
# Start Steedos — public/my-widget/ auto-served as static files
# amis pages load amis-renderer.js and auto-register the component

# ---- Publish ----
cd ../.. && npm publish        # files field ensures public/my-widget is included

Minimum Checklist | 最小化清单

# File Description
1 webapps/xxx/src/components/ React business components
2 webapps/xxx/src/amis-jsx-shim.ts JSX bridge (copy directly)
3 webapps/xxx/src/amis-entry.ts amis registration entry (modify component import and type)
4 webapps/xxx/vite.amis.config.ts IIFE build config (modify entry, global name, CSS scope)
5 webapps/xxx/package.json Add build:amis script
6 Root package.json Include public/xxx, main/default/client, main/default/routes in files, add build command
7 main/default/client/xxx.client.js ⚠️ Client loader — triggers loading of amis renderer into frontend
8 main/default/routes/xxx.router.js ⚠️ SPA router — enables standalone SPA access via /api/<pkg>/xxx

Post-Development Prompt | 开发完成后提示

After completing webapp development and configuration, always inform the user about the available access methods:

webapp 开发和配置完成后,务必告知用户可用的访问方式

✅ Webapp {webapp-name} 开发完成!你可以通过以下方式访问:

  • amis 组件方式:在 amis Schema 中使用 "type": "{webapp-name}" 嵌入到任意页面
  • 独立 SPA 方式:通过浏览器访问 {ROOT_URL}/api/{package-name}/{webapp-name}
  • 开发模式:运行 cd webapps/{webapp-name} && npm run dev,访问 http://localhost:5173

FAQ | 常见问题

Q: Why can't React be bundled into the IIFE? A: amis SDK ships its own React. Bundling two copies causes Hooks errors and broken Context sharing. Must use amisRequire("react") to reuse amis's React.

Q: Third-party lib imports react/jsx-runtime? A: amis-jsx-shim.ts delegates jsx()/jsxs() to React.createElement(). Map both react/jsx-runtime and react/jsx-dev-runtime in Vite aliases.

Q: process is not defined error? A: Some dependencies reference process.env.NODE_ENV. Add to Vite define:

define: {
  'process.env.NODE_ENV': JSON.stringify('production'),
  'process.env': JSON.stringify({}),
}

Q: Style conflicts with host page? A: postcss-prefix-selector + root element class name. Exclude :root, html, body, @keyframes from prefixing.

Q: How to expose component methods to amis getComponentById? A: Use amisLib.ScopedContext in the bridge component, register via scoped.registerComponent(), and mount methods like getValue() and validate().

Q: How to handle antd? A: When host page loads antd via CDN, mark antd in rollup external. For dayjs locale (antd DatePicker dependency), handle it in amis-entry.ts.

Related skills
Installs
18
GitHub Stars
1.6K
First Seen
14 days ago