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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/check-lowcoder-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Check Lowcoder SDK

on:
pull_request:
paths:
- 'client/**'
- '.github/workflows/*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
55 changes: 0 additions & 55 deletions .github/workflows/deploy-lowcoder-sdk-webpack-netlify.yml

This file was deleted.

45 changes: 45 additions & 0 deletions .github/workflows/release-lowcoder-sdk.yml
Original file line number Diff line number Diff line change
@@ -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
165 changes: 158 additions & 7 deletions client/packages/lowcoder-sdk-webpack-bundle/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,160 @@
# lowcoder-sdk-webpack-bundle
The code to use Lowcoder Apps on any Web Project
# Lowcoder browser SDK and playground

STEPS to use
The public pages share the Lowcoder website's Ubuntu typography, plum and purple
palette, gradient buttons, floating navigation, logo and hero artwork. Original
assets from the `lowcoder-cloud` website are served locally from `site/brand/`;
the pages do not depend on an external font service. Keep these assets and the
brand tokens in `site/site.css` aligned when the marketing website changes.

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`
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`.

## 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.

## Search and social previews

The four public pages have distinct titles, descriptions and 1200 × 630 PNG share
images. Canonical URLs use Cloudflare's clean paths: `/`, `/guide`, `/reference`
and `/recipes`. `seo.json` is the metadata source; `prepare:site` writes static
HTML tags, Open Graph, X/Twitter cards, JSON-LD, `robots.txt` and `sitemap.xml`.
It also runs before production builds. Commit the generated HTML and crawl files
so validation can catch stale metadata before the build. Titles and descriptions
use concise editorial budgets; search engines may rewrite or truncate snippets.

The image source is `design/social-card.html`, outside the published directory.
To refresh artwork, serve this package with `site/` as a fallback for public assets,
open `/design/social-card.html?page=/guide` (or another canonical path), and capture
a 1200 × 630 viewport after `body[data-ready="true"]` appears. Export actual PNG
bytes (some capture tools return JPEG regardless of the filename). Save each PNG in
`site/social/` as `<card>-<first 12 hex digits of SHA-256>.png`, then update
`social-images.json`. Hashed URLs allow immutable caching without stale images
when artwork changes. Run `prepare:site`, tests, build and `check:dist` afterward.
The tests check static tags, structured data, image dimensions and hashes, sitemap
coverage and clean internal links. The 404 and component sandbox are `noindex`.
`/design/preview.html` shows all four images at feed size alongside the proposed
search copy and character counts; it is a local review page, excluded from builds.

After a stable release publishes these files, share services must fetch the live
page to refresh their own cached previews. The local artwork and metadata checks
do not confirm a social network's cache or search engine indexing.

## Browser integration

```html
<div id="app"></div>
<script src="https://sdk.lowcoder.cloud/bundle.js"></script>
<script>
LowcoderSDK.ready.then(async sdk => {
const app = await sdk.mount('#app', {
appId: 'YOUR_PUBLISHED_APP_ID',
moduleInputs: { customerId: 'customer-42' }
});
app.on('moduleOutputChange', console.log);
app.on('moduleEventTriggered', console.log);
// await sdk.unmount('#app') when your screen is removed.
}).catch(console.error);
</script>
```

`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.
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` 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 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/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.

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.

Setup status (2026-10-11): the `sdk-stable-release-main` hook exists for `main`,
and its URL is stored as `LOWCODER_SDK_DEPLOY_HOOK` in the repository's Actions
secrets. The guarded build and deploy commands above are saved in Cloudflare;
push-triggered production and preview builds remain disabled. This source must
be on `main` before a subsequent stable release can use the workflow. Creating
or merging these files does not itself deploy the SDK or publish a release.

Cloudflare documents branch-head hooks at
https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/.

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.
57 changes: 57 additions & 0 deletions client/packages/lowcoder-sdk-webpack-bundle/design/preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>SDK search and share preview review</title>
<style>
@font-face { font-family: Ubuntu; src: url('/brand/ubuntu-400.woff2'); }
* { box-sizing: border-box; }
body { margin: 0; padding: 32px; background: #f5f1f8; color: #211729; font: 16px/1.5 Ubuntu, sans-serif; }
header, main { max-width: 1200px; margin: auto; }
h1 { margin: 0; font-size: 30px; font-weight: 400; }
header p { margin: 8px 0 28px; color: #65566d; }
main { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
article { overflow: hidden; background: white; border-radius: 16px; border: 1px solid #ded2e5; }
img { width: 100%; height: auto; display: block; }
.content { padding: 20px 24px; }
.url, small { color: #766880; font-size: 13px; }
h2 { margin: 6px 0; color: #50336b; font-weight: 400; font-size: 20px; line-height: 1.3; }
.description { margin: 0 0 12px; color: #514958; font-size: 14px; }
@media (max-width: 700px) { body { padding: 20px; } main { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<header>
<h1>Lowcoder SDK · Search &amp; share previews</h1>
<p>Four pages. Four distinct previews. Artwork at feed size, with the proposed search copy below.</p>
</header>
<main></main>
<script>
(async () => {
const [pages, images] = await Promise.all(['/seo.json', '/social-images.json'].map(url => fetch(url).then(response => response.json())));
for (const [route, page] of Object.entries(pages)) {
const article = document.createElement('article');
const image = document.createElement('img');
image.src = images[page.card]; image.alt = page.imageAlt;
image.width = 1200; image.height = 630;
const content = document.createElement('div'); content.className = 'content';
const fields = [
['div', 'url', 'sdk.lowcoder.cloud' + route],
['h2', '', page.title],
['p', 'description', page.description],
['small', '', `${page.title.length} title characters · ${page.description.length} description characters · 1200 × 630 PNG`],
];
for (const [tag, className, text] of fields) {
const element = document.createElement(tag); element.className = className; element.textContent = text; content.append(element);
}
article.append(image, content); document.querySelector('main').append(article);
}
await document.fonts.ready;
await Promise.all([...document.images].map(image => image.decode()));
document.body.dataset.ready = 'true';
})();
</script>
</body>
</html>
Loading
Loading