From ea84e87dc9ca5b28affeae14e3437e76047b542d Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Sun, 11 Oct 2026 21:09:17 +0200 Subject: [PATCH 1/7] Improve native SDK playground, chunk loading and Cloudflare delivery --- .github/workflows/check-lowcoder-sdk.yml | 40 + .../deploy-lowcoder-sdk-webpack-netlify.yml | 55 -- .../lowcoder-sdk-webpack-bundle/README.md | 102 +- .../lowcoder-sdk-webpack-bundle/index.html | 68 +- .../lowcoder-sdk-webpack-bundle/index.jsx | 114 ++- .../lowcoder-sdk-webpack-bundle/package.json | 5 +- .../plugins/JSEntryWebpackPlugin.cjs | 213 +---- .../scripts/build-release.cjs | 16 + .../scripts/check-dist.cjs | 18 + .../scripts/prepare-release.cjs | 43 + .../lowcoder-sdk-webpack-bundle/site/404.html | 1 + .../lowcoder-sdk-webpack-bundle/site/_headers | 16 + .../site/demo.json | 182 ++++ .../site/guide.html | 57 ++ .../site/playground.js | 67 ++ .../lowcoder-sdk-webpack-bundle/site/site.css | 876 ++++++++++++++++++ .../src/dev-utils/buildVars.cjs | 4 +- .../lowcoder-sdk-webpack-bundle/src/loader.js | 28 + .../test/instance.test.cjs | 53 ++ .../test/loader.test.cjs | 50 + .../webpack.config.cjs | 22 +- .../wrangler.jsonc | 14 + client/packages/lowcoder-sdk/index.d.ts | 4 + client/packages/lowcoder-sdk/package.json | 9 +- client/packages/lowcoder-sdk/vite.config.mts | 27 +- .../lowcoder/src/appView/AppViewInstance.tsx | 28 +- .../lowcoder/src/appView/LowcoderAppView.tsx | 26 +- .../lowcoder/src/appView/bootstrapAt.tsx | 12 +- 28 files changed, 1756 insertions(+), 394 deletions(-) create mode 100644 .github/workflows/check-lowcoder-sdk.yml delete mode 100644 .github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/scripts/prepare-release.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/404.html create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/_headers create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/demo.json create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/guide.html create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/playground.js create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/site.css create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/src/loader.js create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/test/instance.test.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/test/loader.test.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc diff --git a/.github/workflows/check-lowcoder-sdk.yml b/.github/workflows/check-lowcoder-sdk.yml new file mode 100644 index 0000000000..839385785c --- /dev/null +++ b/.github/workflows/check-lowcoder-sdk.yml @@ -0,0 +1,40 @@ +name: Check Lowcoder SDK + +on: + pull_request: + paths: + - 'client/**' + - '.github/workflows/check-lowcoder-sdk.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: check-lowcoder-sdk-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + if: github.repository == 'lowcoder-org/lowcoder' + runs-on: ubuntu-latest + defaults: + run: + working-directory: client + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22.22.2 + cache: yarn + cache-dependency-path: client/yarn.lock + - name: Install dependencies + run: node .yarn/releases/yarn-3.6.4.cjs install --immutable + - name: Test browser SDK contract + run: node .yarn/releases/yarn-3.6.4.cjs workspace lowcoder-sdk-webpack-bundle test + - name: Build browser SDK and website + env: + NODE_OPTIONS: --max_old_space_size=6144 + run: node .yarn/releases/yarn-3.6.4.cjs workspace lowcoder-sdk-webpack-bundle build + - name: Check release sizes and files + run: node packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs diff --git a/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml b/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml deleted file mode 100644 index 13f64a6742..0000000000 --- a/.github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Builds client/packages/lowcoder-sdk-webpack-bundle and deploys its dist/ folder to Netlify. -# -# Deploy uses --no-build so Netlify CLI does not run the site UI "build command" (e.g. expo). -# The webpack bundle is built in the prior CI step. -# -# Repository secrets (Netlify: Site settings → General → Site details → Site ID; -# User settings → Applications → Personal access tokens): -# NETLIFY_AUTH_TOKEN — Netlify personal access token -# NETLIFY_SITE_ID — Site API ID for the Netlify site - -name: Deploy SDK Webpack Bundle to Netlify - -on: - push: - branches: - - main - -permissions: - contents: read - -concurrency: - group: deploy-sdk-webpack-netlify-${{ github.ref }} - cancel-in-progress: true - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 20.x - cache: yarn - cache-dependency-path: client/yarn.lock - - - name: Install dependencies - uses: borales/actions-yarn@v4.2.0 - with: - cmd: install - dir: client - - - name: Build lowcoder-sdk-webpack-bundle - uses: borales/actions-yarn@v4.2.0 - with: - cmd: workspace lowcoder-sdk-webpack-bundle build - dir: client - - - name: Deploy dist to Netlify - working-directory: client/packages/lowcoder-sdk-webpack-bundle - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SDK_SITE_ID }} - run: npx --yes netlify-cli deploy --prod --dir=dist --no-build diff --git a/client/packages/lowcoder-sdk-webpack-bundle/README.md b/client/packages/lowcoder-sdk-webpack-bundle/README.md index 0b465a65a4..5b962d0c50 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/README.md +++ b/client/packages/lowcoder-sdk-webpack-bundle/README.md @@ -1,9 +1,97 @@ -# lowcoder-sdk-webpack-bundle -The code to use Lowcoder Apps on any Web Project +# Lowcoder browser SDK and playground -STEPS to use +This workspace builds the native DOM embedding SDK and the public site at +https://sdk.lowcoder.cloud. The npm SDK lives alongside it in `../lowcoder-sdk`. -1) build the project with `npm run build` -2) host `dist` folder on server -3) add `bundle.js` as an external script on the HTML page where you want to include the app -4) Add the apps as given in demo under `/dist/index_test.html` +## Build and verify + +From `client/`, with Node 22 and the repository's Yarn version: + +```sh +node .yarn/releases/yarn-3.6.4.cjs install --immutable +node .yarn/releases/yarn-3.6.4.cjs workspace lowcoder-sdk-webpack-bundle test +node .yarn/releases/yarn-3.6.4.cjs workspace lowcoder-sdk-webpack-bundle build +node .yarn/releases/yarn-3.6.4.cjs workspace lowcoder-sdk-webpack-bundle check:dist +``` + +`index.html` and `site/` are lightweight static pages. The playground loads the SDK +only on request and mounts `site/demo.json` through the real SDK. It exchanges +module inputs, outputs and events, and demonstrates unmount/remount. + +## Browser integration + +```html +
+ + +``` + +`ready` covers SDK initialization. Mount resolves after app data is loaded and +rendering is scheduled, not after every query/component finishes. One backend per +host page is supported: SDK config, Redux, history, styles and plugin globals are +shared. CSS is not sandboxed. Configure backend access/authentication separately. +Existing `.lowcoder-module-container` markup and callbacks still work; call +`LowcoderSDK.scan()` for later markup. See `/guide.html` for the complete example. + +## Chunk boundaries + +Webpack partitions its dependency graph, with one runtime and one set of React, +styled-components and plugin globals. The loader resolves URLs from its own script, +fetches chunks concurrently, evaluates in dependency order, propagates CSP nonces, +and exposes explicit failure/readiness. Content-hashed assets have immutable cache +headers; `bundle.js` and manifests must revalidate. Never split generated JS bytes. + +The SDK still includes component authoring/editor dependencies. These changes make +transfer/caching more granular; they do not claim a small independent renderer. +`check:dist` measures the emitted initial scripts and enforces release size limits. + +## Cloudflare deployment + +`wrangler.jsonc` deploys the static `dist/` folder to the `lowcoder-sdk` Worker. +The GitHub workflow tests and builds pull requests. Cloudflare Builds handles +production releases through its existing GitHub connection and build token: + +- Repository: `lowcoder-org/lowcoder`; production branch: `main`. +- Root: `client`; Node version: `22.22.2`. +- Build: `node packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs`. +- Deploy: `npx --yes wrangler@4.129.0 deploy --config packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc`. +- Disable preview builds: this production config owns the public SDK route. + +These settings require this change to be promoted to `main` before its next +automatic release. No npm package is published by this pipeline. + +Before publishing an update: + +```sh +node scripts/prepare-release.cjs https://sdk.lowcoder.cloud +node scripts/check-dist.cjs +npx wrangler deploy +``` + +The release step downloads the previous public asset manifest, checks SHA-256, +and retains old runtime files so cached loaders/open tabs survive deployments. +It deliberately fails if the previous release cannot be verified. The first +migration seeded this manifest from the verified Netlify deployment +`684309148dd28d5fbb335d1e` (870 public files; Netlify's deployment configuration is +not a served asset). Netlify remains available for rollback; no site deletion is +part of migration. Retained assets are not loaded by new visitors. + +For a standalone first preview, omit the previous origin. For production updates, +never omit asset retention. Cloudflare serves true 404 responses for missing files, +not the landing page as JavaScript. Keep custom-component sandbox files in place. + +Production uses the `sdk.lowcoder.cloud/*` Worker route and a proxied placeholder +DNS A record (`192.0.2.0`); the static Worker serves every path without an origin. +To roll back to Netlify, restore the proxied CNAME `sdk` → +`lowcoder-sdk.netlify.app`, then remove the SDK Worker route. Leave the Worker +assets available until caches and any new integrations have been considered. diff --git a/client/packages/lowcoder-sdk-webpack-bundle/index.html b/client/packages/lowcoder-sdk-webpack-bundle/index.html index 43d6604063..41651e2098 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/index.html +++ b/client/packages/lowcoder-sdk-webpack-bundle/index.html @@ -1,37 +1,35 @@ - + - - - - - YOUR APP TITLE - + + + + +
+
THE NATIVE EMBEDDING SDK

Your product.
Lowcoder inside.

Build the parts that make your product yours.
Compose the rest visually — and embed them
directly into your app’s DOM.

Native DOM renderingInputs, outputs & eventsReact or plain JavaScriptCloud or self-hosted
+
01 / SEE THE CONNECTION

One page. Two ways to build.

The shell is ordinary HTML. The panel inside it is a real Lowcoder module. Change the customer and watch the input travel.

+
● ● ●your-product.example / customersNative DOM
CUSTOMER WORKSPACE

A familiar home for new features.

LOWCODER MODULEMounted in this page
</>

Meet your next embedded feature.

Load the SDK and mount a customer panel here.
The demo uses sample data; no account needed.

Ready when you are. The SDK loads on demand.
THE BRIDGEHost ↔ Lowcoder
Run the demo to see real module outputs and events.
+

Inspect #embed in your browser. The module renders as DOM elements inside the host page.

Download this demo’s app definition ↓
+
+
02 / BUILD WHERE IT MATTERS

A feature, without a second front end.

Keep your product’s shell in code. Give the workflows inside it a visual home your team can maintain.

01

Customer portals

Bring account tools, forms and service workflows into the product your customers already use.

Pass the customer context →
02

Operational workspaces

Add approval screens, data tables and internal processes without rebuilding the surrounding application.

Return outputs and decisions →
03

Product extensions

Ship a visually maintained module alongside your existing UI, then react to its events in your own code.

Connect to host actions →
+
03 / MAKE IT YOURS

A DOM element.
A few lines.
Your app.

Load the browser SDK once, wait for it to be ready, then mount your published Lowcoder app.

Read the integration guide ↗
npm install lowcoder-sdkFor React projects
index.html
<div id="my-app"></div>
+<script src="https://sdk.lowcoder.cloud/bundle.js"></script>
+<script>
+  LowcoderSDK.ready.then(async (sdk) => {
+    const app = await sdk.mount('#my-app', {
+      appId: 'YOUR_PUBLISHED_APP_ID',
+      moduleInputs: { customerId: 'customer-42' }
+    });
 
-      //Module Event Handler. MAKE SURE you use same method name
-      onModuleEventTriggered = (eventName) => {
-        console.log(eventName);
-      };
-    
-  
-  
-    
-    

App 1

-
- - - -
-
- - - - - + app.on('moduleOutputChange', (output) => { + console.log(output); + }); + }).catch(console.error); +</script>
+

Your UI, connected.

Start with one module. Keep ownership of the product around it.

Build your first module ↗
+
+ diff --git a/client/packages/lowcoder-sdk-webpack-bundle/index.jsx b/client/packages/lowcoder-sdk-webpack-bundle/index.jsx index ceaef9c99e..00222aa695 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/index.jsx +++ b/client/packages/lowcoder-sdk-webpack-bundle/index.jsx @@ -1,62 +1,58 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import { LowcoderAppView } from "./src/index"; +import { bootstrapAppAt } from "./src/index"; -const opChangeDefault = (op) => {}; -const onModuleEventTriggeredDefault = (op) => {}; +const instances = new WeakMap(); +const api = window.LowcoderSDK = window.LowcoderSDK || {}; -for ( - var i = 0; - i < document.querySelectorAll(".lowcoder-module-container").length; - i++ -) { - const App = () => { - return ( - - ); - } +api.mount = async (target, options = {}) => { + const node = typeof target === "string" ? document.querySelector(target) : target; + if (!(node instanceof Element)) throw new Error("Provide an existing element to LowcoderSDK.mount."); + const { appId, ...settings } = options; + if (!appId) throw new Error("Provide an appId to LowcoderSDK.mount."); + const previous = instances.get(node); + const pending = (async () => { + if (previous) (await previous.catch(() => undefined))?.destroy(); + return bootstrapAppAt(appId, node, settings); + })(); + instances.set(node, pending); + try { return await pending; } + catch (error) { if (instances.get(node) === pending) instances.delete(node); throw error; } +}; - ReactDOM.render( - React.createElement(App, {}, null), - document - .querySelectorAll(".lowcoder-module-container") - [i]?.querySelector(".lowcoder-module-display") - ); -} +api.unmount = async (target) => { + const node = typeof target === "string" ? document.querySelector(target) : target; + const pending = node && instances.get(node); + if (!pending) return; + (await pending)?.destroy(); + if (instances.get(node) === pending) instances.delete(node); +}; + +api.scan = async (root = document) => Promise.all( + Array.from(root.querySelectorAll(".lowcoder-module-container")).map(async (container) => { + const node = container.querySelector(".lowcoder-module-display"); + if (!node || instances.has(node)) return; + const value = (name) => container.querySelector(`.${name}`)?.value; + try { + const instance = await api.mount(node, { + appId: value("module-id"), + moduleInputs: JSON.parse(value("module-input") || "{}"), + baseUrl: value("locoder-backend-url") || "https://api-service.lowcoder.cloud", + webUrl: value("locoder-frontend-url") || "https://app.lowcoder.cloud", + orgId: value("locoder-org-id") || undefined, + }); + instance.on("moduleOutputChange", (output) => { + window.opChange?.(output); + container.dispatchEvent(new CustomEvent("lowcoder:output", { detail: output })); + }); + instance.on("moduleEventTriggered", (event) => { + window.onModuleEventTriggered?.(event); + container.dispatchEvent(new CustomEvent("lowcoder:event", { detail: event })); + }); + } catch (error) { + container.dispatchEvent(new CustomEvent("lowcoder:error", { detail: error })); + console.error(error); + } + }) +); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => api.scan(), { once: true }); +} else { api.scan(); } diff --git a/client/packages/lowcoder-sdk-webpack-bundle/package.json b/client/packages/lowcoder-sdk-webpack-bundle/package.json index e5978dfb91..2351668cf8 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/package.json +++ b/client/packages/lowcoder-sdk-webpack-bundle/package.json @@ -4,9 +4,10 @@ "version": "3.0.0", "main": "index.jsx", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node --test test/*.test.cjs", "start": "webpack-dev-server --config ./webpack.config.cjs --mode development --progress --color --port 5000", - "build": "webpack --mode production" + "build": "webpack --mode production", + "check:dist": "node scripts/check-dist.cjs" }, "author": "Lowcoder Software LTD", "license": "AGPL3", diff --git a/client/packages/lowcoder-sdk-webpack-bundle/plugins/JSEntryWebpackPlugin.cjs b/client/packages/lowcoder-sdk-webpack-bundle/plugins/JSEntryWebpackPlugin.cjs index 9182f332f4..436498c74b 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/plugins/JSEntryWebpackPlugin.cjs +++ b/client/packages/lowcoder-sdk-webpack-bundle/plugins/JSEntryWebpackPlugin.cjs @@ -1,200 +1,25 @@ -const path = require('path') +const fs = require("node:fs"); +const path = require("node:path"); +// Keep bundle.js stable; every file it loads is content-addressed. class JSEntryWebpackPlugin { - constructor(options = {}) { - this.options = { - filename: 'index.js', - template: 'auto', - publicPath: options.publicPath === undefined ? 'auto' : options.publicPath, - ...options, - } - } - apply(compiler) { - compiler.hooks.emit.tap('InjectCssEntry', async compilation => { - // console.log(Object.keys(compilation.assets)) - - /** output filenames for the given entry names */ - const entryNames = Object.keys(compiler.options.entry) - const outputFileNames = new Set( - (entryNames.length ? entryNames : ['main']).map(entryName => - // Replace '[name]' with entry name - this.options.filename.replace(/\[name\]/g, entryName), - ), - ) - - /** Option for every entry point */ - const entryOption = Array.from(outputFileNames).map(filename => ({ - ...this.options, - filename, - }))[0] - - /** The public path used inside the html file */ - const publicPath = this.getPublicPath( - compilation, - entryOption.filename, - entryOption.publicPath, - ) - - /** build output path */ - const templatePath = this.getTemplatePath(entryOption.template, compilation.options) - - /** Generated file paths from the entry point names */ - const assets = this.htmlWebpackPluginAssets( - compilation, - // 只处理一个 - Array.from(compilation.entrypoints.keys()).slice(0, 1), - publicPath, - templatePath, - ) - - // js entry - if (!compilation.assets[assets.entry]) return - // const { libsImportCode } = await import("../src/dev-utils/external.js"); - // const libs = libsImportCode(["lowcoder-sdk"]); - // ${libsImportCode(["lowcoder-sdk"])} - let content = `(function() { - - // adding entry points to single bundle.js file - let scripts = ${JSON.stringify(assets.js)}; - for (let i = 0; i < scripts.length; i++) { - const scriptEle = document.createElement('script'); - scriptEle.src = scripts[i]; - document.body.appendChild(scriptEle); + const { Compilation, sources } = compiler.webpack; + compiler.hooks.thisCompilation.tap("SDKLoader", (compilation) => { + compilation.hooks.processAssets.tap( + { name: "SDKLoader", stage: Compilation.PROCESS_ASSETS_STAGE_REPORT }, + () => { + const files = compilation.entrypoints.get("main").getFiles().filter((file) => file.endsWith(".js")); + const template = fs.readFileSync(path.join(__dirname, "../src/loader.js"), "utf8"); + compilation.emitAsset("bundle.js", new sources.RawSource(template.replace("__SDK_FILES__", JSON.stringify(files)))); + compilation.emitAsset("sdk-manifest.json", new sources.RawSource(JSON.stringify({ + version: require("../package.json").version, + entry: "bundle.js", + files: files.map((file) => ({ file, bytes: compilation.getAsset(file).source.size() })), + }, null, 2))); } - })()`; - - compilation.assets[entryOption.filename] = { - source() { - return content - }, - size() { - return content.length - }, - } - }) - } - - htmlWebpackPluginAssets(compilation, entryNames, publicPath, templatePath) { - // https://github.com/jantimon/html-webpack-plugin/blob/main/index.js#L640 - const assets = { - publicPath, - templatePath, - entry: '', - js: [], - css: [], - } - - // Extract paths to .js, .mjs and .css files from the current compilation - const entryPointPublicPathMap = {} - const extensionRegexp = /\.(css|js)(\?|$)/ - for (let i = 0; i < entryNames.length; i++) { - const entryName = entryNames[i] - /** entryPointUnfilteredFiles - also includes hot module update files */ - const entryPointUnfilteredFiles = compilation.entrypoints.get(entryName).getFiles() - - const entryPointFiles = entryPointUnfilteredFiles.filter(chunkFile => { - // compilation.getAsset was introduced in webpack 4.4.0 - // once the support pre webpack 4.4.0 is dropped please - // remove the following guard: - const asset = compilation.getAsset && compilation.getAsset(chunkFile) - if (!asset) { - return true - } - // Prevent hot-module files from being included: - const assetMetaInformation = asset.info || {} - return !(assetMetaInformation.hotModuleReplacement || assetMetaInformation.development) - }) - - // Prepend the publicPath and append the hash depending on the - // webpack.output.publicPath and hashOptions - // E.g. bundle.js -> /bundle.js?hash - const entryPointPublicPaths = entryPointFiles.map(chunkFile => { - const urlPath = this.urlencodePath(chunkFile) - const entryPointPublicPath = publicPath + urlPath - - if (chunkFile.endsWith('.js')) { - assets.entry = urlPath - } - return entryPointPublicPath - }) - - entryPointPublicPaths.forEach(entryPointPublicPath => { - const extMatch = extensionRegexp.exec(entryPointPublicPath) - // Skip if the public path is not a .css, .mjs or .js file - if (!extMatch) { - return - } - // Skip if this file is already known - // (e.g. because of common chunk optimizations) - if (entryPointPublicPathMap[entryPointPublicPath]) { - return - } - entryPointPublicPathMap[entryPointPublicPath] = true - const ext = extMatch[1] - assets[ext].push(entryPointPublicPath) - }) - } - return assets - } - - getPublicPath(compilation, outputName, customPublicPath) { - const compilationHash = compilation.hash - - /** - * @type {string} the configured public path to the asset root - * if a path publicPath is set in the current webpack config use it otherwise - * fallback to a relative path - */ - const webpackPublicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { - hash: compilationHash, - }) - - // Webpack 5 introduced "auto" as default value - const isPublicPathDefined = webpackPublicPath !== 'auto' - - let publicPath = - // If the html-webpack-plugin options contain a custom public path uset it - customPublicPath !== 'auto' - ? customPublicPath - : isPublicPathDefined - ? // If a hard coded public path exists use it - webpackPublicPath - : // If no public path was set get a relative url path - path - .relative( - path.resolve(compilation.options.output.path, path.dirname(outputName)), - compilation.options.output.path, - ) - .split(path.sep) - .join('/') - - if (publicPath.length && publicPath.substr(-1, 1) !== '/') { - publicPath += '/' - } - - return publicPath - } - - getTemplatePath(template, options) { - const { context, output } = options - - return template === 'auto' - ? path.join(output.path, path.sep) - : path.join(context || '', template) - } - - urlencodePath(filePath) { - // some+path/demo.html?value=abc?def - const queryStringStart = filePath.indexOf('?') - const urlPath = queryStringStart === -1 ? filePath : filePath.substr(0, queryStringStart) - const queryString = filePath.substr(urlPath.length) - // Encode all parts except '/' which are not part of the querystring: - const encodedUrlPath = urlPath.split('/').map(encodeURIComponent).join('/') - return encodedUrlPath + queryString + ); + }); } } - -JSEntryWebpackPlugin.version = 1 - -module.exports = JSEntryWebpackPlugin +module.exports = JSEntryWebpackPlugin; diff --git a/client/packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs b/client/packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs new file mode 100644 index 0000000000..34a5a0a8bf --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs @@ -0,0 +1,16 @@ +// Cloudflare Builds entrypoint, run from client/. Keep the tested build and +// asset-retention steps identical for manual and Git-connected releases. +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); +const client = path.resolve(__dirname, '../../..'); +const run = (args) => execFileSync(process.execPath, args, { + cwd: client, + stdio: 'inherit', + env: { ...process.env, NODE_OPTIONS: '--max_old_space_size=6144' }, +}); +const yarn = '.yarn/releases/yarn-3.6.4.cjs'; +run([yarn, 'install', '--immutable']); +run([yarn, 'workspace', 'lowcoder-sdk-webpack-bundle', 'test']); +run([yarn, 'workspace', 'lowcoder-sdk-webpack-bundle', 'build']); +run(['packages/lowcoder-sdk-webpack-bundle/scripts/prepare-release.cjs', 'https://sdk.lowcoder.cloud']); +run(['packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs']); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs b/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs new file mode 100644 index 0000000000..726de5a863 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs @@ -0,0 +1,18 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const zlib = require('node:zlib'); +const assert = require('node:assert/strict'); +const root = path.resolve(__dirname, '../dist'); +const manifest = JSON.parse(fs.readFileSync(path.join(root, 'sdk-manifest.json'))); +let raw = 0, gzip = 0, largest = 0; +for (const {file} of manifest.files) { + assert.match(file, /^assets\/[\w.-]+\.[a-f0-9]{12}\.js$/); + const data = fs.readFileSync(path.join(root,file)); + raw += data.length; gzip += zlib.gzipSync(data).length; largest = Math.max(largest,data.length); +} +assert.ok(largest < 2 * 1024 * 1024, `Initial chunk exceeds 2 MiB: ${largest}`); +assert.ok(raw < 16 * 1024 * 1024, `Initial SDK exceeds 16 MiB: ${raw}`); +for (const file of ['bundle.js','index.html','guide.html','demo.json','site.css','playground.js','_headers','404.html','custom_component.html']) assert.ok(fs.existsSync(path.join(root,file)),file); +const files = fs.readdirSync(root,{recursive:true}).filter(f=>fs.statSync(path.join(root,f)).isFile()); +for (const file of files) assert.ok(fs.statSync(path.join(root,file)).size < 25 * 1024 * 1024, `Cloudflare size limit: ${file}`); +console.log(JSON.stringify({initialRequests:manifest.files.length,initialBytes:raw,initialGzipBytes:gzip,largestInitialChunkBytes:largest,totalFiles:files.length},null,2)); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/scripts/prepare-release.cjs b/client/packages/lowcoder-sdk-webpack-bundle/scripts/prepare-release.cjs new file mode 100644 index 0000000000..da87861637 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/scripts/prepare-release.cjs @@ -0,0 +1,43 @@ +// Preserve versioned files used by open tabs and the pre-migration browser loader. +// Run after building, before publishing. Never replace a freshly built file. +const fs = require('node:fs/promises'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const root = path.resolve(__dirname, '../dist'); +const sha = data => crypto.createHash('sha256').update(data).digest('hex'); +const isRuntimeAsset = file => /^assets\/[\w.-]+$/.test(file) || /^[\w.-]+\.bundle\.js(?:\.LICENSE\.txt)?$/.test(file) || /^[a-f\d]+\.(?:woff2?|ttf|otf)$/.test(file); +async function main() { + const previous = process.argv[2]; + if (previous) { + const origin = new URL(previous); + if (origin.protocol !== 'https:') throw new Error('The previous release must use HTTPS.'); + const response = await fetch(new URL('/sdk-assets.json', origin)); + if (!response.ok) throw new Error(`Cannot retain previous release: HTTP ${response.status}`); + const manifest = await response.json(); + const queue = manifest.files; + if (!Array.isArray(queue) || queue.length > 15000) throw new Error('Invalid asset manifest.'); + let cursor = 0; + await Promise.all(Array.from({length:8},async()=>{ + while(cursor < queue.length) { + const asset = queue[cursor++]; + if (!isRuntimeAsset(asset.file) || !/^[a-f\d]{64}$/.test(asset.sha256)) throw new Error('Invalid asset record.'); + const target = path.join(root,asset.file); + try { await fs.access(target); continue; } catch {} + const response = await fetch(new URL('/'+asset.file,origin)); + if (!response.ok) throw new Error(`Cannot retain ${asset.file}: HTTP ${response.status}`); + const data = Buffer.from(await response.arrayBuffer()); + if (sha(data) !== asset.sha256) throw new Error(`Hash mismatch: ${asset.file}`); + await fs.mkdir(path.dirname(target),{recursive:true}); await fs.writeFile(target,data); + } + })); + } + const files = []; + for (const file of (await fs.readdir(root,{recursive:true})).sort()) { + if (!isRuntimeAsset(file)) continue; + const data = await fs.readFile(path.join(root,file)); + files.push({file,bytes:data.length,sha256:sha(data)}); + } + await fs.writeFile(path.join(root,'sdk-assets.json'),JSON.stringify({files},null,2)+'\n'); + console.log(`Release contains ${files.length} verified runtime assets.`); +} +main().catch(error=>{console.error(error.message);process.exitCode=1}); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/404.html b/client/packages/lowcoder-sdk-webpack-bundle/site/404.html new file mode 100644 index 0000000000..0d73d9878a --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/404.html @@ -0,0 +1 @@ +Page not found — Lowcoder SDK

Nothing mounted here.

This page or SDK asset does not exist.

Back to the SDK playground
diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/_headers b/client/packages/lowcoder-sdk-webpack-bundle/site/_headers new file mode 100644 index 0000000000..7fe4e66ef9 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/_headers @@ -0,0 +1,16 @@ +/* + Access-Control-Allow-Origin: * + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + +/assets/* + Cache-Control: public, max-age=31536000, immutable + +/bundle.js + Cache-Control: public, max-age=0, must-revalidate + +/sdk-manifest.json + Cache-Control: public, max-age=0, must-revalidate + +/sdk-assets.json + Cache-Control: public, max-age=0, must-revalidate diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/demo.json b/client/packages/lowcoder-sdk-webpack-bundle/site/demo.json new file mode 100644 index 0000000000..ac1ab7c749 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/demo.json @@ -0,0 +1,182 @@ +{ + "settings": { + "themeId": "default", + "maxWidth": { + "input": 0, + "dropdown": "1920" + } + }, + "ui": { + "comp": { + "container": { + "layout": { + "intro": { + "w": 24, + "h": 9, + "x": 0, + "y": 0, + "i": "intro" + }, + "details": { + "w": 24, + "h": 8, + "x": 0, + "y": 9, + "i": "details" + }, + "action": { + "w": 12, + "h": 5, + "x": 0, + "y": 18, + "i": "action" + } + }, + "items": { + "intro": { + "name": "welcome", + "compType": "text", + "comp": { + "text": "### {{customerName.value}}\nCustomer workspace \u00b7 {{plan.value}} plan", + "type": "markdown", + "autoHeight": "auto" + } + }, + "details": { + "name": "details", + "compType": "text", + "comp": { + "text": "**{{seats.value}} seats** in this workspace.\n\nThis panel is rendered by Lowcoder. Change the customer above to update its inputs.", + "type": "markdown", + "autoHeight": "auto" + } + }, + "action": { + "name": "openCustomerButton", + "compType": "button", + "comp": { + "text": "Open customer \u2192", + "type": "", + "onEvent": [ + { + "name": "click", + "handler": { + "compType": "triggerModuleEvent", + "comp": { + "name": "openCustomer" + } + } + } + ] + } + } + } + }, + "containerRowCount": 0, + "events": { + "enableEventTestMessage": false, + "list": [ + { + "name": "openCustomer" + } + ] + }, + "containerSize": { + "width": 24, + "height": 28 + }, + "io": { + "inputs": [ + { + "name": "customerName", + "type": "string", + "defaultValue": { + "compType": "string", + "comp": "Morgan Lee" + } + }, + { + "name": "plan", + "type": "string", + "defaultValue": { + "compType": "string", + "comp": "Growth" + } + }, + { + "name": "seats", + "type": "number", + "defaultValue": { + "compType": "number", + "comp": "12" + } + } + ], + "outputs": [ + { + "name": "customer", + "value": "{{customerName.value}}" + }, + { + "name": "plan", + "value": "{{plan.value}}" + }, + { + "name": "seats", + "value": "{{seats.value}}" + } + ] + } + }, + "compType": "module" + }, + "refTree": { + "value": "" + }, + "hooks": [ + { + "compType": "urlParams", + "comp": {}, + "name": "url" + }, + { + "compType": "dayJsLib", + "comp": {}, + "name": "dayjs" + }, + { + "compType": "lodashJsLib", + "comp": {}, + "name": "_" + }, + { + "compType": "utils", + "comp": {}, + "name": "utils" + }, + { + "compType": "message", + "comp": {}, + "name": "message" + }, + { + "compType": "localStorage", + "comp": {}, + "name": "localStorage" + }, + { + "compType": "currentUser", + "comp": {}, + "name": "currentUser" + }, + { + "compType": "theme", + "comp": {}, + "name": "theme" + } + ], + "preload": { + "css": "", + "script": "" + } +} diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html b/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html new file mode 100644 index 0000000000..96becec522 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html @@ -0,0 +1,57 @@ +Integration guide — Lowcoder SDK
FROM A VISUAL APP TO YOUR PRODUCT

Put Lowcoder
inside your app.

The SDK renders Lowcoder components directly in a DOM element. Your host app controls where they live and how data moves between the two.

Start with plain JavaScript

Publish an app or module in Lowcoder and copy its application ID. Create a mount element with enough space for its layout, then load the browser SDK once.

HTML + JavaScript
<div id="customer-panel" style="min-height: 360px"></div>
+<script src="https://sdk.lowcoder.cloud/bundle.js"></script>
+<script>
+  async function start() {
+    const sdk = await LowcoderSDK.ready;
+    const app = await sdk.mount('#customer-panel', {
+      appId: 'YOUR_PUBLISHED_APP_ID',
+      baseUrl: 'https://api-service.lowcoder.cloud',
+      webUrl: 'https://app.lowcoder.cloud',
+      moduleInputs: { customerId: 'customer-42' }
+    });
+    app.on('moduleOutputChange', console.log);
+    app.on('moduleEventTriggered', console.log);
+  }
+  start().catch(console.error);
+</script>

LowcoderSDK.ready resolves when the SDK scripts have loaded. mount() resolves after the application definition is loaded and rendering is scheduled. Individual components and queries may still be loading.

Inside a React application

The npm package provides LowcoderAppView. Install it with npm install lowcoder-sdk and import the stylesheet once in your application.

CustomerPanel.tsx
import { LowcoderAppView } from 'lowcoder-sdk';
+import 'lowcoder-sdk/dist/style.css';
+
+export function CustomerPanel({ customerId }) {
+  return (
+    <LowcoderAppView
+      appId="YOUR_PUBLISHED_APP_ID"
+      moduleInputs={{ customerId }}
+      onModuleOutputChange={(output) => {
+        console.log(output);
+      }}
+      onModuleEventTriggered={(eventName) => {
+        console.log(eventName);
+      }}
+    />
+  );
+}

Use the npm package for React integration or the browser loader for a plain DOM integration. The npm release and hosted browser SDK are separate distribution channels; check your installed version before relying on newly added APIs.

Make the connection work both ways

Define inputs, outputs, events and methods in a Lowcoder module. Use those same names in your host application. A normal app can be embedded too; the module interface is what provides this explicit communication contract.

DirectionAPIUse it for
Host → moduleapp.setModuleInputs(values)Customer context, filters and other named inputs.
Module → hostapp.on('moduleOutputChange', handler)Receive the module’s current output object.
Module → hostapp.on('moduleEventTriggered', handler)React to a named event such as openCustomer.
Host → moduleapp.invokeMethod(name, args)Call a method explicitly exposed by the module after it has rendered.
app.setModuleInputs({ customerId: 'customer-99' });
+
+const unsubscribe = app.on('moduleEventTriggered', (name) => {
+  if (name === 'openCustomer') {
+    // Run your host application's navigation or action here.
+  }
+});
+
+// Each event has one active handler per instance.
+unsubscribe();

Mount, update, clean up

Keep the returned instance while the panel is visible. Update inputs when context changes. Unmount it when your host removes that screen.

// Browser SDK
+await LowcoderSDK.unmount('#customer-panel');
+
+// Or dispose the instance directly.
+app.destroy();
+
+// Mounting again on the same element replaces its previous instance.
+const next = await LowcoderSDK.mount('#customer-panel', {
+  appId: 'ANOTHER_PUBLISHED_APP_ID'
+});

The browser SDK reports failed downloads through the rejected ready promise and the lowcoder:error window event. Handle mount errors in your host UI so visitors see a useful retry message.

Choose your backend. Check the boundary.

Set baseUrl to your Lowcoder API origin and webUrl to your Lowcoder frontend. For workspace-specific login, pass orgId. Hosting the SDK assets elsewhere does not move your backend or change application permissions.

  • Public apps must be explicitly published and accessible to their intended audience.
  • Private apps still require authentication. Configure your API’s allowed origins and cookie settings for the host application; test the actual production domains.
  • Module inputs are client-visible context, never an authorization mechanism. Enforce access on the server.
  • The SDK shares runtime configuration and state. Use one Lowcoder deployment per host page; independent backend isolation is not supported.
  • Native embedding shares the page’s CSS and routing environment. Test typography, overlays and navigation with your host application. The SDK is not a style sandbox.
The browser loader downloads content-hashed chunks from its own location. Keep bundle.js and the complete emitted asset directory together when self-hosting. Do not concatenate, rename or selectively omit the chunks.

The landing page does not load the SDK until you start the demo. Splitting the runtime improves transfer parallelism and cache reuse; it does not by itself remove the cost of rendering components.

Already using bundle.js?

The existing .lowcoder-module-container markup continues to work, including the historically spelled locoder-backend-url and locoder-frontend-url inputs. Global opChange and onModuleEventTriggered callbacks remain supported.

<div class="lowcoder-module-container">
+  <input type="hidden" class="module-id"
+    value="YOUR_PUBLISHED_APP_ID">
+  <input type="hidden" class="locoder-backend-url"
+    value="https://api-service.lowcoder.cloud">
+  <div class="lowcoder-module-display"></div>
+</div>
+<script src="https://sdk.lowcoder.cloud/bundle.js"></script>

For markup added after page load, call await LowcoderSDK.ready followed by LowcoderSDK.scan(). For new integrations, use the explicit mount API so ownership and cleanup are clear.

Legacy custom-component example ↗ · Full SDK documentation ↗

diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/playground.js b/client/packages/lowcoder-sdk-webpack-bundle/site/playground.js new file mode 100644 index 0000000000..741d6d1dae --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/playground.js @@ -0,0 +1,67 @@ +(() => { + const $ = (selector) => document.querySelector(selector); + const customers = { morgan: { customerName: 'Morgan Lee', plan: 'Growth', seats: 12 }, alex: { customerName: 'Alex Rivera', plan: 'Starter', seats: 0 } }; + let instance; + let active = false; + const entries = []; + const log = (label, data) => { + entries.unshift(`${label} ${typeof data === 'string' ? data : JSON.stringify(data)}`); + $('#event-log').textContent = entries.slice(0, 4).join('\n'); + }; + function loadSDK() { + if (window.LowcoderSDK) return window.LowcoderSDK.ready; + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = new URL('/bundle.js', location.href).href; + script.onload = () => window.LowcoderSDK.ready.then(resolve, reject); + script.onerror = () => { script.remove(); reject(new Error('The SDK could not be downloaded. Please try again.')); }; + document.head.appendChild(script); + }); + } + async function run() { + const button = $('#load-demo'); + button.disabled = true; + $('#status').textContent = 'Loading the SDK and demo module…'; + try { + const [sdk, dsl] = await Promise.all([loadSDK(), fetch('/demo.json').then((response) => { if (!response.ok) throw new Error('Demo definition unavailable.'); return response.json(); })]); + $('#embed').replaceChildren(); + instance = await sdk.mount('#embed', { appId: 'sdk-customer-demo', appDsl: dsl, moduleInputs: customers[$('#customer').value] }); + instance.on('moduleOutputChange', (output) => { log('LOWCODER → HOST', output); $('#status').textContent = 'Live module connected. Change the customer or click its button.'; }); + instance.on('moduleEventTriggered', (event) => { log('LOWCODER → HOST', event); $('#status').textContent = 'The host received the module event: ' + event; }); + log('HOST → LOWCODER', customers[$('#customer').value]); + active = true; + $('#reset-demo').hidden = false; + $('#status').textContent = 'SDK mounted. Waiting for the module to render…'; + } catch (error) { + $('#status').textContent = error.message; + log('LOAD ERROR', error.message); + $('#embed').replaceChildren(button); + button.disabled = false; + button.textContent = 'Reload and try again'; + button.removeEventListener('click', run); + button.addEventListener('click', () => location.reload(), { once: true }); + } + } + $('#load-demo')?.addEventListener('click', run); + $('#customer')?.addEventListener('change', () => { + if (!active) return; + const input = customers[$('#customer').value]; + instance.setModuleInputs(input); + log('HOST → LOWCODER', input); + }); + $('#reset-demo')?.addEventListener('click', async () => { + await window.LowcoderSDK.unmount('#embed'); + active = false; + $('#reset-demo').hidden = true; + const button = document.createElement('button'); + button.className = 'button primary'; button.id = 'load-demo'; button.textContent = 'Mount the module again'; + button.addEventListener('click', run); + $('#embed').replaceChildren(button); + $('#status').textContent = 'Module unmounted. The host page is still here.'; + log('HOST', 'Module destroyed; listeners and React root cleaned up.'); + }); + document.querySelectorAll('[data-copy]').forEach((button) => button.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(document.getElementById(button.dataset.copy).textContent); button.textContent = 'Copied'; } + catch { button.textContent = 'Select code to copy'; } + })); +})(); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/site.css b/client/packages/lowcoder-sdk-webpack-bundle/site/site.css new file mode 100644 index 0000000000..82a2757426 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/site.css @@ -0,0 +1,876 @@ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;450;500;550;600;650;700&family=Manrope:wght@400;500;600;650;700;750;800&display=swap"); +.sdk-site { + --ink: #172d2c; + --muted: #526563; + --accent: #067666; + --line: #dce4df; + --paper: #f7f9f5; + background: var(--paper); + color: var(--ink); + font: + 16px/1.6 "DM Sans", + sans-serif; + margin: 0; +} +.sdk-site * { + box-sizing: border-box; +} +.sdk-site a { + color: inherit; + text-decoration: none; +} +.sdk-site button, +.sdk-site select, +.sdk-site input { + font: inherit; +} +.sdk-site button, +.sdk-site a, +.sdk-site select { + -webkit-tap-highlight-color: transparent; +} +.sdk-site :focus-visible { + outline: 3px solid #12a18a; + outline-offset: 4px; +} +.sdk-site h1, +.sdk-site h2, +.sdk-site h3, +.sdk-site p { + margin: 0; +} +.sdk-site h1, +.sdk-site h2, +.sdk-site h3 { + font-family: Manrope, sans-serif; + color: var(--ink); +} +.sdk-site h1 { + font-size: clamp(58px, 7vw, 98px); + font-weight: 650; + line-height: 1.07; + letter-spacing: -5px; + margin: 28px 0 30px; +} +.sdk-site h1 span { + color: var(--accent); +} +.sdk-site h2 { + font-size: clamp(29px, 3.4vw, 44px); + line-height: 1.18; + letter-spacing: -1.7px; + font-weight: 650; +} +.sdk-site h3 { + font-size: 22px; + line-height: 1.4; + letter-spacing: -0.5px; +} +.sdk-site .wrap { + width: min(1180px, calc(100% - 88px)); + margin: auto; +} +.sdk-site .site-header { + max-width: 1360px; + margin: auto; + height: 94px; + padding: 0 44px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--line); +} +.sdk-site .brand { + font-size: 25px; + font-weight: 750; + letter-spacing: -1.1px; + display: flex; + align-items: center; + gap: 12px; +} +.sdk-site .brand span { + font: + 11px/1.5 "DM Sans", + sans-serif; + letter-spacing: 1.6px; + border: 1px solid #afc9bf; + border-radius: 5px; + padding: 3px 7px; +} +.sdk-site nav { + display: flex; + gap: 32px; + font-size: 14px; +} +.sdk-site nav a:hover, +.sdk-site .text-link:hover { + color: var(--accent); +} +.sdk-site .hero { + padding-top: 70px; + padding-bottom: 0; +} +.sdk-site .eyebrow { + font-size: 10px; + letter-spacing: 1.9px; + font-weight: 700; + display: flex; + align-items: center; + gap: 9px; +} +.sdk-site .dot { + display: inline-block; + vertical-align: middle; + width: 7px; + height: 7px; + background: #138c73; + border-radius: 50%; +} +.sdk-site .hero-bottom { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 50px; + gap: 30px; +} +.sdk-site .hero-bottom p { + font-size: 20px; + line-height: 1.55; + color: var(--muted); +} +.sdk-site .hero-actions { + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 8px; + min-width: 210px; +} +.sdk-site .button { + display: inline-flex; + justify-content: center; + align-items: center; + gap: 30px; + min-height: 50px; + padding: 12px 22px; + border: 0; + border-radius: 7px; + cursor: pointer; + font-weight: 600; + font-size: 14px; +} +.sdk-site .primary { + background: var(--ink); + color: white; +} +.sdk-site .primary:hover { + background: #096e60; +} +.sdk-site button:disabled { + opacity: 0.6; + cursor: wait; +} +.sdk-site .text-link { + font-size: 14px; + font-weight: 600; +} +.sdk-site .proof-strip { + display: flex; + justify-content: space-between; + padding: 22px 0; + border-top: 1px solid var(--line); + font-size: 12px; + color: var(--muted); +} +.sdk-site .proof-strip span:before { + content: "↳"; + margin-right: 9px; + color: var(--accent); +} +.sdk-site .playground-section { + padding: 65px 0; + background: #edf2ec; + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} +.sdk-site .section-intro { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 40px; + margin-bottom: 36px; +} +.sdk-site .section-intro .eyebrow { + margin-bottom: 14px; +} +.sdk-site .section-intro > p { + max-width: 345px; + color: var(--muted); + font-size: 14px; + line-height: 1.65; +} +.sdk-site .workbench { + border: 1px solid #ccd7d0; + border-radius: 12px; + background: white; + overflow: hidden; + box-shadow: 0 20px 45px #203c2f09; +} +.sdk-site .browser-bar { + height: 43px; + background: #f8faf7; + border-bottom: 1px solid #e4e9e3; + display: flex; + align-items: center; + gap: 25px; + padding: 0 18px; + font: 11px/1.4 monospace; + color: #687b75; +} +.sdk-site .window-dots { + letter-spacing: 3px; + color: #a8b8ad; +} +.sdk-site .native-badge { + margin-left: auto; + color: #067666; + font: + 10px "DM Sans", + sans-serif; + background: #e7f4ed; + border-radius: 20px; + padding: 3px 10px; +} +.sdk-site .demo-shell { + display: grid; + grid-template-columns: 190px 1fr; + min-height: 385px; +} +.sdk-site .demo-sidebar { + padding: 28px 20px; + background: #f7f9f5; + display: flex; + flex-direction: column; + gap: 12px; + font-size: 12px; + border-right: 1px solid #e5ebe3; +} +.sdk-site .demo-sidebar strong { + font-size: 20px; + margin-bottom: 26px; + letter-spacing: -0.6px; +} +.sdk-site .demo-sidebar strong span { + font-size: 10px; + font-weight: 400; + letter-spacing: 0; +} +.sdk-site .sidebar-label { + font-size: 9px; + color: #78877f; + letter-spacing: 1px; +} +.sdk-site .demo-sidebar > .selected { + background: #e2ebe1; + padding: 9px; + border-radius: 5px; + margin: 0 -9px; + color: #285c4d; + font-weight: 600; + display: flex; + justify-content: space-between; +} +.sdk-site .sidebar-footer { + margin-top: auto; + padding-top: 35px; +} +.sdk-site .sidebar-footer small { + font-size: 10px; + color: #79877d; +} +.sdk-site .demo-main { + padding: 28px; + min-width: 0; +} +.sdk-site .host-controls { + display: flex; + justify-content: space-between; + gap: 20px; + margin-bottom: 27px; +} +.sdk-site .host-controls h3 { + font-size: 18px; + margin-top: 9px; +} +.sdk-site .host-controls .eyebrow { + font-size: 8px; + color: #778579; +} +.sdk-site .host-controls label { + font-size: 10px; + display: flex; + flex-direction: column; + gap: 5px; + min-width: 190px; + color: #5b6d62; +} +.sdk-site select, +.sdk-site input { + border: 1px solid #cbd8ce; + background: white; + color: var(--ink); + padding: 10px; + border-radius: 5px; + min-width: 0; + font-size: 12px; + max-width: 100%; +} +.sdk-site .embed-label { + display: flex; + justify-content: space-between; + border: 1px solid #b6d3c1; + border-bottom: 0; + border-radius: 7px 7px 0 0; + padding: 8px 12px; + background: #eaf4eb; + font-size: 8px; + letter-spacing: 1px; + color: #48765a; +} +.sdk-site .embed-label .dot { + width: 5px; + height: 5px; + margin-right: 5px; +} +.sdk-site .embed-stage { + height: 300px; + min-height: 260px; + border: 1px solid #b6d3c1; + border-radius: 0 0 7px 7px; + overflow: hidden; + padding: 12px; + background: white; +} +.sdk-site .demo-placeholder { + text-align: center; + padding: 16px 0 10px; +} +.sdk-site .code-glyph { + font: 22px monospace; + color: #69a18b; +} +.sdk-site .demo-placeholder h3 { + font-size: 18px; + margin: 10px 0; +} +.sdk-site .demo-placeholder p { + font-size: 12px; + color: var(--muted); + margin-bottom: 17px; +} +.sdk-site .demo-placeholder button { + font-size: 12px; + min-height: 40px; + padding: 7px 17px; +} +.sdk-site .demo-status { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 10px; + color: #62786a; + margin-top: 12px; +} +.sdk-site .quiet-button { + border: 0; + background: none; + text-decoration: underline; + color: var(--accent); + padding: 0; + cursor: pointer; +} +.sdk-site .event-panel { + display: grid; + grid-template-columns: 190px 1fr; + border-top: 1px solid #e5ebe3; + background: #fbfcfa; + padding: 17px 20px; + gap: 28px; + align-items: center; +} +.sdk-site .event-panel strong { + font-size: 12px; + font-weight: 500; +} +.sdk-site .event-panel .eyebrow { + font-size: 8px; + letter-spacing: 1px; + color: #779382; +} +.sdk-site .event-panel pre { + margin: 0; + font-size: 10px; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: #23674d; + max-height: 140px; + overflow: auto; +} +.sdk-site .under-demo { + display: flex; + justify-content: space-between; + gap: 20px; + margin-top: 18px; + font-size: 11px; + color: #5e7165; +} +.sdk-site .under-demo a { + text-decoration: underline; + white-space: nowrap; +} +.sdk-site .use-cases { + padding-top: 90px; + padding-bottom: 90px; +} +.sdk-site .use-case-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + margin-top: 45px; + border-top: 1px solid var(--line); +} +.sdk-site .use-case-grid article { + padding: 25px 26px 0 0; +} +.sdk-site .use-case-grid article + article { + padding-left: 28px; + border-left: 1px solid var(--line); +} +.sdk-site .case-number { + font: 11px monospace; + color: var(--accent); +} +.sdk-site .use-case-grid h3 { + font-size: 21px; + margin: 26px 0 15px; +} +.sdk-site .use-case-grid p { + font-size: 14px; + color: var(--muted); + min-height: 92px; +} +.sdk-site .case-foot { + font-size: 11px; + color: var(--accent); + display: block; + margin-top: 28px; +} +.sdk-site .quickstart { + border-top: 1px solid var(--line); + padding-top: 65px; + padding-bottom: 70px; + display: grid; + grid-template-columns: 1fr 1.3fr; + gap: 75px; +} +.sdk-site .quickstart h2 { + margin: 16px 0 20px; +} +.sdk-site .quickstart p { + font-size: 14px; + color: var(--muted); + margin-bottom: 25px; + max-width: 300px; +} +.sdk-site .install-line { + margin-top: 35px; + display: flex; + flex-direction: column; + gap: 7px; + font-size: 11px; + color: #698075; +} +.sdk-site .install-line code { + font-size: 13px; + color: var(--ink); +} +.sdk-site .code-card { + background: #17312c; + color: #c8e6d7; + border-radius: 9px; + min-width: 0; + align-self: start; +} +.sdk-site .code-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid #39514a; + color: #b2cebd; + font-size: 11px; +} +.sdk-site .copy-button { + background: transparent; + color: inherit; + border: 1px solid #638176; + border-radius: 4px; + padding: 5px 10px; + cursor: pointer; + font-size: 11px; +} +.sdk-site .code-card pre { + font: 12px/1.9 monospace; + white-space: pre-wrap; + overflow-wrap: anywhere; + margin: 0; + padding: 24px; +} +.sdk-site .final-note { + text-align: center; + border-top: 1px solid var(--line); + padding: 65px 0; +} +.sdk-site .final-note p { + color: var(--muted); + margin: 18px 0 28px; +} +.sdk-site footer { + padding: 28px 0; + border-top: 1px solid var(--line); + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + color: var(--muted); +} +.sdk-site footer .brand { + font-size: 20px; + color: var(--ink); +} +.sdk-site .skip { + position: absolute; + top: -60px; + left: 20px; + background: white; + padding: 10px; + z-index: 100; +} +.sdk-site .skip:focus { + top: 10px; +} +.sdk-site [hidden] { + display: none !important; +} +.sdk-site .guide { + padding: 55px 0 80px; + display: grid; + grid-template-columns: 210px minmax(0, 1fr); + gap: 60px; +} +.sdk-site .guide aside { + position: sticky; + top: 30px; + align-self: start; + display: flex; + flex-direction: column; + gap: 14px; + font-size: 13px; +} +.sdk-site .guide article { + max-width: 810px; +} +.sdk-site .guide h1 { + font-size: 52px; + letter-spacing: -2.5px; + margin-top: 16px; +} +.sdk-site .guide h2 { + font-size: 28px; + margin-bottom: 18px; +} +.sdk-site .guide section { + padding: 35px 0; + border-bottom: 1px solid var(--line); + scroll-margin-top: 25px; +} +.sdk-site .guide p { + margin: 12px 0; + color: var(--muted); +} +.sdk-site .guide .code-card { + margin: 22px 0; +} +.sdk-site .guide table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} +.sdk-site .guide td, +.sdk-site .guide th { + padding: 13px 10px; + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: top; +} +.sdk-site .guide a:not(.button) { + text-decoration: underline; + text-underline-offset: 4px; +} +.sdk-site .notice { + padding: 18px 20px; + background: #eaf0e6; + border-left: 3px solid #759376; + font-size: 14px; +} +.sdk-site .guide li { + margin: 8px 0; + color: var(--muted); +} +@media (min-width: 1500px) { + .sdk-site .hero { + padding-top: 90px; + } +} +@media (max-width: 900px) { + .sdk-site .wrap { + width: calc(100% - 48px); + } + .sdk-site .site-header { + padding: 0 24px; + } + .sdk-site .hero { + padding-top: 50px; + } + .sdk-site h1 { + letter-spacing: -3px; + } + .sdk-site .demo-shell { + grid-template-columns: 145px 1fr; + } + .sdk-site .demo-sidebar { + padding: 22px 15px; + } + .sdk-site .demo-sidebar strong span { + display: none; + } + .sdk-site .host-controls { + flex-direction: column; + } + .sdk-site .host-controls label { + max-width: 250px; + } + .sdk-site .section-intro { + align-items: flex-start; + } + .sdk-site .section-intro > p { + max-width: 290px; + } + .sdk-site .event-panel { + grid-template-columns: 140px 1fr; + gap: 10px; + } + .sdk-site .quickstart { + gap: 35px; + } + .sdk-site .under-demo { + flex-direction: column; + gap: 8px; + } + .sdk-site .guide { + grid-template-columns: 150px minmax(0, 1fr); + gap: 30px; + } +} +@media (max-width: 600px) { + .sdk-site .embed-stage { + height: 360px; + } + .sdk-site .wrap { + width: calc(100% - 36px); + } + .sdk-site .site-header { + padding: 0 18px; + height: 76px; + } + .sdk-site .brand { + font-size: 22px; + gap: 8px; + } + .sdk-site nav { + gap: 16px; + font-size: 11px; + } + .sdk-site .nav-github { + display: none; + } + .sdk-site h1 { + font-size: 53px; + letter-spacing: -2.8px; + } + .sdk-site .hero-bottom { + flex-direction: column; + gap: 26px; + margin-bottom: 36px; + } + .sdk-site .hero-bottom p { + font-size: 17px; + } + .sdk-site .hero-actions { + flex-direction: row; + align-items: center; + gap: 24px; + width: 100%; + padding: 0; + } + .sdk-site .hero-actions .button { + font-size: 12px; + gap: 20px; + } + .sdk-site .hero-actions .text-link { + font-size: 12px; + } + .sdk-site .proof-strip { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + font-size: 10px; + } + .sdk-site .playground-section { + padding: 40px 0; + } + .sdk-site .section-intro { + flex-direction: column; + gap: 19px; + margin-bottom: 25px; + } + .sdk-site .section-intro > p { + max-width: none; + } + .sdk-site .browser-bar { + gap: 10px; + font-size: 8px; + padding: 0 10px; + } + .sdk-site .native-badge { + font-size: 8px; + padding: 3px 6px; + } + .sdk-site .demo-sidebar { + display: none; + } + .sdk-site .demo-shell { + display: block; + } + .sdk-site .demo-main { + padding: 17px; + } + .sdk-site .host-controls h3 { + font-size: 16px; + } + .sdk-site .embed-label { + font-size: 7px; + padding: 8px; + } + .sdk-site .demo-placeholder h3 { + font-size: 16px; + } + .sdk-site .demo-placeholder p { + font-size: 11px; + } + .sdk-site .event-panel { + grid-template-columns: 1fr; + padding: 15px; + gap: 10px; + } + .sdk-site .event-panel > div { + display: flex; + justify-content: space-between; + } + .sdk-site .under-demo { + font-size: 10px; + } + .sdk-site .under-demo a { + white-space: normal; + } + .sdk-site .use-cases { + padding-top: 48px; + padding-bottom: 48px; + } + .sdk-site .use-case-grid { + grid-template-columns: 1fr; + margin-top: 30px; + } + .sdk-site .use-case-grid article, + .sdk-site .use-case-grid article + article { + border-left: 0; + padding: 22px 0; + border-bottom: 1px solid var(--line); + } + .sdk-site .use-case-grid h3 { + margin: 12px 0; + } + .sdk-site .use-case-grid p { + min-height: 0; + } + .sdk-site .case-foot { + margin-top: 17px; + } + .sdk-site .quickstart { + grid-template-columns: 1fr; + padding-top: 40px; + padding-bottom: 40px; + } + .sdk-site .quickstart h2 br { + display: none; + } + .sdk-site .quickstart h2 { + max-width: 310px; + } + .sdk-site .quickstart p { + max-width: none; + } + .sdk-site .code-card pre { + font-size: 10px; + padding: 18px; + } + .sdk-site .final-note { + padding: 45px 0; + } + .sdk-site .final-note p { + font-size: 14px; + } + .sdk-site footer { + flex-wrap: wrap; + gap: 20px; + } + .sdk-site footer > span { + display: none; + } + .sdk-site .guide { + display: block; + padding-top: 28px; + } + .sdk-site .guide aside { + position: static; + flex-direction: row; + flex-wrap: wrap; + gap: 12px; + font-size: 11px; + margin-bottom: 30px; + } + .sdk-site .guide h1 { + font-size: 42px; + } + .sdk-site .guide td, + .sdk-site .guide th { + padding: 10px 4px; + font-size: 11px; + } + .sdk-site .guide code { + overflow-wrap: anywhere; + } +} +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} diff --git a/client/packages/lowcoder-sdk-webpack-bundle/src/dev-utils/buildVars.cjs b/client/packages/lowcoder-sdk-webpack-bundle/src/dev-utils/buildVars.cjs index 1f960bb229..20aaef534a 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/src/dev-utils/buildVars.cjs +++ b/client/packages/lowcoder-sdk-webpack-bundle/src/dev-utils/buildVars.cjs @@ -23,6 +23,8 @@ const buildVars = [ name: "REACT_APP_NODE_SERVICE_URL", defaultValue: "", }, + { name: "REACT_APP_HOCUSPOCUS_URL", defaultValue: "" }, + { name: "REACT_APP_HOCUSPOCUS_SECRET", defaultValue: "" }, { name: "REACT_APP_ENV", defaultValue: "production", @@ -59,4 +61,4 @@ const buildVars = [ module.exports = { buildVars, -}; \ No newline at end of file +}; diff --git a/client/packages/lowcoder-sdk-webpack-bundle/src/loader.js b/client/packages/lowcoder-sdk-webpack-bundle/src/loader.js new file mode 100644 index 0000000000..ea3850ec69 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/src/loader.js @@ -0,0 +1,28 @@ +(function () { + if (window.LowcoderSDK && window.LowcoderSDK.ready) return; + var source = document.currentScript; + var base = new URL(".", source.src); + var api = window.LowcoderSDK = {}; + var files = __SDK_FILES__; + api.ready = Promise.all(files.map(function (file) { + return new Promise(function (resolve, reject) { + var script = document.createElement("script"); + script.src = new URL(file, base).href; + // Fetch in parallel, evaluate in entrypoint order (one React/runtime instance). + script.async = false; + script.crossOrigin = "anonymous"; + if (source.nonce) script.nonce = source.nonce; + script.onload = resolve; + script.onerror = function () { reject(new Error("Lowcoder SDK could not load " + script.src)); }; + (document.head || document.documentElement).appendChild(script); + }); + })).then(function () { + if (!api.mount) throw new Error("Lowcoder SDK did not initialize."); + window.dispatchEvent(new CustomEvent("lowcoder:ready")); + return api; + }); + api.ready.catch(function (error) { + window.dispatchEvent(new CustomEvent("lowcoder:error", { detail: error })); + console.error(error); + }); +})(); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/test/instance.test.cjs b/client/packages/lowcoder-sdk-webpack-bundle/test/instance.test.cjs new file mode 100644 index 0000000000..f8cfa23819 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/test/instance.test.cjs @@ -0,0 +1,53 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const fs = require('node:fs'); +const path = require('node:path'); +const ts = require('typescript'); +const lodash = require('lodash'); +const source = fs.readFileSync(path.join(__dirname, '../../lowcoder/src/appView/AppViewInstance.tsx'), 'utf8'); +const code = ts.transpileModule(source, {compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX,target:ts.ScriptTarget.ES2020,esModuleInterop:true}}).outputText; +function fixture(get = async () => { throw new Error('API unavailable'); }) { + const jsx = (type, props) => ({type,props}); + const dependencies = { + lodash, axios: { create: () => ({ get }) }, + 'react/jsx-runtime': {jsx, jsxs:jsx}, react: {lazy:() => 'AppView', Suspense:'Suspense'}, + 'styled-components': {StyleSheetManager:'Styles'}, 'antd/es/flex':'Flex', + 'components/button':{TacoButton:'Button'}, 'comps/utils/globalSettings':{setGlobalSettings(){}}, + 'constants/sdkConfig':{sdkConfig:{}}, 'constants/apiConstants':{API_STATUS_CODES:{REQUEST_NOT_AUTHORISED:401}}, + 'constants/routesURL':{AUTH_LOGIN_URL:'/auth/login'}, 'constants/authConstants':{AuthSearchParams:{}}, + 'pages/userAuth/authUtils':{saveAuthSearchParams(){}}, + '@lowcoder-ee/api/datasourceApi':{DatasourceApi:{fetchJsDatasourceByApp:async () => ({data:{data:[]}})}}, + '@lowcoder-ee/constants/queryConstants':{registryDataSourcePlugin(){}}, + }; + const context = vm.createContext({ exports:{}, require(name) { if (!(name in dependencies)) throw Error(name); return dependencies[name]; }, window:{location:{href:'https://host.example'}} }); + vm.runInContext(code, context); + const rendered=[]; let unmounts=0; + return {Instance:context.exports.AppViewInstance, root:{render(tree){rendered.push(tree)},unmount(){unmounts++}}, rendered, unmounts:()=>unmounts}; +} +const dsl = {ui:{compType:'module',comp:{io:{inputs:[{name:'value',defaultValue:{comp:'default'}}]}}}}; +test('preserves false, zero and empty inputs without mutating the supplied app definition', async () => { + for (const value of [0,false,'']) { + const f=fixture(); const app=new f.Instance('demo',{},f.root,{appDsl:dsl,moduleInputs:{value}}); + await app.ready; + const passed=f.rendered[0].props.children.props.children.props.dsl; + assert.equal(passed.ui.comp.io.inputs[0].defaultValue.comp,value); + assert.equal(dsl.ui.comp.io.inputs[0].defaultValue.comp,'default'); + } +}); +test('destroy is idempotent and prevents late rendering and callbacks', async () => { + const f=fixture(); const app=new f.Instance('demo',{},f.root,{appDsl:dsl}); let calls=0; + app.on('moduleEventTriggered',()=>calls++); app.destroy(); app.destroy(); await app.ready; + app.emit('moduleEventTriggered',['event']); app.setModuleInputs({value:1}); + assert.equal(calls,0); assert.equal(f.rendered.length,0); assert.equal(f.unmounts(),1); +}); +test('unsubscribing an old handler does not remove its replacement', async () => { + const f=fixture(); const app=new f.Instance('demo',{},f.root,{appDsl:dsl}); await app.ready; let calls=0; + const off=app.on('moduleEventTriggered',()=>{}); + app.on('moduleEventTriggered',()=>calls++); off(); app.emit('moduleEventTriggered',['event']); + assert.equal(calls,1); +}); +test('network errors reject readiness rather than render a blank app', async () => { + const f=fixture(); const app=new f.Instance('demo',{},f.root,{}); + await assert.rejects(app.ready,/API unavailable/); assert.equal(f.rendered.length,0); +}); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/test/loader.test.cjs b/client/packages/lowcoder-sdk-webpack-bundle/test/loader.test.cjs new file mode 100644 index 0000000000..fb2a151346 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/test/loader.test.cjs @@ -0,0 +1,50 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const vm = require('node:vm'); +const fs = require('node:fs'); +const path = require('node:path'); +const source = fs.readFileSync(path.join(__dirname, '../src/loader.js'), 'utf8').replace('__SDK_FILES__', JSON.stringify(['assets/runtime.abc.js', 'assets/vendor.def.js', 'assets/main.ghi.js'])); +function page() { + const scripts = [], events = []; + const window = { dispatchEvent(event) { events.push(event.type); } }; + const context = vm.createContext({ window, URL, Promise, CustomEvent: class { constructor(type) { this.type = type; } }, console: { error() {} }, document: { + currentScript: { src: 'https://preview.example/versions/3/bundle.js', nonce: 'nonce-value' }, + createElement: () => ({}), head: { appendChild(script) { scripts.push(script); } }, + }}); + vm.runInContext(source, context); + return { window, scripts, events, context }; +} +test('fetches every chunk immediately, preserves evaluation order, origin, path and CSP nonce', async () => { + const p = page(); + assert.equal(p.scripts.length, 3); + p.scripts.forEach(s => { assert.equal(s.async, false); assert.equal(s.nonce, 'nonce-value'); assert.equal(s.crossOrigin, 'anonymous'); }); + assert.equal(p.scripts[0].src, 'https://preview.example/versions/3/assets/runtime.abc.js'); + let ready = false; + p.window.LowcoderSDK.ready.then(() => { ready = true; }); + p.scripts[2].onload(); p.scripts[1].onload(); + await Promise.resolve(); assert.equal(ready, false); + p.window.LowcoderSDK.mount = () => {}; + p.scripts[0].onload(); + assert.equal(await p.window.LowcoderSDK.ready, p.window.LowcoderSDK); + assert.deepEqual(p.events, ['lowcoder:ready']); +}); +test('duplicate bundle tags share one runtime and one readiness promise', () => { + const p = page(), promise = p.window.LowcoderSDK.ready; + vm.runInContext(source, p.context); + assert.equal(p.scripts.length, 3); assert.equal(p.window.LowcoderSDK.ready, promise); +}); +test('a failed chunk rejects readiness and reports the failed URL', async () => { + const p = page(); p.scripts[1].onerror(); + await assert.rejects(p.window.LowcoderSDK.ready, /vendor.def.js/); + assert.deepEqual(p.events, ['lowcoder:error']); +}); +test('loaded scripts without an initialized entry do not report success', async () => { + const p = page(); p.scripts.forEach(s => s.onload()); + await assert.rejects(p.window.LowcoderSDK.ready, /did not initialize/); +}); +test('the browser build supplies every frontend build constant', () => { + const declarations = fs.readFileSync(path.join(__dirname, '../../lowcoder/src/app-env.d.ts'), 'utf8'); + const names = [...declarations.matchAll(/declare var (REACT_APP_\w+)/g)].map(m => m[1]); + const defined = new Set(require('../src/dev-utils/buildVars.cjs').buildVars.map(v => v.name)); + for (const name of names) assert.ok(defined.has(name), `Missing SDK build definition: ${name}`); +}); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs b/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs index 02efbc1572..a25e548c01 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs +++ b/client/packages/lowcoder-sdk-webpack-bundle/webpack.config.cjs @@ -95,8 +95,11 @@ module.exports = { }, output: { path: path.resolve(__dirname, 'dist'), - publicPath: "https://sdk.lowcoder.cloud/", - filename: '[name].bundle.js', + publicPath: "auto", + filename: 'assets/[name].[contenthash:12].js', + chunkFilename: 'assets/[name].[contenthash:12].js', + uniqueName: 'lowcoder-embed-sdk', + crossOriginLoading: 'anonymous', clean: true, }, plugins: [ @@ -128,22 +131,25 @@ module.exports = { from: 'src/custom_component/index_custom_component_files', // Path to your folder to: 'index_custom_component_files/', // Destination path in the dist folder }, - { - from: 'netlify.toml', // Path to your netlify.toml file - to: 'netlify.toml', // Destination filename in the dist folder - }, + { from: 'site', to: '.' }, ], }), isVisualizerEnabled && new BundleAnalyzerPlugin() - ], + ].filter(Boolean), optimization: { minimize: true, minimizer: [new TerserPlugin({ - // Your customization if any + parallel: 2, })], sideEffects: true, splitChunks: { chunks: 'all', + // Partition the module graph without creating independent runtimes. + maxSize: 1600000, + minSize: 60000, + maxInitialRequests: 40, + maxAsyncRequests: 40, + hidePathInfo: true, }, runtimeChunk: 'single', }, diff --git a/client/packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc b/client/packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc new file mode 100644 index 0000000000..409981f9c4 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc @@ -0,0 +1,14 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "lowcoder-sdk", + "account_id": "4727dece8192846e8e1425afe6715cb4", + "compatibility_date": "2026-10-11", + "workers_dev": true, + "routes": [ + { "pattern": "sdk.lowcoder.cloud/*", "zone_name": "lowcoder.cloud" } + ], + "assets": { + "directory": "./dist", + "not_found_handling": "404-page" + } +} diff --git a/client/packages/lowcoder-sdk/index.d.ts b/client/packages/lowcoder-sdk/index.d.ts index 03ca7ae38f..5afde470b8 100644 --- a/client/packages/lowcoder-sdk/index.d.ts +++ b/client/packages/lowcoder-sdk/index.d.ts @@ -16,6 +16,8 @@ export interface AppViewInstanceOptions { appDsl?: any; moduleDslMap?: any; baseUrl?: string; + webUrl?: string; + orgId?: string; } export interface LowcoderAppViewProps extends AppViewInstanceOptions { @@ -26,6 +28,8 @@ export interface LowcoderAppViewProps extends AppViewInstanceOptions { } export interface AppViewInstance { + readonly ready: Promise; + destroy(): void; on>(event: K, handler?: EventHandlerMap[K]): Off; setModuleInputs(inputs: I): void; invokeMethod(methodName: string, params?: any[]): void; diff --git a/client/packages/lowcoder-sdk/package.json b/client/packages/lowcoder-sdk/package.json index 6b60b3eb94..0846af07ff 100644 --- a/client/packages/lowcoder-sdk/package.json +++ b/client/packages/lowcoder-sdk/package.json @@ -18,12 +18,14 @@ "module": "./dist/lowcoder-sdk.js", "exports": { ".": { + "types": "./index.d.ts", "import": "./dist/lowcoder-sdk.js", "require": "./dist/lowcoder-sdk.js" }, "./appView": { - "import": "./dist/appView.js", - "require": "./dist/appView.js" + "types": "./index.d.ts", + "import": "./dist/lowcoder-sdk.js", + "require": "./dist/lowcoder-sdk.js" }, "./dataSource": { "types": "./dataSource.d.ts" @@ -70,5 +72,6 @@ "license": "MIT", "dependencies": { "prettier": "^3.1.1" - } + }, + "types": "./index.d.ts" } diff --git a/client/packages/lowcoder-sdk/vite.config.mts b/client/packages/lowcoder-sdk/vite.config.mts index 881e5fb730..697c6d0ce6 100644 --- a/client/packages/lowcoder-sdk/vite.config.mts +++ b/client/packages/lowcoder-sdk/vite.config.mts @@ -8,8 +8,8 @@ import { buildVars } from "./src/dev-utils/buildVars"; import { globalDepPlugin } from "./src/dev-utils/globalDepPlguin"; import dynamicImport from 'vite-plugin-dynamic-import'; import { visualizer } from "rollup-plugin-visualizer"; -import { terser } from 'rollup-plugin-terser'; import strip from "@rollup/plugin-strip"; +import { terser } from "rollup-plugin-terser"; const isVisualizerEnabled = !!process.env.ENABLE_VISUALIZER; @@ -36,7 +36,8 @@ export const viteConfig: UserConfig = { }, base: ensureLastSlash(process.env.PUBLIC_URL), build: { - minify: "terser", + // Vite 4 skips its Terser pass for ES libraries. Minify exactly once below. + minify: false, chunkSizeWarningLimit: 500, lib: { formats: ["es"], @@ -51,7 +52,7 @@ export const viteConfig: UserConfig = { tryCatchDeoptimization: false, unknownGlobalSideEffects: false, }, - external: ["react", "react-dom"], + external: [/^react(?:\/|$)/, /^react-dom(?:\/|$)/], output: { chunkFileNames: "chunks/[name]-[hash].js", entryFileNames: "lowcoder-sdk.js", @@ -68,9 +69,9 @@ export const viteConfig: UserConfig = { // TEXT EDITORS & PARSERS if (id.includes("codemirror")) return "codemirror"; + if (id.includes("react-quill")) return "react-quill"; if (id.includes("quill")) return "quill"; if (id.includes("react-json-view")) return "react-json-view"; - if (id.includes("react-quill")) return "react-quill"; if (id.includes("remark-gfm")) return "remark-gfm"; if (id.includes("rehype-raw")) return "rehype-raw"; if (id.includes("rehype-sanitize")) return "rehype-sanitize"; @@ -93,11 +94,11 @@ export const viteConfig: UserConfig = { // UTILITIES & HELPERS if (id.includes("clsx")) return "clsx"; if (id.includes("immer")) return "immer"; - if (id.includes("lodash")) return "lodash"; if (id.includes("lodash-es")) return "lodash-es"; + if (id.includes("lodash")) return "lodash"; if (id.includes("uuid")) return "uuid"; if (id.includes("ua-parser-js")) return "ua-parser-js"; - if (id.includes("html2canvas")) return "ua-parser-js"; + if (id.includes("html2canvas")) return "html2canvas"; if (id.includes("numbro")) return "numbro"; // FILE & DATA PROCESSING @@ -146,19 +147,11 @@ export const viteConfig: UserConfig = { return null; } }, - experimental: { - minChunkSize: 300000, // 📏 Force smaller chunks (~300KB) - }, plugins: [ terser({ - compress: { - drop_console: true, - drop_debugger: true, - pure_funcs: ["console.info", "console.debug", "console.log"], - }, - format: { - comments: /(@vite-ignore|webpackIgnore)/ - }, + numWorkers: 2, + compress: { drop_debugger: true, pure_funcs: ["console.info", "console.debug", "console.log"] }, + format: { comments: /(@vite-ignore|webpackIgnore)/ }, }) as PluginOption, strip({ functions: ["console.log", "debugger"], // ✅ Remove logs diff --git a/client/packages/lowcoder/src/appView/AppViewInstance.tsx b/client/packages/lowcoder/src/appView/AppViewInstance.tsx index 9c01c77724..89ae84f321 100644 --- a/client/packages/lowcoder/src/appView/AppViewInstance.tsx +++ b/client/packages/lowcoder/src/appView/AppViewInstance.tsx @@ -3,7 +3,7 @@ import axios from "axios"; import type { RootComp } from "comps/comps/rootComp"; import { setGlobalSettings } from "comps/utils/globalSettings"; import { sdkConfig } from "constants/sdkConfig"; -import { get, set, isEqual } from "lodash"; +import { get, set, isEqual, cloneDeep } from "lodash"; import type { Root } from "react-dom/client"; import { StyleSheetManager } from "styled-components"; import type { ModuleDSL, ModuleDSLIoInput } from "types/dsl"; @@ -52,6 +52,8 @@ export class AppViewInstance { webUrl: "https://app.lowcoder.cloud", }; private authorizedUser: boolean = true; + private destroyed = false; + readonly ready: Promise; constructor(private appId: string, private node: Element, private root: Root, options: AppViewInstanceOptions = {}) { Object.assign(this.options, options); @@ -63,7 +65,7 @@ export class AppViewInstance { } this.dataPromise = this.loadData(); - this.render(); + this.ready = this.render(); } private isModuleDSL(dsl: any): dsl is ModuleDSL { @@ -73,7 +75,7 @@ export class AppViewInstance { private async loadData() { const { baseUrl, appDsl, moduleDslMap, webUrl } = this.options; - let finalAppDsl = appDsl; + let finalAppDsl = appDsl ? cloneDeep(appDsl) : appDsl; let finalModuleDslMap = moduleDslMap; setGlobalSettings({ @@ -102,9 +104,10 @@ export class AppViewInstance { } }; } + throw e; }); - await DatasourceApi.fetchJsDatasourceByApp(this.appId).then((res) => { + if (this.authorizedUser) await DatasourceApi.fetchJsDatasourceByApp(this.appId).then((res) => { res.data?.data?.forEach((i) => { registryDataSourcePlugin(i.type, i.id, i.pluginDefinition); }); @@ -122,7 +125,7 @@ export class AppViewInstance { const inputsPath = "ui.comp.io.inputs"; const nextInputs = get(finalAppDsl, inputsPath, []).map((i: ModuleDSLIoInput) => { const inputValue = this.options.moduleInputs[i.name]; - if (inputValue) { + if (inputValue !== undefined) { return { ...i, defaultValue: { @@ -143,6 +146,7 @@ export class AppViewInstance { } private handleCompChange(comp: RootComp | null) { + if (this.destroyed) return; this.comp = comp; if (!comp) { @@ -167,6 +171,7 @@ export class AppViewInstance { private async render() { const data = await this.dataPromise; + if (this.destroyed) return; const loginUrl = this.options.orgId ? `${this.options.webUrl}/org/${this.options.orgId}/auth/login` : `${this.options.webUrl}${AUTH_LOGIN_URL}`; @@ -226,10 +231,21 @@ export class AppViewInstance { return () => {}; } this.events.set(event, handler); - return () => this.events.delete(event); + return () => { + if (this.events.get(event) === handler) this.events.delete(event); + }; + } + + destroy() { + if (this.destroyed) return; + this.destroyed = true; + this.events.clear(); + this.comp = null; + this.root.unmount(); } setModuleInputs(inputs: I) { + if (this.destroyed) return; this.options.moduleInputs = inputs; this.render(); } diff --git a/client/packages/lowcoder/src/appView/LowcoderAppView.tsx b/client/packages/lowcoder/src/appView/LowcoderAppView.tsx index 5800b7ea5b..61d40fdc26 100644 --- a/client/packages/lowcoder/src/appView/LowcoderAppView.tsx +++ b/client/packages/lowcoder/src/appView/LowcoderAppView.tsx @@ -30,16 +30,34 @@ function LowcoderAppViewBase( if (!node) { return; } - bootstrapAppAt(appId, node, options).then(setInstance); + // An effect owns its mount node, including during StrictMode's async cleanup. + const mountNode = document.createElement("div"); + node.appendChild(mountNode); + let cancelled = false; + let mounted: AppViewInstance | undefined; + setInstance(undefined); + bootstrapAppAt(appId, mountNode, options).then((next) => { + if (cancelled) next.destroy(); + else { mounted = next; setInstance(next); } + }).catch((error) => { + if (!cancelled) console.error("Lowcoder app could not mount", error); + }); + return () => { + cancelled = true; + mountNode.remove(); + // Avoid synchronously unmounting a nested root during React's commit. + if (mounted) queueMicrotask(() => mounted?.destroy()); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appId]); + }, [appId, options.baseUrl, options.webUrl, options.orgId]); useEffect(() => { if (!instance) { return; } - instance.on("moduleOutputChange", onModuleOutputChange); - instance.on("moduleEventTriggered", onModuleEventTriggered); + const offOutput = instance.on("moduleOutputChange", onModuleOutputChange); + const offEvent = instance.on("moduleEventTriggered", onModuleEventTriggered); + return () => { offOutput(); offEvent(); }; }, [instance, onModuleEventTriggered, onModuleOutputChange]); useEffect(() => { diff --git a/client/packages/lowcoder/src/appView/bootstrapAt.tsx b/client/packages/lowcoder/src/appView/bootstrapAt.tsx index ed31fd8c7e..f34aedbd79 100644 --- a/client/packages/lowcoder/src/appView/bootstrapAt.tsx +++ b/client/packages/lowcoder/src/appView/bootstrapAt.tsx @@ -8,12 +8,18 @@ export async function bootstrapAppAt( options: AppViewInstanceOptions = {} ) { if (!node) { - console.error("node must be not null."); - return; + throw new Error("bootstrapAppAt requires an existing DOM element."); } loadComps(); const { AppViewInstance } = await import("./AppViewInstance"); - return new AppViewInstance(appId, node, createRoot(node), options); + const instance = new AppViewInstance(appId, node, createRoot(node), options); + try { + await instance.ready; + return instance; + } catch (error) { + instance.destroy(); + throw error; + } } From 34f33354af20550b34ef7a9ad2dba9d844352264 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Sun, 11 Oct 2026 21:10:15 +0200 Subject: [PATCH 2/7] Document Cloudflare build environment --- client/packages/lowcoder-sdk-webpack-bundle/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/client/packages/lowcoder-sdk-webpack-bundle/README.md b/client/packages/lowcoder-sdk-webpack-bundle/README.md index 5b962d0c50..b72627bdc8 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/README.md +++ b/client/packages/lowcoder-sdk-webpack-bundle/README.md @@ -63,6 +63,7 @@ production releases through its existing GitHub connection and build token: - Repository: `lowcoder-org/lowcoder`; production branch: `main`. - Root: `client`; Node version: `22.22.2`. +- Build variables: `NODE_VERSION=22.22.2`, `SKIP_DEPENDENCY_INSTALL=true`. - Build: `node packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs`. - Deploy: `npx --yes wrangler@4.129.0 deploy --config packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc`. - Disable preview builds: this production config owns the public SDK route. From a63da435e7653377bcf6751ed39cafd777261126 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Sun, 11 Oct 2026 21:37:08 +0200 Subject: [PATCH 3/7] Document the native embedding API and prepare stable SDK releases --- .github/workflows/check-lowcoder-sdk.yml | 2 +- .github/workflows/release-lowcoder-sdk.yml | 45 + .../lowcoder-sdk-webpack-bundle/README.md | 32 +- .../lowcoder-sdk-webpack-bundle/index.html | 254 +++- .../scripts/check-dist.cjs | 2 +- .../scripts/check-stable-release.cjs | 40 + .../site/guide.html | 256 +++- .../site/recipes.html | 404 ++++++ .../site/reference.css | 185 +++ .../site/reference.html | 1235 +++++++++++++++++ .../site/reference.js | 73 + .../test/documentation.test.cjs | 92 ++ .../test/stable-release.test.cjs | 13 + .../create-a-new-app/modules.md | 2 +- .../embedd-an-app/native-embed-sdk/README.md | 851 ++---------- 15 files changed, 2682 insertions(+), 804 deletions(-) create mode 100644 .github/workflows/release-lowcoder-sdk.yml create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/recipes.html create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/reference.css create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/reference.html create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/site/reference.js create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/test/documentation.test.cjs create mode 100644 client/packages/lowcoder-sdk-webpack-bundle/test/stable-release.test.cjs diff --git a/.github/workflows/check-lowcoder-sdk.yml b/.github/workflows/check-lowcoder-sdk.yml index 839385785c..0160777fec 100644 --- a/.github/workflows/check-lowcoder-sdk.yml +++ b/.github/workflows/check-lowcoder-sdk.yml @@ -4,7 +4,7 @@ on: pull_request: paths: - 'client/**' - - '.github/workflows/check-lowcoder-sdk.yml' + - '.github/workflows/*lowcoder-sdk.yml' workflow_dispatch: permissions: diff --git a/.github/workflows/release-lowcoder-sdk.yml b/.github/workflows/release-lowcoder-sdk.yml new file mode 100644 index 0000000000..d72797c37a --- /dev/null +++ b/.github/workflows/release-lowcoder-sdk.yml @@ -0,0 +1,45 @@ +name: Release Lowcoder SDK + +on: + release: + types: [released] + +permissions: + contents: read + +concurrency: + group: release-lowcoder-sdk + cancel-in-progress: false + +jobs: + publish: + if: github.repository == 'lowcoder-org/lowcoder' && !github.event.release.prerelease && !github.event.release.draft + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + - uses: actions/setup-node@v4 + with: + node-version: 22.22.2 + - name: Verify this stable release is the current main commit + run: node client/packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs + - name: Request the Cloudflare production build + env: + LOWCODER_SDK_DEPLOY_HOOK: ${{ secrets.LOWCODER_SDK_DEPLOY_HOOK }} + run: | + node <<'NODE' + const hook = process.env.LOWCODER_SDK_DEPLOY_HOOK; + if (!hook) throw new Error('Configure the LOWCODER_SDK_DEPLOY_HOOK repository secret.'); + const url = new URL(hook); + if (url.origin !== 'https://api.cloudflare.com' || !url.pathname.startsWith('/client/v4/workers/builds/deploy_hooks/')) { + throw new Error('Unexpected Cloudflare deploy hook URL.'); + } + (async () => { + const response = await fetch(url, { method: 'POST', signal: AbortSignal.timeout(30000) }); + const result = await response.json(); + if (!response.ok || !result.success || !result.result?.build_uuid) throw new Error('Cloudflare did not accept the build request.'); + console.log('Cloudflare build accepted: ' + result.result.build_uuid); + console.log('Publication is confirmed by the Cloudflare build, not by this trigger request.'); + })().catch(error => { console.error(error.message); process.exitCode = 1; }); + NODE diff --git a/client/packages/lowcoder-sdk-webpack-bundle/README.md b/client/packages/lowcoder-sdk-webpack-bundle/README.md index b72627bdc8..ae876a98e3 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/README.md +++ b/client/packages/lowcoder-sdk-webpack-bundle/README.md @@ -40,6 +40,11 @@ module inputs, outputs and events, and demonstrates unmount/remount. rendering is scheduled, not after every query/component finishes. One backend per host page is supported: SDK config, Redux, history, styles and plugin globals are shared. CSS is not sandboxed. Configure backend access/authentication separately. +The full embedding contract is maintained in `site/reference.html`; integration +recipes live in `site/recipes.html`. The documentation test checks every public +instance member, option, event and React prop against the source, and validates +anchors, snippet syntax and search behavior. + Existing `.lowcoder-module-container` markup and callbacks still work; call `LowcoderSDK.scan()` for later markup. See `/guide.html` for the complete example. @@ -59,17 +64,34 @@ transfer/caching more granular; they do not claim a small independent renderer. `wrangler.jsonc` deploys the static `dist/` folder to the `lowcoder-sdk` Worker. The GitHub workflow tests and builds pull requests. Cloudflare Builds handles -production releases through its existing GitHub connection and build token: +production builds through its existing GitHub connection and build token. +The release policy is **stable releases from main**, not every main push: - Repository: `lowcoder-org/lowcoder`; production branch: `main`. - Root: `client`; Node version: `22.22.2`. - Build variables: `NODE_VERSION=22.22.2`, `SKIP_DEPENDENCY_INSTALL=true`. -- Build: `node packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs`. -- Deploy: `npx --yes wrangler@4.129.0 deploy --config packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc`. +- Build: `node packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs && node packages/lowcoder-sdk-webpack-bundle/scripts/build-release.cjs`. +- Deploy: `node packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs && npx --yes wrangler@4.129.0 deploy --config packages/lowcoder-sdk-webpack-bundle/wrangler.jsonc`. - Disable preview builds: this production config owns the public SDK route. -These settings require this change to be promoted to `main` before its next -automatic release. No npm package is published by this pipeline. +Keep push-triggered production builds disabled. Create a Cloudflare Deploy Hook +for this Worker's `main` branch and store its URL in the GitHub repository secret +`LOWCODER_SDK_DEPLOY_HOOK`. `.github/workflows/release-lowcoder-sdk.yml` requests +that build only for a published stable release. Treat the hook URL as a credential; +it allows its holder to trigger this Worker's builds. Never put it in source. + +The release guard runs before the request, before building and before deploying. +It requires the checked-out commit, current `main`, and the latest stable release +tag to resolve to the same commit. A pre-release, moved main branch, older release +or unavailable GitHub verification fails closed. Publish the stable release while +main still points at that commit. The guard is deliberately stricter than ancestry. + +Activation requires these files on `main` and the deploy-hook secret. The GitHub +job confirms that Cloudflare accepted the request; check Cloudflare's build result +and the live domain to confirm publication. No npm package is published here. + +Cloudflare documents branch-head hooks at +https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/. Before publishing an update: diff --git a/client/packages/lowcoder-sdk-webpack-bundle/index.html b/client/packages/lowcoder-sdk-webpack-bundle/index.html index 41651e2098..184d562482 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/index.html +++ b/client/packages/lowcoder-sdk-webpack-bundle/index.html @@ -1,22 +1,219 @@ - - - Lowcoder SDK — Your product. Lowcoder inside. - - - - - - -
-
THE NATIVE EMBEDDING SDK

Your product.
Lowcoder inside.

Build the parts that make your product yours.
Compose the rest visually — and embed them
directly into your app’s DOM.

Native DOM renderingInputs, outputs & eventsReact or plain JavaScriptCloud or self-hosted
-
01 / SEE THE CONNECTION

One page. Two ways to build.

The shell is ordinary HTML. The panel inside it is a real Lowcoder module. Change the customer and watch the input travel.

-
● ● ●your-product.example / customersNative DOM
CUSTOMER WORKSPACE

A familiar home for new features.

LOWCODER MODULEMounted in this page
</>

Meet your next embedded feature.

Load the SDK and mount a customer panel here.
The demo uses sample data; no account needed.

Ready when you are. The SDK loads on demand.
THE BRIDGEHost ↔ Lowcoder
Run the demo to see real module outputs and events.
-

Inspect #embed in your browser. The module renders as DOM elements inside the host page.

Download this demo’s app definition ↓
-
-
02 / BUILD WHERE IT MATTERS

A feature, without a second front end.

Keep your product’s shell in code. Give the workflows inside it a visual home your team can maintain.

01

Customer portals

Bring account tools, forms and service workflows into the product your customers already use.

Pass the customer context →
02

Operational workspaces

Add approval screens, data tables and internal processes without rebuilding the surrounding application.

Return outputs and decisions →
03

Product extensions

Ship a visually maintained module alongside your existing UI, then react to its events in your own code.

Connect to host actions →
-
03 / MAKE IT YOURS

A DOM element.
A few lines.
Your app.

Load the browser SDK once, wait for it to be ready, then mount your published Lowcoder app.

Read the integration guide ↗
npm install lowcoder-sdkFor React projects
index.html
<div id="my-app"></div>
+  
+    
+    
+    Lowcoder SDK — Your product. Lowcoder inside.
+    
+    
+    
+  
+  
+    
+    
+    
+
+
+ THE NATIVE EMBEDDING SDK +
+

Your product.
Lowcoder inside.

+
+

+ Build the parts that make your product yours.
Compose the rest + visually — and embed them
+ directly into your app’s DOM. +

+ +
+
+ Native DOM renderingInputs, outputs & eventsReact or plain JavaScriptCloud or self-hosted +
+
+
+
+
+
+
01 / SEE THE CONNECTION
+

One page. Two ways to build.

+
+

+ The shell is ordinary HTML. The panel inside it is a real Lowcoder + module. Change the customer and watch the input travel. +

+
+
+
+ ● ● ●your-product.example / customersNative DOM +
+
+ +
+
+
+ CUSTOMER WORKSPACE +

A familiar home for new features.

+
+ +
+
+ LOWCODER MODULEMounted in this page +
+
+
+ </> +

Meet your next embedded feature.

+

+ Load the SDK and mount a customer panel here.
The + demo uses sample data; no account needed. +

+ +
+
+
+ Ready when you are. The SDK loads on demand. +
+
+
+
+
+ THE BRIDGEHost ↔ Lowcoder +
+
+Run the demo to see real module outputs and events.
+
+
+
+

+ Inspect #embed in your browser. The module renders as + DOM elements inside the host page. +

+ Download this demo’s app definition ↓ +
+
+
+
+
+
+
02 / BUILD WHERE IT MATTERS
+

A feature, without a second front end.

+
+

+ Keep your product’s shell in code. Give the workflows inside it a + visual home your team can maintain. +

+
+

+ CRM, website, ticketing & POS integration recipes ↗ +

+
+
+ 01 +

Customer portals

+

+ Bring account tools, forms and service workflows into the product + your customers already use. +

+ Pass the customer context → +
+
+ 02 +

Operational workspaces

+

+ Add approval screens, data tables and internal processes without + rebuilding the surrounding application. +

+ Return outputs and decisions → +
+
+ 03 +

Product extensions

+

+ Ship a visually maintained module alongside your existing UI, then + react to its events in your own code. +

+ Connect to host actions → +
+
+
+
+
+
03 / MAKE IT YOURS
+

A DOM element.
A few lines.
Your app.

+

+ Load the browser SDK once, wait for it to be ready, then mount your + published Lowcoder app. +

+ Read the integration guide ↗ +

+ Explore every function, option and event ↗ +

+
+ npm install lowcoder-sdkFor React projects +
+
+
+
+ index.html +
+
+<div id="my-app"></div>
 <script src="https://sdk.lowcoder.cloud/bundle.js"></script>
 <script>
   LowcoderSDK.ready.then(async (sdk) => {
@@ -29,7 +226,22 @@
       console.log(output);
     });
   }).catch(console.error);
-</script>
-

Your UI, connected.

Start with one module. Keep ownership of the product around it.

Build your first module ↗
-
- +</script>
+
+
+
+

Your UI, connected.

+

Start with one module. Keep ownership of the product around it.

+ Build your first module ↗ +
+
+ + + diff --git a/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs b/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs index 726de5a863..deb3911daa 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs +++ b/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-dist.cjs @@ -12,7 +12,7 @@ for (const {file} of manifest.files) { } assert.ok(largest < 2 * 1024 * 1024, `Initial chunk exceeds 2 MiB: ${largest}`); assert.ok(raw < 16 * 1024 * 1024, `Initial SDK exceeds 16 MiB: ${raw}`); -for (const file of ['bundle.js','index.html','guide.html','demo.json','site.css','playground.js','_headers','404.html','custom_component.html']) assert.ok(fs.existsSync(path.join(root,file)),file); +for (const file of ['bundle.js','index.html','guide.html','reference.html','reference.css','reference.js','recipes.html','demo.json','site.css','playground.js','_headers','404.html','custom_component.html']) assert.ok(fs.existsSync(path.join(root,file)),file); const files = fs.readdirSync(root,{recursive:true}).filter(f=>fs.statSync(path.join(root,f)).isFile()); for (const file of files) assert.ok(fs.statSync(path.join(root,file)).size < 25 * 1024 * 1024, `Cloudflare size limit: ${file}`); console.log(JSON.stringify({initialRequests:manifest.files.length,initialBytes:raw,initialGzipBytes:gzip,largestInitialChunkBytes:largest,totalFiles:files.length},null,2)); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs b/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs new file mode 100644 index 0000000000..f546b421b2 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/scripts/check-stable-release.cjs @@ -0,0 +1,40 @@ +// Deploy Hooks build a branch head, not a release tag. Fail closed unless that +// exact head is the current stable release and is still the head of main. +const { execFileSync } = require('node:child_process'); +const path = require('node:path'); +const assert = require('node:assert/strict'); + +function validateRelease(release, head, main, tagCommit) { + assert.equal(release.draft, false, 'Drafts must not deploy'); + assert.equal(release.prerelease, false, 'Pre-releases must not deploy'); + assert.match(release.tag_name, /^v?\d+\.\d+\.\d+$/, 'Expected a stable version tag'); + assert.equal(head, main, 'Only the current main commit may deploy'); + assert.equal(head, tagCommit, 'Main must point to the exact stable release commit'); +} + +async function checkStableRelease() { + const response = await fetch('https://api.github.com/repos/lowcoder-org/lowcoder/releases/latest', { + headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'lowcoder-sdk-release' }, + signal: AbortSignal.timeout(30000), + }); + if (!response.ok) throw new Error(`Cannot verify stable release: HTTP ${response.status}`); + const release = await response.json(); + // Validate before using the tag in Git arguments. + assert.match(release.tag_name, /^v?\d+\.\d+\.\d+$/, 'Expected a stable version tag'); + const git = args => execFileSync('git', args, { + cwd: path.resolve(__dirname, '../../../..'), encoding: 'utf8', timeout: 60000, + }).trim(); + const head = git(['rev-parse', 'HEAD']); + // Read both refs together; support lightweight and annotated release tags. + const refs = git(['ls-remote', 'origin', 'refs/heads/main', `refs/tags/${release.tag_name}`, `refs/tags/${release.tag_name}^{}`]).split('\n'); + const main = (refs.find(line => line.endsWith('refs/heads/main')) || '').split(/\s/)[0]; + const tagLines = refs.filter(line => line.includes('refs/tags/')); + const tagRef = tagLines.find(line => line.endsWith('^{}')) || tagLines[0]; + const tagCommit = tagRef.split(/\s/)[0]; + validateRelease(release, head, main, tagCommit); + console.log(`Verified stable release ${release.tag_name} at ${head}`); + return release; +} + +module.exports = { validateRelease, checkStableRelease }; +if (require.main === module) checkStableRelease().catch(error => { console.error(error.message); process.exitCode = 1; }); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html b/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html index 96becec522..6338923e96 100644 --- a/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/guide.html @@ -1,4 +1,63 @@ -Integration guide — Lowcoder SDK
FROM A VISUAL APP TO YOUR PRODUCT

Put Lowcoder
inside your app.

The SDK renders Lowcoder components directly in a DOM element. Your host app controls where they live and how data moves between the two.

Start with plain JavaScript

Publish an app or module in Lowcoder and copy its application ID. Create a mount element with enough space for its layout, then load the browser SDK once.

HTML + JavaScript
<div id="customer-panel" style="min-height: 360px"></div>
+
+
+  
+    
+    
+    Integration guide — Lowcoder SDK
+    
+    
+    
+  
+  
+    
+    
+    
+ +
+
FROM A VISUAL APP TO YOUR PRODUCT
+

Put Lowcoder
inside your app.

+

+ The SDK renders Lowcoder components directly in a DOM element. Your + host app controls where they live and how data moves between the two. +

+
+ Looking for every function, option, return value and event? Open the + complete embedding API reference or the + CRM, website, ticketing and POS recipes. +
+
+

Start with plain JavaScript

+

+ Publish an app or module in Lowcoder and copy its application ID. + Create a mount element with enough space for its layout, then load + the browser SDK once. +

+
+
+ HTML + JavaScript +
+
+<div id="customer-panel" style="min-height: 360px"></div>
 <script src="https://sdk.lowcoder.cloud/bundle.js"></script>
 <script>
   async function start() {
@@ -13,7 +72,30 @@
     app.on('moduleEventTriggered', console.log);
   }
   start().catch(console.error);
-</script>

LowcoderSDK.ready resolves when the SDK scripts have loaded. mount() resolves after the application definition is loaded and rendering is scheduled. Individual components and queries may still be loading.

Inside a React application

The npm package provides LowcoderAppView. Install it with npm install lowcoder-sdk and import the stylesheet once in your application.

CustomerPanel.tsx
import { LowcoderAppView } from 'lowcoder-sdk';
+</script>
+
+

+ LowcoderSDK.ready resolves when the SDK scripts have + loaded. mount() resolves after the application + definition is loaded and rendering is scheduled. Individual + components and queries may still be loading. +

+
+
+

Inside a React application

+

+ The npm package provides LowcoderAppView. Install it + with npm install lowcoder-sdk and import the stylesheet + once in your application. +

+
+
+ CustomerPanel.tsx +
+
+import { LowcoderAppView } from 'lowcoder-sdk';
 import 'lowcoder-sdk/dist/style.css';
 
 export function CustomerPanel({ customerId }) {
@@ -29,7 +111,66 @@
       }}
     />
   );
-}

Use the npm package for React integration or the browser loader for a plain DOM integration. The npm release and hosted browser SDK are separate distribution channels; check your installed version before relying on newly added APIs.

Make the connection work both ways

Define inputs, outputs, events and methods in a Lowcoder module. Use those same names in your host application. A normal app can be embedded too; the module interface is what provides this explicit communication contract.

DirectionAPIUse it for
Host → moduleapp.setModuleInputs(values)Customer context, filters and other named inputs.
Module → hostapp.on('moduleOutputChange', handler)Receive the module’s current output object.
Module → hostapp.on('moduleEventTriggered', handler)React to a named event such as openCustomer.
Host → moduleapp.invokeMethod(name, args)Call a method explicitly exposed by the module after it has rendered.
app.setModuleInputs({ customerId: 'customer-99' });
+}
+
+

+ Use the npm package for React integration or the browser loader for + a plain DOM integration. The published npm 3.0.0 package does not + yet include the hosted SDK’s readiness and disposal fixes, and its + appView subpath points to a missing file. Use the root import above; + check the + distribution table before + choosing lifecycle APIs. +

+
+
+

Make the connection work both ways

+

+ Define inputs, outputs, events and methods in a Lowcoder + module. Use those same names in your host + application. A normal app can be embedded too; the module interface + is what provides this explicit communication contract. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectionAPIUse it for
Host → moduleapp.setModuleInputs(values)Customer context, filters and other named inputs.
Module → hostapp.on('moduleOutputChange', handler)Receive the module’s current output object.
Module → hostapp.on('moduleEventTriggered', handler) + React to a named event such as openCustomer. +
Host → moduleapp.invokeMethod(name, args) + Call a method explicitly exposed by the module after it has + rendered. +
+
+
+app.setModuleInputs({ customerId: 'customer-99' });
 
 const unsubscribe = app.on('moduleEventTriggered', (name) => {
   if (name === 'openCustomer') {
@@ -38,7 +179,19 @@
 });
 
 // Each event has one active handler per instance.
-unsubscribe();

Mount, update, clean up

Keep the returned instance while the panel is visible. Update inputs when context changes. Unmount it when your host removes that screen.

// Browser SDK
+unsubscribe();
+
+
+
+

Mount, update, clean up

+

+ Keep the returned instance while the panel is visible. Update inputs + when context changes. Unmount it when your host removes that screen. +

+
+
+// Browser SDK
 await LowcoderSDK.unmount('#customer-panel');
 
 // Or dispose the instance directly.
@@ -47,11 +200,102 @@
 // Mounting again on the same element replaces its previous instance.
 const next = await LowcoderSDK.mount('#customer-panel', {
   appId: 'ANOTHER_PUBLISHED_APP_ID'
-});

The browser SDK reports failed downloads through the rejected ready promise and the lowcoder:error window event. Handle mount errors in your host UI so visitors see a useful retry message.

Choose your backend. Check the boundary.

Set baseUrl to your Lowcoder API origin and webUrl to your Lowcoder frontend. For workspace-specific login, pass orgId. Hosting the SDK assets elsewhere does not move your backend or change application permissions.

  • Public apps must be explicitly published and accessible to their intended audience.
  • Private apps still require authentication. Configure your API’s allowed origins and cookie settings for the host application; test the actual production domains.
  • Module inputs are client-visible context, never an authorization mechanism. Enforce access on the server.
  • The SDK shares runtime configuration and state. Use one Lowcoder deployment per host page; independent backend isolation is not supported.
  • Native embedding shares the page’s CSS and routing environment. Test typography, overlays and navigation with your host application. The SDK is not a style sandbox.
The browser loader downloads content-hashed chunks from its own location. Keep bundle.js and the complete emitted asset directory together when self-hosting. Do not concatenate, rename or selectively omit the chunks.

The landing page does not load the SDK until you start the demo. Splitting the runtime improves transfer parallelism and cache reuse; it does not by itself remove the cost of rendering components.

Already using bundle.js?

The existing .lowcoder-module-container markup continues to work, including the historically spelled locoder-backend-url and locoder-frontend-url inputs. Global opChange and onModuleEventTriggered callbacks remain supported.

<div class="lowcoder-module-container">
+});
+
+

+ The browser SDK reports failed downloads through the rejected + ready promise and the + lowcoder:error window event. Handle mount errors in + your host UI so visitors see a useful retry message. +

+
+
+

Choose your backend. Check the boundary.

+

+ Set baseUrl to your Lowcoder API origin and + webUrl to your Lowcoder frontend. For + workspace-specific login, pass orgId. Hosting the SDK + assets elsewhere does not move your backend or change application + permissions. +

+
    +
  • + Public apps must be explicitly published and accessible to their + intended audience. +
  • +
  • + Private apps still require authentication. Configure your API’s + allowed origins and cookie settings for the host application; test + the actual production domains. +
  • +
  • + Module inputs are client-visible context, never an authorization + mechanism. Enforce access on the server. +
  • +
  • + The SDK shares runtime configuration and state. Use one Lowcoder + deployment per host page; independent backend isolation is not + supported. +
  • +
  • + Native embedding shares the page’s CSS and routing environment. + Test typography, overlays and navigation with your host + application. The SDK is not a style sandbox. +
  • +
+
+ The browser loader downloads content-hashed chunks from its own + location. Keep bundle.js and the complete emitted asset + directory together when self-hosting. Do not concatenate, rename or + selectively omit the chunks. +
+

+ The landing page does not load the SDK until you start the demo. + Splitting the runtime improves transfer parallelism and cache reuse; + it does not by itself remove the cost of rendering components. +

+
+
+

Already using bundle.js?

+

+ The existing .lowcoder-module-container markup + continues to work, including the historically spelled + locoder-backend-url and + locoder-frontend-url inputs. Global + opChange and + onModuleEventTriggered callbacks remain supported. +

+
+
+<div class="lowcoder-module-container">
   <input type="hidden" class="module-id"
     value="YOUR_PUBLISHED_APP_ID">
   <input type="hidden" class="locoder-backend-url"
     value="https://api-service.lowcoder.cloud">
   <div class="lowcoder-module-display"></div>
 </div>
-<script src="https://sdk.lowcoder.cloud/bundle.js"></script>

For markup added after page load, call await LowcoderSDK.ready followed by LowcoderSDK.scan(). For new integrations, use the explicit mount API so ownership and cleanup are clear.

Legacy custom-component example ↗ · Full SDK documentation ↗

+<script src="https://sdk.lowcoder.cloud/bundle.js"></script>
+
+

+ For markup added after page load, call + await LowcoderSDK.ready followed by + LowcoderSDK.scan(). For new integrations, use the + explicit mount API so ownership and cleanup are clear. +

+

+ Legacy custom-component example ↗ + · Full API reference ↗ +

+
+
+
+ + + diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/recipes.html b/client/packages/lowcoder-sdk-webpack-bundle/site/recipes.html new file mode 100644 index 0000000000..cf06212398 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/recipes.html @@ -0,0 +1,404 @@ + + + + + + Four ways to connect. — Lowcoder SDK + + + + + + + + +
+ +
+
YOUR PRODUCT, CONNECTED
+

Four ways to connect.

+

+ Practical module contracts and host code for CRM, websites, ticketing + and point of sale. +

+ +
+

Start with a module contract

+

+ These are integration recipes for your own host application, not + prebuilt connectors to a specific CRM, ticket vendor or payment + provider. Each names the module inputs, outputs and events you need + to create in Lowcoder first. Replace the placeholder application IDs + and host actions. +

+

+ Load bundle.js once as shown in the + quickstart, then use these examples + inside an async function or JavaScript module. They use the hosted + browser SDK. Remove each embed through + await sdk.unmount(element) when its screen closes. +

+
+ Named module events contain only their name. Use outputs for data; + an output update and a separate event are not an atomic message. For + business mutations, prefer a single structured output with a request + ID and deduplicate it in the host/backend. +
+
+
+

CRM: a customer workspace

+

+ Keep customer navigation and account selection in your CRM. Embed a + visually maintained account panel that receives the selected + customer and can ask the host to open their record. +

+
+ + + + + + + + + + + + + + + + + + + + + +
Module contractCreate in Lowcoder
Inputs + customerId (String), + customerName (String) +
Outputs + healthScore (Number) for the host’s summary +
Event + openCustomer triggered by a module button +
+
+
+
JavaScript
+
const sdk = await LowcoderSDK.ready;
+let selected = { id: "customer-42", name: "Morgan Lee" };
+const app = await sdk.mount("#crm-panel", {
+  appId: "YOUR_CRM_MODULE_ID",
+  moduleInputs: { customerId: selected.id, customerName: selected.name }
+});
+app.on("moduleOutputChange", ({ healthScore }) => {
+  document.querySelector("#health-score").textContent = healthScore ?? "—";
+});
+app.on("moduleEventTriggered", (name) => {
+  if (name === "openCustomer") {
+    // Use the authoritative selection already held by the host.
+    location.assign("/customers/" + encodeURIComponent(selected.id));
+  }
+});
+function selectCustomer(customer) {
+  selected = customer;
+  app.setModuleInputs({ customerId: customer.id, customerName: customer.name });
+}
+
+

+ Create #crm-panel and #health-score in + your host. Wire its customer picker to selectCustomer. + The health output may arrive later than mount, so the host should + display a placeholder until it receives a value. +

+
+
+

Website: an interactive product configurator

+

+ Place a configuration module inside a product page. The site owns + the product route; Lowcoder owns the form, calculations and visual + editing. A named event can direct the host to its contact section. +

+
+ + + + + + + + + + + + + + + + + + + + + +
Module contractCreate in Lowcoder
Inputs + productId (String), + options (Array) +
Outputs + estimatedTotal (Number), calculated by the + module +
EventrequestContact triggered by the visitor
+
+
+
JavaScript
+
const sdk = await LowcoderSDK.ready;
+const app = await sdk.mount("#configurator", {
+  appId: "YOUR_CONFIGURATOR_MODULE_ID",
+  moduleInputs: {
+    productId: "desk-01",
+    options: JSON.stringify([
+      { id: "oak", label: "Oak", price: 120 },
+      { id: "walnut", label: "Walnut", price: 160 }
+    ])
+  }
+});
+app.on("moduleOutputChange", ({ estimatedTotal }) => {
+  document.querySelector("#estimate").textContent =
+    typeof estimatedTotal === "number"
+      ? new Intl.NumberFormat("en", { style: "currency", currency: "EUR" })
+          .format(estimatedTotal)
+      : "Choose your options";
+});
+app.on("moduleEventTriggered", (name) => {
+  if (name === "requestContact") {
+    document.querySelector("#contact").scrollIntoView();
+  }
+});
+
+

+ Create the #configurator, #estimate and + #contact elements. Publish the module for the intended + audience. Treat the displayed estimate as client-side information; + calculate an authoritative price on your backend before accepting an + order. +

+
+
+

Ticket system: context and escalation

+

+ Embed a triage panel alongside a ticket. Send the ticket context as + a Data input. Return an escalation request as one structured output, + so its ticket ID, reason and request ID travel together. +

+
+ + + + + + + + + + + + + + + + + + + + + +
Module contractCreate in Lowcoder
Input + ticket (Data): + { id, subject, priority } +
Output + escalationRequest (Data): null, or + { requestId, ticketId, reason } +
Module action + On explicit user escalation, assign a fresh request ID and + set the complete output object +
+
+
+
JavaScript
+
const sdk = await LowcoderSDK.ready;
+const ticket = { id: "T-1042", subject: "Export failed", priority: "normal" };
+const app = await sdk.mount("#ticket-tools", {
+  appId: "YOUR_TRIAGE_MODULE_ID",
+  moduleInputs: { ticket: JSON.stringify(ticket) }
+});
+const handled = new Set();
+app.on("moduleOutputChange", ({ escalationRequest: request }) => {
+  if (!request || typeof request.requestId !== "string" || !request.requestId ||
+      request.ticketId !== ticket.id || typeof request.reason !== "string" ||
+      handled.has(request.requestId)) return;
+  handled.add(request.requestId);
+  // Show a host-owned confirmation using safe text rendering.
+  // Its submit action calls your ticket API, which validates access
+  // and deduplicates requestId. This output is a request, not authorization.
+  document.querySelector("#escalation-reason").textContent = request.reason;
+  document.querySelector("#escalation-confirmation").hidden = false;
+});
+
+

+ Create #ticket-tools and a host confirmation panel + containing #escalation-reason. Initialize the module’s + request output to null and change it only on a + deliberate user action. Keep backend deduplication across remounts; + the in-memory set only covers this screen’s lifetime. +

+
+
+

POS: a loyalty panel beside the cart

+

+ Keep the cart, tender and payment flow in your POS. Embed a loyalty + or service panel that receives the current cart and requests an + optional lookup or host action. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Module contractCreate in Lowcoder
Inputs + cart (Array), customerId (String) +
EventrequestCustomerLookup
Method + refreshOffers, with a single + customerId parameter +
Output + offerStatus (String): module-owned + loading/success/failure information +
+
+
+
JavaScript
+
const sdk = await LowcoderSDK.ready;
+const app = await sdk.mount("#loyalty-panel", {
+  appId: "YOUR_LOYALTY_MODULE_ID",
+  moduleInputs: {
+    customerId: "customer-42",
+    cart: JSON.stringify([{ sku: "coffee-01", quantity: 2 }])
+  }
+});
+app.on("moduleEventTriggered", (name) => {
+  if (name === "requestCustomerLookup") {
+    document.querySelector("#customer-lookup").showModal();
+  }
+});
+app.on("moduleOutputChange", ({ offerStatus }) => {
+  document.querySelector("#offer-status").textContent = offerStatus ?? "";
+});
+// Enable this host control only once the module is visibly usable.
+const refreshOffers = () => app.invokeMethod("refreshOffers", ["customer-42"]);
+document.querySelector("#refresh-offers").addEventListener("click", refreshOffers);
+
+// When leaving the POS screen, remove host-owned listeners too:
+async function closeLoyaltyPanel() {
+  document.querySelector("#refresh-offers").removeEventListener("click", refreshOffers);
+  await sdk.unmount("#loyalty-panel");
+}
+
+

+ Create the panel, a + <dialog id="customer-lookup">, status text and + refresh button. A method call is not awaitable and can do nothing + before rendering; a real POS should make loading and action + availability explicit. Your POS/backend remains responsible for + prices, discounts and payment state. The SDK does not provide a + payment-terminal or offline transaction API. +

+
+
+
+ + + diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/reference.css b/client/packages/lowcoder-sdk-webpack-bundle/site/reference.css new file mode 100644 index 0000000000..ac107372cd --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/reference.css @@ -0,0 +1,185 @@ +.sdk-site .reference { + grid-template-columns: 230px minmax(0, 1fr); + gap: 54px; +} +.sdk-site .reference aside { + gap: 8px; + max-height: calc(100vh - 60px); +} +.sdk-site .reference aside .docs-back { + color: var(--accent); + margin-bottom: 15px; +} +.sdk-site .search-label { + font-weight: 650; + font-size: 12px; +} +.sdk-site .search-box { + position: relative; +} +.sdk-site .search-box input { + width: 100%; + padding: 12px 34px 12px 12px; + font-size: 12px; +} +.sdk-site .search-box button { + position: absolute; + right: 5px; + top: 5px; + border: 0; + background: transparent; + font-size: 22px; + cursor: pointer; + color: var(--muted); +} +.sdk-site .reference #search-status { + min-height: 18px; + margin: 0; + font-size: 11px; +} +.sdk-site #reference-nav { + display: flex; + flex-direction: column; + gap: 0; + overflow-y: auto; + padding: 4px 6px 4px 0; +} +.sdk-site #reference-nav a { + font-size: 12px; + text-decoration: none; + padding: 7px 8px; + border-radius: 4px; + overflow-wrap: anywhere; +} +.sdk-site #reference-nav a:hover { + background: #e6eee5; +} +.sdk-site .reference h1 { + font-size: clamp(40px, 5vw, 64px); +} +.sdk-site .reference .reference-lead { + font-size: 20px; + max-width: 620px; + margin-bottom: 25px; +} +.sdk-site .reference h2 { + overflow-wrap: anywhere; + font-size: clamp(24px, 3vw, 31px); + letter-spacing: -1px; +} +.sdk-site .reference h3 { + font-size: 19px; + margin: 24px 0 12px; +} +.sdk-site .reference code { + overflow-wrap: anywhere; + font-size: 0.88em; +} +.sdk-site .reference .signature { + padding: 14px 16px; + border: 1px solid #d2dfd3; + border-radius: 6px; + background: #eff4ec; + color: var(--ink); +} +.sdk-site .reference pre code { + font-size: inherit; +} +.sdk-site .reference .code-card pre { + white-space: pre; + overflow-x: auto; + overflow-wrap: normal; + font-size: 12px; + line-height: 1.8; +} +.sdk-site .table-scroll { + overflow-x: auto; + margin: 22px 0; +} +.sdk-site .table-scroll table { + min-width: 530px; +} +.sdk-site .table-scroll th { + color: var(--ink); + font-weight: 700; + background: #eef3e9; +} +.sdk-site .table-scroll td { + color: #405953; + line-height: 1.65; +} +.sdk-site .table-scroll td:first-child { + min-width: 150px; + color: var(--ink); +} +.sdk-site .release-label { + display: inline-block; + font-size: 11px; + border: 1px solid #bcd3bf; + border-radius: 20px; + padding: 3px 10px; + margin-right: 6px; + color: var(--accent); +} +.sdk-site .reference #search-empty { + padding: 30px; + border: 1px solid var(--line); + border-radius: 8px; +} +@media (max-width: 1000px) { + .sdk-site .reference { + grid-template-columns: 190px minmax(0, 1fr); + gap: 30px; + } +} +@media (max-width: 700px) { + .sdk-site .site-header { + flex-wrap: wrap; + height: auto; + min-height: 94px; + padding-top: 15px; + padding-bottom: 15px; + gap: 12px; + } + .sdk-site .site-header nav { + gap: 17px; + width: 100%; + font-size: 12px; + } + .sdk-site .reference { + display: block; + padding-top: 24px; + } + .sdk-site .reference aside { + position: static; + display: block; + max-height: none; + margin-bottom: 35px; + } + .sdk-site .reference .docs-back { + display: inline-block; + } + .sdk-site .search-label { + display: block; + margin-bottom: 6px; + } + .sdk-site #reference-nav { + max-height: 190px; + border-bottom: 1px solid var(--line); + } + .sdk-site .reference .reference-lead { + font-size: 17px; + } + .sdk-site .reference h1 { + letter-spacing: -2px; + } + .sdk-site .reference .table-scroll td, + .sdk-site .reference .table-scroll th { + font-size: 12px; + padding: 12px 10px; + } + .sdk-site .reference .code-card pre { + font-size: 11px; + padding: 17px; + } +} diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/reference.html b/client/packages/lowcoder-sdk-webpack-bundle/site/reference.html new file mode 100644 index 0000000000..e1956ecbeb --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/reference.html @@ -0,0 +1,1235 @@ + + + + + + The API, explained. — Lowcoder SDK + + + + + + + + +
+ +
+
THE NATIVE EMBEDDING CONTRACT
+

The API, explained.

+

+ Every embedding function, option and event: what it does, how to use + it, and what to expect. +

+ +
+

The embedding interface

+

+ A host owns the page and its navigation. Lowcoder owns the app + rendered inside one element. For communication, build a + module and define its inputs, outputs, events and + methods in the editor. Their names form your integration contract. +

+

+ A regular Lowcoder app can also be mounted, but these module APIs do + not give the host arbitrary access to its components, queries or + internal state. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Host needs to…Use
Load the browser runtime + LowcoderSDK.ready +
Create or remove an embed + mount() · + unmount() +
Send context into a module + setModuleInputs() +
Read values or react to an action + on() +
Run a module’s exposed action + invokeMethod() +
Render in React + LowcoderAppView +
Mount with the npm package + bootstrapAppAt() +
Use existing declarative markup + scan() +
+
+

+ Use the quickstart for your first embed or + jump to the + CRM, website, ticketing and POS recipes. + This reference covers the complete native embedding interface. + Component builders, editor controls, datasource plugin APIs and + re-exported utilities in the broader npm package are separate + extension APIs. +

+
+
+

Choose a distribution

+

+ Verified 11 October 2026 The + hosted browser SDK and npm package are released separately. This + page describes the current hosted SDK, with npm differences called + out below. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CapabilityHosted browser SDKnpm lowcoder-sdk@3.0.0
Entry pointhttps://sdk.lowcoder.cloud/bundle.jsimport … from "lowcoder-sdk"
Browser facade + LowcoderSDK.ready / mount / unmount / scan + + Not exported; use React or bootstrapAppAt +
Instance methods + on / setModuleInputs / invokeMethod / destroy + + on / setModuleInputs / invokeMethod; no + destroy +
Instance readiness + instance.ready; mount awaits definition loading + + No instance.ready; bootstrap resolves before + definition loading finishes +
Initial false / 0 / empty string inputsPreservedCan fall back to the module default
React lifecycle fixesPresent in current repository sourceNot yet in the published 3.0.0 package
Import subpathNot applicable + lowcoder-sdk/appView points to a missing file + in 3.0.0; use the root import +
+
+

+ The npm tarball was checked directly against the + published 3.0.0 metadata + and package contents. Its bundled declarations also omit + webUrl and orgId even though the runtime + accepts them. Do not infer installed behavior from the repository + version number alone. +

+

+ For the complete lifecycle documented here, use the hosted browser + interface. npm users should pin and test their installed version + before adopting newer source APIs. The npm build needs a bundler, + React and React DOM (declared peers: ≥18), and + import 'lowcoder-sdk/dist/style.css'. It is not a + standalone script-tag build. Import it only on the client in + server-rendered applications. +

+
+
+

LowcoderSDK.ready

+
+
JavaScript
+
const sdk = await LowcoderSDK.ready;
+// sdk.mount, sdk.unmount and sdk.scan are now available.
+
+

+ Promise<LowcoderSDK> · Browser only +

+

+ Resolves after the initial SDK scripts have loaded and the mount API + exists. It does not wait for apps discovered by the automatic markup + scan, component rendering or queries. +

+

+ Rejects if an initial chunk fails to load or initialization does not + install the mount API. Catch the promise to show a retry message. A + repeated bundle.js tag reuses an existing loader and + its promise; it does not reset a failed load. After correcting a + network or policy failure, reload the page. +

+
+
+

LowcoderSDK.mount()

+
+
JavaScript
+
const sdk = await LowcoderSDK.ready;
+const app = await sdk.mount("#customer-panel", {
+  appId: "YOUR_PUBLISHED_MODULE_ID",
+  moduleInputs: { customerId: "customer-42" }
+});
+
+

+ mount(target: string | Element, options: MountOptions): + Promise<AppViewInstance> +

+
+ + + + + + + + + + + + + + + + + + + + + +
ParameterMeaning
target + An existing DOM element or CSS selector. A selector uses its + first match. +
options.appIdRequired application or module ID. Must be non-empty.
options + The shared options below, plus + appId. +
+
+

+ Creates a React root in the element and returns the instance after + definition loading and scheduling its render. Calling mount again on + an element managed by this facade waits for the previous + initialization and destroys its instance before replacing it. +

+

+ Rejects for a missing/invalid target, invalid selector, missing app + ID or an initialization failure. Failed initialization cleans up its + root. An initial API 401 instead renders the built-in + login prompt and resolves; resolution is not proof of authorization. + Later component or query errors are outside this promise. +

+

+ Give the container dimensions through your host CSS. The SDK has no + width, height, timeout or + AbortSignal mount options. +

+
+
+

LowcoderSDK.unmount()

+
+
JavaScript
+
const container = document.querySelector("#customer-panel");
+await LowcoderSDK.unmount(container);
+container.remove(); // The host owns removal of the element.
+
+

+ unmount(target: string | Element): Promise<void> +

+

+ Waits for a mount tracked by this browser facade, destroys the + instance and removes its tracking entry. The container stays in the + DOM. An unknown or already unmounted element is a no-op. A pending + initialization failure can reject this promise; an invalid CSS + selector can also reject. +

+

+ Unmount before removing the element, or keep and pass the element + reference. A selector cannot find a detached element. Instances + created directly with bootstrapAppAt are not tracked + here. +

+
+
+

Every initialization option

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionType / defaultBehavior
baseUrl + string · https://api-service.lowcoder.cloud + + Lowcoder backend base URL, without a trailing + /api. The SDK requests + /api/applications/{appId}/view beneath it. +
webUrlstring · https://app.lowcoder.cloudLowcoder frontend URL for the authentication flow.
orgIdstring · omitted + Workspace ID used to select the workspace login URL. Does + not grant access or set the authenticated identity. +
moduleInputsinput-name map · omitted + Initial values for declared inputs on a root module. See + input encoding and updates. +
appDslapplication definition object · omitted + Supply a Lowcoder application DSL instead of fetching the + initial definition. The hosted SDK clones it before applying + initial inputs. +
moduleDslMapmap of module ID → DSL · omitted + Definitions for nested modules when supplying + appDsl. Without appDsl, the server + response supplies and replaces this map. +
+
+

+ appId is additionally required by browser mount and + React, and is the first argument to bootstrapAppAt. Use + the actual ID of your published app; the local demo uses a synthetic + ID because it supplies its own complete definition. +

+

+ Supplying appDsl skips the initial app-definition + request and JavaScript datasource discovery. It does not promise + offline execution: queries, remote components, assets or missing + nested definitions may still use the network. Keep DSL compatible + with the runtime that loads it. +

+

+ There are no public token, headers, + fetch, theme, onReady or + onError initialization options. +

+
+
JavaScript
+
const app = await LowcoderSDK.mount("#module", {
+  appId: "YOUR_PUBLISHED_MODULE_ID",
+  baseUrl: "https://lowcoder.example.com",
+  webUrl: "https://lowcoder.example.com",
+  orgId: "YOUR_WORKSPACE_ID",
+  moduleInputs: { customerId: "customer-42" }
+});
+
+
+
+

instance.ready

+
+
JavaScript
+
await app.ready; // Already fulfilled when hosted sdk.mount() returns.
+
+

+ Promise<void> · Hosted/current source; absent in + npm 3.0.0 +

+

+ Waits for initial definition loading (and datasource discovery when + applicable), then schedules rendering. It rejects on initial loading + failures. It does not wait for a React commit, lazy + components, queries or callable module methods. +

+

+ There is no universal “everything rendered” event. If the host needs + business readiness, design a named output or event in the module, + such as dataLoaded. That name is your contract, not a + built-in SDK event. Event subscriptions do not replay previous + values; an early event can be missed. Do not use a one-time startup + event as your only readiness guarantee. +

+
+
+

instance.setModuleInputs()

+
+
JavaScript
+
app.setModuleInputs({
+  customerName: "Ada",          // String input
+  quantity: 0,                  // Number input
+  enabled: false,               // Boolean input
+  customer: JSON.stringify({ id: "customer-42" }), // Data input
+  rows: JSON.stringify([{ id: 1, total: 25 }])     // Array input
+});
+
+

+ setModuleInputs(inputs: InputMap): void +

+

+ Updates declared inputs on the mounted root module and schedules + rendering. Unknown input names are ignored. Omitted keys and + undefined leave existing module values unchanged; this + is not a reset or deep merge. Pass a fresh map for each update. +

+

+ The editor supports Data, String, Array, Number, Boolean and Query + input types. Values go through Lowcoder’s code controls: they are + source values converted to text, not an arbitrary JavaScript object + bridge. Serialize structured Data and Array values with + JSON.stringify. Raw objects become + [object Object]; arrays are not automatically + JSON-encoded. +

+

+ Plain strings, numbers and booleans work with the corresponding + scalar controls. The hosted SDK preserves 0, + false and ""; npm 3.0.0 can drop them at + initial load. JavaScript null is not a general “clear” + operation; for a Data input containing JSON null, pass + "null". +

+
+ Inputs can contain expressions. + {{ … }} is interpreted by Lowcoder’s template engine, + including inside serialized strings. JSON serialization does not + turn this into a guaranteed literal-value channel. Define bounded + input contracts and validate untrusted text. Inputs are + client-visible context, never authentication or permission checks. +
+

+ Query inputs represent Lowcoder query bindings. Passing a host + function does not create a supported callback bridge. Use module + events to request host actions. These update rules also apply to the + moduleInputs initialization option and React prop. +

+
+
+

instance.on()

+
+
JavaScript
+
let latestOutput;
+const stopOutputs = app.on("moduleOutputChange", (output) => {
+  latestOutput = output;
+  console.log("Current module outputs", output);
+});
+const stopEvents = app.on("moduleEventTriggered", (eventName) => {
+  console.log("Module event", eventName);
+});
+
+// When these subscriptions are no longer needed:
+stopOutputs();
+stopEvents();
+
+

+ on(event, handler?): () => void +

+
+ + + + + + + + + + + + + + + + + + + + +
EventHandler payloadWhen it fires
+ moduleOutputChange + (output: OutputMap) => void + The whole named output object when its values change by deep + equality. Not a patch. May initially be empty. +
+ moduleEventTriggered + (eventName: string) => void + The name of an event explicitly fired by the module. No + custom payload or DOM event object. +
+
+

+ Each instance has one handler per event type. + Registering another replaces the previous one. Fan out within your + own handler if several host features need notifications. The + returned function removes that subscription; there is no separate + off() method. +

+

+ In the hosted SDK, an old unsubscribe function cannot remove a newer + replacement handler. npm 3.0.0 does not have that safeguard. Calling + on(event, undefined) returns a no-op function; it does + not clear a previous handler. +

+

+ Subscriptions do not replay a current snapshot. Register immediately + after mounting, but do not assume the first output or startup event + cannot have occurred already. To exchange results, expose named + outputs. There is no atomic event-with-data payload or guaranteed + ordering between an event and a separate output update. +

+

+ Only the two names above are instance events. + lowcoder:ready and lowcoder:error belong + to the separate + browser DOM event interface. +

+
+
+

instance.invokeMethod()

+
+
JavaScript
+
// In the Lowcoder editor, define a method named "selectCustomer"
+// with parameters in this order: customerId, focusDetails.
+// Call after the user can interact with the rendered module.
+app.invokeMethod("selectCustomer", ["customer-42", true]);
+
+

+ invokeMethod(methodName: string, params: any[] = []): void +

+

+ Runs an action exposed as a named module method. Arguments are + positional, in the order configured in the editor. A missing, + undefined or null argument uses that + parameter’s default; 0, false and + "" are retained. Extra arguments are ignored. +

+

+ The name must identify a configured module method, not an arbitrary + component function or query. An unknown method, a normal app instead + of a module, or a call before the module root exists silently does + nothing. Calls are not queued. +

+
+ No return-value promise. The SDK discards the + internal action result. + await app.invokeMethod(...) does not wait for the + action to finish and does not catch its asynchronous errors. Model + completion, results and failures as module outputs/events. +
+

+ Use a request ID in your module contract when correlating + asynchronous work with host actions. For actions such as saving, + submitting or charging, perform authoritative validation and state + changes on your backend. +

+
+
+

instance.destroy()

+
+
JavaScript
+
app.destroy(); // Synchronous; safe to call more than once.
+
+

+ destroy(): void · Hosted/current source; absent in npm + 3.0.0 +

+

+ Clears instance handlers, releases the module reference and unmounts + its React root. It leaves the host element in place. Further input + updates and method calls have no effect. +

+

+ It does not cancel outstanding HTTP requests or undo shared SDK + configuration, global CSS or page-level state. For instances owned + by the browser facade, prefer + await LowcoderSDK.unmount(container), which also clears + that facade’s tracking entry. Do not manually destroy an instance + owned by a React wrapper that is managing its lifecycle. +

+
+
+

Instance & TypeScript types

+

+ AppViewInstance<I, O> represents an embedded app. + I describes its input-name map and O its + output object. Keep the instance returned by a factory; do not + construct a second React root on an element already owned by an + embed. +

+

+ The runtime class constructor is + new AppViewInstance(appId, node, root, options = {}), + where root is a React DOM Root. It is a + low-level implementation surface: use bootstrapAppAt or + browser mount so component registration, root creation + and initialization cleanup happen together. +

+
    +
  • + AppViewInstanceOptions<I>: the six + initialization options, with moduleInputs?: I. +
  • +
  • + LowcoderAppViewProps<I, O>: those options plus + appId, className and the two callbacks. +
  • +
  • + OutputChangeHandler<O> = (output: O) => void. +
  • +
  • + EventTriggerHandler = (eventName: string) => void. +
  • +
+

+ InputMap, OutputMap and MountOptions in this reference describe the + shapes of values; they are not named npm exports. Generic types do + not validate values at runtime. The browser facade is a script + global, not an npm export. Published npm 3.0.0 has incomplete + declaration routing as well as the missing members listed in the + distribution table; verify your toolchain instead of assuming + current source declarations are installed. +

+
+
+

bootstrapAppAt()

+
+
JavaScript
+
import { bootstrapAppAt } from "lowcoder-sdk";
+import "lowcoder-sdk/dist/style.css";
+
+const app = await bootstrapAppAt(
+  "YOUR_PUBLISHED_MODULE_ID",
+  document.getElementById("customer-panel"),
+  { moduleInputs: { customerId: "customer-42" } }
+);
+
+

+ bootstrapAppAt(appId: string, node: Element, options = {}): + Promise<AppViewInstance> +

+

+ The low-level imperative factory exported by the npm package. Unlike + browser mount, it requires an element, not a selector. + Supply the same initialization options, with appId as + the first argument. The browser bundle also exposes the broader + exports through window.$lowcoder_sdk; prefer + LowcoderSDK.mount for browser integrations. +

+

+ In current source, the factory checks for a node, waits for + instance.ready, and cleans up on initialization + failure. It does not track or replace an existing root on that + element. The published npm 3.0.0 factory returns before definition + loading finishes, and its instance has neither + ready nor destroy. Its declaration + requires the options argument, so pass {} when using + defaults. Use the hosted facade for screens requiring the documented + cleanup lifecycle until an npm release includes those fixes. +

+
+
+

LowcoderAppView

+
+
React / JSX
+
import { useMemo } from "react";
+import { LowcoderAppView } from "lowcoder-sdk";
+import "lowcoder-sdk/dist/style.css";
+
+export function CustomerPanel({ customerId }) {
+  const inputs = useMemo(() => ({ customerId }), [customerId]);
+  return (
+    <div style={{ minHeight: 360 }}>
+      <LowcoderAppView
+        appId="YOUR_PUBLISHED_MODULE_ID"
+        className="customer-module"
+        moduleInputs={inputs}
+        onModuleOutputChange={(output) => console.log(output)}
+        onModuleEventTriggered={(name) => console.log(name)}
+      />
+    </div>
+  );
+}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropMeaning
appId + Required string: the published application or module ID. +
All six initialization options + baseUrl, webUrl, + orgId, moduleInputs, + appDsl, moduleDslMap. +
className + Optional string on the wrapper element. Set size through a + host wrapper or CSS. +
+ onModuleOutputChange + + Optional (output) => void; same payload as + the instance output event. +
+ onModuleEventTriggered + Optional (eventName: string) => void.
ref + Forwarded instance reference; initially undefined. Not a + guarantee that all components or queries have finished. +
+
+

Lifecycle in current source

+

+ Changes to appId, baseUrl, + webUrl or orgId recreate the instance. A + new moduleInputs object updates inputs; callback prop + changes replace subscriptions. Changes to appDsl or + moduleDslMap alone do not reload the app: change the + React key when replacing a supplied definition. Cleanup + disposes the owned instance. +

+

+ These lifecycle fixes are not in npm 3.0.0. Do not + rely on cleanup or backend switching described above in that + published version. The npm React wrapper also does not provide the + new instance readiness/disposal API. +

+

+ Arbitrary HTML props such as style and + id are not forwarded; put them on your host wrapper. + There is no public onError prop. The current wrapper + logs initial mount failures to the console. Use the hosted + imperative API if the host needs to own loading, failure and retry + UI. +

+

+ The wrapper uses the same single event-handler slots as + instance.on. Registering a second handler through the + ref can replace the corresponding callback prop subscription. +

+
+
+

LowcoderSDK.scan()

+
+
JavaScript
+
await LowcoderSDK.ready;
+await LowcoderSDK.scan(document.getElementById("new-widgets"));
+
+

+ scan(root: ParentNode = document): Promise<void[]> +

+

+ Finds descendant .lowcoder-module-container elements + and mounts their display elements. The root itself is not included. + The browser entry scans the document automatically at DOM readiness. + Call this again for markup inserted later. +

+

+ Skips containers without a display element and display elements + already tracked by the facade. It returns an array of undefined + results, not app instances. Individual JSON or mount errors are + caught and emitted on the affected container as + lowcoder:error; the scan can resolve even if an embed + failed. An invalid root can reject. +

+

+ Listen directly to each container for its events. Use explicit mount + when you need an instance, individual failure handling or lifecycle + control. +

+
+
+

Declarative HTML contract

+
+
HTML
+
<div class="lowcoder-module-container" id="customer-widget">
+  <input type="hidden" class="module-id" value="YOUR_PUBLISHED_MODULE_ID">
+  <input type="hidden" class="module-input" value='{"customerId":"customer-42"}'>
+  <input type="hidden" class="locoder-backend-url" value="https://api-service.lowcoder.cloud">
+  <input type="hidden" class="locoder-frontend-url" value="https://app.lowcoder.cloud">
+  <input type="hidden" class="locoder-org-id" value="YOUR_WORKSPACE_ID">
+  <div class="lowcoder-module-display"></div>
+</div>
+<script src="https://sdk.lowcoder.cloud/bundle.js"></script>
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClassMeaning
lowcoder-module-containerParent container found by the scanner.
lowcoder-module-displayDescendant DOM element where the app is mounted.
module-idInput whose value is the required app ID.
module-input + Input containing a JSON object of module inputs; defaults to + {}. +
locoder-backend-url + Optional backend URL; same default as baseUrl. +
locoder-frontend-url + Optional frontend URL; same default as webUrl. +
locoder-org-idOptional workspace login ID.
+
+

+ The historical spelling locoder-… is required. Changing + a hidden field after mounting does not update that instance; the + scanner skips existing mounts. Unmount its display element before + scanning again, or use explicit mount and + setModuleInputs. +

+

+ The scanner also calls optional globals + window.opChange(output) and + window.onModuleEventTriggered(eventName). They are + shared across all scanned embeds and carry no instance identity. + Prefer listeners on the individual container. A throwing global + callback can interrupt subsequent container event delivery. +

+
+
+

Browser DOM events

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Event targetNameevent.detail
windowlowcoder:readyNo payload. Initial browser runtime loaded.
windowlowcoder:error + Error from initial loader failure; not a catch-all runtime + error event. +
Scanned containerlowcoder:outputWhole module output object.
Scanned containerlowcoder:eventModule event name string.
Scanned containerlowcoder:errorError parsing its JSON or mounting its app.
+
+
+
JavaScript
+
const container = document.getElementById("customer-widget");
+container.addEventListener("lowcoder:output", (event) => {
+  console.log(event.detail);
+});
+container.addEventListener("lowcoder:error", (event) => {
+  console.error("This embed failed", event.detail);
+});
+
+

+ These CustomEvents do not bubble. Register on the + stated target before the event can fire. Explicit + mount() does not emit the scanner’s container events; + subscribe on its returned instance instead. Prefer the ready promise + to a window readiness listener that could be attached too late. +

+
+
+

Authentication & deployment boundaries

+

+ The initial app request uses credentials/cookies. An initial + 401 displays a built-in login prompt. Login opens + {webUrl}/user/auth/login, or + {webUrl}/org/{orgId}/auth/login when a workspace is + supplied. The prompt’s Refresh action reloads the + entire host page. Other initial failures, including + 403, reject hosted mounting. +

+

+ Configure backend CORS and cookie settings for the actual host + origin, and test the browser’s cross-site cookie behavior. CORS on + the SDK’s static files does not grant access to the Lowcoder API. + There is no public bearer-token option, request-header hook, logout + method or automatic transfer of your host’s SSO session. +

+

+ Multiple embeds can coexist, but the runtime shares configuration, + Redux state, history and CSS. Use one Lowcoder deployment per page. + Passing a different backend to each instance does not provide + independent isolation. Native DOM integration is not a Shadow DOM or + CSS sandbox; test host styles, overlays and routing. +

+

+ To self-host, serve bundle.js with its complete emitted + asset directory. The loader resolves chunks relative to its own URL. + Retain old hashed assets during upgrades so already open pages can + still load their lazy chunks. The loader propagates its script nonce + to initial scripts; this is not a guarantee that the full runtime + supports your strict CSP. +

+
+
+

Troubleshooting by symptom

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SymptomWhat to check
The page stays blank + Catch LowcoderSDK.ready and + mount() errors. Check chunk requests, backend + access, container size and runtime console errors. +
Inputs appear unchanged + Embed a module; declare the exact input names and types. + Send a fresh map. Serialize Data/Array values. Omitted keys + do not reset values. +
A method does nothing + Use a declared module method, wait for the rendered module + to be usable, and match parameter order. Mount readiness + alone is insufficient. +
An awaited method finishes instantly + invokeMethod returns void. Expose completion + through your module contract. +
A listener stops firing + There is one handler per event. Another registration or a + React callback prop may have replaced it. +
An initial output was missed + Subscriptions have no replay. Avoid depending on a single + startup emission for a handshake. +
A private embed shows Login + Mount can resolve to the auth prompt. Verify cookies, + allowed origins, publication/access settings and frontend + login URL. +
destroy / ready / appView import is missing + Check the distribution table; npm + 3.0.0 is not equivalent to the hosted bundle. +
Host styles or another embed change + Configuration, routing and CSS are shared. Keep the same + deployment across embeds and check host integration + boundaries. +
+
+

+ This reference is derived from the browser facade, loader, + AppViewInstance, bootstrap factory, React wrapper and + module input/method implementations in + the Lowcoder client source. The distribution notes record the separately inspected npm + artifact. Repository changes and published artifacts can differ; the + version table makes that distinction explicit. +

+
+
+
+ + + diff --git a/client/packages/lowcoder-sdk-webpack-bundle/site/reference.js b/client/packages/lowcoder-sdk-webpack-bundle/site/reference.js new file mode 100644 index 0000000000..c817debac2 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/site/reference.js @@ -0,0 +1,73 @@ +(() => { + const search = document.getElementById("reference-search"); + const clear = document.getElementById("clear-search"); + const status = document.getElementById("search-status"); + const empty = document.getElementById("search-empty"); + const sections = Array.from(document.querySelectorAll("article > section")); + const links = Array.from(document.querySelectorAll("#reference-nav a")); + const searchable = sections.map((section) => + section.textContent.toLowerCase(), + ); + + function filter() { + const terms = search.value + .trim() + .toLowerCase() + .split(/\s+/) + .filter(Boolean); + let count = 0; + sections.forEach((section, index) => { + const matches = terms.every((term) => searchable[index].includes(term)); + section.hidden = !matches; + links[index].hidden = !matches; + if (matches) count++; + }); + clear.hidden = !search.value; + empty.hidden = count !== 0; + status.textContent = terms.length + ? `${count} matching ${count === 1 ? "section" : "sections"}` + : ""; + } + search.addEventListener("input", filter); + clear.addEventListener("click", () => { + search.value = ""; + filter(); + search.focus(); + }); + // A cross-reference must still reveal its target while a filter is active. + function revealHash() { + let id; + try { + id = decodeURIComponent(location.hash.slice(1)); + } catch { + return; + } + const target = document.getElementById(id); + if (target?.hidden && sections.includes(target)) { + search.value = ""; + filter(); + target.scrollIntoView(); + } + } + window.addEventListener("hashchange", revealHash); + revealHash(); + + document.querySelectorAll(".code-card").forEach((card, index) => { + const pre = card.querySelector("pre"); + pre.id = `example-${index + 1}`; + const button = document.createElement("button"); + button.className = "copy-button"; + button.type = "button"; + button.textContent = "Copy"; + button.setAttribute("aria-label", `Copy example ${index + 1}`); + button.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(pre.textContent); + button.textContent = "Copied"; + } catch { + button.textContent = "Select code to copy"; + } + }); + card.querySelector(".code-toolbar").appendChild(button); + }); +})(); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/test/documentation.test.cjs b/client/packages/lowcoder-sdk-webpack-bundle/test/documentation.test.cjs new file mode 100644 index 0000000000..e5cdf4c245 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/test/documentation.test.cjs @@ -0,0 +1,92 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const ts = require('typescript'); +const { JSDOM } = require('jsdom'); +const root = path.resolve(__dirname, '..'); +const read = file => fs.readFileSync(path.join(root, file), 'utf8'); +const document = new JSDOM(read('site/reference.html')).window.document; +const source = file => ts.createSourceFile(file, read(file), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); +const declaration = (tree, name) => tree.statements.find(node => node.name?.text === name); +const names = nodes => nodes.map(node => node.name?.text).filter(Boolean); +const documented = attribute => [...document.querySelectorAll(`[${attribute}]`)].map(node => node.getAttribute(attribute)); + +test('reference covers each public instance member, option, event and React prop in source', () => { + const instance = source('../lowcoder/src/appView/AppViewInstance.tsx'); + const members = declaration(instance, 'AppViewInstance').members.filter(node => + !ts.isConstructorDeclaration(node) && + !node.modifiers?.some(modifier => [ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword].includes(modifier.kind)) + ); + for (const name of names(members)) assert.ok(documented('data-api').includes(`AppViewInstance.${name}`), name); + assert.deepEqual(documented('data-option').sort(), names(declaration(instance, 'AppViewInstanceOptions').members).sort()); + assert.deepEqual(documented('data-event').sort(), names(declaration(instance, 'EventHandlerMap').members).sort()); + const react = source('../lowcoder/src/appView/LowcoderAppView.tsx'); + assert.deepEqual(documented('data-prop').sort(), names(declaration(react, 'LowcoderAppViewProps').members).sort()); + for (const api of ['LowcoderAppView', 'bootstrapAppAt']) assert.ok(documented('data-api').includes(api), api); +}); + +test('reference covers the browser facade exported by the entry and loader', () => { + const expected = new Set(); + for (const file of ['index.jsx', 'src/loader.js']) { + function visit(node) { + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(node.left) && node.left.expression.getText() === 'api') { + expected.add(`LowcoderSDK.${node.left.name.text}`); + } + ts.forEachChild(node, visit); + } + visit(source(file)); + } + assert.deepEqual(documented('data-api').filter(name => name.startsWith('LowcoderSDK.')).sort(), [...expected].sort()); +}); + +test('documentation has valid local anchors, unique IDs and parseable JS/JSX examples', () => { + const pages = new Map([ + ['/', new JSDOM(read('index.html')).window.document], + ...['guide', 'reference', 'recipes'].map(name => [`/${name}.html`, new JSDOM(read(`site/${name}.html`)).window.document]), + ]); + for (const [name, page] of pages) { + const ids = [...page.querySelectorAll('[id]')].map(node => node.id); + assert.equal(ids.length, new Set(ids).size, `${name}: duplicate IDs`); + for (const link of page.querySelectorAll('a[href]')) { + const href = link.getAttribute('href'); + if (!href.startsWith('/') && !href.startsWith('#')) continue; + const url = new URL(href, `https://sdk.lowcoder.cloud${name}`); + const target = pages.get(url.pathname); + if (target && url.hash) assert.ok(target.getElementById(url.hash.slice(1)), `${name}: broken ${href}`); + } + for (const card of page.querySelectorAll('.code-card')) { + const language = card.querySelector('.code-toolbar span')?.textContent; + if (!['JavaScript', 'React / JSX'].includes(language)) continue; + const example = ts.createSourceFile('example.tsx', card.querySelector('pre').textContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + assert.equal(example.parseDiagnostics.length, 0, `${name}: example syntax error`); + } + assert.equal(page.querySelectorAll('script[src*="bundle.js"]').length, 0, `${name}: docs must not download the runtime`); + } +}); + +test('search filters full section content, clears, and reveals a hidden cross-reference', () => { + const dom = new JSDOM(read('site/reference.html'), { url: 'https://sdk.lowcoder.cloud/reference.html', runScripts: 'outside-only' }); + const { window } = dom; + window.HTMLElement.prototype.scrollIntoView = function () {}; + window.eval(read('site/reference.js')); + const input = window.document.getElementById('reference-search'); + const search = value => { input.value = value; input.dispatchEvent(new window.Event('input')); }; + search('positional'); + assert.equal(window.document.getElementById('methods').hidden, false); + assert.equal(window.document.getElementById('sdk-ready').hidden, true); + assert.match(window.document.getElementById('search-status').textContent, /matching/); + search('not-a-real-api'); + assert.equal(window.document.getElementById('search-empty').hidden, false); + window.document.getElementById('clear-search').click(); + assert.equal(input.value, ''); + assert.equal(window.document.querySelectorAll('article > section[hidden]').length, 0); + search('positional'); + window.location.hash = '#sdk-ready'; + window.dispatchEvent(new window.HashChangeEvent('hashchange')); + assert.equal(window.document.getElementById('sdk-ready').hidden, false); + assert.equal(input.value, ''); + assert.equal(window.document.querySelectorAll('.copy-button').length, window.document.querySelectorAll('.code-card').length); + window.close(); +}); diff --git a/client/packages/lowcoder-sdk-webpack-bundle/test/stable-release.test.cjs b/client/packages/lowcoder-sdk-webpack-bundle/test/stable-release.test.cjs new file mode 100644 index 0000000000..da169cde67 --- /dev/null +++ b/client/packages/lowcoder-sdk-webpack-bundle/test/stable-release.test.cjs @@ -0,0 +1,13 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { validateRelease } = require('../scripts/check-stable-release.cjs'); +const stable = { tag_name: 'v3.0.1', draft: false, prerelease: false }; +test('only an exact stable main release passes publication checks', () => { + assert.doesNotThrow(() => validateRelease(stable, 'release', 'release', 'release')); + for (const invalid of [{ ...stable, draft: true }, { ...stable, prerelease: true }, { ...stable, tag_name: 'v3.0.1-rc1' }]) { + assert.throws(() => validateRelease(invalid, 'release', 'release', 'release')); + } + assert.throws(() => validateRelease(stable, 'feature', 'main', 'feature')); + assert.throws(() => validateRelease(stable, 'main', 'main', 'older-release')); + assert.throws(() => validateRelease(stable, 'old-main', 'new-main', 'old-main')); +}); diff --git a/docs/build-applications/create-a-new-app/modules.md b/docs/build-applications/create-a-new-app/modules.md index 938797ca61..17520aa936 100644 --- a/docs/build-applications/create-a-new-app/modules.md +++ b/docs/build-applications/create-a-new-app/modules.md @@ -95,7 +95,7 @@ In the **Settings** tab, click **+** **Add** to create a new Module Method. Clic

From an App, one can call Methods of a embedded Module.

{% hint style="warning" %} -When creating a Method for a Module, you cannot create input parameters for the Method and can only reference parameters within the scope of the current Module. +Module methods can define named parameters and defaults. When calling a method from the native SDK, pass arguments in the configured order with `instance.invokeMethod(methodName, args)`. This call returns no result or completion promise; expose results through module outputs or events. See the [SDK method reference](https://sdk.lowcoder.cloud/reference.html#methods). {% endhint %} #### Call a Method diff --git a/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md b/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md index 79dfe6a08f..b1a7ebb22d 100644 --- a/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md +++ b/docs/publish-apps/embedd-an-app/native-embed-sdk/README.md @@ -1,777 +1,90 @@ # Native embed SDK -The key advantage for React developers using Lowcoder is the ease of embedding Lowcoder apps directly into the React component hierarchy. This native integration means that Lowcoder apps can be treated as part of the React tree, ensuring a smooth and cohesive user experience. Developers can place Lowcoder apps within their existing React components, maintaining the structural and stylistic consistency of their applications. - -Moreover, the interaction between React components and Lowcoder apps is exceptionally fluid. React developers can directly manipulate the state of Lowcoder apps, invoke methods, and handle events, creating a highly interactive and responsive environment. This direct interaction is a game-changer, as it allows for real-time data exchange and dynamic behavior within the application. - -For instance, a React developer can easily update the state of a Lowcoder app based on user interactions or external data changes in their React components. This two-way communication ensures that the embedded Lowcoder apps are not just static elements but integral, interactive parts of the overall application. - -The ability to interact with Lowcoder apps through state management, method calls, and event handling in React also simplifies the development process. Developers can leverage their existing knowledge of React's ecosystem to control and customize Lowcoder apps, reducing the learning curve and accelerating development timelines. - -## Lowcoder SDK usable imports - -This is an overview list of exports available for developers using the Lowcoder SDK. - - - -{% hint style="warning" %} -It is important to keep in mind, that this List was build during a reverse engineering attempt. We cannot guarantee the completeness. Also you will find at many Items that the description was guessed (by ChatGPT). \ -\ -We try step by step to add missing Items and optimize the documentation. -{% endhint %} - -### External Libraries - -```plaintext -numbro - A library for formatting and manipulating numbers. -Papa - A robust CSV parsing library for in-browser use. -uuid - A library for generating unique identifiers. -``` - -### Redux Store - -```plaintext -redux/store/store - Exports from Redux store for state management. -``` - -### Utilities - -Utilities provide a wide range of functionality, from application utilities to hooks and performance utilities. - -* **util/appUtils** - General application utilities. -* **util/bottomResUtils** - Utilities for bottom resolution management. -* **util/cacheUtils** - Caching mechanisms for performance optimization. -* **util/commonUtils** - Commonly used utilities across applications. -* **util/convertUtils** - Data type conversion utilities. -* **util/dateTimeUtils** - Utilities for handling dates and times. -* **util/editoryHistory** - Utilities for managing editor history. -* **util/envUtils** - Environment interaction utilities. -* **util/fileUtils** - File operation utilities. -* **util/history** - Navigation history management utilities. -* **util/historyManager** - Utilities for managing history instances. -* **util/hooks** - Custom React hooks. -* **util/hotkeys** - Keyboard shortcut utilities. -* **util/jsonTypes** - JSON data type handling utilities. -* **util/keyUtils** - Key management and operation utilities. -* **util/localStorageUtil** - Local storage interaction utilities. -* **util/memoize** - Memoization utilities for performance optimization. -* **util/objectUtils** - Object manipulation and operation utilities. -* **util/perfUtils** - Performance monitoring and optimization utilities. -* **util/permissionUtils** - User permission management utilities. -* **util/reducerUtils** - Redux reducer utilities. -* **util/scheduleUtils** - Scheduled task management utilities. -* **util/stringUtils** - String manipulation and operation utilities. -* **util/tutorialUtils** - Tutorial creation and management utilities. -* **util/uaUtils** - User agent detection utilities. -* **util/urlUtils** - URL manipulation and parsing utilities. -* **util/context/ExternalEditorContext** - Context for external editor integration. With the help of the EditorContext Object you can access also EditorState and understand the current usage of the Editor / Canvas. - -### Constants - -```plaintext -constants/domLocators/CanvasContainerID - Identifier for the canvas container element. -``` - -### Components and Component Utilities - -Components and utilities for building and managing UI components. - -```plaintext - -comps/comps/rootComp - Root component structure. -comps/utils/useCompInstance - Utilities for component instance management. -comps/utils/idGenerator - Unique identifier generator for components. -comps/utils/propertyUtils - Component property management utilities. -comps/utils/globalSettings - Global settings management utilities. -comps/utils/themeContext - Theme management context. -``` - -### Design and Core - -```plaintext -lowcoder-design - Core design elements for building applications. -lowcoder-core - Core functionalities for application development. -``` - -### Wrappers - -Wrappers play crucial roles in structuring the UI and UX of the SDK's components, ensuring that complex functionalities are manageable and that the interface remains user-friendly. - -* **ControlPropertyViewWrapper** - A wrapper for property views within control components, likely enhancing layout or providing additional UI context for control settings. -* **ModalFooterWrapper** - A wrapper specifically for the footer section of modal dialogs, potentially standardizing layout or spacing for action buttons. -* **SecondStepPropertyViewWrapper** - A wrapper used for the second step in a multi-step property configuration process, possibly organizing or grouping properties for clarity. -* **GlobalShortcutsWrapper** - A wrapper for global shortcut configurations, ensuring shortcut keys are managed or displayed in a consistent format across the application. -* **QueryConfigItemWrapper** - A wrapper for individual items within query configuration settings, providing a consistent UI structure for each query parameter or option. -* **QueryConfigWrapper** - A more general wrapper for the entire set of query configuration settings, encapsulating all query config items for cohesive presentation. -* **QueryPropertyViewWrapper** - A wrapper for property views related to queries, likely organizing and displaying query-related settings in a user-friendly manner. -* **QuerySectionWrapper** - A wrapper for sections within a query configuration or editor, segmenting the query UI into logical parts for better user navigation. -* **ShortcutsWrapper** - A wrapper for shortcut configurations, similar to GlobalShortcutsWrapper, but potentially for a more specific context or component. -* **SuspensionBoxWrapper** - A wrapper that might be used to suspend or temporarily hold components, possibly for lazy loading or conditional rendering scenarios. -* **SwitchWrapper** - A wrapper for switch components, providing additional styling or context for toggle switches within the UI. -* **ValueAndMsgWrapper** - A wrapper for components or UI elements that display both a value and a message, ensuring consistent formatting and presentation. -* **WrapContextNodeV2Wrapper** - A wrapper for context nodes in a version 2 format, possibly providing a new or enhanced way to manage context in component trees. -* **WrapNodeWrapper** - A general wrapper for nodes, likely used to encapsulate components or elements within additional logic or styling context. -* **WrapperToControlItem** - A wrapper that converts generic components or elements into control items, likely for use in a control panel or settings menu. - -### Controls - -Controls for managing Properties for UI elements and interactions. - -* **ActionSelectorControl** - Enables selection from a list of predefined actions. -* **ArrayControl** - Manages arrays, allowing users to add, remove, and edit array items. -* **ArrayNumberControl** - Specifically for arrays of numbers, facilitating their management. -* **ArrayOrJSONObjectControl** - Allows input to be either an array or a JSON object, supporting flexible data structures. -* **ArrayStringControl** - Designed for arrays of strings, enabling easy manipulation of string lists. -* **ArrayStringOrNumberControl** - Accepts arrays composed of either strings or numbers, offering versatility in data handling. -* **BooleanStateControl** - Toggles a boolean state, useful for settings that can be switched on or off. -* **ButtonEventHandlerControl** - Configures event handlers for button components, customizing their behavior. -* **ColorCodeControl** - Inputs color codes (e.g., HEX, RGB), providing a precise method for color selection. -* **ColorControl** - Offers a color picker for intuitive color selection. -* **ColumnOptionControl** - Selects options for table columns, such as visibility and sorting. -* **DropdownOptionControl** - Manages options within a dropdown, allowing for the organization of selectable items. -* **FunctionControl** - Defines functions, enabling custom logic or calculations. -* **JSONObjectArrayControl** - Manages arrays of JSON objects, facilitating complex data structures. -* **JSONObjectControl** - Inputs JSON objects, allowing users to define key-value pairs. -* **JSONValueControl** - Inputs values in JSON format, supporting various data types. -* **NumberControl** - Inputs numeric values, often with validation for specific ranges. -* **NumberOrJSONObjectArrayControl** - Allows either a single number or an array of JSON objects as input. -* **NumberStateControl** - Manages numeric state properties, useful for numeric configurations. -* **ObjectControl** - Inputs generic objects, providing a flexible interface for defining properties. -* **ParamsArrayStringControl** - Manages string arrays as parameters, useful for multiple text inputs. -* **ParamsBooleanCodeControl** - Inputs boolean parameters with code support, allowing dynamic logic. -* **ParamsBooleanControl** - Inputs boolean parameters, typically for toggling settings. -* **ParamsJsonControl** - Inputs parameters in JSON format, supporting complex configurations. -* **ParamsNumberControl** - Inputs numeric parameters, with support for range constraints. -* **ParamsPositiveNumberControl** - Specifically for positive numeric parameters, ensuring positive inputs. -* **ParamsStringControl** - Inputs string parameters, allowing text input for configurations. -* **ParamsStringJsonControl** - Inputs string parameters with JSON support, offering flexible text inputs. -* **SelectEventHandlerControl** - Selects event handlers, customizing interactive component responses. -* **SelectInputOptionControl** - Configures options within select components, enhancing dropdown functionality. -* **SelectOptionControl** - Manages options in select components, facilitating choice organization. -* **TransformerCodeControl** - Inputs transformation code, used for data modification. -* **IconControl** - Enables the selection of icons from a predefined set, useful for visually representing actions or statuses. -* **InputEventHandlerControl** - Configures event handlers for input fields, enabling custom reactions to user inputs. -* **KeyValueControl** - Allows users to input key-value pairs, facilitating the management of object properties or configurations. -* **LabelControl** - A control for inputting and editing text labels, often used for form fields, buttons, or other interactive elements. -* **OptionsControl** - Manages selectable options across various components, enhancing configurability. -* **PositionControl** - Allows users to specify the position of elements, useful for layout and design settings. -* **QuerySelectControl** - Enables the selection of queries from a list, often used in data-binding or API-related configurations. -* **RadiusControl** - Inputs radius values, controlling the curvature of borders or other circular elements. -* **RangeControl** - Provides a UI for selecting a range of values, typically represented by a slider with two handles. -* **RefControl** - Manages references to other components or data sources, facilitating complex data interactions. -* **RegexControl** - Inputs regular expressions, allowing for pattern matching and validation settings. - -### Generators - -Generators for creating components and enhancing them with additional functionalities. - -* **changeDataType** - Generator for changing data types. -* **hookToComp** - Generator for converting hooks to components. -* **index** - Base generator exports. -* **list** - Generator for list components. -* **multi** - Generator for multi-component structures. -* **sameTypeMap** - Generator for mapping same type components. -* **simpleGenerators** - Simple component generators. -* **uiCompBuilder** - UI component builder generator. -* **withContext** - Generator for adding context to components. -* **withDispatchHook** - Generator for adding dispatch hooks. -* **withErrorBoundary** - Generator for adding error boundaries. -* **withExposing** - Generator for exposing component functionalities. -* **withIsLoading** - Generator for loading state management. -* **withMethodExposing** - Generator to expose methods on a component. -* **withType** - Generator for type management. - -### Components - -Components cover a wide range of functionalities, from input and selection to display and multimedia, facilitating the development of interactive and dynamic web applications. - -* **AbstractComp** - Represents an abstract component, likely serving as a base or common interface for other components. -* **AudioComp** - Component for audio playback functionality. -* **AutoCompleteComp** - Component for autocomplete functionality, allowing users to quickly find and select from a list of values as they type. -* **CalendarComp** - Component for displaying and interacting with a calendar, typically used for date selection. -* **CascaderComp** - Component that provides a cascading select interface, where the selection in one level determines the options available in the next. -* **CheckboxComp** - Component for checkbox functionality, allowing users to select one or more options from a set. -* **CollapsibleContainerComp** - Component that can expand and collapse to show or hide its content. -* **ContainerComp** - Generic container component, used to group other components together. -* **DateComp** - Component for date input, allowing users to select a date. -* **DateRangeComp** - Component for selecting a range of dates, from a start date to an end date. -* **DropdownComp** - Component for dropdown selection functionality, allowing users to select an option from a dropdown menu. -* **FileViewerComp** - Component for viewing files, potentially supporting various file types. -* **FormComp** - Component for form functionality, allowing users to input and submit data. -* **HookComp** - Likely a component designed to utilize React hooks for state management or side effects. -* **IFrameComp** - Component for embedding an IFrame, allowing for the inclusion of another HTML page within the current page. -* **InputComp** - Component for text input, allowing users to enter and edit text. -* **JsonEditorComp** - Component for editing JSON data, providing a user-friendly interface for modifying JSON structures. -* **JsonFormComp** - Component for generating forms based on JSON data, allowing dynamic form creation and manipulation. -* **ListViewComp** - Component for displaying a list of items, potentially supporting scrolling, selection, and other list interactions. -* **ModalComp** - Component for modal dialog functionality, allowing for pop-up dialogs that overlay the main content. -* **NumberInputComp** - Component for number input, allowing users to enter numeric values. -* **PasswordComp** - Component for password input, typically hiding the entered characters for security. -* **ProcessCircleComp** - Component likely representing a process or progress indicator in the form of a circle. -* **ProgressComp** - Component for displaying progress, such as a progress bar. -* **QRCodeComp** - Component for displaying QR codes. -* **RadioComp** - Component for radio button functionality, allowing users to select a single option from a set. -* **RatingComp** - Component for rating functionality, allowing users to provide a rating, typically represented by stars or similar indicators. -* **RichTextEditorComp** - Component for rich text editing, providing a user interface for formatting text with options like bold, italic, lists, etc. -* **SliderComp** - Component for slider functionality, allowing users to select a value from a range by sliding a handle. -* **SwitchComp** - Component for switch functionality, allowing users to toggle between two states, such as on/off. -* **TableComp** - Component for table functionality, displaying data in rows and columns. -* **TextAreaComp** - Component for multi-line text input, allowing users to enter text over multiple lines. -* **TextComp** - Component for displaying text. -* **ToggleButtonComp** - Component for a toggle button that can switch between two states. -* **VideoComp** - Component for video playback functionality. - -### Styles - -``` -styled - An object combining default and named Styles -so you can use it like "const YourDiv = styled.div`... {your CSS}`; -``` - -* **BigButtonStyle** - Style definition for large buttons, likely specifying size, padding, and possibly color. -* **BlockGrayLabelStyle** - Style for a label component with a block display and gray background, typically used for categorization or emphasis. -* **BlockLabelStyle** - General style for label components that are displayed as blocks, focusing on background color, margin, and padding. -* **BlueButtonStyle** - Style specifically for blue buttons, defining the color scheme and possibly hover states for buttons. -* **ButtonStyle** - General style for button components, including padding, font size, and color. -* **CalendarStyle** - Style for calendar components, affecting the layout, colors, and typography of the calendar UI. -* **CarouselStyle** - Style for carousel components, which may include settings for transitions, navigation buttons, and pagination indicators. -* **CascaderStyle** - Style for cascader components, affecting how nested options are displayed and interacted with. -* **CheckboxStyle** - Style for checkbox components, including the box size, checkmark appearance, and spacing. -* **CommentStyle** - Style for comment sections or components, focusing on typography, spacing, and background color. -* **ContainerBodyStyle** - Style specifically for the body of container components, affecting padding, margin, and background color. -* **ContainerFooterStyle** - Style for the footer area of container components, which may include padding, border, and background color settings. -* **ContainerHeaderStyle** - Style for the header part of container components, typically defining background color, typography, and padding. -* **ContainerStyle** - General style for container components, affecting overall layout, padding, and border properties. -* **DividerStyle** - Style for divider components, focusing on line color, thickness, and margin. -* **DropdownContainerStyle** - Style for the container part of dropdown components, affecting the dropdown list's appearance and positioning. -* **DropdownMenuStyle** - Style for dropdown menu components, including item spacing, background color, and typography. -* **FormCheckboxItemStyle** - Style for checkbox items within forms, specifying the layout and appearance of checkboxes in form contexts. -* **FormInputItemStyle** - Style for input items within forms, focusing on border, padding, and typography for form inputs. -* **FormSectionStyle** - Style for sections within forms, used to group related form items and visually separate different parts of a form. -* **GridIconStyle** - Likely a style for icons within a grid layout, focusing on size and spacing of icons. -* **IconWrapperStyle** - Style for a wrapper around icons, potentially affecting size, padding, or background color to enhance icon presentation. -* **IframeStyle** - Style for iframe components, affecting border, size, and possibly responsiveness. -* **ImageStyle** - General style for image components, including size, border-radius, and object-fit properties. -* **InputLikeStyle** - Style for components that resemble input fields, focusing on border, background color, and typography to match the appearance of standard inputs. -* **JsonEditorStyle** - Style for JSON editor components, affecting the layout, color scheme, and typography of the editor interface. -* **JsonFormCompStyle** - Style for components that generate forms based on JSON data, likely affecting the layout and appearance of dynamically created forms. -* **ListViewStyle** - Style for list view components, focusing on item spacing, scroll behavior, and possibly item hover states. -* **ModalStyle** - Style for modal dialog components, including background overlay, modal positioning, and content padding. -* **MultiSelectStyle** - Style for multi-select components, affecting how selected items are displayed and how the selection list is presented. -* **NavLayoutItemStyle** - Style for items within a navigation layout, specifying the appearance of navigation links or buttons. -* **ProgressStyle** - Style for progress bar components, including bar color, height, and border-radius. -* **RatingStyle** - Style for rating components, such as star ratings, including size, color, and spacing of rating icons. -* **ResponsiveLayoutColStyle** - Style for columns in responsive layouts, affecting width, padding, and breakpoint behaviors. -* **ResponsiveLayoutRowStyle** - Style for rows in responsive layouts, focusing on spacing between columns and alignment within the row. -* **RichTextEditorStyle** - Style for rich text editor components, affecting the toolbar, typography, and editor area appearance. -* **SegmentStyle** - Style for segment controls or segmented buttons, including border, background color, and selected state appearance. -* **SelectStyle** - Style for select dropdown components, focusing on dropdown appearance, item spacing, and hover states. -* **SignatureStyle** - Style for signature components, likely affecting the canvas area where signatures are drawn. -* **SliderStyle** - Style for slider components, including track and handle appearance, as well as responsive behavior. -* **SwitchStyle** - Style for switch components, focusing on the toggle appearance, color, and size. -* **TableStyle** - General style for table components, affecting cell padding, border, and header styling. -* **TextWrapperStyle** - Style for text wrapper components, potentially affecting typography, line-height, and text alignment. -* **TimeLineStyle** - Style for timeline components, focusing on line color, event marker appearance, and spacing. -* **ToggleButtonStyle** - Style for toggle button components, including the appearance of the toggle state and transition effects. -* **TreeStyle** - Style for tree view components, affecting node spacing, indentation, and expand/collapse icons. -* **VideoStyle** - Style for video components, including controls appearance, aspect ratio, and border. - -### Labels - -* **BlockGrayLabel** - A style or component for displaying labels with a gray background, typically used for subtle emphasis. -* **BlockLabel** - A component or style for block-level labels, used for headings or important textual information. -* **CommonGrayLabel** - A predefined style for labels with a common gray theme, likely used for neutral or secondary information. -* **DropdownOptionLabelWithDesc** - A component for dropdown options that includes both a label and a description for more detailed selections. -* **CollapseLabel** - Likely a label used within a collapsible component to indicate the section or content that can be expanded or collapsed. -* **IconWrapperLabel** - A label associated with an icon wrapper, possibly used for tooltips or descriptions of icons. -* **QueryConfigLabel** - A label used within query configuration settings, possibly to identify different query parameters or options. -* **TooltipLabel** - A label specifically designed for use within tooltips, providing brief descriptions or additional information on hover. -* **ShowBorderLabel** - A label that might be used to toggle or indicate the visibility of borders in a UI component or layout section. -* **TextLabel** - A general-purpose label for text, used across various components and contexts to display information. -* **FormSectionLabel** - A label used to title or describe sections within a form, helping to organize and categorize form fields. -* **DropdownOptionLabel** - Similar to DropdownOptionLabelWithDesc but possibly without the description, used for simpler dropdown selections. -* **InputLabel** - A label typically used for input fields, indicating the type of information expected from the user. -* **MenuItemLabel** - A label used within menu items, providing clear identification of the menu option. -* **OptionLabel** - A label used for options in select dropdowns, checkboxes, or radio buttons, providing clear identification of each choice. - -### Options - -* **ActionOptions** - Likely refers to configurations or settings related to actions that can be performed within the application, such as button actions or event triggers. -* **AlignOptions** - Configurations for alignment properties, possibly including options for text or element alignment within a UI component (e.g., left, right, center). - -### Checkers - -* **CheckEmailValid** - A utility or function designed to validate email addresses, ensuring they meet a specific format or criteria. -* **CheckIsMobile** - A function or method to determine if the user's device is a mobile device, based on screen size, user agent, or other factors. -* **CheckOtpValid** - A validation function for one-time passwords (OTPs), ensuring that the inputted OTP is correct and valid within a given context or timeframe. -* **CheckPhoneValid** - A utility designed to validate phone numbers, verifying their format and possibly their authenticity or region-specific characteristics. - -### Constants / Patterns - -* **CACHE\_PREFIX** - Likely a constant used as a prefix in caching mechanisms to differentiate or namespace cached items. -* **COLOR\_PALETTE** - A predefined set of colors available for use throughout the application, ensuring consistency in the UI's color scheme. -* **DATE\_FORMAT** - A constant representing the default format for displaying dates within the application. -* **DATE\_FORMAT\_EN** - Specifies the date format specifically for English locales, ensuring dates are presented in a region-appropriate manner. -* **DATE\_TIME\_12\_FORMAT** - Represents a date and time format that includes a 12-hour clock, possibly with AM/PM indicators. -* **DATE\_TIME\_FORMAT** - A general constant for the format used to display both date and time information. -* **DEFAULT\_IMG\_URL** - A fallback URL for images, used when a specific image is not available or cannot be loaded. -* **DEP\_TYPE** - Likely denotes the type of dependency, possibly used in the context of module imports or external library dependencies. -* **EMAIL\_PATTERN** - A regular expression pattern used for validating email addresses in form inputs or data processing. -* **SURFACE\_COLOR** - A constant that might define a default or primary surface color used in the UI, part of a theme or design system. -* **TIME\_FORMAT** - Specifies the format for displaying time within the application, potentially in a 24-hour clock format. -* **TIME\_FORMAT\_MINUTES** - A more specific time format constant, possibly focusing on displaying time with minute precision. -* **TIMESTAMP\_FORMAT** - Defines the format for displaying timestamps, which include both date and time information. -* **TIME\_12\_FORMAT** - Similar to **DATE\_TIME\_12\_FORMAT**, representing a time format using a 12-hour clock. -* **URL\_PATTERN** - A regular expression pattern for validating URLs, ensuring that input or linked URLs are in a correct format. - -### Application View and IDE - -```plaintext -appView/bootstrapAt - Bootstrap utility for application initialization. -appView/LowcoderAppView - Main application view component. -appView/AppViewInstance - Application view instance management. -ide/CompIde - Component IDE for development. -ide/CompPlayground - Component playground for testing. -``` - -### Ant Design - -```plaintext -antd - Exports from Ant Design for comprehensive UI design utilities and components. -``` - -## Event Handler & Trigger - -Event Handlers enable Application Creators to use visually guided Handler Creation for Events that may occur in Visual Components. Use these Event handlers to trigger Events and ensure a good UI. - -The event handler itself does not contain any application logic. It manages only the display of the Event handler. For you as Developer, you can use them to prepare a good visual user interface for Application Creators. - -

Meeting related Event Handlers. Developers can bind logical triggers in their component code to this visual Event Handlers.

- -### Importing Event Handlers - -To utilize an event handler, first import it from the designated module: - -```javascript -import { InputEventHandlerControl } from "path/to/eventHandlerControl"; -``` - -You can then attach this handler directly to component events or use intermediary functions for specific user actions. - -### Using Event Handlers - -A typical use of event handlers is in combination with the childrenmap of a component. - -
const childrenMap = {
-    ... 
-    onEvent: InputEventHandlerControl,
-    ...
+Embed a Lowcoder app directly in your web application's DOM. Your application owns the surrounding page, navigation and host actions; Lowcoder renders the embedded app inside an element you provide.
+
+For communication, build a **module** and define its inputs, outputs, events and methods in the Lowcoder editor. A regular app can also be embedded, but the module interface is the explicit contract for exchanging data with your host. It does not expose arbitrary internal component state.
+
+## Documentation and examples
+
+The source-checked documentation is maintained alongside the browser SDK:
+
+* [Quickstart: plain JavaScript and React](https://sdk.lowcoder.cloud/guide.html)
+* [Complete native embedding API reference](https://sdk.lowcoder.cloud/reference.html)
+* [CRM, website, ticketing and POS integration recipes](https://sdk.lowcoder.cloud/recipes.html)
+* [Live native DOM playground](https://sdk.lowcoder.cloud/#playground)
+
+The reference documents signatures, parameters, defaults, return values, errors, lifecycle behavior and examples for every embedding function. It includes React props, legacy HTML attributes, authentication requirements, shared runtime boundaries and the differences between hosted and npm distributions.
+
+## Plain JavaScript
+
+Publish your app or module, use its application ID, and load the browser SDK once:
+
+```html
+
+ + +``` + +`LowcoderSDK.ready` waits for the runtime scripts. `mount()` waits for the initial app definition and schedules rendering; components and queries may still be loading. When leaving the host screen, call `await LowcoderSDK.unmount(element)` before removing its element. + +## What the host can do + +| API | Purpose | +| --- | --- | +| `LowcoderSDK.ready` | Wait for the browser runtime. | +| `LowcoderSDK.mount(target, options)` | Mount into an element or CSS selector and receive an instance. | +| `LowcoderSDK.unmount(target)` | Dispose a browser-managed embed. | +| `LowcoderSDK.scan(root)` | Mount legacy declarative markup beneath a root. | +| `instance.ready` | Wait for initial definition loading and render scheduling in the hosted/current-source SDK. | +| `instance.setModuleInputs(inputs)` | Update named module inputs. | +| `instance.on('moduleOutputChange', handler)` | Receive the complete output object when it changes. | +| `instance.on('moduleEventTriggered', handler)` | Receive a module event's name. | +| `instance.invokeMethod(name, args)` | Invoke a declared module method with positional arguments. | +| `instance.destroy()` | Dispose an instance in the hosted/current-source SDK. | +| `bootstrapAppAt(appId, element, options)` | Mount through the npm package's imperative factory. | +| `LowcoderAppView` | Render an embed using the npm React wrapper. | + +Important behavior to design around: + +* Each event supports one active handler per instance. Registering another replaces it. The returned function unsubscribes; subscriptions do not replay an initial snapshot. +* Data and Array inputs need JSON serialization. Inputs pass through Lowcoder code controls, including `{{ … }}` expression evaluation; this is not a guaranteed literal-value channel. +* `invokeMethod()` returns `void`, not an awaitable result. Calls before the module root exists are not queued. Model completion and results through module outputs/events. +* Initial `401` responses show the built-in authentication prompt. Mounting successfully is not proof of authorization. Configure backend CORS and cookies for the actual host domain. +* Embeds share runtime configuration, CSS and page-level state. Use one Lowcoder deployment per host page and test the interaction with your own styles and routing. + +## React and npm versions + +```jsx +import { LowcoderAppView } from 'lowcoder-sdk'; +import 'lowcoder-sdk/dist/style.css'; + +export function CustomerPanel({ customerId }) { + return ( + console.log(output)} + onModuleEventTriggered={(name) => console.log(name)} + /> + ); } - -// other code - -return new UICompBuilder(childrenMap, (props: { - ... - onFocus: () => props.onEvent("focus"), - onBlur: () => props.onEvent("blur"), - onPressEnter: () => props.onEvent("submit"), - ... -}) => { -... -
- -{% hint style="info" %} -The further code examples are just abstract examples to list the event handlers. -{% endhint %} - -### Available Event Handlers - -#### **Input Events** - -Handles text-input related events, including changes, focus, blur, and submit actions. - -**Module Import:** - -```javascript -import { InputEventHandlerControl } from "path/to/eventHandlerControl"; ``` -**Usage:** - -```javascript - InputEventHandlerControl("focus")} - onBlur={() => InputEventHandlerControl("blur")} - onPressEnter={() => InputEventHandlerControl("submit")} -/> -``` - -**Exposed Events:** - -* `change` -* `focus` -* `blur` -* `submit` +The hosted browser SDK and npm package are separate releases. As verified on **11 October 2026**, published `lowcoder-sdk@3.0.0` does not include instance `ready` or `destroy`, the updated React cleanup, or all current declarations. Its `lowcoder-sdk/appView` export points to a missing file; use the root import above. The current repository source includes changes that have not yet reached that npm artifact. -#### **Button Events** - -Manages click events for button components. - -**Module Import:** - -```javascript -import { ButtonEventHandlerControl } from "path/to/eventHandlerControl"; -``` - -**Usage:** - -```javascript -