feat: add upgradeable Portal SDK (#13)

* feat: add upgradeable Portal SDK

* ci: auto-detect Portal release targets

* refactor: consume Portal SDK source in workspace

* fix: validate Portal SDK compatibility metadata
This commit is contained in:
YANG QIA
2026-08-01 14:08:51 +08:00
committed by GitHub
parent fd07308bc8
commit ef3562ccaf
165 changed files with 2129 additions and 656 deletions
+275 -18
View File
@@ -3,8 +3,22 @@ name: Publish npm package
on:
workflow_dispatch:
inputs:
release_target:
description: Packages to release (auto detects changes from release tags)
required: true
default: auto
type: choice
options:
- auto
- template
- sdk
- both
version:
description: Exact version (leave blank to increment patch)
description: Exact template version (template/both; blank increments patch)
required: false
type: string
sdk_version:
description: Exact Portal SDK version (sdk/both; blank uses package.json)
required: false
type: string
tag:
@@ -57,50 +71,293 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Resolve release version
- name: Resolve release
id: release
env:
REF_TYPE: ${{ github.ref_type }}
REQUESTED_RELEASE_TARGET: ${{ inputs.release_target }}
REQUESTED_VERSION: ${{ inputs.version }}
REQUESTED_SDK_VERSION: ${{ inputs.sdk_version }}
run: |
if [ "$REF_TYPE" != "branch" ]; then
echo "Run this workflow from a branch, not a tag."
exit 1
fi
if [ -n "$REQUESTED_VERSION" ]; then
npm version "$REQUESTED_VERSION" --no-git-tag-version
else
npm version patch --no-git-tag-version
RELEASE_TARGET=$REQUESTED_RELEASE_TARGET
if [ "$RELEASE_TARGET" = auto ]; then
LATEST_TEMPLATE_TAG=$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)
LATEST_SDK_TAG=$(git tag --list 'portal-sdk-v*' --sort=-version:refname | head -n 1)
TEMPLATE_CHANGED=true
if [ -n "$LATEST_TEMPLATE_TAG" ] && git diff --quiet "${LATEST_TEMPLATE_TAG}..HEAD" -- . ':(exclude)sdk'; then
TEMPLATE_CHANGED=false
fi
SDK_CHANGED=true
if [ -n "$LATEST_SDK_TAG" ] && git diff --quiet "${LATEST_SDK_TAG}..HEAD" -- sdk; then
SDK_CHANGED=false
fi
if [ "$TEMPLATE_CHANGED" = true ] && [ "$SDK_CHANGED" = true ]; then
RELEASE_TARGET=both
elif [ "$TEMPLATE_CHANGED" = true ]; then
RELEASE_TARGET=template
elif [ "$SDK_CHANGED" = true ]; then
RELEASE_TARGET=sdk
else
echo "No SDK or template changes were found after the latest release tags."
exit 1
fi
echo "Detected release target: ${RELEASE_TARGET}"
echo "Latest template tag: ${LATEST_TEMPLATE_TAG:-none}"
echo "Latest SDK tag: ${LATEST_SDK_TAG:-none}"
fi
VERSION=$(node -p "require('./package.json').version")
if git rev-parse --verify --quiet "refs/tags/v${VERSION}"; then
echo "Git tag v${VERSION} already exists."
RELEASE_TEMPLATE=false
RELEASE_SDK=false
case "$RELEASE_TARGET" in
template)
RELEASE_TEMPLATE=true
;;
sdk)
RELEASE_SDK=true
;;
both)
RELEASE_TEMPLATE=true
RELEASE_SDK=true
;;
*)
echo "Unsupported release target: ${RELEASE_TARGET}"
exit 1
;;
esac
CURRENT_VERSION=$(node -p "require('./package.json').version")
VERSION=$CURRENT_VERSION
PUBLISH_TEMPLATE=false
VERIFY_TEMPLATE=false
if [ "$RELEASE_TEMPLATE" = true ]; then
CURRENT_TEMPLATE_TAG_EXISTS=false
if git rev-parse --verify --quiet "refs/tags/v${CURRENT_VERSION}"; then
CURRENT_TEMPLATE_TAG_EXISTS=true
fi
if [ -n "$REQUESTED_VERSION" ] && [ "$REQUESTED_VERSION" != "$CURRENT_VERSION" ]; then
npm version "$REQUESTED_VERSION" --no-git-tag-version
elif [ -z "$REQUESTED_VERSION" ] && [ "$CURRENT_TEMPLATE_TAG_EXISTS" = true ]; then
npm version patch --no-git-tag-version
fi
VERSION=$(node -p "require('./package.json').version")
node -e "const fs=require('fs');const pkg=require('./package.json');pkg.nocobase.defaultTemplateVersion=pkg.version;fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n')"
if git rev-parse --verify --quiet "refs/tags/v${VERSION}"; then
echo "Git tag v${VERSION} already exists."
exit 1
fi
if npm view "@nocobase/portal-template-default@${VERSION}" version >/dev/null 2>&1; then
VERIFY_TEMPLATE=true
else
PUBLISH_TEMPLATE=true
fi
elif [ -n "$REQUESTED_VERSION" ]; then
echo "version can only be used for template or both releases."
exit 1
fi
CURRENT_SDK_VERSION=$(node -p "require('./sdk/package.json').version")
if [ "$RELEASE_SDK" = true ]; then
CURRENT_SDK_TAG_EXISTS=false
if git rev-parse --verify --quiet "refs/tags/portal-sdk-v${CURRENT_SDK_VERSION}"; then
CURRENT_SDK_TAG_EXISTS=true
fi
if [ -n "$REQUESTED_SDK_VERSION" ] && [ "$REQUESTED_SDK_VERSION" != "$CURRENT_SDK_VERSION" ]; then
npm version "$REQUESTED_SDK_VERSION" --no-git-tag-version --prefix sdk
elif [ -z "$REQUESTED_SDK_VERSION" ] && [ "$CURRENT_SDK_TAG_EXISTS" = true ]; then
npm version patch --no-git-tag-version --prefix sdk
fi
elif [ -n "$REQUESTED_SDK_VERSION" ]; then
echo "sdk_version can only be used for sdk or both releases."
exit 1
fi
SDK_VERSION=$(node -p "require('./sdk/package.json').version")
SDK_EXISTS=false
if npm view "@nocobase/portal-sdk@${SDK_VERSION}" version >/dev/null 2>&1; then
SDK_EXISTS=true
fi
SDK_TAG="portal-sdk-v${SDK_VERSION}"
SDK_TAG_EXISTS=false
SDK_MATCHES_TAG=false
if git rev-parse --verify --quiet "refs/tags/${SDK_TAG}"; then
SDK_TAG_EXISTS=true
if git diff --quiet "${SDK_TAG}..HEAD" -- sdk; then
SDK_MATCHES_TAG=true
fi
fi
PUBLISH_SDK=false
VERIFY_SDK=false
TAG_SDK=false
if [ "$RELEASE_SDK" = true ]; then
if [ "$SDK_EXISTS" = false ] && [ "$SDK_TAG_EXISTS" = true ]; then
echo "Git tag ${SDK_TAG} exists, but @nocobase/portal-sdk@${SDK_VERSION} is not published."
exit 1
fi
if [ "$SDK_EXISTS" = false ]; then
PUBLISH_SDK=true
elif [ "$SDK_MATCHES_TAG" = false ]; then
VERIFY_SDK=true
fi
if [ "$SDK_TAG_EXISTS" = false ]; then
TAG_SDK=true
fi
if [ "$PUBLISH_SDK" = false ] && [ "$VERIFY_SDK" = false ] && [ "$TAG_SDK" = false ]; then
echo "@nocobase/portal-sdk@${SDK_VERSION} is already fully released."
echo "Choose template, or provide a new sdk_version."
exit 1
fi
else
if [ "$SDK_EXISTS" = false ]; then
echo "Template releases require a published @nocobase/portal-sdk@${SDK_VERSION}."
echo "Publish the SDK first or choose release_target=both."
exit 1
fi
if [ "$SDK_MATCHES_TAG" = false ]; then
VERIFY_SDK=true
fi
fi
echo "release_target=${RELEASE_TARGET}" >> "$GITHUB_OUTPUT"
echo "release_template=${RELEASE_TEMPLATE}" >> "$GITHUB_OUTPUT"
echo "release_sdk=${RELEASE_SDK}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "sdk_version=${SDK_VERSION}" >> "$GITHUB_OUTPUT"
echo "publish_sdk=${PUBLISH_SDK}" >> "$GITHUB_OUTPUT"
echo "verify_sdk=${VERIFY_SDK}" >> "$GITHUB_OUTPUT"
echo "tag_sdk=${TAG_SDK}" >> "$GITHUB_OUTPUT"
echo "publish_template=${PUBLISH_TEMPLATE}" >> "$GITHUB_OUTPUT"
echo "verify_template=${VERIFY_TEMPLATE}" >> "$GITHUB_OUTPUT"
- name: Materialize default extensions
if: steps.release.outputs.release_template == 'true'
run: node scripts/registry.mjs materialize
- name: Inspect package
run: npm pack --dry-run
- name: Build
run: pnpm build
- name: Publish package
run: npm publish --access public --tag "${{ inputs.tag }}" --loglevel verbose
- name: Test SDK and integrations
run: |
pnpm --dir sdk test
node tests/auth-session-regression.mjs
node registry/nocobase-acl/tests/acl-regression.mjs
node registry/nocobase-i18n/tests/i18n-regression.mjs
node --experimental-strip-types --test tests/file-upload-regression.test.ts
node registry/nocobase-mail/tests/mail-regression.mjs
node registry/nocobase-route-surfaces/tests/route-surfaces-regression.mjs
- name: Pack SDK
if: steps.release.outputs.release_sdk == 'true' || steps.release.outputs.verify_sdk == 'true'
run: |
mkdir -p "$RUNNER_TEMP/portal-sdk-local" "$RUNNER_TEMP/portal-sdk-local-unpacked"
pnpm --dir sdk pack --pack-destination "$RUNNER_TEMP/portal-sdk-local"
tar -xzf "$RUNNER_TEMP/portal-sdk-local/"*.tgz -C "$RUNNER_TEMP/portal-sdk-local-unpacked"
node sdk/tests/verify-packed-package.mjs "$RUNNER_TEMP/portal-sdk-local-unpacked/package"
- name: Pack template
if: steps.release.outputs.release_template == 'true'
run: |
mkdir -p "$RUNNER_TEMP/portal-template-local"
pnpm pack --pack-destination "$RUNNER_TEMP/portal-template-local"
- name: Verify existing SDK package
if: steps.release.outputs.verify_sdk == 'true'
env:
SDK_VERSION: ${{ steps.release.outputs.sdk_version }}
run: |
mkdir -p "$RUNNER_TEMP/portal-sdk-remote" "$RUNNER_TEMP/portal-sdk-remote-unpacked"
npm pack "@nocobase/portal-sdk@${SDK_VERSION}" --pack-destination "$RUNNER_TEMP/portal-sdk-remote"
tar -xzf "$RUNNER_TEMP/portal-sdk-remote/"*.tgz -C "$RUNNER_TEMP/portal-sdk-remote-unpacked"
if ! diff -ru "$RUNNER_TEMP/portal-sdk-remote-unpacked/package" "$RUNNER_TEMP/portal-sdk-local-unpacked/package"; then
echo "Published @nocobase/portal-sdk@${SDK_VERSION} differs from the current SDK package."
echo "Rerun with a new sdk_version."
exit 1
fi
- name: Verify existing template package
if: steps.release.outputs.verify_template == 'true'
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
mkdir -p "$RUNNER_TEMP/portal-template-remote" "$RUNNER_TEMP/portal-template-local-unpacked" "$RUNNER_TEMP/portal-template-remote-unpacked"
npm pack "@nocobase/portal-template-default@${VERSION}" --pack-destination "$RUNNER_TEMP/portal-template-remote"
tar -xzf "$RUNNER_TEMP/portal-template-local/"*.tgz -C "$RUNNER_TEMP/portal-template-local-unpacked"
tar -xzf "$RUNNER_TEMP/portal-template-remote/"*.tgz -C "$RUNNER_TEMP/portal-template-remote-unpacked"
if ! diff -ru "$RUNNER_TEMP/portal-template-remote-unpacked/package" "$RUNNER_TEMP/portal-template-local-unpacked/package"; then
echo "Published @nocobase/portal-template-default@${VERSION} differs from the current template package."
echo "Rerun with a new template version."
exit 1
fi
- name: Publish SDK
if: steps.release.outputs.publish_sdk == 'true'
run: pnpm --dir sdk publish --no-git-checks --access public --tag "${{ inputs.tag }}"
- name: Publish template
if: steps.release.outputs.publish_template == 'true'
run: pnpm publish --no-git-checks --access public --tag "${{ inputs.tag }}"
- name: Commit and tag release
env:
TARGET_BRANCH: ${{ github.ref_name }}
RELEASE_TARGET: ${{ steps.release.outputs.release_target }}
RELEASE_TEMPLATE: ${{ steps.release.outputs.release_template }}
RELEASE_SDK: ${{ steps.release.outputs.release_sdk }}
VERSION: ${{ steps.release.outputs.version }}
SDK_VERSION: ${{ steps.release.outputs.sdk_version }}
TAG_SDK: ${{ steps.release.outputs.tag_sdk }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore(release): v${VERSION}"
git tag -a "v${VERSION}" -m "v${VERSION}"
git push --atomic origin "HEAD:refs/heads/${TARGET_BRANCH}" "refs/tags/v${VERSION}"
if [ "$RELEASE_TEMPLATE" = true ]; then
git add package.json
fi
if [ "$RELEASE_SDK" = true ]; then
git add sdk/package.json
fi
if ! git diff --cached --quiet; then
case "$RELEASE_TARGET" in
sdk)
git commit -m "chore(release): portal SDK v${SDK_VERSION}"
;;
template)
git commit -m "chore(release): portal template v${VERSION}"
;;
both)
git commit -m "chore(release): portal template v${VERSION} and SDK v${SDK_VERSION}"
;;
esac
fi
PUSH_REFS=("HEAD:refs/heads/${TARGET_BRANCH}")
if [ "$RELEASE_TEMPLATE" = true ]; then
git tag -a "v${VERSION}" -m "v${VERSION}"
PUSH_REFS+=("refs/tags/v${VERSION}")
fi
if [ "$TAG_SDK" = true ]; then
git tag -a "portal-sdk-v${SDK_VERSION}" -m "@nocobase/portal-sdk@${SDK_VERSION}"
PUSH_REFS+=("refs/tags/portal-sdk-v${SDK_VERSION}")
fi
git push --atomic origin "${PUSH_REFS[@]}"
+3
View File
@@ -12,4 +12,7 @@ registry.config.json
registry.json
public/r/
scripts/registry.mjs
sdk/
tests/
.github/
!.gitignore
+1 -1
View File
@@ -28,6 +28,6 @@ Route access currently centralizes role constraints only. Keep NocoBase resource
## Develop Portal Registry items
Canonical NocoBase Registry source lives under `registry/`. In this source repository, normal development and builds load it directly; do not copy it into `src/extensions` for preview. Imports from a Registry item to the host application must use the `@/` alias. Relative imports must stay within that Registry item's own root so the item remains portable after installation. Registry items target this Portal Template's React, shadcn Base UI, and pnpm toolchain. They must never import Ant Design or NocoBase's Ant Design-based client components.
Canonical NocoBase Registry source lives under `registry/`. In this source repository, normal development and builds load it directly; do not copy it into `src/extensions` for preview. Registry items must import stable Portal runtime, client, authentication, ACL, routing, and extension contracts from documented `@nocobase/portal-sdk` exports. Imports to user-owned host UI and composition must use the `@/` alias. Relative imports must stay within that Registry item's own root so the item remains portable after installation. Registry items target this Portal Template's React, shadcn Base UI, and pnpm toolchain. They must never import Ant Design or NocoBase's Ant Design-based client components.
Keep Registry items portable and focused on reusable API adapters, hooks, components, and small demos. Update `registry.config.json` whenever an item's files, dependencies, or installation target changes. Validate Registry changes with the normal application build and the relevant regression scripts.
+27 -5
View File
@@ -55,9 +55,17 @@ pnpm build:html
Application pages and resources live under `src`. The included Users page is a
working example based on the standard NocoBase `users` collection.
Application translations live in `src/locales`. Shared UI components live in
Application translations live in `src/locales`. Default locale, available
locale labels, i18next options, and post-switch behavior remain customizable in
`src/providers/i18n/runtime.ts`. Shared UI components live in
`src/components/ui` and follow the shadcn Base UI conventions.
Stable NocoBase runtime behavior is provided by `@nocobase/portal-sdk`.
The template records its inherited base version in
`nocobase.defaultTemplateVersion`; SDK installation, development, and builds
stop with an actionable error when that version is outside the SDK's supported
range. Application composition and visual source remain owned by this project.
## Extensions
Installed extensions live in `src/extensions` and are ordinary
@@ -73,6 +81,11 @@ be installed from the `@nocobase` Registry configured in `components.json`.
After installation, the generated source can be customized and committed with
the application.
Registry implementations import runtime APIs from documented
`@nocobase/portal-sdk` subpaths and continue to use `@/components` for
application-owned visual composition. Each Registry item declares the minimum
compatible SDK version in `registry.config.json`.
In this Registry source repository, canonical extension source lives in
`registry` and is loaded directly by `pnpm dev` and `pnpm build`. The published
npm template contains the default extensions under `src/extensions` instead.
@@ -87,14 +100,23 @@ Customize `src/pages/login/index.tsx` to replace the whole login page, or use
`DefaultSignInPage` to keep dynamic authentication while overriding individual
authenticator UI.
## Tests
## Source repository checks
```bash
pnpm test:auth
pnpm test:acl
pnpm test:i18n
pnpm --dir sdk test
node tests/auth-session-regression.mjs
node registry/nocobase-acl/tests/acl-regression.mjs
node registry/nocobase-i18n/tests/i18n-regression.mjs
node --experimental-strip-types --test tests/file-upload-regression.test.ts
node registry/nocobase-mail/tests/mail-regression.mjs
node registry/nocobase-route-surfaces/tests/route-surfaces-regression.mjs
```
Registry source checks are maintainer commands for this source repository; the
published template contains materialized extensions instead of `registry/`.
SDK breaking-change releases and project upgrade steps are documented in the
[Portal SDK migration guide](https://github.com/nocobase/portal-template-default/blob/main/sdk/MIGRATION.md).
## Learn more
- [NocoBase documentation](https://docs.nocobase.com/)
+9 -9
View File
@@ -1,9 +1,15 @@
{
"name": "@nocobase/portal-template-default",
"displayName": "Default Template",
"version": "1.0.22",
"version": "2.0.0",
"packageManager": "[email protected]",
"type": "module",
"nocobase": {
"defaultTemplateVersion": "2.0.0"
},
"dependencies": {
"@nocobase/portal-sdk": "workspace:^"
},
"devDependencies": {
"@ai-sdk/react": "2.0.0",
"@base-ui/react": "^1.6.0",
@@ -52,7 +58,6 @@
"eslint-plugin-react-refresh": "^0.4.19",
"eventsource-parser": "3.1.0",
"globals": "^16.0.0",
"i18next": "^25.3.2",
"ignore": "^7.0.6",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
@@ -88,14 +93,9 @@
"dev": "vite --host 0.0.0.0",
"build": "tsc && refine build",
"build:html": "node ./scripts/build-html.mjs",
"test:auth": "node tests/auth-session-regression.mjs",
"test:acl": "node registry/nocobase-acl/tests/acl-regression.mjs",
"test:i18n": "node registry/nocobase-i18n/tests/i18n-regression.mjs",
"test:file-upload": "node --experimental-strip-types --test tests/file-upload-regression.test.ts",
"test:mail": "node registry/nocobase-mail/tests/mail-regression.mjs",
"test:route-surfaces": "node registry/nocobase-route-surfaces/tests/route-surfaces-regression.mjs",
"start": "refine start",
"refine": "refine"
"refine": "refine",
"sdk:check": "pnpm exec portal-sdk check"
},
"browserslist": {
"production": [
+45 -4
View File
@@ -7,6 +7,10 @@ settings:
importers:
.:
dependencies:
'@nocobase/portal-sdk':
specifier: workspace:^
version: link:sdk
devDependencies:
'@ai-sdk/react':
specifier: 2.0.0
@@ -149,9 +153,6 @@ importers:
globals:
specifier: ^16.0.0
version: 16.5.0
i18next:
specifier: ^25.3.2
version: 25.10.10([email protected])
ignore:
specifier: ^7.0.6
version: 7.0.6
@@ -222,6 +223,46 @@ importers:
specifier: ^3.25.76
version: 3.25.76
sdk:
dependencies:
'@refinedev/core':
specifier: ^5.0.0
version: 5.0.12(@tanstack/[email protected]([email protected]))(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@tanstack/react-query':
specifier: ^5.0.0
version: 5.101.4([email protected])
i18next:
specifier: ^25.3.2
version: 25.10.10([email protected])
ignore:
specifier: ^7.0.6
version: 7.0.6
react:
specifier: ^19.0.0
version: 19.1.0
react-router:
specifier: ^7.0.0
version: 7.18.1([email protected]([email protected]))([email protected])
semver:
specifier: ^7.8.5
version: 7.8.5
vite:
specifier: '>=6.0.0 <8.0.0'
version: 6.4.3(@types/[email protected])([email protected])([email protected])
devDependencies:
'@types/node':
specifier: ^20.0.0
version: 20.19.43
'@types/react':
specifier: ^19.1.0
version: 19.2.17
'@types/semver':
specifier: ^7.7.0
version: 7.7.1
typescript:
specifier: ^5.8.3
version: 5.9.3
packages:
'@ai-sdk/[email protected]':
@@ -6709,7 +6750,7 @@ snapshots:
json-schema-typed: 7.0.3
onetime: 5.1.2
pkg-up: 3.1.0
semver: 7.5.2
semver: 7.8.5
[email protected]:
dependencies:
+4
View File
@@ -1,2 +1,6 @@
packages:
- sdk
allowBuilds:
'@nocobase/portal-sdk': true
esbuild: true
+32 -7
View File
@@ -8,6 +8,7 @@
"title": "NocoBase Route Surfaces",
"description": "URL-backed drawer, dialog, and page containers with nested drawer stacking and a shared close lifecycle.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@base-ui/react@^1.6.0",
"@refinedev/core@^5.0.8",
"lucide-react@^0.487.0",
@@ -36,6 +37,7 @@
"title": "NocoBase Users Example",
"description": "Complete Users resource example with ACL-aware list, filters, route-backed drawers, nested role details, and optional AI page context.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@refinedev/core@^5.0.8",
"@refinedev/react-hook-form@^5.0.0",
"@refinedev/react-table@^6.0.0",
@@ -66,8 +68,9 @@
"name": "i18n",
"type": "registry:block",
"title": "NocoBase i18n",
"description": "Optional NocoBase server-language resources, user preference persistence, reusable language controls, and an integrated showcase for the Starter's built-in i18n runtime.",
"description": "Optional NocoBase server-language resources, user preference persistence, reusable language controls, and an integrated showcase for the Portal SDK i18n runtime.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@refinedev/core@^5.0.8",
"lucide-react@^0.487.0",
"react-router@^7.0.2"
@@ -82,7 +85,7 @@
"select",
"textarea"
],
"docs": "Requires a compatible NocoBase Starter with the built-in i18n provider, shared System Settings bootstrap, and AppExtension Provider/UserMenuItems host fields. The Registry reuses systemSettings:get data and calls app:getLang only for registered dynamic namespaces such as lm-collections.",
"docs": "Requires a compatible Portal SDK i18n and System Settings runtime plus the template's AppExtension Provider/UserMenuItems host fields. The Registry reuses cached systemSettings:get data and calls app:getLang only for registered dynamic namespaces such as lm-collections.",
"source": {
"root": "registry/nocobase-i18n",
"target": "src/extensions/nocobase-i18n",
@@ -95,6 +98,7 @@
"title": "NocoBase OIDC Authentication",
"description": "Dynamic OIDC sign-in adapter, headless sign-in hook, and integrated component demo.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"lucide-react@^0.487.0",
"react-router@^7.0.2"
],
@@ -111,6 +115,7 @@
"title": "NocoBase SAML Authentication",
"description": "Dynamic SAML sign-in adapter, automatic redirect provider, headless hook, and integrated demo.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"lucide-react@^0.487.0",
"react-router@^7.0.2"
],
@@ -127,6 +132,7 @@
"title": "NocoBase CAS Authentication",
"description": "Dynamic CAS sign-in adapter, headless sign-in hook, and integrated component demo.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"lucide-react@^0.487.0",
"react-router@^7.0.2"
],
@@ -142,6 +148,7 @@
"type": "registry:block",
"title": "NocoBase LDAP Authentication",
"description": "Dynamic LDAP account/password form, headless sign-in hook, and integrated component demo.",
"dependencies": ["@nocobase/portal-sdk@^1.0.0"],
"docs": "Requires the Starter authentication runtime and an enabled LDAP authenticator in NocoBase.",
"source": {
"root": "registry/nocobase-auth-ldap",
@@ -154,6 +161,7 @@
"type": "registry:block",
"title": "NocoBase SMS Authentication",
"description": "Dynamic SMS verification-code form, delivery countdown, headless hook, and integrated demo.",
"dependencies": ["@nocobase/portal-sdk@^1.0.0"],
"docs": "Requires the Starter authentication runtime, an enabled SMS authenticator, and a configured public SMS verifier in NocoBase.",
"source": {
"root": "registry/nocobase-auth-sms",
@@ -167,6 +175,7 @@
"title": "NocoBase WeCom Authentication",
"description": "Dynamic WeCom sign-in adapter, browser automatic login, headless hook, and integrated demo.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"lucide-react@^0.487.0",
"react-router@^7.0.2"
],
@@ -183,6 +192,7 @@
"title": "NocoBase DingTalk Authentication",
"description": "Dynamic DingTalk sign-in adapter, automatic login, headless hook, and integrated demo.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"[email protected]",
"lucide-react@^0.487.0",
"react-router@^7.0.2"
@@ -200,6 +210,7 @@
"title": "NocoBase AI Runtime",
"description": "NocoBase AI services, providers, transports, controllers, and avatars.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@ai-sdk/[email protected]",
"[email protected]",
"@dicebear/core@^9.4.3",
@@ -266,7 +277,11 @@
"type": "registry:block",
"title": "NocoBase AI Components and Demo",
"description": "Complete NocoBase AI component runtime with application resources, routes, and component showcase pages.",
"dependencies": ["@refinedev/core@^5.0.8", "react-router@^7.0.2"],
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@refinedev/core@^5.0.8",
"react-router@^7.0.2"
],
"registryDependencies": [
"@nocobase/ai-chat",
"@nocobase/ai-react-hook-form",
@@ -293,6 +308,7 @@
"title": "NocoBase File Upload",
"description": "Controlled shadcn/ui file upload field for NocoBase file collections.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@tanstack/react-query@^5.81.5",
"lucide-react@^0.487.0"
],
@@ -302,7 +318,7 @@
"dialog",
"tooltip"
],
"docs": "Requires the Starter NocoBase client and a field targeting a file-template collection.",
"docs": "Requires a compatible Portal SDK client and a field targeting a file-template collection.",
"source": {
"root": "registry/nocobase-file-upload",
"target": "src/extensions/nocobase-file-upload",
@@ -315,6 +331,7 @@
"title": "NocoBase Mail",
"description": "Mail inbox, compose, detail, and label components backed by the NocoBase mail API.",
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@refinedev/core@^5.0.8",
"@tiptap/extension-link@^3.29.0",
"@tiptap/extension-placeholder@^3.29.0",
@@ -346,7 +363,7 @@
"textarea",
"toggle"
],
"docs": "Requires a compatible NocoBase Starter with the mail plugin enabled. The mail API client uses the Starter's built-in API_URL, getNocoBaseHeaders, and NOCOBASE_TOKEN_KEY from @/providers/constants.",
"docs": "Requires a compatible Portal SDK and a NocoBase server with the mail plugin enabled. The mail API adapter uses the authenticated SDK client.",
"source": {
"root": "registry/nocobase-mail",
"target": "src/extensions/nocobase-mail",
@@ -358,7 +375,11 @@
"type": "registry:block",
"title": "NocoBase ACL UI",
"description": "Page, region, field, and role-switching components backed by the Starter's NocoBase accessControlProvider.",
"dependencies": ["@refinedev/core@^5.0.8", "lucide-react@^0.487.0"],
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@refinedev/core@^5.0.8",
"lucide-react@^0.487.0"
],
"registryDependencies": ["badge", "select"],
"docs": "Requires a compatible NocoBase Starter with the built-in accessControlProvider and ACL store.",
"source": {
@@ -372,7 +393,11 @@
"type": "registry:block",
"title": "NocoBase ACL Components and Demo",
"description": "Complete ACL composition demo with live role switching, action matrix, multi-collection regions, and field permissions.",
"dependencies": ["@refinedev/core@^5.0.8", "react-router@^7.0.2"],
"dependencies": [
"@nocobase/portal-sdk@^1.0.0",
"@refinedev/core@^5.0.8",
"react-router@^7.0.2"
],
"registryDependencies": [
"@nocobase/acl-ui",
"button",
+2 -2
View File
@@ -30,7 +30,7 @@ effective role names. Its result follows the familiar Refine query shape with
ACL role set, not every role assigned to the signed-in user.
```tsx
import { useGetRoles } from "@/lib/nocobase/acl";
import { useGetRoles } from "@nocobase/portal-sdk/acl";
function CurrentAccessContext() {
const { data: roles, isLoading } = useGetRoles();
@@ -41,7 +41,7 @@ function CurrentAccessContext() {
```
```tsx
import { defineAppRoutes } from "@/app/route-runtime";
import { defineAppRoutes } from "@nocobase/portal-sdk/routing";
export const appRoutes = defineAppRoutes([
{
@@ -2,8 +2,8 @@ import type { BaseKey } from "@refinedev/core";
import type { PropsWithChildren, ReactNode } from "react";
import { AccessDenied } from "@/components/access-control/access-denied";
import { useAclEvaluator } from "@/lib/nocobase/acl";
import type { RoleConstraint } from "@/lib/nocobase/acl";
import { useAclEvaluator } from "@nocobase/portal-sdk/acl";
import type { RoleConstraint } from "@nocobase/portal-sdk/acl";
export type AclPermission = {
resource: string;
@@ -1,5 +1,5 @@
import type { Role, RoleMode } from "@/lib/nocobase/acl";
import { resolveTranslatableText } from "@/lib/i18n";
import type { Role, RoleMode } from "@nocobase/portal-sdk/acl";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
export const UNION_ROLE = "__union__";
export const ANONYMOUS_ROLE = "anonymous";
@@ -16,7 +16,7 @@ import {
useAclState,
type AclIdentity,
type Role,
} from "@/lib/nocobase/acl";
} from "@nocobase/portal-sdk/acl";
import { cn } from "@/lib/utils";
import { getRoleOptions, resolveRoleTitle, UNION_ROLE } from "./role-options";
@@ -5,7 +5,7 @@ import {
type AclActionParams,
type AclState,
type AclStore,
} from "@/lib/nocobase/acl";
} from "@nocobase/portal-sdk/acl";
type PreviewRecordPermission = {
dataSourceKey?: string;
+3 -3
View File
@@ -14,8 +14,8 @@ import type { TreeMenuItem } from "@refinedev/core";
import {
buildRouteResources,
defineAppRoutes,
RouteAccessGuard,
} from "@/app/route-runtime";
} from "@nocobase/portal-sdk/routing";
import { RouteAccessGuard } from "@/app/route-access-guard";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { CanAccess } from "@/components/access-control/can-access";
@@ -31,7 +31,7 @@ import {
filterMenuItemsByAcl,
type RoleConstraint,
useAclState,
} from "@/lib/nocobase/acl";
} from "@nocobase/portal-sdk/acl";
import { AclField, AclPage, AclRegion } from "../components/acl-boundary";
import { AclPreviewProvider } from "./acl-preview-provider";
import { AclBoundaryApi } from "./boundary-api";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AppExtension } from "@/app/extensions";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { Blocks, PanelsTopLeft, ShieldCheck } from "lucide-react";
import { lazy } from "react";
import { Outlet, Route } from "react-router";
@@ -13,20 +13,20 @@ try {
getEffectiveRoles,
getPermissionsForDataSource,
resolveActionPermission,
} = await server.ssrLoadModule("/src/lib/nocobase/acl/evaluator.ts");
} = await server.ssrLoadModule("/sdk/src/acl/evaluator.ts");
const { resolveAclDataSourceKey } = await server.ssrLoadModule(
"/src/lib/nocobase/acl/data-source.ts"
"/sdk/src/acl/data-source.ts"
);
const { filterMenuItemsByAcl, findFirstAccessibleRoute } =
await server.ssrLoadModule("/src/lib/nocobase/acl/menu.ts");
await server.ssrLoadModule("/sdk/src/acl/menu.ts");
const { buildRouteResources, defineAppRoutes } = await server.ssrLoadModule(
"/src/app/route-runtime.ts"
"/sdk/src/routing/index.ts"
);
const {
clearRecordPermissions,
getRecordActionPermission,
updateRecordPermissions,
} = await server.ssrLoadModule("/src/lib/nocobase/acl/record-permissions.ts");
} = await server.ssrLoadModule("/sdk/src/acl/record-permissions.ts");
const { RoleSwitcher } = await server.ssrLoadModule(
"@/extensions/nocobase-acl/components/role-switcher.tsx"
);
+3 -2
View File
@@ -3,8 +3,9 @@
The Registry is split by capability so applications only install the code and dependencies they use.
These entries target the compatible NocoBase admin starter family. The host must
provide the `@` source alias, `@/lib/nocobase/client`, the standard shadcn UI
components, and (for `@nocobase/ai`) the `@/app/extension` discovery contract.
provide the `@` source alias, `@nocobase/portal-sdk/client`, the standard shadcn
UI components, and (for `@nocobase/ai`) the
`@nocobase/portal-sdk/extensions` discovery contract.
Applications with a different host can still reuse the providers and components,
but should supply their own `AIService` adapter and integration entry.
+1 -1
View File
@@ -1,5 +1,5 @@
import { NocoBaseAIExtensionProvider } from "./global-ai-chat";
import type { AppExtension } from "@/app/extensions";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { LoadingState } from "@/components/app-shell/loading-state";
import {
Bot,
@@ -1,4 +1,4 @@
import { nocobaseClient, type NocoBaseClient } from "@/lib/nocobase/client";
import { nocobaseClient, type NocoBaseClient } from "@nocobase/portal-sdk/client";
import type {
AIChatMessage,
AIConversation,
@@ -40,7 +40,7 @@ try {
"/registry/nocobase-ai/providers/sub-agent-stream.ts"
);
const { NocoBaseClient } = await server.ssrLoadModule(
"/src/lib/nocobase/client.ts"
"@nocobase/portal-sdk/client"
);
{
@@ -1,13 +1,13 @@
import { LogIn } from "lucide-react";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { resolveTranslatableText } from "@/lib/i18n";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
import { useCasSignIn } from "./use-cas-sign-in";
+1 -1
View File
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { KeyRound } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo";
const CasSignInButton = lazy(() => import("./cas-sign-in-button"));
@@ -1,9 +1,9 @@
import { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router";
import type { Authenticator } from "@/components/auth/types";
import { nocobaseClient } from "@/lib/nocobase/client";
import { resolvePortalUrl } from "@/providers/runtime-config";
import type { Authenticator } from "@nocobase/portal-sdk/auth";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
export function useCasSignIn(authenticator: Authenticator) {
const [searchParams] = useSearchParams();
@@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router";
import { LoadingState } from "@/components/app-shell/loading-state";
import { clearAcl } from "@/lib/nocobase/acl";
import { nocobaseClient } from "@/lib/nocobase/client";
import { resolvePortalUrl } from "@/providers/runtime-config";
import { clearAcl } from "@nocobase/portal-sdk/acl";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
import { isDingtalkBrowser } from "./use-dingtalk-sign-in";
@@ -1,13 +1,13 @@
import { MessageSquare } from "lucide-react";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { resolveTranslatableText } from "@/lib/i18n";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
import { useDingtalkSignIn } from "./use-dingtalk-sign-in";
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { MessageSquare } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo";
const DingtalkSignInButton = lazy(() => import("./dingtalk-sign-in-button"));
@@ -1,9 +1,9 @@
import { useCallback, useMemo, useState } from "react";
import { useSearchParams } from "react-router";
import type { Authenticator } from "@/components/auth/types";
import { nocobaseClient } from "@/lib/nocobase/client";
import { resolvePortalUrl } from "@/providers/runtime-config";
import type { Authenticator } from "@nocobase/portal-sdk/auth";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
type DingtalkAuthUrlResponse = {
url?: string;
+1 -1
View File
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { ContactRound } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo";
const LdapSignInForm = lazy(() => import("./ldap-sign-in-form"));
@@ -1,7 +1,7 @@
import { useState } from "react";
import { InputPassword } from "@/components/auth/input-password";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -1,4 +1,4 @@
import { useAuthenticatorSignIn } from "@/components/auth";
import { useAuthenticatorSignIn } from "@nocobase/portal-sdk/auth";
export function useLdapSignIn(authenticator: string) {
return useAuthenticatorSignIn(authenticator);
+1 -1
View File
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { LogIn } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo";
const OidcSignInButton = lazy(() => import("./oidc-sign-in-button"));
@@ -1,13 +1,13 @@
import { LogIn } from "lucide-react";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { resolveTranslatableText } from "@/lib/i18n";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
import { useOidcSignIn } from "./use-oidc-sign-in";
@@ -1,9 +1,9 @@
import { useCallback, useMemo, useState } from "react";
import { useSearchParams } from "react-router";
import type { Authenticator } from "@/components/auth/types";
import { nocobaseClient } from "@/lib/nocobase/client";
import { resolvePortalUrl } from "@/providers/runtime-config";
import type { Authenticator } from "@nocobase/portal-sdk/auth";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
export function useOidcSignIn(authenticator: Authenticator) {
const [searchParams] = useSearchParams();
+1 -1
View File
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { BadgeCheck } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo";
const SamlSignInButton = lazy(() => import("./saml-sign-in-button"));
@@ -1,13 +1,13 @@
import { LogIn } from "lucide-react";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { resolveTranslatableText } from "@/lib/i18n";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
import { useSamlSignIn } from "./use-saml-sign-in";
@@ -1,9 +1,9 @@
import { useCallback, useMemo, useState } from "react";
import { useSearchParams } from "react-router";
import type { Authenticator } from "@/components/auth/types";
import { nocobaseClient } from "@/lib/nocobase/client";
import { resolvePortalUrl } from "@/providers/runtime-config";
import type { Authenticator } from "@nocobase/portal-sdk/auth";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
export function useSamlSignIn(authenticator: Authenticator) {
const [searchParams] = useSearchParams();
+1 -1
View File
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Smartphone } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extensions";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo/auth-demo-route";
const SmsSignInForm = lazy(() => import("./sms-sign-in-form"));
@@ -1,6 +1,6 @@
import { useState } from "react";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import {
Alert,
AlertDescription,
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAuthenticatorSignIn } from "@/components/auth";
import type { Authenticator } from "@/components/auth/types";
import { nocobaseClient } from "@/lib/nocobase/client";
import { useAuthenticatorSignIn } from "@nocobase/portal-sdk/auth";
import type { Authenticator } from "@nocobase/portal-sdk/auth";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
type SmsCodeResponse = {
expiresAt?: string;
+1 -1
View File
@@ -2,7 +2,7 @@ import { lazy } from "react";
import { Building2 } from "lucide-react";
import { Route } from "react-router";
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { AuthDemoRoute } from "@/components/auth/demo";
const WecomSignInButton = lazy(() => import("./wecom-sign-in-button"));
@@ -1,9 +1,9 @@
import { useCallback, useMemo, useState } from "react";
import { useSearchParams } from "react-router";
import type { Authenticator } from "@/components/auth/types";
import { nocobaseClient } from "@/lib/nocobase/client";
import { resolvePortalUrl } from "@/providers/runtime-config";
import type { Authenticator } from "@nocobase/portal-sdk/auth";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { resolvePortalUrl } from "@nocobase/portal-sdk/runtime";
type WecomAuthUrlResponse = {
url?: string;
@@ -1,6 +1,6 @@
import { Building2 } from "lucide-react";
import type { AuthenticatorComponentProps } from "@/components/auth/types";
import type { AuthenticatorComponentProps } from "@nocobase/portal-sdk/auth";
import {
Alert,
AlertDescription,
@@ -12,7 +12,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { resolveTranslatableText } from "@/lib/i18n";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
import { useWecomSignIn } from "./use-wecom-sign-in";
+1 -1
View File
@@ -1,4 +1,4 @@
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { getDataSourceHeaders } from "./storage";
import { isNocoBaseManagedFileUrl } from "./file-url-policy";
@@ -1,4 +1,4 @@
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { getDataSourceHeaders } from "./storage";
import type {
@@ -1,4 +1,4 @@
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { getDataSourceHeaders } from "./storage";
import type { FileUploadOptions, NocoBaseFileRecord } from "./types";
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { getDataSourceHeaders } from "./storage";
import type { FileFieldDescriptor, StorageCheckResult } from "./types";
+8 -7
View File
@@ -3,10 +3,11 @@
Optional NocoBase server-language integration and language controls for the
NocoBase Admin Starter.
The Starter owns the Refine i18n provider, local i18next runtime, application
resources, locale selection, and the shared `systemSettings:get` bootstrap.
This Registry reuses those foundations and adds only the NocoBase-specific
remote capabilities:
The Portal SDK owns the Refine i18n provider, i18next runtime, translation
registration protocol, locale state, and shared `systemSettings:get` store.
The application keeps its locale configuration and translation resources in
template-owned source. This Registry adds the NocoBase-specific remote
capabilities:
- load registered dynamic namespaces from `app:getLang`;
- persist a signed-in user's selected language through `users:updateLang`;
@@ -23,9 +24,9 @@ Application-owned React translations belong in `src/locales`, outside the
installed Registry directory:
```ts
import { registerLocaleResources } from "@/providers/i18n";
import { registerTranslationResources } from "@nocobase/portal-sdk/i18n";
registerLocaleResources("my-feature", {
registerTranslationResources("my-feature", {
"en-US": { title: "Orders" },
"zh-CN": { title: "订单" },
});
@@ -41,4 +42,4 @@ registerServerResourceNamespace("my-dynamic-namespace");
Namespaces registered after startup are loaded incrementally. Existing exact
NocoBase expressions such as `{{t("Orders")}}` remain supported by the
Starter's translation compatibility helper.
Portal SDK's translation compatibility helper.
@@ -1,4 +1,5 @@
import { useGetLocale, useSetLocale, useTranslate } from "@refinedev/core";
import { useEnabledLocales } from "@nocobase/portal-sdk/i18n";
import { Languages, Loader2 } from "lucide-react";
import { useState, type ReactNode } from "react";
@@ -18,7 +19,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { useEnabledLocales } from "@/providers/i18n";
export type LanguageSwitcherProps = {
className?: string;
+3 -5
View File
@@ -1,4 +1,5 @@
import { useGetLocale, useTranslate } from "@refinedev/core";
import { useEnabledLocales } from "@nocobase/portal-sdk/i18n";
import { Database, Languages, PanelsTopLeft } from "lucide-react";
import { Badge } from "@/components/ui/badge";
@@ -9,11 +10,8 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { nocobaseClient } from "@/lib/nocobase/client";
import {
LanguageSwitcher,
useEnabledLocales,
} from "..";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { LanguageSwitcher } from "..";
import { I18nPromptGenerator } from "./prompt-generator";
export function I18nDemoPage() {
@@ -28,12 +28,12 @@ Application scene
- Translate the complete visible user flow, including empty, loading, validation, and error states.
Implementation contract
- Use the Starter's @/providers/i18n runtime and the application's translation and locale hooks.
- Put application-owned React messages in src/locales and register the app namespace through registerTranslationResources from @/lib/i18n.
- Keep each Registry component's messages in its own namespace and register them with registerLocaleResources().
- Use the documented @nocobase/portal-sdk/i18n runtime and the application's translation and locale hooks.
- Put application-owned React messages in src/locales and register the app namespace through registerTranslationResources from @nocobase/portal-sdk/i18n.
- Keep each Registry component's messages in its own namespace and register them with registerTranslationResources().
- Do not place application messages inside src/extensions/nocobase-i18n/locales because installed Registry files may be refreshed independently.
- Use the reusable LanguageSwitcher when the page needs an explicit language control; the signed-in user menu already includes one.
- Reuse the Starter's shared system settings instead of requesting systemSettings:get again.
- Reuse useSystemSettings from @nocobase/portal-sdk/system-settings instead of requesting systemSettings:get again.
- Keep Starter and Registry UI resources local. Use app:getLang only for registered dynamic server namespaces such as lm-collections when the NocoBase i18n Registry is installed.
- Let the shared NocoBase client send the selected locale through X-Locale.
- Preserve compatibility when server metadata contains exact {{t("...")}} expressions.
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AppExtension } from "@/app/extensions";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { LoadingState } from "@/components/app-shell/loading-state";
import { Languages } from "lucide-react";
import { lazy, Suspense } from "react";
-4
View File
@@ -1,7 +1,3 @@
export * from "./components";
export * from "./provider";
export * from "./server-resources";
export {
registerLocaleResources,
useEnabledLocales,
} from "@/providers/i18n";
+2 -2
View File
@@ -1,8 +1,8 @@
import { registerLocaleResources } from "@/providers/i18n";
import { registerTranslationResources } from "@nocobase/portal-sdk/i18n";
import enUS from "./en-US";
import zhCN from "./zh-CN";
registerLocaleResources("nocobase-i18n", {
registerTranslationResources("nocobase-i18n", {
"en-US": enUS,
"zh-CN": zhCN,
});
+3 -3
View File
@@ -1,9 +1,9 @@
import { useEffect, useState, type PropsWithChildren } from "react";
import { LoadingState } from "@/components/app-shell/loading-state";
import { nocobaseClient } from "@/lib/nocobase/client";
import { setLocalePersistence } from "@/providers/i18n";
import { useSystemSettings } from "@/providers/system-settings";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import { setLocalePersistence } from "@nocobase/portal-sdk/i18n";
import { useSystemSettings } from "@nocobase/portal-sdk/system-settings";
import { loadServerLocaleResources } from "./server-resources";
export function NocoBaseI18nBootstrap({ children }: PropsWithChildren) {
+2 -2
View File
@@ -1,4 +1,4 @@
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import {
type LocaleSystemSettings,
applyDocumentLocale,
@@ -6,7 +6,7 @@ import {
i18n,
registerLocale,
setEnabledLocales,
} from "@/providers/i18n";
} from "@nocobase/portal-sdk/i18n";
type ServerLanguagePayload = {
lang?: string;
@@ -8,16 +8,19 @@ const server = await createServer({
});
try {
const { portalI18nReady } = await server.ssrLoadModule(
"/src/providers/i18n/runtime.ts"
);
await portalI18nReady;
await server.ssrLoadModule("/src/locales/index.ts");
const {
applySystemLocale,
getCurrentLocale,
i18n,
registerLocaleResources,
registerTranslationResources,
resolveTranslatableText,
translate,
} = await server.ssrLoadModule("/src/providers/i18n/runtime.ts");
const { resolveTranslatableText } =
await server.ssrLoadModule("/src/lib/i18n.ts");
} = await server.ssrLoadModule("@nocobase/portal-sdk/i18n");
await i18n.changeLanguage("en-US");
assert.equal(translate("buttons.create", { ns: "starter" }, "Create"), "Create");
@@ -30,7 +33,7 @@ try {
"全部权限"
);
registerLocaleResources("example-feature", {
registerTranslationResources("example-feature", {
"en-US": { title: "Example" },
"zh-CN": { title: "示例" },
});
+1 -1
View File
@@ -27,7 +27,7 @@ After installation, import the components from `@/extensions/nocobase-mail`.
- Message detail resolves authenticated `cid:` inline images and keeps those images out of the
regular attachment list.
The mail API client (`mailApi`) uses the Starter's built-in `API_URL`, `getNocoBaseHeaders`, and `NOCOBASE_TOKEN_KEY` from `@/providers/constants`. A compatible NocoBase backend with the mail plugin enabled is required.
The mail API client (`mailApi`) uses `nocobaseClient` from `@nocobase/portal-sdk/client`. A compatible NocoBase backend with the mail plugin enabled is required.
## Send modes
@@ -1,4 +1,4 @@
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "@nocobase/portal-sdk/client";
import type {
MailAccount,
MailIdentity,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import {
ListFilter,
Mail,
@@ -114,7 +114,7 @@ try {
"/registry/nocobase-mail/components/mail-api.ts"
);
const { nocobaseClient } = await server.ssrLoadModule(
"/src/lib/nocobase/client.ts"
"@nocobase/portal-sdk/client"
);
const { default: mailExtension } = await server.ssrLoadModule(
"/registry/nocobase-mail/extension.tsx"
+5 -2
View File
@@ -19,8 +19,8 @@ when the child should replace the list as a full page.
- `RouteDrawer` supports URL-backed nested drawers and push-style stacking.
- `RouteDialog` provides the same close contract for modal routes.
- `RoutePage` provides the close context without an overlay.
- `useRouteSurfaceClose` lets content request a close without knowing how it is
presented.
- SDK `useRouteSurfaceClose` lets content request a close without knowing how it
is presented.
- `useRefineUnsavedChangesGuard` adapts Refine's unsaved-change state to the
route surface close lifecycle and renders a shadcn Alert Dialog confirmation.
@@ -30,6 +30,9 @@ but use relative navigation and a host-specific `closeTo` URL; do not send a
child action to a fixed top-level resource URL. Preserve the complete opening
URL in navigation state so closing restores list filters, pagination, tabs, and
hash state; use the resolved parent route as the direct-entry fallback.
Use `createRouteSurfaceNavigationState` and `resolveRouteSurfaceCloseTo` from
`@nocobase/portal-sdk/routing` for that shared navigation protocol instead of
copying host-specific state helpers.
Route definitions, ACL guards, resource data fetching, and application-specific
paths remain application concerns.
@@ -1,5 +1,11 @@
import { ArrowLeft, ArrowRight, Eye, Layers3, Pencil, Plus } from "lucide-react";
import { useCallback, useRef } from "react";
import {
buildRouteLocationHref,
createRouteSurfaceNavigationState,
resolveRouteSurfaceCloseTo,
useRouteSurfaceClose,
} from "@nocobase/portal-sdk/routing";
import {
useLocation,
useNavigate,
@@ -21,16 +27,10 @@ import {
RouteDialog,
RouteDrawer,
RoutePage,
useRouteSurfaceClose,
} from "../index";
import { RouteSurfacePromptGenerator } from "./prompt-generator";
import { ResourceActionGuide } from "./resource-action-guide";
import { routeSurfaceScenarios } from "./scenarios";
import {
buildRouteLocationHref,
createRouteSurfaceNavigationState,
resolveRouteSurfaceCloseTo,
} from "./contextual-navigation";
const routeSurfaceRoot = "/route-surfaces";
@@ -99,7 +99,7 @@ Scenario:
- ${presentation}
Implementation requirements:
- Import RouteDrawer, RouteDialog, RoutePage, and useRouteSurfaceClose from @/extensions/nocobase-route-surfaces as needed.
- Import RouteDrawer, RouteDialog, and RoutePage from @/extensions/nocobase-route-surfaces; import useRouteSurfaceClose from @nocobase/portal-sdk/routing when actions need to close the active surface.
- Define application-owned resource routes once with defineAppRoutes. resourceAction only binds a child path to the parent Refine resource's create, edit, or show URL; it does not choose a presentation.
- With the default automatic resource outlet, every resourceAction element must render a RouteDrawer or RouteDialog. Never place a plain full-page form or detail component there because it will render below the list.
- When a resource action must replace the list as a full page, set outlet: "manual" on the resource route and use an application-owned layout that renders or consumes useOutlet().
@@ -1,4 +1,4 @@
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { Layers3 } from "lucide-react";
import { lazy } from "react";
import { Route } from "react-router";
@@ -1,7 +1,4 @@
export * from "./route-dialog";
export * from "./route-drawer";
export * from "./route-page";
export * from "./route-surface-context";
export * from "./use-refine-unsaved-changes";
export * from "./use-route-surface-close";
export type { RouteSurfaceBeforeClose } from "./use-route-surface-state";
@@ -1,17 +1,15 @@
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { X } from "lucide-react";
import { type ReactNode, useCallback, useContext } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
RouteOverlayDepthContext,
RouteSurfaceContext,
} from "./route-surface-context";
import {
type RouteSurfaceBeforeClose,
useRouteSurfaceState,
} from "./use-route-surface-state";
} from "@nocobase/portal-sdk/routing";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function RouteDialog({
title,
@@ -6,17 +6,15 @@ import {
useCallback,
useContext,
} from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import {
RouteOverlayDepthContext,
RouteSurfaceContext,
} from "./route-surface-context";
import {
type RouteSurfaceBeforeClose,
useRouteSurfaceState,
} from "./use-route-surface-state";
} from "@nocobase/portal-sdk/routing";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function RouteDrawer({
title,
@@ -1,10 +1,9 @@
import { type ReactNode } from "react";
import { RouteSurfaceContext } from "./route-surface-context";
import {
RouteSurfaceContext,
type RouteSurfaceBeforeClose,
useRouteSurfaceState,
} from "./use-route-surface-state";
} from "@nocobase/portal-sdk/routing";
export function RoutePage({
children,
@@ -13,9 +13,7 @@ try {
buildRouteLocationHref,
createRouteSurfaceNavigationState,
resolveRouteSurfaceCloseTo,
} = await server.ssrLoadModule(
"/registry/nocobase-route-surfaces/demo/contextual-navigation.ts"
);
} = await server.ssrLoadModule("@nocobase/portal-sdk/routing");
const location = {
pathname: "/customers",
search: "?status=renewal&page=2",
@@ -1,6 +1,6 @@
import { UsersRound } from "lucide-react";
import { defineAppRoutes } from "@/app/route-runtime";
import { defineAppRoutes } from "@nocobase/portal-sdk/routing";
import { AccessDenied } from "@/components/access-control/access-denied";
import { CanAccess } from "@/components/access-control/can-access";
import { UserCreate } from "./create";
+1 -1
View File
@@ -1,6 +1,7 @@
import { type HttpError, useTranslate } from "@refinedev/core";
import { useForm } from "@refinedev/react-hook-form";
import { useMemo } from "react";
import { useRouteSurfaceClose } from "@nocobase/portal-sdk/routing";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
@@ -13,7 +14,6 @@ import {
RouteDrawer,
RouteDrawerFooter,
useRefineUnsavedChangesGuard,
useRouteSurfaceClose,
} from "@/extensions/nocobase-route-surfaces";
import {
applyAIUserFormValues,
+1 -1
View File
@@ -2,6 +2,7 @@ import { type HttpError, useTranslate } from "@refinedev/core";
import { useForm } from "@refinedev/react-hook-form";
import { useMemo } from "react";
import { useParams } from "react-router";
import { useRouteSurfaceClose } from "@nocobase/portal-sdk/routing";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
@@ -14,7 +15,6 @@ import {
RouteDrawer,
RouteDrawerFooter,
useRefineUnsavedChangesGuard,
useRouteSurfaceClose,
} from "@/extensions/nocobase-route-surfaces";
import {
applyAIUserFormValues,
@@ -1,4 +1,4 @@
import type { AppExtension } from "@/app/extension";
import type { AppExtension } from "@nocobase/portal-sdk/extensions";
import { usersExampleRoutes } from "./app-routes";
import "./locales";
+1 -1
View File
@@ -16,7 +16,7 @@ import { EditButton } from "@/components/resources/buttons/edit";
import { ShowButton } from "@/components/resources/buttons/show";
import { ListView } from "@/components/resources/views/list-view";
import { useAIPageElementHandle } from "./optional-ai";
import type { Role } from "@/lib/nocobase/acl";
import type { Role } from "@nocobase/portal-sdk/acl";
import { RoleBadges } from "./role-badges";
import { resolveRoleLabel } from "./role-utils";
import { getRolePath } from "./routes";
@@ -1,4 +1,4 @@
import { registerTranslationResources } from "@/lib/i18n";
import { registerTranslationResources } from "@nocobase/portal-sdk/i18n";
import { usersExample as enUS } from "./en-US";
import { usersExample as zhCN } from "./zh-CN";
@@ -1,4 +1,4 @@
import type { Role } from "@/lib/nocobase/acl";
import type { Role } from "@nocobase/portal-sdk/acl";
import { Badge } from "@/components/ui/badge";
import { ChevronRight } from "lucide-react";
import { resolveRoleLabel } from "./role-utils";
@@ -7,8 +7,8 @@ import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { useAIPageElementHandle } from "./optional-ai";
import { RouteDrawer } from "@/extensions/nocobase-route-surfaces";
import type { Role } from "@/lib/nocobase/acl";
import { resolveTranslatableText } from "@/lib/i18n";
import type { Role } from "@nocobase/portal-sdk/acl";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
import { resolveRoleLabel } from "./role-utils";
import { getUserShowPath, userRoutes } from "./routes";
import type { RoleRecord } from "./types";
@@ -1,5 +1,5 @@
import type { Role } from "@/lib/nocobase/acl";
import { resolveTranslatableText } from "@/lib/i18n";
import type { Role } from "@nocobase/portal-sdk/acl";
import { resolveTranslatableText } from "@nocobase/portal-sdk/i18n";
export function resolveRoleLabel(role: Role) {
return resolveTranslatableText(role.title || role.name, { ns: "starter" });
+1 -1
View File
@@ -2,7 +2,7 @@ import type {
FileUploadFieldValue,
SerializedFileFieldValue,
} from "@/extensions/nocobase-file-upload";
import type { Role } from "@/lib/nocobase/acl";
import type { Role } from "@nocobase/portal-sdk/acl";
export type UserRecord = {
id: string | number;
+20 -5
View File
@@ -52,6 +52,7 @@ const config = JSON.parse(
);
const itemNames = new Set();
const filesByRoot = new Map();
const portalSdkImport = "@nocobase/portal-sdk";
const sourceItems = config.items.map((item) => {
if (!item.name || itemNames.has(item.name)) {
throw new Error(`Registry item name must be unique: ${item.name}`);
@@ -84,6 +85,25 @@ const sourceItems = config.items.map((item) => {
);
}
const includedSource = includedFiles
.filter((file) => /\.[cm]?[jt]sx?$/.test(file))
.map((file) => ({
file,
content: fs.readFileSync(path.join(sourceRoot, file), "utf8"),
}));
const usesPortalSdk = includedSource.some(({ content }) =>
content.includes(portalSdkImport)
);
const portalSdkDependency = item.dependencies?.find((dependency) =>
dependency.startsWith(`${portalSdkImport}@`)
);
if (usesPortalSdk && !portalSdkDependency) {
throw new Error(
`Registry item ${item.name} imports ${portalSdkImport} without declaring a versioned dependency`
);
}
return { item, includedFiles };
});
@@ -116,11 +136,6 @@ if (action === "build") {
for (const item of items) {
console.log(`${item.name}: ${item.files.length} files`);
}
} else if (action === "link" || action === "unlink") {
for (const source of sourceMappings.values()) {
if (action === "link") linkSource(source);
else unlinkSource(source);
}
} else {
const outputRootIndex = process.argv.indexOf("--output-root");
if (outputRootIndex !== -1 && !process.argv[outputRootIndex + 1]) {
+111
View File
@@ -0,0 +1,111 @@
# Portal SDK migrations
This guide defines how maintainers publish breaking Portal SDK changes and how
Portal projects adopt them. The SDK version and the Default Template version
are independent SemVer values connected by compatibility metadata.
## Version policy
- Patch SDK releases fix behavior without changing documented APIs.
- Minor SDK releases add backward-compatible APIs.
- Major SDK releases may remove or change documented APIs.
- `nocobase.supportedDefaultTemplateRange` declares the base template versions
with which that SDK release can run.
- A derived Portal keeps its own package name and version, but preserves the
exact base release it has incorporated in
`nocobase.defaultTemplateVersion`.
Do not update `nocobase.defaultTemplateVersion` merely to satisfy the
compatibility check. Update it only after the corresponding base template
changes have actually been merged and verified.
## Choosing the next versions
When an SDK major release does not require template source changes, increment
only the SDK major and declare the template range it genuinely supports.
When a breaking SDK release requires coordinated template source changes, use
a new compatibility generation:
```text
@nocobase/portal-sdk 1.x -> 2.0.0
Default Portal Template 2.x -> 3.0.0
```
The coordinated packages then declare:
```json
// @nocobase/portal-sdk@2.0.0
{
"nocobase": {
"supportedDefaultTemplateRange": ">=3.0.0 <4.0.0"
}
}
```
```json
// Default Portal Template 3.0.0
{
"nocobase": {
"defaultTemplateVersion": "3.0.0"
},
"dependencies": {
"@nocobase/portal-sdk": "^2.0.0"
}
}
```
This generation rule keeps an already-published SDK range truthful. Use a
template minor boundary only when the older SDK remains valid for later
templates in the same template major.
## Maintainer release checklist
1. Increment the SDK major before changing or removing documented APIs.
2. Adapt the base template, Registry source, tests, and examples.
3. Change every affected Registry dependency to the new SDK major range.
4. Set the SDK's minimum supported base template to the first release that
contains all required adaptations.
5. Add a section below that lists removed APIs, replacements, manual edits, and
codemods.
6. Build and test the SDK, template, Registry integrations, and packed
artifacts.
7. Publish the SDK and coordinated template release through the repository
release workflow.
## Release workflow targets
The release workflow defaults to `auto`. It compares `sdk/` with the latest
`portal-sdk-v*` tag and the rest of the repository with the latest template
`v*` tag, then selects one of these targets:
- `sdk` publishes and tags only the SDK.
- `template` publishes and tags only the template while verifying the installed
SDK package.
- `both` publishes coordinated SDK and template releases.
When no exact version is supplied, an already-tagged package receives a patch
increment. Supply `sdk_version` or `version` for intentional minor or major
releases. The workflow retains manual target overrides for release recovery and
exceptional maintenance, but normal releases should use automatic detection.
## Portal project upgrade checklist
1. Commit or back up application-owned changes.
2. Read the migration section for every skipped SDK major.
3. Merge the required base template release without overwriting application
pages, UI composition, translations, or installed extension customizations.
4. Apply API replacements to application code and customized Registry source.
5. Update `nocobase.defaultTemplateVersion` to the base release actually
incorporated by the project.
6. Update `@nocobase/portal-sdk` to the required major.
7. Run `pnpm sdk:check`, install dependencies, build, and exercise direct URLs,
authentication, ACL, locale switching, and routed surfaces.
The compatibility checker detects an invalid version combination; it does not
merge template source or rewrite customized application code.
## Current migrations
There are no published major-version migrations yet. SDK 1.x is the initial
public API generation for Default Portal Template 2.x.
+49
View File
@@ -0,0 +1,49 @@
# @nocobase/portal-sdk
Stable, upgradeable runtime APIs for applications based on the NocoBase
Default Portal Template. Application composition, pages, visual components,
themes, and Registry implementations remain owned by the template project.
Use documented package exports only. Imports from `src/` are not public API.
## Public entry points
- `@nocobase/portal-sdk/runtime` — Portal base, API URL, callback, and settings URL resolution.
- `@nocobase/portal-sdk/client` — authenticated NocoBase client, session storage, and HTTP errors.
- `@nocobase/portal-sdk/auth` — Refine authentication provider, callback capture, authenticator types, and headless hooks.
- `@nocobase/portal-sdk/data` — NocoBase Refine data provider.
- `@nocobase/portal-sdk/acl` — ACL store, evaluator, record permissions, hooks, and access-control provider.
- `@nocobase/portal-sdk/routing` — application route definitions, Refine resource compilation, and route-surface lifecycle state.
- `@nocobase/portal-sdk/extensions` — stable `AppExtension` ABI and pure contribution collection.
- `@nocobase/portal-sdk/i18n` — translation registration, configurable i18next runtime, locale state, and Refine adapter.
- `@nocobase/portal-sdk/system-settings` — cached System Settings access, context, and hooks.
- `@nocobase/portal-sdk/vite` — build compatibility and raw Portal HTML plugins.
## Workspace development
The workspace manifest resolves public SDK entry points directly from `src/`,
so the Portal Template's Vite process compiles and watches SDK changes without a
separate SDK watcher. During `pnpm pack` and `pnpm publish`, pnpm applies
`publishConfig.exports` and rewrites those entry points to the compiled `dist/`
files. SDK releases must therefore use the repository's pnpm release workflow.
The release workflow verifies every packed export before publishing, including
the corresponding JavaScript and declaration files.
## Compatibility
The SDK package declares `nocobase.supportedDefaultTemplateRange`. Projects
preserve their inherited base template version as
`nocobase.defaultTemplateVersion`, even when they use a custom package name and
version. `portal-sdk check`, the SDK install script, and the Vite plugin enforce
the same compatibility decision.
See [MIGRATION.md](./MIGRATION.md) for the breaking-change version policy and
the coordinated template, Registry, and application upgrade process.
## Ownership boundary
The SDK intentionally excludes shadcn components, Tailwind styling, App Shell,
branding, business routes and pages, login UI, visual Registry components,
application locale configuration, and translation content. Those remain source
assets owned by each Portal project.
+145
View File
@@ -0,0 +1,145 @@
{
"name": "@nocobase/portal-sdk",
"version": "1.0.0",
"description": "Stable runtime and extension APIs for NocoBase Portal applications.",
"type": "module",
"sideEffects": false,
"files": [
"dist",
"scripts",
"README.md",
"MIGRATION.md"
],
"exports": {
"./compat": {
"types": "./src/compat/index.ts",
"import": "./src/compat/index.ts"
},
"./runtime": {
"types": "./src/runtime/index.ts",
"import": "./src/runtime/index.ts"
},
"./client": {
"types": "./src/client/index.ts",
"import": "./src/client/index.ts"
},
"./auth": {
"types": "./src/auth/index.ts",
"import": "./src/auth/index.ts"
},
"./data": {
"types": "./src/data/index.ts",
"import": "./src/data/index.ts"
},
"./routing": {
"types": "./src/routing/index.ts",
"import": "./src/routing/index.ts"
},
"./extensions": {
"types": "./src/extensions/index.ts",
"import": "./src/extensions/index.ts"
},
"./i18n": {
"types": "./src/i18n/index.ts",
"import": "./src/i18n/index.ts"
},
"./system-settings": {
"types": "./src/system-settings/index.ts",
"import": "./src/system-settings/index.ts"
},
"./vite": {
"types": "./src/vite/index.ts",
"import": "./src/vite/index.ts"
},
"./acl": {
"types": "./src/acl/index.ts",
"import": "./src/acl/index.ts"
}
},
"bin": {
"portal-sdk": "./scripts/cli.mjs"
},
"nocobase": {
"supportedDefaultTemplateRange": ">=2.0.0 <3.0.0"
},
"publishConfig": {
"access": "public",
"exports": {
"./compat": {
"types": "./dist/compat/index.d.ts",
"import": "./dist/compat/index.js"
},
"./runtime": {
"types": "./dist/runtime/index.d.ts",
"import": "./dist/runtime/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
},
"./auth": {
"types": "./dist/auth/index.d.ts",
"import": "./dist/auth/index.js"
},
"./data": {
"types": "./dist/data/index.d.ts",
"import": "./dist/data/index.js"
},
"./routing": {
"types": "./dist/routing/index.d.ts",
"import": "./dist/routing/index.js"
},
"./extensions": {
"types": "./dist/extensions/index.d.ts",
"import": "./dist/extensions/index.js"
},
"./i18n": {
"types": "./dist/i18n/index.d.ts",
"import": "./dist/i18n/index.js"
},
"./system-settings": {
"types": "./dist/system-settings/index.d.ts",
"import": "./dist/system-settings/index.js"
},
"./vite": {
"types": "./dist/vite/index.d.ts",
"import": "./dist/vite/index.js"
},
"./acl": {
"types": "./dist/acl/index.d.ts",
"import": "./dist/acl/index.js"
}
}
},
"scripts": {
"build": "node ./scripts/clean-dist.mjs && tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
"preinstall": "node ./scripts/check-template-compatibility.mjs",
"prepack": "pnpm build",
"pretest": "pnpm build",
"test": "node --test ./tests/*.test.mjs"
},
"dependencies": {
"ignore": "^7.0.6",
"i18next": "^25.3.2",
"semver": "^7.8.5"
},
"peerDependencies": {
"@refinedev/core": "^5.0.0",
"@tanstack/react-query": "^5.0.0",
"react": "^19.0.0",
"react-router": "^7.0.0",
"vite": ">=6.0.0 <8.0.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^19.1.0",
"@types/semver": "^7.7.0",
"typescript": "^5.8.3"
}
}
@@ -0,0 +1,129 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import semver from "semver";
const sdkRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
".."
);
const readPackage = (directory) => {
const packagePath = path.join(directory, "package.json");
if (!fs.existsSync(packagePath)) return undefined;
try {
return {
directory,
path: packagePath,
value: JSON.parse(fs.readFileSync(packagePath, "utf8")),
};
} catch {
return undefined;
}
};
const findProjectPackage = () => {
const starts = [
process.env.INIT_CWD,
process.env.npm_config_local_prefix,
process.cwd(),
].filter(Boolean);
const visited = new Set();
let fallback;
for (const start of starts) {
let directory = path.resolve(start);
while (!visited.has(directory)) {
visited.add(directory);
const candidate = readPackage(directory);
if (candidate && candidate.directory !== sdkRoot) {
fallback ??= candidate;
const pkg = candidate.value;
if (
pkg?.nocobase?.defaultTemplateVersion ||
pkg?.name === "@nocobase/portal-template-default"
) {
return candidate;
}
}
const parent = path.dirname(directory);
if (parent === directory) break;
directory = parent;
}
}
return fallback;
};
export const checkTemplateCompatibility = ({ silent = false } = {}) => {
const sdkPackage = readPackage(sdkRoot)?.value;
const projectPackage = findProjectPackage();
const supportedRange =
sdkPackage?.nocobase?.supportedDefaultTemplateRange;
const project = projectPackage?.value;
const templateVersion =
project?.nocobase?.defaultTemplateVersion ??
(project?.name === "@nocobase/portal-template-default"
? project.version
: undefined);
if (!supportedRange) {
throw new Error(
"@nocobase/portal-sdk does not declare nocobase.supportedDefaultTemplateRange."
);
}
if (!templateVersion) {
throw new Error(
[
"Unable to determine the NocoBase Default Template version.",
`Project: ${projectPackage?.path ?? "not found"}`,
"Add nocobase.defaultTemplateVersion to the project package.json before installing the SDK.",
].join("\n")
);
}
if (!semver.valid(templateVersion)) {
throw new Error(
`Invalid nocobase.defaultTemplateVersion: ${templateVersion}`
);
}
if (!semver.validRange(supportedRange)) {
throw new Error(
`Invalid SDK supportedDefaultTemplateRange: ${supportedRange}`
);
}
const compatible = semver.satisfies(templateVersion, supportedRange);
if (!compatible) {
throw new Error(
[
"Incompatible NocoBase Portal SDK.",
`Current Default Template: ${templateVersion}`,
`Installing Portal SDK: ${sdkPackage.version}`,
`Supported Default Template range: ${supportedRange}`,
"Upgrade the base template first, or install a compatible @nocobase/portal-sdk version.",
].join("\n")
);
}
if (!silent) {
process.stdout.write(
`Portal SDK ${sdkPackage.version} supports Default Template ${templateVersion} (${supportedRange}).\n`
);
}
return {
compatible,
defaultTemplateVersion: templateVersion,
supportedDefaultTemplateRange: supportedRange,
};
};
if (process.argv[1] === fileURLToPath(import.meta.url)) {
try {
checkTemplateCompatibility();
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
process.exitCode = 1;
}
}
+10
View File
@@ -0,0 +1,10 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const sdkRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
".."
);
fs.rmSync(path.join(sdkRoot, "dist"), { recursive: true, force: true });
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
import { checkTemplateCompatibility } from "./check-template-compatibility.mjs";
const command = process.argv[2] ?? "check";
if (command !== "check") {
process.stderr.write("Usage: portal-sdk check\n");
process.exitCode = 1;
} else {
try {
checkTemplateCompatibility();
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
process.exitCode = 1;
}
}
@@ -2,12 +2,11 @@ import type { AccessControlProvider } from "@refinedev/core";
import {
aclStore,
evaluateAccess,
getAclState,
loadAcl,
type AclAccessRequest,
type RoleConstraint,
} from "@/lib/nocobase/acl";
} from "./store.ts";
import { evaluateAccess } from "./evaluator.ts";
import type { AclAccessRequest, RoleConstraint } from "./types.ts";
const toAclAccessRequest = ({
resource,
@@ -1,7 +1,7 @@
import { createContext, useContext, useSyncExternalStore } from "react";
import type { RecordPermissionResolver } from "./evaluator";
import type { AclState } from "./types";
import type { RecordPermissionResolver } from "./evaluator.ts";
import type { AclState } from "./types.ts";
export type AclStore = {
getState: () => AclState;
@@ -1,4 +1,4 @@
import type { ResourceAcl } from "./types";
import type { ResourceAcl } from "./types.ts";
type DataSourceMeta = {
dataSourceKey?: unknown;
@@ -1,7 +1,7 @@
import ignore from "ignore";
import { resolveAclDataSourceKey } from "./data-source";
import { getRecordActionPermission } from "./record-permissions";
import { resolveAclDataSourceKey } from "./data-source.ts";
import { getRecordActionPermission } from "./record-permissions.ts";
import type {
AclAccessRequest,
AclActionParams,
@@ -9,7 +9,7 @@ import type {
AclResourcePermissions,
ResourceAcl,
RoleConstraint,
} from "./types";
} from "./types.ts";
const REFINE_ACTION_MAP: Record<string, string> = {
list: "list",
@@ -1,8 +1,8 @@
import { useSyncExternalStore } from "react";
import { useAclState, useAclStore } from "./context";
import { evaluateAccess } from "./evaluator";
import type { AclAccessRequest } from "./types";
import { useAclState, useAclStore } from "./context.ts";
import { evaluateAccess } from "./evaluator.ts";
import type { AclAccessRequest } from "./types.ts";
export const useAclEvaluator = () => {
const store = useAclStore();
+10
View File
@@ -0,0 +1,10 @@
export * from "./access-control.ts";
export * from "./context.ts";
export * from "./data-source.ts";
export * from "./evaluator.ts";
export * from "./hooks.ts";
export * from "./menu.ts";
export * from "./record-permissions.ts";
export * from "./store.ts";
export * from "./store-provider.tsx";
export * from "./types.ts";
@@ -1,7 +1,7 @@
import type { TreeMenuItem } from "@refinedev/core";
import { evaluateAccess, matchesRoleConstraint } from "./evaluator";
import type { AclPermissionSet, RouteAccessConstraint } from "./types";
import { evaluateAccess, matchesRoleConstraint } from "./evaluator.ts";
import type { AclPermissionSet, RouteAccessConstraint } from "./types.ts";
const matchesRouteAccess = (
item: TreeMenuItem,
@@ -1,6 +1,6 @@
import type { PropsWithChildren } from "react";
import { AclStoreContext, type AclStore } from "./context";
import { AclStoreContext, type AclStore } from "./context.ts";
export function AclStoreProvider({
children,
@@ -1,13 +1,12 @@
import { nocobaseClient } from "@/lib/nocobase/client";
import { NocoBaseHttpError } from "@/lib/nocobase/error";
import type { AclStore } from "./context";
import { nocobaseClient, NocoBaseHttpError } from "../client/index.ts";
import type { AclStore } from "./context.ts";
import {
clearRecordPermissions,
getRecordActionPermission,
getRecordPermissions,
subscribeRecordPermissions,
} from "./record-permissions";
import type { AclPermissionSet, AclResponse, AclState } from "./types";
} from "./record-permissions.ts";
import type { AclPermissionSet, AclResponse, AclState } from "./types.ts";
const listeners = new Set<() => void>();
let activeRequest:
@@ -1,5 +1,5 @@
import { clearAcl } from "@/lib/nocobase/acl";
import { nocobaseClient } from "@/lib/nocobase/client";
import { clearAcl } from "../acl/index.ts";
import { nocobaseClient } from "../client/index.ts";
export function captureAuthenticationCallback() {
if (typeof window === "undefined") return false;
+5
View File
@@ -0,0 +1,5 @@
export * from "./callback.ts";
export * from "./provider.ts";
export * from "./types.ts";
export * from "./use-authenticator-sign-in.ts";
export * from "./use-public-authenticators.ts";
@@ -1,15 +1,15 @@
import type { AuthProvider } from "@refinedev/core";
import { nocobaseClient } from "@/lib/nocobase/client";
import { nocobaseClient } from "../client/index.ts";
import {
clearAcl,
loadAcl,
type Role,
} from "@/lib/nocobase/acl";
} from "../acl/index.ts";
import {
getNocoBaseErrorMessage,
NocoBaseHttpError,
} from "@/lib/nocobase/error";
} from "../client/index.ts";
type NocoBaseUser = {
id: number | string;

Some files were not shown because too many files have changed in this diff Show More