snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
indent_size=4
|
||||
indent_style=space
|
||||
insert_final_newline=true
|
||||
trim_trailing_whitespace=true
|
||||
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: univer
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
68
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
68
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: 🐛 Bug report
|
||||
description: Create a report to help us improve.
|
||||
type: Bug
|
||||
labels: bug 🐛
|
||||
title: '[Bug] '
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for helping us improve Univer!**
|
||||
|
||||
> [!CAUTION]
|
||||
> Before submitting a bug report, please ensure that you have read the [Univer GitHub Issues Community Guide](https://github.com/dream-num/univer/wiki/Univer-GitHub-Issues-Community-Guide).
|
||||
|
||||
- type: checkboxes
|
||||
id: initial-checklist
|
||||
attributes:
|
||||
label: Before you submit this issue, have you checked the following
|
||||
options:
|
||||
- label: Is this really a problem?
|
||||
required: true
|
||||
- label: I have searched the Github Issues for similar issues, but did not find anything.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: affected-versions
|
||||
attributes:
|
||||
label: Affected packages and versions
|
||||
description: Please test using the latest version of the relevant packages to make sure your issue has not already been fixed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Reproduction link
|
||||
description: |
|
||||
> [!CAUTION]
|
||||
> Please note that it is essential to provide a reproduction and describe the reproduction steps in detail. **If you cannot provide a reproduction link, your issue may be closed directly.**
|
||||
|
||||
We provide a [Vite online template](https://stackblitz.com/edit/univer-sheets-vite) and a [UMD online template](https://stackblitz.com/edit/univer-sheets-umd). If your issue can be reproduced in one of these templates, please provide the reproduction link here.
|
||||
We also provide some other [template repositories](https://github.com/awesome-univer), you can choose one of them to reproduce your issue.
|
||||
If your issue cannot be reproduced in the template, please provide as detailed reproduction steps as possible.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What should happen?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: What happens instead?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System information
|
||||
description: Output of `npx envinfo --system --binaries --browsers`
|
||||
84
.github/ISSUE_TEMPLATE/bug_report.zh-CN.yml
vendored
Normal file
84
.github/ISSUE_TEMPLATE/bug_report.zh-CN.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: 🐛 Bug 报告
|
||||
description: 尽管我们提供了中文 Bug 报告的入口,但还是建议您使用英语描述问题,这可以让更多的人来帮助您。
|
||||
type: Bug
|
||||
labels: bug 🐛
|
||||
title: '[Bug] '
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**感谢您帮助我们改进 Univer!**
|
||||
|
||||
> [!CAUTION]
|
||||
> 在提交 Bug 报告之前,请确保您已阅读 [Univer GitHub Issues 社区手册](https://github.com/dream-num/univer/wiki/Univer-GitHub-Issues-%E7%A4%BE%E5%8C%BA%E6%89%8B%E5%86%8C)。
|
||||
|
||||
- type: checkboxes
|
||||
id: initial-checklist
|
||||
attributes:
|
||||
label: 在您提交此问题之前,您是否检查了以下内容?
|
||||
options:
|
||||
- label: 这真的是个问题吗?
|
||||
required: true
|
||||
- label: 我已经在 Github Issues 中搜索过了,但没有找到类似的问题。
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: affected-versions
|
||||
attributes:
|
||||
label: 受影响的包和版本
|
||||
description: 请使用最新版本的相关包进行测试,以确保您的问题尚未解决。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: 复现链接
|
||||
description: |
|
||||
> [!CAUTION]
|
||||
> **请注意,请务必认真提供复现并详细描述复现步骤,**如果您无法提供复现链接,你的问题会被直接关闭。**
|
||||
|
||||
我们提供了 [Vite 在线模板](https://stackblitz.com/edit/univer-sheets-vite) 和 [UMD 在线模板](https://stackblitz.com/edit/univer-sheets-umd),如果您的问题可以在这个模板中复现,那么请在这里提供复现链接。
|
||||
我们也提供了一些其他你可能会用到的 [模板仓库](https://github.com/awesome-univer),你可以选择其中一个来复现你的问题。
|
||||
如果您的问题无法在模板中复现,请提供尽可能详细的复现步骤。
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: 预期行为
|
||||
description: 本应发生什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: 实际行为
|
||||
description: 实际发生了什么?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: javascript-runtime
|
||||
attributes:
|
||||
label: 运行环境
|
||||
description: |
|
||||
您使用的浏览器是?
|
||||
multiple: true
|
||||
options:
|
||||
- Chrome
|
||||
- Safari
|
||||
- Firefox
|
||||
- Electron
|
||||
- 其他 (请在复现步骤中说明)
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: 系统信息
|
||||
description: 在这里粘贴 `npx envinfo --system --binaries --browsers` 命令的输出结果
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 💬 Github Discussions
|
||||
url: https://github.com/dream-num/univer/discussions
|
||||
about: Use github issues for bug reports and feature requests. For questions please use discussions. | Github issues 仅用于错误报告和功能请求。如有疑问请使用 Github Discussions。
|
||||
|
||||
- name: 👾 Discord Community
|
||||
url: https://discord.gg/z3NKNT6D2f
|
||||
about: Ask for help in the Discord community. | 在 Discord 社区寻求帮助。
|
||||
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: 🚀 Feature request
|
||||
description: Suggest an idea
|
||||
type: Feature
|
||||
labels: enhancement 🚀
|
||||
title: '[Feature] '
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: initial-checklist
|
||||
attributes:
|
||||
label: Initial checklist
|
||||
options:
|
||||
- label: Is this really a problem?
|
||||
required: true
|
||||
- label: I have searched the Github Issues for similar issues, but did not find anything.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: Please describe the problem you are trying to solve here.
|
||||
validations:
|
||||
required: true
|
||||
27
.github/ISSUE_TEMPLATE/feature_request.zh-CN.yml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/feature_request.zh-CN.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 🚀 新功能请求
|
||||
description: 尽管我们提供了中文 Feature request 的入口,但还是建议您使用英语描述需求,这可以让更多的人来帮助您。
|
||||
type: Feature
|
||||
labels: enhancement 🚀
|
||||
title: '[Feature] '
|
||||
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: initial-checklist
|
||||
attributes:
|
||||
label: 初始清单
|
||||
options:
|
||||
- label: 这真的是个问题吗?
|
||||
required: true
|
||||
- label: 我已经在 Github Issues 中搜索过了,但没有找到类似的问题。
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 问题
|
||||
description: |
|
||||
请详细描述您的需求,如果您的描述无法被理解,**那么您的问题可能会被直接关闭**。
|
||||
- 您应当清楚地提供需求的背景、目的、以及您希望的解决方案。
|
||||
- 如果您有任何的设计稿、流程图、或者其他相关的附件,请在这里提供。
|
||||
validations:
|
||||
required: true
|
||||
41
.github/actions/setup-node/action.yml
vendored
Normal file
41
.github/actions/setup-node/action.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Node Setup
|
||||
description: Node.js setup for CI, including cache configuration
|
||||
|
||||
inputs:
|
||||
save-cache:
|
||||
description: Save a new pnpm store cache entry from this job
|
||||
default: 'false'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup pnpm and Node.js
|
||||
uses: pnpm/setup@v2
|
||||
with:
|
||||
runtime: node@24.3.0
|
||||
install: false
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
shell: bash
|
||||
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm store
|
||||
id: pnpm-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-${{ runner.os }}-${{ runner.arch }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
shell: bash
|
||||
|
||||
- name: Save pnpm store
|
||||
if: inputs.save-cache == 'true' && github.ref_name == github.event.repository.default_branch && steps.pnpm-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml') }}
|
||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm # See documentation for possible values
|
||||
directory: / # Location of package manifests
|
||||
schedule:
|
||||
interval: weekly
|
||||
versioning-strategy: auto
|
||||
open-pull-requests-limit: 5
|
||||
26
.github/pull_request_template.md
vendored
Normal file
26
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Thank you for submitting a Pull Request.
|
||||
Please read our Pull Request guidelines:
|
||||
https://github.com/dream-num/univer/blob/dev/CONTRIBUTING.md#submitting-pull-requests
|
||||
-->
|
||||
|
||||
<!-- Associate issues with the pull request if there is one. Separate them width commas. -->
|
||||
<!-- Feel free to delete this if there is no related issue. -->
|
||||
close #xxx
|
||||
|
||||
<!-- A description of the proposed changes. -->
|
||||
|
||||
<!-- How to test them. -->
|
||||
|
||||
<!-- Uncomment the below lines if there are breaking changes introduced in this PR. -->
|
||||
<!-- BREAKING CHANGE:
|
||||
Before:
|
||||
|
||||
After: -->
|
||||
|
||||
## Pull Request Checklist
|
||||
|
||||
- [ ] Related tickets or issues have been linked in the PR description (or missing issue).
|
||||
- [ ] [Naming convention](https://github.com/dream-num/univer/blob/dev/docs/NAMING_CONVENTION.md) is followed (**do please** check it especially when you created new plugins, commands and resources).
|
||||
- [ ] Unit tests have been added for the changes (if applicable).
|
||||
- [ ] Breaking changes have been documented (or no breaking changes introduced in this PR).
|
||||
61
.github/workflows/build-packages.yml
vendored
Normal file
61
.github/workflows/build-packages.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: 📦 Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TURBO_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'dream-num/univer' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
save-cache: 'true'
|
||||
|
||||
- name: 📦 Build
|
||||
run: pnpm build:ci
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
run: |
|
||||
if [[ -n $(git status --porcelain) ]]; then
|
||||
echo "Error: Uncommitted changes detected after build."
|
||||
echo "Changed files:"
|
||||
git status --porcelain
|
||||
echo "Diff of changes:"
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
128
.github/workflows/ci-quality-checks.yml
vendored
Normal file
128
.github/workflows/ci-quality-checks.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
name: 🧪 CI Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
TURBO_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
test-shard:
|
||||
name: test shard (${{ matrix.shard-index }}/${{ matrix.shard-total }})
|
||||
if: github.repository == 'dream-num/univer' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
shard-index: [1, 2, 3, 4]
|
||||
shard-total: [4]
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: 🩺 Run test
|
||||
run: node --experimental-strip-types ./scripts/coverage-shard.mts --shard-index=${{ matrix.shard-index }} --shard-total=${{ matrix.shard-total }} --concurrency=2
|
||||
|
||||
- name: 🧬 Report coverage
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
name: unit-${{ matrix.shard-index }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
test:
|
||||
name: test
|
||||
if: always() && github.repository == 'dream-num/univer' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: minimum-runner
|
||||
timeout-minutes: 5
|
||||
needs: test-shard
|
||||
|
||||
steps:
|
||||
- name: Check test shard results
|
||||
run: |
|
||||
if [ "${{ needs.test-shard.result }}" != "success" ]; then
|
||||
echo "One or more test shards failed: ${{ needs.test-shard.result }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All test shards passed"
|
||||
|
||||
eslint:
|
||||
if: github.repository == 'dream-num/univer' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Restore ESLint cache
|
||||
id: eslint-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .eslintcache
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'eslint.config.ts') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
eslint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'eslint.config.ts') }}-
|
||||
|
||||
- name: 🌡️ Run ESLint
|
||||
run: pnpm lint --quiet --cache --cache-location .eslintcache
|
||||
|
||||
- name: Save ESLint cache
|
||||
if: github.ref_name == github.event.repository.default_branch && steps.eslint-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: .eslintcache
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'package.json', 'eslint.config.ts') }}-${{ github.sha }}
|
||||
|
||||
typecheck:
|
||||
if: github.repository == 'dream-num/univer' && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: 🧼 Type checking
|
||||
run: pnpm typecheck
|
||||
65
.github/workflows/collect-pr-metadata.yml
vendored
Normal file
65
.github/workflows/collect-pr-metadata.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: 🎬 Setup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- closed
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TURBO_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
set-pr-info:
|
||||
name: set PR info
|
||||
if: github.repository == 'dream-num/univer' && !github.event.pull_request.draft
|
||||
runs-on: minimum-runner
|
||||
steps:
|
||||
- name: Save PR info
|
||||
env:
|
||||
PR_ID: ${{ github.event.pull_request.number || '' }}
|
||||
PR_REF: ${{ github.head_ref || github.ref_name }}
|
||||
PR_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
PR_STATE: ${{ github.event.pull_request.state || '' }}
|
||||
run: |
|
||||
printf '%s\n' "$PR_ID" > ./pr-id.txt
|
||||
printf '%s\n' "$PR_REF" > ./pr-ref.txt
|
||||
printf '%s\n' "$PR_REPO" > ./pr-repo.txt
|
||||
printf '%s\n' "$PR_STATE" > ./pr-state.txt
|
||||
|
||||
- name: Upload PR number
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pr-id
|
||||
path: ./pr-id.txt
|
||||
|
||||
- name: Upload PR ref
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pr-ref
|
||||
path: ./pr-ref.txt
|
||||
|
||||
- name: Upload PR repo
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pr-repo
|
||||
path: ./pr-repo.txt
|
||||
|
||||
- name: Upload PR state
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: pr-state
|
||||
path: ./pr-state.txt
|
||||
24
.github/workflows/dispatch-sync-univer-pro.yml
vendored
Normal file
24
.github/workflows/dispatch-sync-univer-pro.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: 🔫 Sync to Pro
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
dispatch-sync:
|
||||
if: github.repository == 'dream-num/univer'
|
||||
runs-on: minimum-runner
|
||||
|
||||
steps:
|
||||
- name: Dispatch sync event to univer-pro
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
repository: dream-num/univer-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
event-type: sync-univer
|
||||
client-payload: >
|
||||
{
|
||||
"ref": "${{ github.ref_name }}",
|
||||
"sha": "${{ github.sha }}"
|
||||
}
|
||||
62
.github/workflows/release-npm.yml
vendored
Normal file
62
.github/workflows/release-npm.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: 🍰 Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- 'v*.*.*-alpha.*'
|
||||
- 'v*.*.*-beta.*'
|
||||
- 'v*.*.*-rc.*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TURBO_TELEMETRY_DISABLED: '1'
|
||||
|
||||
jobs:
|
||||
release-npm:
|
||||
if: github.repository == 'dream-num/univer'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install pnpm and Node.js
|
||||
uses: pnpm/setup@v2
|
||||
with:
|
||||
runtime: node@24.3.0
|
||||
install: false
|
||||
|
||||
- name: Configure npm registry
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org
|
||||
package-manager-cache: false
|
||||
|
||||
- name: 🚚 Install dependencies and build
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
|
||||
- name: 🐙 Publish
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
if [[ "$REF_NAME" =~ -(alpha|beta|rc)\. ]]; then
|
||||
pnpm publish --access public --tag "${BASH_REMATCH[1]}" -r --no-git-checks --provenance
|
||||
else
|
||||
pnpm publish --access public -r --no-git-checks --provenance
|
||||
fi
|
||||
191
.github/workflows/security-semgrep.yml
vendored
Normal file
191
.github/workflows/security-semgrep.yml
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
name: 🔐 Semgrep
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
if: github.repository == 'dream-num/univer'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Semgrep
|
||||
id: install-semgrep
|
||||
continue-on-error: true
|
||||
run: python -m pip install --upgrade pip semgrep
|
||||
|
||||
- name: Run Semgrep
|
||||
id: semgrep
|
||||
if: ${{ always() }}
|
||||
shell: bash
|
||||
env:
|
||||
INSTALL_OUTCOME: ${{ steps.install-semgrep.outcome }}
|
||||
run: |
|
||||
mkdir -p reports
|
||||
|
||||
if [ "$INSTALL_OUTCOME" = "success" ]; then
|
||||
set +e
|
||||
semgrep scan \
|
||||
--config auto \
|
||||
--sarif \
|
||||
--output reports/semgrep.sarif
|
||||
exit_code=$?
|
||||
set -e
|
||||
else
|
||||
exit_code=2
|
||||
fi
|
||||
|
||||
if [ ! -s reports/semgrep.sarif ]; then
|
||||
printf '%s\n' \
|
||||
'{' \
|
||||
' "version": "2.1.0",' \
|
||||
' "$schema": "https://json.schemastore.org/sarif-2.1.0.json",' \
|
||||
' "runs": [' \
|
||||
' {' \
|
||||
' "tool": {' \
|
||||
' "driver": {' \
|
||||
' "name": "Semgrep",' \
|
||||
' "informationUri": "https://semgrep.dev"' \
|
||||
' }' \
|
||||
' },' \
|
||||
' "invocations": [' \
|
||||
' {' \
|
||||
' "executionSuccessful": false,' \
|
||||
' "toolExecutionNotifications": [' \
|
||||
' {' \
|
||||
' "level": "error",' \
|
||||
' "message": {' \
|
||||
' "text": "Semgrep did not produce a SARIF report. Check the workflow logs for the setup or scan failure."' \
|
||||
' }' \
|
||||
' }' \
|
||||
' ]' \
|
||||
' }' \
|
||||
' ],' \
|
||||
' "results": []' \
|
||||
' }' \
|
||||
' ]' \
|
||||
'}' > reports/semgrep.sarif
|
||||
fi
|
||||
|
||||
echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Semgrep SARIF to GitHub
|
||||
id: upload-sarif
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: reports/semgrep.sarif
|
||||
category: semgrep
|
||||
|
||||
- name: Upload Semgrep report artifact
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: semgrep-sarif-${{ github.run_id }}
|
||||
path: reports/semgrep.sarif
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Summarize Semgrep report
|
||||
if: ${{ always() }}
|
||||
shell: bash
|
||||
env:
|
||||
SEMGREP_EXIT_CODE: ${{ steps.semgrep.outputs.exit_code }}
|
||||
INSTALL_OUTCOME: ${{ steps.install-semgrep.outcome }}
|
||||
SARIF_UPLOAD_OUTCOME: ${{ steps.upload-sarif.outcome }}
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
report_path = Path("reports/semgrep.sarif")
|
||||
with report_path.open() as file:
|
||||
sarif = json.load(file)
|
||||
|
||||
results = []
|
||||
for run in sarif.get("runs", []):
|
||||
results.extend(run.get("results", []))
|
||||
|
||||
counts = {"error": 0, "warning": 0, "note": 0, "other": 0}
|
||||
for result in results:
|
||||
level = result.get("level", "warning")
|
||||
counts[level if level in counts else "other"] += 1
|
||||
|
||||
sarif_upload_outcome = os.environ.get("SARIF_UPLOAD_OUTCOME") or "skipped"
|
||||
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
|
||||
summary.write("## Semgrep report\n")
|
||||
summary.write(f"- Total findings: {len(results)}\n")
|
||||
summary.write(f"- Error: {counts['error']}\n")
|
||||
summary.write(f"- Warning: {counts['warning']}\n")
|
||||
summary.write(f"- Note: {counts['note']}\n")
|
||||
summary.write(f"- Other: {counts['other']}\n")
|
||||
summary.write(f"- Exit code: {os.environ['SEMGREP_EXIT_CODE']}\n")
|
||||
summary.write(f"- Semgrep install: {os.environ['INSTALL_OUTCOME']}\n")
|
||||
summary.write("- Artifact: `semgrep-sarif-${{ github.run_id }}`\n")
|
||||
summary.write(f"- Code scanning upload: {sarif_upload_outcome}\n")
|
||||
PY
|
||||
|
||||
- name: Fail on Semgrep error findings or execution errors
|
||||
if: ${{ always() }}
|
||||
shell: bash
|
||||
env:
|
||||
SEMGREP_EXIT_CODE: ${{ steps.semgrep.outputs.exit_code }}
|
||||
run: |
|
||||
if [ -z "$SEMGREP_EXIT_CODE" ]; then
|
||||
echo "Semgrep exit code is missing."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ "$SEMGREP_EXIT_CODE" -ne 0 ]; then
|
||||
echo "Semgrep execution failed."
|
||||
echo "See the uploaded SARIF artifact for details."
|
||||
exit "$SEMGREP_EXIT_CODE"
|
||||
fi
|
||||
|
||||
python - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
report_path = Path("reports/semgrep.sarif")
|
||||
with report_path.open() as file:
|
||||
sarif = json.load(file)
|
||||
|
||||
error_results = [
|
||||
result
|
||||
for run in sarif.get("runs", [])
|
||||
for result in run.get("results", [])
|
||||
if result.get("level") == "error"
|
||||
]
|
||||
|
||||
if error_results:
|
||||
print(f"Semgrep reported {len(error_results)} error-level finding(s).")
|
||||
print("See the uploaded SARIF artifact for details.")
|
||||
sys.exit(1)
|
||||
PY
|
||||
21
.github/workflows/sync-gitee-mirror.yml
vendored
Normal file
21
.github/workflows/sync-gitee-mirror.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: 🔮 Sync Mirror
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'dream-num/univer'
|
||||
runs-on: minimum-runner
|
||||
steps:
|
||||
- name: Sync Gitee
|
||||
uses: jikkai/sync-gitee@main
|
||||
with:
|
||||
repository: dream-num/univer
|
||||
username: ${{ secrets.GITEE_USERNAME }}
|
||||
password: ${{ secrets.GITEE_PASSWORD }}
|
||||
tags: true
|
||||
branches: main dev
|
||||
25
.github/workflows/validate-pr-title.yml
vendored
Normal file
25
.github/workflows/validate-pr-title.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: 📌 Check PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: validate PR title
|
||||
if: github.repository == 'dream-num/univer' && !github.event.pull_request.draft
|
||||
runs-on: minimum-runner
|
||||
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
subjectPattern: ^(?![A-Z]).+$
|
||||
127
.gitignore
vendored
Normal file
127
.gitignore
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
#ide
|
||||
*.DS_Store
|
||||
.idea
|
||||
.cursor
|
||||
# .vscode
|
||||
|
||||
#npm
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
dist
|
||||
local
|
||||
lib
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Runtime config
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL History
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
#lerna-changelog
|
||||
.changelog
|
||||
|
||||
#editor workspace file
|
||||
.idea/
|
||||
|
||||
.history
|
||||
# examples
|
||||
|
||||
screenshot
|
||||
*.heapsnapshot
|
||||
|
||||
univer-custom-build
|
||||
universheet-custom-build
|
||||
|
||||
temp
|
||||
etc
|
||||
|
||||
# api extractor, api documenter
|
||||
docs/input
|
||||
docs/markdown
|
||||
doc
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
.vercel
|
||||
|
||||
# storybook
|
||||
storybook-static
|
||||
|
||||
# playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright-assets/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
playwright-report.json
|
||||
/artifacts/
|
||||
|
||||
*storybook.log
|
||||
typedoc.json
|
||||
|
||||
# tsx
|
||||
**/tsx-0
|
||||
|
||||
.pnpm-store
|
||||
|
||||
# Generated preset locales
|
||||
/presets/src/locales/
|
||||
/presets/packages/*/src/locales/
|
||||
1
.husky/commit-msg
Executable file
1
.husky/commit-msg
Executable file
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit ${1}
|
||||
1
.husky/pre-commit
Executable file
1
.husky/pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
pnpm lint-staged
|
||||
12
.semgrepignore
Normal file
12
.semgrepignore
Normal file
@@ -0,0 +1,12 @@
|
||||
submodules/
|
||||
node_modules/
|
||||
coverage/
|
||||
dist/
|
||||
lib/
|
||||
.next/
|
||||
.turbo/
|
||||
.vercel/
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
playwright/.cache/
|
||||
14
.vscode/extensions.json
vendored
Normal file
14
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"gruntfuggly.todo-tree",
|
||||
"intellsmi.comment-translate",
|
||||
"kingwl.vscode-vitest-runner",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"tldraw-org.tldraw-vscode",
|
||||
"usernamehw.errorlens",
|
||||
"vitest.explorer",
|
||||
"davidanson.vscode-markdownlint"
|
||||
]
|
||||
}
|
||||
88
.vscode/launch.json
vendored
Normal file
88
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Sheet (Chrome)",
|
||||
"request": "launch",
|
||||
"type": "chrome",
|
||||
"url": "http://localhost:5173/#sheets",
|
||||
"webRoot": "${workspaceFolder}/examples",
|
||||
"presentation": {
|
||||
"group": "0_example",
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Sheet (Edge)",
|
||||
"request": "launch",
|
||||
"type": "msedge",
|
||||
"url": "http://localhost:5173/#sheets",
|
||||
"webRoot": "${workspaceFolder}/examples",
|
||||
"presentation": {
|
||||
"group": "0_example",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Doc (Chrome)",
|
||||
"request": "launch",
|
||||
"type": "chrome",
|
||||
"url": "http://localhost:5173/#docs",
|
||||
"webRoot": "${workspaceFolder}/examples",
|
||||
"presentation": {
|
||||
"group": "0_example",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Doc (Edge)",
|
||||
"request": "launch",
|
||||
"type": "msedge",
|
||||
"url": "http://localhost:5173/#docs",
|
||||
"webRoot": "${workspaceFolder}/examples",
|
||||
"presentation": {
|
||||
"group": "0_example",
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Slide (Chrome)",
|
||||
"request": "launch",
|
||||
"type": "chrome",
|
||||
"url": "http://localhost:5173/#slides",
|
||||
"webRoot": "${workspaceFolder}/examples",
|
||||
"presentation": {
|
||||
"group": "0_example",
|
||||
"order": 6
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Launch Slide (Edge)",
|
||||
"request": "launch",
|
||||
"type": "msedge",
|
||||
"url": "http://localhost:5173/#slides",
|
||||
"webRoot": "${workspaceFolder}/examples",
|
||||
"presentation": {
|
||||
"group": "0_example",
|
||||
"order": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"args": ["--runInBand", "--watchAll=false"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||
"windows": {
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
30
.vscode/redi.code-snippets
vendored
Normal file
30
.vscode/redi.code-snippets
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
// Place your univer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"Redi Injection Identifier": {
|
||||
"prefix": ["@I"],
|
||||
"body": ["@I${1:identifier} private readonly _${2:name}: I${1:identifier},"],
|
||||
"description": "Inject an identifier with Redi.",
|
||||
"scope": "typescript,typescriptreact"
|
||||
},
|
||||
"Redi Injection": {
|
||||
"prefix": ["@In"],
|
||||
"body": ["@Inject(${1:identifier}) private readonly _${2:name}: ${1:identifier},"],
|
||||
"description": "Inject a class item with Redi.",
|
||||
"scope": "typescript,typescriptreact"
|
||||
}
|
||||
}
|
||||
63
.vscode/settings.json
vendored
Normal file
63
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
|
||||
"editor.rulers": [120],
|
||||
|
||||
"javascript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
],
|
||||
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
|
||||
"vitest.maximumConfigs": 80
|
||||
}
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [huwenzhao@univer.ai](mailto:huwenzhao@univer.ai). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version].
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
223
CONTRIBUTING.md
Normal file
223
CONTRIBUTING.md
Normal file
@@ -0,0 +1,223 @@
|
||||
<!-- https://univer.ai/guides/sheet/contributing/ -->
|
||||
|
||||
# Univer Contributing Guide
|
||||
|
||||
Welcome, and thank you for your interest in contributing to Univer!
|
||||
|
||||
Please read our [Code of Conduct](./CODE_OF_CONDUCT.md) before you join in the Univer community.
|
||||
|
||||
## Asking Questions
|
||||
|
||||
If you encounter any issues while using Univer, you can seek help in the following communities:
|
||||
|
||||
- [Discord community](https://discord.gg/z3NKNT6D2f)
|
||||
- [GitHub Discussions](https://github.com/dream-num/univer/discussions)
|
||||
- Join the Univer Chinese community (please scan the QR code [here](https://univer.ai/contact))
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you encounter any issues or have any suggestions while using Univer, please feel free to raise them in the Github Issues. We will respond promptly.
|
||||
|
||||
To better assist you, we recommend:
|
||||
|
||||
- Before submitting an issue, please search to see if someone has already raised a similar question.
|
||||
- We provide an [issue template](https://github.com/dream-num/univer/issues/new/choose) and encourage you to fill it out with sufficient information, which helps us quickly identify the problem.
|
||||
- Please try to describe the issue in English, as it enables more people to participate in the discussion. We will also make an effort to reply in English to benefit a wider audience.
|
||||
|
||||
> If you're new to submitting issues, we recommend to read [How To Ask Questions The Smart Way](http://www.catb.org/~esr/faqs/smart-questions.html) and [How to Report Bugs Effectively](https://www.chiark.greenend.org.uk/~sgtatham/bugs.html) before posting. Well-written bug reports help us and help you!
|
||||
|
||||
## Contributing Code
|
||||
|
||||
### Install dependencies
|
||||
|
||||
The development of Univer requires Node.js >= 22.18 and pnpm >= 11. Please make sure you have the correct versions installed.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/dream-num/univer
|
||||
cd univer
|
||||
|
||||
# install package manager pnpm
|
||||
npm i -g pnpm
|
||||
|
||||
# install dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Start dev server
|
||||
|
||||
```shell
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
Please refer to [Architecture](https://docs.univer.ai/guides/recipes/architecture/univer), and also [ISOMORPHIC.md](./docs/ISOMORPHIC.md) for more guidance on how to set up plugins.
|
||||
|
||||
For public API compatibility expectations, experimental APIs, internal APIs, and deprecation rules, read [API_STABILITY.md](./docs/API_STABILITY.md).
|
||||
|
||||
### Source code organization
|
||||
|
||||
The structure of the repository is as follows:
|
||||
|
||||
```txt
|
||||
.
|
||||
├── common/ shared configuration, mock data, Storybook, and utilities
|
||||
├── docs/ documentation
|
||||
├── examples/ all-in-one Vite workbench for browser development
|
||||
├── packages/ Univer core and plugins
|
||||
├── presets/ curated plugin collections
|
||||
└── tests/ additional integration test projects
|
||||
```
|
||||
|
||||
The file structure of a plugin should be organized as follows:
|
||||
|
||||
```txt
|
||||
|- common/
|
||||
|- models/
|
||||
|- services/
|
||||
|- commands/
|
||||
|- commands/
|
||||
|- mutations/
|
||||
|- operations/
|
||||
|- controllers/
|
||||
|- views/
|
||||
|- components/
|
||||
|- parts/
|
||||
|- plugin.ts
|
||||
|- index.ts
|
||||
```
|
||||
|
||||
There are some limits on what paths could a file import from.
|
||||
|
||||
- common cannot import file in other folders
|
||||
- models can only import files from common
|
||||
- services can only import files from models and common
|
||||
- commands can only import files from common, models and services
|
||||
|
||||
During the refactoring process, it is recommended to remove legacy folders such as `Enum`, `Interface`, `Basics`, and `Shared`.
|
||||
|
||||
Avoid creating barrel imports (index.ts) unless it is the main root index.ts file of a plugin.
|
||||
|
||||
#### Desktop & Mobile
|
||||
|
||||
We added experimental support for mobile platforms since June 2024. After that, all UI plugins should split ui related code by their running platforms:
|
||||
|
||||
```txt
|
||||
|- controllers/
|
||||
|- render-controllers/
|
||||
|- common/
|
||||
|- desktop/
|
||||
|- mobile/
|
||||
|- views/
|
||||
|- components/
|
||||
|- common/
|
||||
|- desktop/
|
||||
|- mobile/
|
||||
|- parts/
|
||||
|- common/
|
||||
|- desktop/
|
||||
|- mobile/
|
||||
```
|
||||
|
||||
### Connecting context
|
||||
|
||||
To effectively contribute as a member of a software engineering team (and community as well!), it is crucial to establish strong contextual connections. Providing links to relevant resources can greatly assist others in understanding the context. Consider the following practices:
|
||||
|
||||
- Include the link to the corresponding issue in your commit message.
|
||||
- Leave comments with links to the relevant context within the code.
|
||||
- Whenever possible, document important information directly within the codebase itself.
|
||||
|
||||
By adopting these practices, you can enhance collaboration and facilitate a better understanding of the codebase within our community.
|
||||
|
||||
### Naming convention
|
||||
|
||||
Please refer to [Univer Naming Convention](./docs/NAMING_CONVENTION.md).
|
||||
|
||||
### Submitting pull requests
|
||||
|
||||
Before merging a pull request, please make sure the following requirements are met:
|
||||
|
||||
- All tests pass. ESLint, including its formatting checks, passes.
|
||||
- Test coverage is not decreased.
|
||||
|
||||
We provide preview deployments for pull requests. You can view the preview deployment by clicking the "Preview" link in the "View Deployment" section.
|
||||
|
||||
### Storybook
|
||||
|
||||
We use Storybook to develop and test components in isolation. It is a great tool to develop UI components in isolation, which can improve component reuse, testability, and development efficiency. You can use the following command to start Storybook:
|
||||
|
||||
```shell
|
||||
pnpm storybook:dev
|
||||
```
|
||||
|
||||
When a new pull request is submitted, a Storybook deployment will be automatically generated. You can view the Storybook deployment by clicking the "Preview" link in the "View Storybook" section.
|
||||
|
||||
### Debugging
|
||||
|
||||
We provided some Visual Studio Code (vscode) tasks to help you debug in vscode.
|
||||
|
||||
After stating dev server, you can launch Edge or Chrome instance and debug in vscode, and you can debug directly in vscode!
|
||||
|
||||

|
||||
|
||||
### Unit test
|
||||
|
||||
To ensure the quality of the code and move with confidence, we require that all code must be covered by unit tests. We use vitest as the unit test framework. You can use the following command to run unit tests:
|
||||
|
||||
```shell
|
||||
pnpm test
|
||||
```
|
||||
|
||||
Also, with the help of vscode and its rich ecosystem, you could directly debug unit tests in vscode. Please install the extension we recommend, and you will see the debug button in the side bar. The `packages/*` glob in `vitest.workspace.ts` automatically includes new plugins created under `packages/`.
|
||||
|
||||

|
||||
|
||||
### Browser workbench
|
||||
|
||||
`pnpm dev` starts one Vite workbench with lazy-loaded Sheets, Docs, and Slides routes. Use the navigation bar or `#sheets`, `#docs`, and `#slides` URLs to switch products; only one Univer instance stays mounted. The Settings menu provides every built-in language, LTR/RTL direction, region, theme, appearance, ribbon, UI chrome, and zoom controls. Preferences are stored in localStorage and applied to the active instance through runtime APIs without remounting it. Development locale data is loaded on demand so unused product and language graphs do not remain in Vite's memory. End-to-end suites should consume built packages outside this repository instead of coupling tests to source examples.
|
||||
|
||||
### Build Preview
|
||||
|
||||
Build the production artifacts to check for differences between the source and compiled output:
|
||||
|
||||
```shell
|
||||
pnpm build
|
||||
pnpm dev:umd
|
||||
```
|
||||
|
||||
### Clean code
|
||||
|
||||
> Programs are meant to be ready by humans and only incidentally for computers to execute. - Harold Abelson
|
||||
|
||||
- Do not expose properties or methods those are not necessary to be exposed.
|
||||
- Group related methods or properties together. Do not always use blank lines to separate them.
|
||||
- Keep your concepts consistent by naming your variables consistently.
|
||||
|
||||
References:
|
||||
|
||||
- [Make your code readable](https://www.notonlycode.org/make-your-code-readable/)
|
||||
|
||||
### New Package
|
||||
|
||||
Create a new package with our CLI tool:
|
||||
|
||||
```shell
|
||||
# pnpm
|
||||
pnpm create @univerjs/cli init <project-name>
|
||||
|
||||
# npm
|
||||
npm create @univerjs/cli init <project-name>
|
||||
```
|
||||
|
||||
### How to Contribute to Facade API
|
||||
|
||||
Please refer to [How to Contribute to Facade API](./docs/CONTRIBUTING-FACADE.md).
|
||||
|
||||
### Deprecate API
|
||||
|
||||
If you are going to deprecate an API, follow the [API Stability Policy](./docs/API_STABILITY.md) and the steps below:
|
||||
|
||||
1. Mark the API as deprecated in the JSDoc, and use `{@link}` to refer to the new API.
|
||||
2. In the implementation, call `deprecate` of `ILogService` to log a deprecation message.
|
||||
3. Document the migration path in the relevant guide, README, changelog, or release notes when the API is broadly used.
|
||||
4. Deprecated stable APIs should remain available until the next major release unless the API is unsafe, broken, or impossible to preserve. In pre-1.0 releases, removals may happen in a minor release, but they should be documented as breaking changes.
|
||||
46
DREAMNUM.md
Normal file
46
DREAMNUM.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# dream-num/univer
|
||||
|
||||
> The open-source, isomorphic office SDK for building embeddable spreadsheet, document, and presentation experiences in browsers and Node.js.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
### Owns
|
||||
|
||||
- The open-source Univer runtime, plugin system, command and service infrastructure, Facade API, formula engine, and Canvas rendering engine. See the [repository overview](README.md#-what-is-univer).
|
||||
- Open-source models, editing capabilities, and UI plugins for Sheets, Docs, and Slides, including shared drawing, commenting, validation, formatting, and localization infrastructure. See the [open-source capability boundary](README.md#-open-source-and-pro).
|
||||
- Browser, Node.js, and Web Worker integration paths, plus React-based UI and Vue 3 and Web Component adapters. See the [compatibility and integration overview](README.md#-compatibility).
|
||||
- First-party open-source package presets and the examples and test projects used to exercise the SDK. See the [workspace definition](pnpm-workspace.yaml) and [repository guide](README.md#-repository-guide).
|
||||
- Public API compatibility rules for stable, experimental, internal, and deprecated interfaces. See the [API Stability Policy](docs/API_STABILITY.md).
|
||||
|
||||
The repository does not own Univer Pro's commercial collaboration, import/export, server, or enterprise capabilities; those are a separate extension layer described in the [open-source and Pro boundary](README.md#-open-source-and-pro).
|
||||
|
||||
## Provides
|
||||
|
||||
- `@univerjs/core`, `@univerjs/engine-formula`, and `@univerjs/engine-render` — Core runtime and data models, Facade APIs, formula calculation, and shared rendering. Contract: [package manifests](packages/core/package.json) and [API reference](https://docs.univer.ai/reference/classes/univer).
|
||||
- `@univerjs/sheets`, `@univerjs/docs`, `@univerjs/slides`, and their UI and feature packages — Composable office models, plugins, and editing interfaces. Contract: [package directory](packages) and [API stability rules](docs/API_STABILITY.md).
|
||||
- `@univerjs/presets` and `@univerjs/preset-*` — Curated plugin collections for browser and Node.js integrations. Contract: [preset packages](presets/packages) and [preset-mode guide](README.md#-quick-start).
|
||||
- `@univerjs/ui-adapter-vue3` and `@univerjs/ui-adapter-web-component` — Framework adapters for integrating Univer UI services. Contract: [Vue 3 adapter](packages/ui-adapter-vue3/README.md) and [Web Component adapter](packages/ui-adapter-web-component/README.md).
|
||||
- `sync-univer` repository-dispatch event — Notifies the separate Univer Pro repository after changes reach `dev`. Contract: [dispatch workflow](.github/workflows/dispatch-sync-univer-pro.yml).
|
||||
|
||||
## Depends on
|
||||
|
||||
- [`dream-num/univer-icons`](https://github.com/dream-num/univer-icons) — Owns the React icon components and SVG assets consumed by Univer UI packages. Contract: [`@univerjs/ui` manifest](packages/ui/package.json) and [SVG asset dependency](packages/sheets-conditional-formatting/package.json).
|
||||
- [`jikkai/verso`](https://github.com/jikkai/verso) — Owns the workspace release CLI used to version and tag Univer packages. Contract: [root package manifest](package.json).
|
||||
|
||||
## Authoritative sources
|
||||
|
||||
- **Product documentation:** [docs.univer.ai](https://docs.univer.ai).
|
||||
- **API reference:** [Univer Facade API](https://docs.univer.ai/reference/classes/univer).
|
||||
- **Development guidance:** [README.md](README.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
- **Package contracts:** [`packages/`](packages), [`presets/`](presets), and [API Stability Policy](docs/API_STABILITY.md).
|
||||
- **Package release source:** [npm release workflow](.github/workflows/release-npm.yml).
|
||||
- **Security policy:** [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Update contract
|
||||
|
||||
Update this file in the same change when any of these facts change:
|
||||
|
||||
- repository responsibilities or boundaries;
|
||||
- outgoing cross-repository dependencies;
|
||||
- public packages, APIs, protocols, events, images, or data contracts;
|
||||
- deployment source, runbook, or data classification.
|
||||
176
LICENSE
Normal file
176
LICENSE
Normal file
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
372
README.md
Normal file
372
README.md
Normal file
@@ -0,0 +1,372 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="./docs/img/banner.png" alt="Univer SDK" width="800" />
|
||||
|
||||
**A full-stack, isomorphic office SDK for building spreadsheets, documents, and presentations.**
|
||||
|
||||
Build embeddable productivity experiences with a plugin architecture, Canvas-based rendering,
|
||||
a formula engine, and one Facade API that works in the browser and on Node.js.
|
||||
|
||||
English | [简体中文](./docs/readme/zh-CN.md) | [繁體中文](./docs/readme/zh-TW.md) | [日本語](./docs/readme/ja-JP.md) | [한국어](./docs/readme/ko-KR.md) | [Español](./docs/readme/es-ES.md)
|
||||
|
||||
[📖 Documentation](https://docs.univer.ai) | [✨ Showcase](https://docs.univer.ai/showcase) | [📘 API Reference](https://docs.univer.ai/reference/classes/univer) | [📝 Blog](https://docs.univer.ai/blog)
|
||||
|
||||
[](https://github.com/dream-num/univer/releases)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/dream-num/univer/actions/workflows/build-packages.yml)
|
||||
[](https://www.codefactor.io/repository/github/dream-num/univer/overview/dev)
|
||||
[](https://codecov.io/gh/dream-num/univer)
|
||||
|
||||
[](https://github.com/dream-num/univer/stargazers)
|
||||
[](https://github.com/dream-num/univer/graphs/contributors)
|
||||
[](https://github.com/dream-num/univer/issues)
|
||||
[](https://github.com/dream-num/univer/commits/dev/)
|
||||
|
||||
[](https://discord.gg/z3NKNT6D2f)
|
||||
[](https://twitter.com/univerhq)
|
||||
[](https://opencollective.com/univer)
|
||||
|
||||
[](https://trendshift.io/repositories/4376)
|
||||
|
||||
</div>
|
||||
|
||||
## ✨ What is Univer?
|
||||
|
||||
Univer is an open-source SDK for creating office applications inside your own product. It gives you the building blocks for spreadsheet, document, and presentation experiences without forcing you into a hosted app or a fixed UI.
|
||||
|
||||
Use Univer when you need to:
|
||||
|
||||
- Embed spreadsheet or document editing into a SaaS product, internal tool, BI workflow, or AI application.
|
||||
- Run workbook/document processing on the server with the same architecture used in the browser.
|
||||
- Compose only the features you need through plugins or start quickly with presets.
|
||||
- Extend behavior through custom plugins, commands, services, UI components, and Facade APIs.
|
||||
|
||||
Univer is not a spreadsheet file viewer only. It is a framework for building your own productivity surface.
|
||||
|
||||
## 🌟 Highlights
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="33%">
|
||||
⚡<br />
|
||||
<strong>Built for large surfaces</strong><br />
|
||||
<sub>Canvas-based rendering and a dedicated formula engine keep complex workbooks responsive.</sub>
|
||||
</td>
|
||||
<td align="center" width="33%">
|
||||
🧩<br />
|
||||
<strong>Plugin-shaped by default</strong><br />
|
||||
<sub>Compose, replace, lazy-load, or extend capabilities without taking the whole stack.</sub>
|
||||
</td>
|
||||
<td align="center" width="33%">
|
||||
🤖<br />
|
||||
<strong>Headless for AI infrastructure</strong><br />
|
||||
<sub>Run workbook and document logic in Node.js to power agents, automation, and server-side workflows.</sub>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="33%">
|
||||
🛠️<br />
|
||||
<strong>Product-ready SDK</strong><br />
|
||||
<sub>Framework adapters, Facade APIs, presets, and headless runtime fit real integration paths.</sub>
|
||||
</td>
|
||||
<td align="center" width="33%">
|
||||
🌗<br />
|
||||
<strong>Dark-mode ready</strong><br />
|
||||
<sub>UI components and the rendering engine both adapt to light and dark themes.</sub>
|
||||
</td>
|
||||
<td align="center" width="33%">
|
||||
🔌<br />
|
||||
<strong>Unified Facade API</strong><br />
|
||||
<sub>One consistent API surface for workbooks, ranges, formulas, and documents across browser and Node.js.</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 🚀 Why Univer?
|
||||
|
||||
- **Isomorphic by design**: run UI apps in browsers and headless processing in Node.js.
|
||||
- **Plugin-first architecture**: every capability is delivered as a composable plugin, so features can be added, removed, replaced, or lazy-loaded.
|
||||
- **Preset mode for fast integration**: use the curated plugin collections in [`presets/`](./presets) when you want a working app quickly.
|
||||
- **Plugin mode for full control**: manually compose packages when you need custom loading, smaller bundles, or deep integration.
|
||||
- **Facade API**: work with workbooks, worksheets, ranges, documents, formulas, commands, and events through a higher-level API.
|
||||
- **Canvas rendering engine**: support large editable document surfaces with a rendering layer shared across document types.
|
||||
- **Extensible UI**: integrate with React, Vue, Web Components, and framework-specific application shells.
|
||||
|
||||
## ⚡ Quick Start
|
||||
|
||||
Use **Plugin Mode** for complete product coverage and precise composition. For supported Sheets, Docs, and Node profiles, **Preset Mode** provides a shorter curated setup.
|
||||
|
||||
<details open>
|
||||
<summary><strong>Plugin Mode</strong></summary>
|
||||
|
||||
Plugin Mode gives you lower-level control over packages, style imports, locale merging, Facade API registration, and plugin configuration.
|
||||
|
||||
```bash
|
||||
pnpm add @univerjs/core @univerjs/design @univerjs/docs @univerjs/docs-ui @univerjs/engine-formula @univerjs/engine-render @univerjs/sheets @univerjs/sheets-formula @univerjs/sheets-formula-ui @univerjs/sheets-numfmt @univerjs/sheets-numfmt-ui @univerjs/sheets-ui @univerjs/ui
|
||||
```
|
||||
|
||||
```ts
|
||||
import { LocaleType, mergeLocales, Univer } from '@univerjs/core'
|
||||
import { FUniver } from '@univerjs/core/facade'
|
||||
import DesignEnUS from '@univerjs/design/locale/en-US'
|
||||
import { UniverDocsPlugin } from '@univerjs/docs'
|
||||
import { UniverDocsUIPlugin } from '@univerjs/docs-ui'
|
||||
import DocsUIEnUS from '@univerjs/docs-ui/locale/en-US'
|
||||
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'
|
||||
import { UniverRenderEnginePlugin } from '@univerjs/engine-render'
|
||||
import { UniverSheetsPlugin } from '@univerjs/sheets'
|
||||
import SheetsEnUS from '@univerjs/sheets/locale/en-US'
|
||||
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'
|
||||
import SheetsFormulaEnUS from '@univerjs/sheets-formula/locale/en-US'
|
||||
import { UniverSheetsFormulaUIPlugin } from '@univerjs/sheets-formula-ui'
|
||||
import SheetsFormulaUIEnUS from '@univerjs/sheets-formula-ui/locale/en-US'
|
||||
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'
|
||||
import { UniverSheetsNumfmtUIPlugin } from '@univerjs/sheets-numfmt-ui'
|
||||
import SheetsNumfmtUIEnUS from '@univerjs/sheets-numfmt-ui/locale/en-US'
|
||||
import { UniverSheetsUIPlugin } from '@univerjs/sheets-ui'
|
||||
import SheetsUIEnUS from '@univerjs/sheets-ui/locale/en-US'
|
||||
import { UniverUIPlugin } from '@univerjs/ui'
|
||||
import UIEnUS from '@univerjs/ui/locale/en-US'
|
||||
|
||||
import '@univerjs/design/lib/index.css'
|
||||
import '@univerjs/ui/lib/index.css'
|
||||
import '@univerjs/docs-ui/lib/index.css'
|
||||
import '@univerjs/sheets-ui/lib/index.css'
|
||||
import '@univerjs/sheets-formula-ui/lib/index.css'
|
||||
import '@univerjs/sheets-numfmt-ui/lib/index.css'
|
||||
|
||||
import '@univerjs/engine-formula/facade'
|
||||
import '@univerjs/ui/facade'
|
||||
import '@univerjs/sheets/facade'
|
||||
import '@univerjs/sheets-ui/facade'
|
||||
import '@univerjs/sheets-formula/facade'
|
||||
import '@univerjs/sheets-numfmt/facade'
|
||||
|
||||
const univer = new Univer({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: mergeLocales(
|
||||
DesignEnUS,
|
||||
UIEnUS,
|
||||
DocsUIEnUS,
|
||||
SheetsEnUS,
|
||||
SheetsUIEnUS,
|
||||
SheetsFormulaEnUS,
|
||||
SheetsFormulaUIEnUS,
|
||||
SheetsNumfmtUIEnUS,
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
univer.registerPlugin(UniverRenderEnginePlugin)
|
||||
univer.registerPlugin(UniverFormulaEnginePlugin)
|
||||
univer.registerPlugin(UniverUIPlugin, { container: 'app' })
|
||||
univer.registerPlugin(UniverDocsPlugin)
|
||||
univer.registerPlugin(UniverDocsUIPlugin)
|
||||
univer.registerPlugin(UniverSheetsPlugin)
|
||||
univer.registerPlugin(UniverSheetsUIPlugin)
|
||||
univer.registerPlugin(UniverSheetsFormulaPlugin)
|
||||
univer.registerPlugin(UniverSheetsFormulaUIPlugin)
|
||||
univer.registerPlugin(UniverSheetsNumfmtPlugin)
|
||||
univer.registerPlugin(UniverSheetsNumfmtUIPlugin)
|
||||
|
||||
const univerAPI = FUniver.newAPI(univer)
|
||||
univerAPI.createWorkbook({})
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Preset Mode</strong></summary>
|
||||
|
||||
Presets are curated collections of Univer plugins that include the required Facade API registrations and styles.
|
||||
|
||||
```bash
|
||||
pnpm add @univerjs/presets @univerjs/preset-sheets-core
|
||||
```
|
||||
|
||||
```ts
|
||||
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'
|
||||
import UniverPresetSheetsCoreEnUS from '@univerjs/preset-sheets-core/locales/en-US'
|
||||
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets'
|
||||
|
||||
import '@univerjs/preset-sheets-core/lib/index.css'
|
||||
|
||||
const { univerAPI } = createUniver({
|
||||
locale: LocaleType.EN_US,
|
||||
locales: {
|
||||
[LocaleType.EN_US]: mergeLocales(UniverPresetSheetsCoreEnUS),
|
||||
},
|
||||
presets: [
|
||||
UniverSheetsCorePreset({
|
||||
container: 'app',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
univerAPI.createWorkbook({})
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Your page needs a container:
|
||||
|
||||
```html
|
||||
<div id="app" style="height: 100vh"></div>
|
||||
```
|
||||
|
||||
Learn more in the [Installation & Basic Usage guide](https://docs.univer.ai/guides/sheets/getting-started/installation), the [`createUniver` reference](https://docs.univer.ai/reference/methods/create-univer), and the [Facade API reference](https://docs.univer.ai/reference/classes/univer).
|
||||
|
||||
## 🧩 Preset Mode vs Plugin Mode
|
||||
|
||||
| Choose | When to use it | Start here |
|
||||
| --- | --- | --- |
|
||||
| **Plugin Mode** | You need strict control over packages, configured dependencies, lazy loading, or custom runtime composition. | This repository's [`examples/`](./examples) and [architecture guide](https://docs.univer.ai/guides/recipes/architecture/univer) |
|
||||
| **Preset Mode** | You want a working Sheets, Docs, or Node setup with minimal configuration. | This repository's [`presets/`](./presets) and the [getting started guide](https://docs.univer.ai/guides/sheets/getting-started/installation) |
|
||||
| **Headless Mode** | You need server-side workbook/document processing, formula calculation, or automation without UI. | [Headless Univer](https://docs.univer.ai/guides/sheets/getting-started/node) |
|
||||
|
||||
Keep the `@univerjs/*` SDK packages in the same coordinated Univer release line on the same version. Independently released packages, including `@univerjs/icons` and `@univerjs/icons-svg`, use the compatible versions declared by package manifests instead of the SDK version. If you use Univer Pro packages, keep `@univerjs-pro/*` aligned with the matching coordinated release line.
|
||||
|
||||
For API compatibility expectations, experimental APIs, internal APIs, and deprecation rules, see the [API Stability Policy](./docs/API_STABILITY.md).
|
||||
|
||||
## 🧭 Compatibility
|
||||
|
||||
- **Browser runtime**: Univer is compiled with a Chrome 88 target and aims to work on Edge `>=88`, Firefox `>=90`, Chrome `>=88`, Safari `>=14.1`, and Electron `>=12`.
|
||||
- **Polyfills**: Univer relies on `Intl.Segmenter`. Add a polyfill such as `@formatjs/intl-segmenter` if your target browser or runtime does not provide it.
|
||||
- **Build tools**: We recommend Vite, esbuild, or Webpack 5. If your build tool does not support the `exports` field in `package.json` (common in Webpack 4), you may need extra path mapping.
|
||||
- **React**: Univer's view layer is built on React 18, supports React 18 and 19, and provides minimal compatibility support for React 16.9+ and 17.
|
||||
- **Node.js runtime**: Headless Univer supports Node.js `>=18.17.0`. Developing this monorepo requires Node.js `>=22.18`.
|
||||
|
||||
## 🛠️ What You Can Build
|
||||
|
||||
| Area | Open-source capabilities | Univer Pro extensions |
|
||||
| --- | --- | --- |
|
||||
| **Sheets** | Workbooks, worksheets, ranges, selection, formulas, number formatting, filtering, sorting, data validation, conditional formatting, hyperlinks, comments, find and replace, notes, tables, drawing integration, and extensible UI plugins. | Real-time collaboration, edit history, import/export, printing, charts, pivot tables, sparklines, outlines, shapes, in-cell graphics, data connectors, server-side calculation, and performance-enhanced formula features. |
|
||||
| **Docs** | Rich document model, editing UI, lists, hyperlinks, drawing integration, comments, quick insert, and shared document architecture. | Collaboration, import/export, printing, enhanced tables and lists, columns, callouts, code blocks, quotes, shapes, and remote comment resources. |
|
||||
| **Slides** | Presentation data model and UI packages under active development. | Pro slide model and UI, slide import/export, chart and table model/UI plugins, and shared shape-editing infrastructure. |
|
||||
| **Bases** | Build custom structured-data experiences on top of Univer's plugin, command, and model architecture. | Base database model, commands, formula integration, workbench UI, field editors, and render-engine views. |
|
||||
| **Runtime** | Browser apps, Node.js headless usage, Web Worker/RPC patterns, multi-instance usage, and server-oriented automation. | Collaboration client/server packages, Node.js collaboration client, Pro server services, SSR, computing delegation, server-side calculation, and changeset replay tooling. |
|
||||
| **Integrations** | React, Vue, Web Components, framework templates, theming, localization, and custom plugins. | Pro presets and enterprise deployment packages. |
|
||||
|
||||
Sheets are the most mature product surface today. Docs and Slides share Univer's architecture and continue to evolve in the same SDK.
|
||||
|
||||
## 🔓 Open Source and Pro
|
||||
|
||||
This repository contains Univer's open-source core and first-party OSS plugins. Univer Pro is developed separately as a commercial extension layer for advanced product surfaces, collaboration, server features, and enterprise integrations.
|
||||
|
||||
| Category | Open source | Univer Pro / commercial |
|
||||
| --- | --- | --- |
|
||||
| **Foundation** | Core SDK, plugin system, rendering engine, formula engine, Facade API, themes, i18n, and framework adapters. | Pro presets and enterprise deployment packages. |
|
||||
| **Sheets** | Core spreadsheet editing, formulas, number formatting, filter/sort, data validation, conditional formatting, notes, tables, hyperlinks, comments, drawing, find and replace. | Collaboration, edit history, import/export, print, charts, pivot tables, sparklines, outlines, shapes, in-cell graphics, data connectors, range preprocessing, and enhanced formula engine features. |
|
||||
| **Docs** | Document model and editor UI, lists, hyperlinks, comments, quick insert, and drawing integration. | Collaboration, import/export, print, enhanced tables/lists, columns, callouts, code blocks, quotes, shapes, and remote thread-comment resources. |
|
||||
| **Slides** | OSS presentation model and UI packages. | Pro slide model/UI packages, slide import/export, charts, tables, and reusable shape editor UI. |
|
||||
| **Bases** | Extensible plugin architecture for custom data-centric products. | Base database core model, commands, mutations, formula integration, workbench UI, field editors, and render-engine integration. |
|
||||
| **Server and runtime** | Node.js headless runtime, RPC/Web Worker patterns, and server-oriented automation primitives. | Collaboration server, Node.js collaboration client, SSR services, computing delegation, server-side calculation, and collaboration changeset replay tooling. |
|
||||
|
||||
Pro features are documented in the [Univer Pro guide](https://docs.univer.ai/guides/pro). They are intentionally separated here so the OSS package surface is clear.
|
||||
|
||||
Boundary principles:
|
||||
|
||||
- OSS packages in this repository are intended to be useful on their own under the Apache-2.0 license. Univer Pro is optional and is not required to use the public OSS SDK APIs.
|
||||
- Bugs, regressions, and security issues in OSS packages should be reported and fixed in the OSS repository, even when a related Pro feature exists.
|
||||
- OSS documentation should not imply that Pro-only capabilities are available in public `@univerjs/*` packages. Pro-only APIs, packages, and deployment paths should be named explicitly.
|
||||
- When an OSS feature has a Pro enhancement, the OSS behavior should remain documented independently so users can evaluate the open-source surface without reading commercial docs first.
|
||||
|
||||
## 🌐 Ecosystem
|
||||
|
||||
- **Core SDK**: [`dream-num/univer`](https://github.com/dream-num/univer), this monorepo.
|
||||
- **Presets**: this repository's [`presets/`](./presets), curated plugin collections for browser and Node.js apps.
|
||||
- **AI agent skills**: [`dream-num/univer-sdk-skills`](https://github.com/dream-num/univer-sdk-skills), reusable instructions for AI agents working with Univer integration, Pro features, plugin development, and Node backends. See the [AI Skills guide](https://docs.univer.ai/guides/skills).
|
||||
- **Documentation**: [docs.univer.ai](https://docs.univer.ai), including Sheets, Docs, Slides, recipes, and Pro guides.
|
||||
- **API Reference**: [docs.univer.ai/reference](https://docs.univer.ai/reference/classes/univer), the Facade API and generated API reference.
|
||||
- **Examples and showcase**: [Univer Showcase](https://docs.univer.ai/showcase) and this repository's [`examples/`](./examples).
|
||||
- **AI-native spreadsheets**: [`dream-num/univer-mcp`](https://github.com/dream-num/univer-mcp), Univer Platform / MCP integration for driving Univer Sheets with natural language.
|
||||
|
||||
## 📦 Repository Guide
|
||||
|
||||
```text
|
||||
.
|
||||
├── packages/ Core packages, engines, document types, UI plugins, and feature plugins
|
||||
├── examples/ All-in-one Vite workbench used for local browser development
|
||||
├── common/ Shared internal tooling, storybook, and utilities
|
||||
├── tests/ Additional integration test projects
|
||||
└── docs/ Architecture notes, images, and repository-local documentation
|
||||
```
|
||||
|
||||
Package-level READMEs live beside each package under [`packages/`](./packages).
|
||||
|
||||
## 🧑💻 Development
|
||||
|
||||
Requirements:
|
||||
|
||||
- Node.js `>=22.18`
|
||||
- pnpm `>=11`
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dream-num/univer.git
|
||||
cd univer
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Useful commands:
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `pnpm dev` | Start the all-in-one Sheets, Docs, and Slides workbench with Vite 8 Bundled Dev and HMR. |
|
||||
| `pnpm build` | Build workspace packages, excluding internal common packages. |
|
||||
| `pnpm test` | Run unit tests through Turbo. |
|
||||
| `pnpm typecheck` | Run TypeScript checks through Turbo. |
|
||||
| `pnpm lint` | Run ESLint. |
|
||||
| `pnpm storybook:dev` | Start Storybook for UI component development. |
|
||||
|
||||
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a pull request.
|
||||
|
||||
## 📝 Contributor Notes
|
||||
|
||||
Repository-local notes worth reading before deeper changes:
|
||||
|
||||
- [Building Isomorphic Univer](./docs/ISOMORPHIC.md): how to split browser, Node.js, UI, and shared plugin logic.
|
||||
- [API Stability Policy](./docs/API_STABILITY.md): stable, experimental, internal, deprecated, and breaking-change expectations.
|
||||
- [Contributing to Facade API](./docs/CONTRIBUTING-FACADE.md): API design expectations for `FUniver`, `FWorkbook`, `FRange`, and related Facade classes.
|
||||
- [Naming Convention](./docs/NAMING_CONVENTION.md): file, folder, interface, plugin, command, and dependency injection token conventions.
|
||||
- [Fixing Memory Leaks](./docs/FIX_MEMORY_LEAK.md): common leak patterns and debugging workflow for Univer instances.
|
||||
- [Architecture TLDRs](./docs/tldr): concise notes for formula engine, web worker, permission, selection, and ref-range behavior.
|
||||
|
||||
## 💬 Community
|
||||
|
||||
- Ask questions in [GitHub Discussions](https://github.com/dream-num/univer/discussions).
|
||||
- Chat with the community on [Discord](https://discord.gg/z3NKNT6D2f).
|
||||
- Follow [Twitter / X](https://twitter.com/univerhq) and [YouTube](https://www.youtube.com/@dreamNum).
|
||||
|
||||
Please read the [Code of Conduct](./CODE_OF_CONDUCT.md) before participating.
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
If you believe you have found a security issue, please follow the [Security Policy](./SECURITY.md).
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
Univer is supported by the community and sponsors. You can support the project through [Open Collective](https://opencollective.com/univer).
|
||||
|
||||
<a href="https://opencollective.com/univer/sponsor/0/website"><img src="https://opencollective.com/univer/sponsor/0/avatar.svg" alt="Sponsor 0" /></a>
|
||||
<a href="https://opencollective.com/univer/sponsor/1/website"><img src="https://opencollective.com/univer/sponsor/1/avatar.svg" alt="Sponsor 1" /></a>
|
||||
<a href="https://opencollective.com/univer/sponsor/2/website"><img src="https://opencollective.com/univer/sponsor/2/avatar.svg" alt="Sponsor 2" /></a>
|
||||
<a href="https://opencollective.com/univer/sponsor/3/website"><img src="https://opencollective.com/univer/sponsor/3/avatar.svg" alt="Sponsor 3" /></a>
|
||||
<a href="https://opencollective.com/univer/sponsor/4/website"><img src="https://opencollective.com/univer/sponsor/4/avatar.svg" alt="Sponsor 4" /></a>
|
||||
<a href="https://opencollective.com/univer/sponsor/5/website"><img src="https://opencollective.com/univer/sponsor/5/avatar.svg" alt="Sponsor 5" /></a>
|
||||
<a href="https://opencollective.com/univer/sponsor/6/website"><img src="https://opencollective.com/univer/sponsor/6/avatar.svg" alt="Sponsor 6" /></a>
|
||||
|
||||
<a href="https://opencollective.com/univer/backer/0/website"><img src="https://opencollective.com/univer/backer/0/avatar.svg" alt="Backer 0" /></a>
|
||||
<a href="https://opencollective.com/univer/backer/1/website"><img src="https://opencollective.com/univer/backer/1/avatar.svg" alt="Backer 1" /></a>
|
||||
<a href="https://opencollective.com/univer/backer/2/website"><img src="https://opencollective.com/univer/backer/2/avatar.svg" alt="Backer 2" /></a>
|
||||
<a href="https://opencollective.com/univer/backer/3/website"><img src="https://opencollective.com/univer/backer/3/avatar.svg" alt="Backer 3" /></a>
|
||||
<a href="https://opencollective.com/univer/backer/4/website"><img src="https://opencollective.com/univer/backer/4/avatar.svg" alt="Backer 4" /></a>
|
||||
<a href="https://opencollective.com/univer/backer/5/website"><img src="https://opencollective.com/univer/backer/5/avatar.svg" alt="Backer 5" /></a>
|
||||
<a href="https://opencollective.com/univer/backer/6/website"><img src="https://opencollective.com/univer/backer/6/avatar.svg" alt="Backer 6" /></a>
|
||||
|
||||
## 📄 License
|
||||
|
||||
Copyright (c) 2021-present DreamNum Co., Ltd.
|
||||
|
||||
Licensed under the [Apache-2.0](./LICENSE) license.
|
||||
72
SECURITY.md
Normal file
72
SECURITY.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Security Policy
|
||||
|
||||
This Security Policy outlines the security practices for Univer (SDK and Univer Services) to ensure the integrity of our code, dependencies, and data. We welcome contributions from community members, developers, and security researchers to identify and address potential security issues collaboratively.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We provide security updates for the following versions of Univer.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| latest | :white_check_mark: |
|
||||
|
||||
As Univer is still not reaching version 1.0 and we are moving fast with latest feature updates, unsupported versions may contain unpatched vulnerabilities. We recommend upgrading to the latest supported version.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We encourage responsible disclosure of security vulnerabilities. If you discover a potential issue, please follow these steps:
|
||||
|
||||
1. How to Report:
|
||||
- Email us at: developer@univer.ai
|
||||
- Alternatively, submit a private Security Advisory via GitHub (if applicable).
|
||||
2. What to Include:
|
||||
- Description: A detailed explanation of the vulnerability and its potential impact.
|
||||
- Steps to Reproduce: Clear instructions to replicate the issue.
|
||||
- Affected Versions: Specify which versions are impacted.
|
||||
- (Optional) Suggested Fix: Any recommendations for resolution.
|
||||
3. Response Timeline:
|
||||
- We will acknowledge your report within 1 business days.
|
||||
- Depending on the severity, we aim to release a fix within 7-30 days.
|
||||
4. Confidentiality:
|
||||
- Please refrain from disclosing vulnerability details in public Issues, forums, or social media until a fix is released.
|
||||
- We will publicly acknowledge your contribution after the fix (unless you request anonymity).
|
||||
|
||||
## Security Update Process
|
||||
|
||||
- **Detection**: We use Dependabot and GitHub Security Alerts to automatically monitor for known vulnerabilities in dependencies.
|
||||
- **Resolution**: Upon identifying a vulnerability:
|
||||
1. We prioritize releasing security patches for supported versions.
|
||||
2. Dependencies are updated to non-vulnerable versions.
|
||||
3. Updates are announced via GitHub Releases.
|
||||
- **Recommendation:** Users should regularly check for updates and apply the latest security patches.
|
||||
|
||||
## Guidelines for Developers
|
||||
|
||||
To maintain a secure codebase, developers should adhere to these best practices:
|
||||
|
||||
- **Dependency Management**:
|
||||
- Use Dependabot or similar tools to keep dependencies up to date.
|
||||
- Avoid using dependencies with known vulnerabilities.
|
||||
- **Code Review**:
|
||||
- All Pull Requests must be reviewed by at least one team member.
|
||||
- Check for proper input validation, access controls, and handling of sensitive data.
|
||||
- **Security Testing**:
|
||||
- Integrate static analysis tools (e.g., CodeQL) into the CI/CD pipeline.
|
||||
- Conduct periodic manual penetration testing.
|
||||
|
||||
## User Responsibilities
|
||||
|
||||
- Keep your software updated to the latest version to benefit from security fixes.
|
||||
- Monitor the security status of your dependencies to avoid introducing third-party risks.
|
||||
- Report any discovered issues to us promptly.
|
||||
|
||||
## Contact Us
|
||||
|
||||
For questions or further assistance, reach out to:
|
||||
|
||||
- Email: developer@univer.ai
|
||||
- GitHub Issues: https://github.com/dream-num/univer/issues
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
We extend our gratitude to all developers, researchers, and users who contribute to the security of this project!
|
||||
13
codecov.yml
Normal file
13
codecov.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# basic
|
||||
target: 80%
|
||||
threshold: 0
|
||||
base: auto
|
||||
# advanced settings
|
||||
if_ci_failed: error
|
||||
informational: false
|
||||
only_pulls: false
|
||||
patch: off
|
||||
1
commitlint.config.cjs
Normal file
1
commitlint.config.cjs
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = { extends: ['@commitlint/config-conventional'] };
|
||||
32
common/debugger/README.md
Normal file
32
common/debugger/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# @univerjs/debugger
|
||||
|
||||
> [!CAUTION]
|
||||
> NEVER use this plugin in your production environment!
|
||||
|
||||
## Package Overview
|
||||
|
||||
| Package Name | UMD Namespace | Version | License | Downloads | Contains CSS | Contains i18n locales |
|
||||
| --- | --- | --- | --- | --- | :---: | :---: |
|
||||
| `@univerjs/debugger` | `UniverDebugger` | [![][npm-version-shield]][npm-version-link] | ![][npm-license-shield] | ![][npm-downloads-shield] | ⭕️ | ❌ |
|
||||
|
||||
## Introduction
|
||||
|
||||
> This plugin provides a lot of utilities to help you debug Univer.
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
```shell
|
||||
# Using npm
|
||||
npm install @univerjs/debugger
|
||||
|
||||
# Using pnpm
|
||||
pnpm add @univerjs/debugger
|
||||
```
|
||||
|
||||
<!-- Links -->
|
||||
[npm-version-shield]: https://img.shields.io/npm/v/@univerjs/debugger?style=flat-square
|
||||
[npm-version-link]: https://npmjs.com/package/@univerjs/debugger
|
||||
[npm-license-shield]: https://img.shields.io/npm/l/@univerjs/debugger?style=flat-square
|
||||
[npm-downloads-shield]: https://img.shields.io/npm/dm/@univerjs/debugger?style=flat-square
|
||||
54
common/debugger/package.json
Normal file
54
common/debugger/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@univerjs/debugger",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"author": "DreamNum <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/univer"
|
||||
},
|
||||
"homepage": "https://univer.ai",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/dream-num/univer/issues"
|
||||
},
|
||||
"keywords": [],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||
"rxjs": ">=7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@univerjs/core": "workspace:*",
|
||||
"@univerjs/design": "workspace:*",
|
||||
"@univerjs/docs-drawing-ui": "workspace:*",
|
||||
"@univerjs/engine-render": "workspace:*",
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/sheets": "workspace:*",
|
||||
"@univerjs/sheets-drawing-ui": "workspace:*",
|
||||
"@univerjs/themes": "workspace:*",
|
||||
"@univerjs/ui": "workspace:*",
|
||||
"@univerjs/watermark": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs-infra/shared": "workspace:*",
|
||||
"postcss": "^8.5.28",
|
||||
"react": "18.3.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"tailwindcss": "3.4.18",
|
||||
"typescript": "^6.0.3",
|
||||
"vue": "^3.5.42"
|
||||
}
|
||||
}
|
||||
1
common/debugger/postcss.config.mjs
Normal file
1
common/debugger/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@univerjs-infra/shared/postcss';
|
||||
68
common/debugger/src/components/FloatButton.tsx
Normal file
68
common/debugger/src/components/FloatButton.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
export const FloatButton = () => {
|
||||
const divStyle = {
|
||||
width: '100px',
|
||||
height: '30px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center' as const,
|
||||
};
|
||||
const clickHandler = () => {
|
||||
console.warn('click');
|
||||
};
|
||||
return (
|
||||
<div style={divStyle} onClick={clickHandler}>
|
||||
FloatButton
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AIButton = () => {
|
||||
const divStyle = {
|
||||
width: '80px',
|
||||
height: '50px',
|
||||
backgroundColor: '#fff',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center' as const,
|
||||
borderRadius: '25px',
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
|
||||
background: 'linear-gradient(90deg, #00C9FF 0%, #92FE9D 50%, #00C9FF 100%)',
|
||||
backgroundSize: '200% auto',
|
||||
animation: 'gradient 3s linear infinite',
|
||||
|
||||
':hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 10px 20px rgba(0, 201, 255, 0.3)',
|
||||
},
|
||||
};
|
||||
const clickHandler = () => {
|
||||
console.warn('click');
|
||||
};
|
||||
return (
|
||||
<button type="button" style={divStyle} onClick={clickHandler}>
|
||||
AI
|
||||
<style>
|
||||
{`
|
||||
@keyframes gradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(0, 201, 255, 0.3);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
5
common/debugger/src/components/Image.tsx
Normal file
5
common/debugger/src/components/Image.tsx
Normal file
File diff suppressed because one or more lines are too long
15
common/debugger/src/components/RangeLoading.tsx
Normal file
15
common/debugger/src/components/RangeLoading.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { borderClassName, clsx } from '@univerjs/design';
|
||||
|
||||
export const RangeLoading = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(`
|
||||
univer-flex univer-size-full univer-origin-top-left univer-items-center univer-justify-center
|
||||
univer-bg-gray-0
|
||||
dark:!univer-bg-gray-900
|
||||
`, borderClassName)}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
};
|
||||
16
common/debugger/src/components/VueComponent.vue
Normal file
16
common/debugger/src/components/VueComponent.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const state = ref<'sun' | 'moon'>('sun');
|
||||
|
||||
function toggleState() {
|
||||
state.value = state.value === 'sun' ? 'moon' : 'sun';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="toggleState">
|
||||
<span v-if="state === 'sun'">🌞</span>
|
||||
<span v-if="state === 'moon'">🌚</span>
|
||||
</div>
|
||||
</template>
|
||||
47
common/debugger/src/components/WebComponent.ts
Normal file
47
common/debugger/src/components/WebComponent.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export class CounterComponent extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const shadow = this.attachShadow({ mode: 'open' });
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('class', 'counter-container');
|
||||
|
||||
const countDisplay = document.createElement('span');
|
||||
countDisplay.setAttribute('class', 'count-display');
|
||||
countDisplay.textContent = '0';
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.textContent = '🍔';
|
||||
|
||||
a.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const currentCount = Number.parseInt(countDisplay.textContent || '0', 10);
|
||||
countDisplay.textContent = (currentCount + 1).toString();
|
||||
});
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.counter-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.count-display {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 10px;
|
||||
}`;
|
||||
|
||||
shadow.appendChild(style);
|
||||
shadow.appendChild(container);
|
||||
container.appendChild(countDisplay);
|
||||
container.appendChild(a);
|
||||
}
|
||||
}
|
||||
25
common/debugger/src/config/config.ts
Normal file
25
common/debugger/src/config/config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ILanguagePack, LocaleType, UniverInstanceType } from '@univerjs/core';
|
||||
import type { MenuConfig } from '@univerjs/ui';
|
||||
|
||||
export const DEBUGGER_PLUGIN_CONFIG_KEY = 'debugger.config';
|
||||
|
||||
export const configSymbol = Symbol(DEBUGGER_PLUGIN_CONFIG_KEY);
|
||||
|
||||
export type UniverDebuggerLocaleLoader = (locale: LocaleType) => ILanguagePack | Promise<ILanguagePack>;
|
||||
|
||||
export interface IUniverDebuggerConfig {
|
||||
menu?: MenuConfig;
|
||||
fab?: boolean;
|
||||
fabEntryUnitType: UniverInstanceType;
|
||||
localeLoader: UniverDebuggerLocaleLoader;
|
||||
performanceMonitor?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultPluginConfig: Pick<IUniverDebuggerConfig, 'fab' | 'performanceMonitor'> = {
|
||||
fab: true,
|
||||
performanceMonitor: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
44
common/debugger/src/controllers/components.controller.ts
Normal file
44
common/debugger/src/controllers/components.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Disposable, Inject } from '@univerjs/core';
|
||||
import { ComponentManager } from '@univerjs/ui';
|
||||
import { AIButton, FloatButton } from '../components/FloatButton';
|
||||
import { ImageDemo } from '../components/Image';
|
||||
import { RangeLoading } from '../components/RangeLoading';
|
||||
// @ts-ignore
|
||||
// import VueComponent from '../components/VueComponent.vue';
|
||||
// import { CounterComponent } from '../components/WebComponent';
|
||||
import { WatermarkPanel } from '../views/watermark/WatermarkPanel';
|
||||
import { WatermarkPanelFooter } from '../views/watermark/WatermarkPanelFooter';
|
||||
|
||||
export const WATERMARK_PANEL = 'WATERMARK_PANEL';
|
||||
export const WATERMARK_PANEL_FOOTER = 'WATERMARK_PANEL_FOOTER';
|
||||
|
||||
export class ComponentsController extends Disposable {
|
||||
constructor(
|
||||
@Inject(ComponentManager) private readonly _componentManager: ComponentManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._registerComponents();
|
||||
}
|
||||
|
||||
private _registerComponents(): void {
|
||||
([
|
||||
['ImageDemo', ImageDemo],
|
||||
['RangeLoading', RangeLoading],
|
||||
['FloatButton', FloatButton],
|
||||
['AIButton', AIButton],
|
||||
[WATERMARK_PANEL, WatermarkPanel],
|
||||
[WATERMARK_PANEL_FOOTER, WatermarkPanelFooter],
|
||||
] as const).forEach(([key, component]) => {
|
||||
this.disposeWithMe(this._componentManager.register(key, component));
|
||||
});
|
||||
|
||||
// this.disposeWithMe(this._componentManager.register('VueComponent', VueComponent, {
|
||||
// framework: 'vue3',
|
||||
// }));
|
||||
|
||||
// this.disposeWithMe(this._componentManager.register('counter-component', CounterComponent, {
|
||||
// framework: 'web-component',
|
||||
// }));
|
||||
}
|
||||
}
|
||||
62
common/debugger/src/controllers/debugger.controller.ts
Normal file
62
common/debugger/src/controllers/debugger.controller.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { IUniverDebuggerConfig } from '../config/config';
|
||||
import { Disposable, IConfigService, Inject, Injector } from '@univerjs/core';
|
||||
import {
|
||||
BuiltInUIPart,
|
||||
connectInjector,
|
||||
IMenuManagerService,
|
||||
IUIPartsService,
|
||||
// MenuItemType,
|
||||
// RibbonStartGroup,
|
||||
} from '@univerjs/ui';
|
||||
import { DEBUGGER_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { Fab } from '../views/Fab';
|
||||
import { RecordController } from './local-save/record.controller';
|
||||
|
||||
export class DebuggerController extends Disposable {
|
||||
constructor(
|
||||
@Inject(Injector) private readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService,
|
||||
@IUIPartsService protected readonly _uiPartsService: IUIPartsService,
|
||||
@IMenuManagerService private readonly _menuManagerService: IMenuManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._initCustomComponents();
|
||||
// this._menuManagerService.mergeMenu({
|
||||
// [RibbonStartGroup.OTHERS]: {
|
||||
// EMOJI: {
|
||||
// order: 9999,
|
||||
// menuItemFactory: () => {
|
||||
// return {
|
||||
// id: 'EMOJI',
|
||||
// icon: 'VueComponent',
|
||||
// type: MenuItemType.BUTTON,
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
// COUNTER: {
|
||||
// order: 9999,
|
||||
// menuItemFactory: () => {
|
||||
// return {
|
||||
// id: 'COUNTER',
|
||||
// icon: 'counter-component',
|
||||
// type: MenuItemType.BUTTON,
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
this._injector.add([RecordController]);
|
||||
}
|
||||
|
||||
private _initCustomComponents(): void {
|
||||
const configs = this._configService.getConfig<IUniverDebuggerConfig>(DEBUGGER_PLUGIN_CONFIG_KEY);
|
||||
|
||||
if (configs?.fab) {
|
||||
this.disposeWithMe(
|
||||
this._uiPartsService.registerComponent(BuiltInUIPart.GLOBAL, () => connectInjector(Fab, this._injector))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { CommandType, ICommandService, Inject } from '@univerjs/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class RecordController {
|
||||
constructor(@Inject(ICommandService) private _commandService: ICommandService) {
|
||||
// empty
|
||||
}
|
||||
|
||||
record() {
|
||||
return new Observable<{ type: 'start' } | { type: 'finish'; data: Blob }>((subscribe) => {
|
||||
navigator.mediaDevices.getDisplayMedia({ video: true }).then((stream) => {
|
||||
subscribe.next({ type: 'start' });
|
||||
const mime = MediaRecorder.isTypeSupported('video/webm; codecs=vp9')
|
||||
? 'video/webm; codecs=vp9'
|
||||
: 'video/webm';
|
||||
const mediaRecorder = new MediaRecorder(stream, { mimeType: mime });
|
||||
|
||||
const chunks: Blob[] = [];
|
||||
mediaRecorder.addEventListener('dataavailable', function (e) {
|
||||
chunks.push(e.data);
|
||||
});
|
||||
|
||||
mediaRecorder.addEventListener('stop', function () {
|
||||
const blob = new Blob(chunks, { type: chunks[0].type });
|
||||
subscribe.next({ type: 'finish', data: blob });
|
||||
subscribe.complete();
|
||||
});
|
||||
|
||||
mediaRecorder.start();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
startSaveCommands() {
|
||||
type SecondsString = string & {};
|
||||
type CommandString = string & {};
|
||||
type TypeString = string & {};
|
||||
type ParamsString = string & {};
|
||||
const result: Array<[SecondsString, CommandString, TypeString, ParamsString]> = [];
|
||||
const startTime = performance.now();
|
||||
const disposable = this._commandService.beforeCommandExecuted((commandInfo) => {
|
||||
try {
|
||||
result.push([
|
||||
String((performance.now() - startTime) / 1000),
|
||||
commandInfo.id,
|
||||
String(commandInfo.type || CommandType.COMMAND),
|
||||
JSON.stringify(commandInfo.params || ''),
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error(`${commandInfo.id} unable to serialize`);
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
8
common/debugger/src/controllers/local-save/util.ts
Normal file
8
common/debugger/src/controllers/local-save/util.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function createDownloadElement(fileName: string, blob: Blob) {
|
||||
const a = document.createElement('a');
|
||||
a.download = fileName;
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
return () => {
|
||||
a.click();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import type { Subscription } from 'rxjs';
|
||||
import { Inject, IUniverInstanceService, LifecycleService, LifecycleStages, RxDisposable } from '@univerjs/core';
|
||||
import { IRenderManagerService } from '@univerjs/engine-render';
|
||||
import { distinctUntilChanged, filter, take, takeUntil } from 'rxjs';
|
||||
|
||||
export class PerformanceMonitorController extends RxDisposable {
|
||||
private _containerElement: HTMLDivElement | null;
|
||||
|
||||
private _currentUnitSub: Nullable<Subscription>;
|
||||
|
||||
constructor(
|
||||
@Inject(LifecycleService) lifecycleService: LifecycleService,
|
||||
@IUniverInstanceService private readonly _instanceService: IUniverInstanceService,
|
||||
@IRenderManagerService private readonly _renderManagerService: IRenderManagerService
|
||||
) {
|
||||
super();
|
||||
|
||||
lifecycleService.subscribeWithPrevious()
|
||||
.pipe(
|
||||
filter((stage) => stage === LifecycleStages.Rendered),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => this._listenDocumentTypeChange());
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
|
||||
this._disposeCurrentObserver();
|
||||
}
|
||||
|
||||
private _disposeCurrentObserver(): void {
|
||||
this._currentUnitSub?.unsubscribe();
|
||||
this._currentUnitSub = null;
|
||||
}
|
||||
|
||||
private _listenDocumentTypeChange() {
|
||||
this._instanceService.focused$.pipe(takeUntil(this.dispose$), distinctUntilChanged()).subscribe((unitId) => {
|
||||
this._disposeCurrentObserver();
|
||||
if (unitId) {
|
||||
this._listenToRenderer(unitId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _listenToRenderer(unitId: string) {
|
||||
const renderer = this._renderManagerService.getRenderUnitById(unitId);
|
||||
if (renderer) {
|
||||
const { engine } = renderer;
|
||||
this._currentUnitSub = engine.endFrame$.subscribe(() => {
|
||||
if (!this._containerElement) {
|
||||
this._containerElement = document.querySelector('[data-u-comp=debugger-fps]');
|
||||
} else {
|
||||
this._containerElement.textContent = `FPS: ${Math.round(engine.getFps()).toString()}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
2
common/debugger/src/global.css
Normal file
2
common/debugger/src/global.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
4
common/debugger/src/index.ts
Normal file
4
common/debugger/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import './global.css';
|
||||
|
||||
export type { IUniverDebuggerConfig, UniverDebuggerLocaleLoader } from './config/config';
|
||||
export { UniverDebuggerPlugin } from './plugin';
|
||||
66
common/debugger/src/plugin.ts
Normal file
66
common/debugger/src/plugin.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { Dependency } from '@univerjs/core';
|
||||
import type { IUniverDebuggerConfig } from './config/config';
|
||||
import { IConfigService, Inject, Injector, merge, Plugin, registerDependencies, touchDependencies } from '@univerjs/core';
|
||||
import pkg from '../package.json';
|
||||
import { DEBUGGER_PLUGIN_CONFIG_KEY, defaultPluginConfig } from './config/config';
|
||||
import { ComponentsController } from './controllers/components.controller';
|
||||
import { DebuggerController } from './controllers/debugger.controller';
|
||||
import { PerformanceMonitorController } from './controllers/performance-monitor.controller';
|
||||
|
||||
export class UniverDebuggerPlugin extends Plugin {
|
||||
static override pluginName = 'UNIVER_DEBUGGER_PLUGIN';
|
||||
static override packageName = pkg.name;
|
||||
static override version = pkg.version;
|
||||
|
||||
private _debuggerController!: DebuggerController;
|
||||
|
||||
constructor(
|
||||
private readonly _config: IUniverDebuggerConfig,
|
||||
@Inject(Injector) override readonly _injector: Injector,
|
||||
@IConfigService private readonly _configService: IConfigService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Manage the plugin configuration.
|
||||
const { menu, ...rest } = merge(
|
||||
{},
|
||||
defaultPluginConfig,
|
||||
this._config
|
||||
);
|
||||
if (menu) {
|
||||
this._configService.setConfig('menu', menu, { merge: true });
|
||||
}
|
||||
this._configService.setConfig(DEBUGGER_PLUGIN_CONFIG_KEY, rest);
|
||||
}
|
||||
|
||||
override onStarting(): void {
|
||||
const dependencies: Dependency[] = [
|
||||
[DebuggerController],
|
||||
[ComponentsController],
|
||||
];
|
||||
|
||||
if (this._config.performanceMonitor?.enabled !== false) {
|
||||
dependencies.push([PerformanceMonitorController]);
|
||||
}
|
||||
|
||||
registerDependencies(this._injector, dependencies);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[ComponentsController],
|
||||
[DebuggerController],
|
||||
]);
|
||||
}
|
||||
|
||||
override onRendered(): void {
|
||||
touchDependencies(this._injector, [
|
||||
[PerformanceMonitorController],
|
||||
]);
|
||||
}
|
||||
|
||||
getDebuggerController() {
|
||||
this._debuggerController = this._injector.get(DebuggerController);
|
||||
return this._debuggerController;
|
||||
}
|
||||
}
|
||||
116
common/debugger/src/views/Fab.tsx
Normal file
116
common/debugger/src/views/Fab.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { IDropdownMenuProps } from '@univerjs/design';
|
||||
import type { IUniverDebuggerConfig } from '../config/config';
|
||||
import { IConfigService, UniverInstanceType } from '@univerjs/core';
|
||||
import { borderClassName, clsx, DropdownMenu } from '@univerjs/design';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { DEBUGGER_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
import { useCellContent } from './use-cell-content';
|
||||
import { useDarkMode } from './use-dark-mode';
|
||||
import { useDialog } from './use-dialog';
|
||||
import { useDispose } from './use-dispose';
|
||||
import { useEditable } from './use-editable';
|
||||
import { useFloatingDom } from './use-floating-dom';
|
||||
import { useLocale } from './use-locale';
|
||||
import { useMessage } from './use-message';
|
||||
import { useNotification } from './use-notification';
|
||||
import { useRTL } from './use-rtl';
|
||||
import { useSidebar } from './use-sidebar';
|
||||
import { useSnapshot } from './use-snapshot';
|
||||
import { useTheme } from './use-theme';
|
||||
import { useUnits } from './use-units';
|
||||
import { useUser } from './use-user';
|
||||
import { useWatermark } from './use-watermark';
|
||||
|
||||
export function Fab() {
|
||||
const configService = useDependency(IConfigService);
|
||||
const configs = configService.getConfig<IUniverDebuggerConfig>(DEBUGGER_PLUGIN_CONFIG_KEY);
|
||||
const performanceMonitor = configs?.performanceMonitor;
|
||||
const fabEntryUnitType = configs?.fabEntryUnitType;
|
||||
|
||||
const locale = useLocale();
|
||||
const rtl = useRTL();
|
||||
const darkMode = useDarkMode();
|
||||
const theme = useTheme();
|
||||
const watermark = useWatermark();
|
||||
const notification = useNotification();
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const sidebar = useSidebar();
|
||||
const floatingDom = useFloatingDom(fabEntryUnitType);
|
||||
const cellContent = useCellContent(fabEntryUnitType);
|
||||
const units = useUnits();
|
||||
const snapshot = useSnapshot();
|
||||
const editable = useEditable();
|
||||
const user = useUser();
|
||||
const dispose = useDispose();
|
||||
|
||||
const globalItems = [locale, rtl, darkMode, theme];
|
||||
const lightweightItems = [
|
||||
...globalItems,
|
||||
watermark,
|
||||
];
|
||||
const commonDebugItems = [
|
||||
watermark,
|
||||
{ type: 'separator' as const },
|
||||
notification,
|
||||
message,
|
||||
dialog,
|
||||
sidebar,
|
||||
{ type: 'separator' as const },
|
||||
snapshot,
|
||||
editable,
|
||||
dispose,
|
||||
];
|
||||
const sheetItems = [
|
||||
...globalItems,
|
||||
watermark,
|
||||
{ type: 'separator' as const },
|
||||
notification,
|
||||
message,
|
||||
dialog,
|
||||
sidebar,
|
||||
{ type: 'separator' as const },
|
||||
floatingDom,
|
||||
cellContent,
|
||||
units,
|
||||
snapshot,
|
||||
editable,
|
||||
user,
|
||||
dispose,
|
||||
];
|
||||
|
||||
const items: IDropdownMenuProps['items'] = fabEntryUnitType === UniverInstanceType.UNIVER_BASE || fabEntryUnitType === UniverInstanceType.UNIVER_SLIDE
|
||||
? lightweightItems
|
||||
: fabEntryUnitType === UniverInstanceType.UNIVER_DOC
|
||||
? [...globalItems, ...commonDebugItems, floatingDom].filter(Boolean) as IDropdownMenuProps['items']
|
||||
: sheetItems.filter(Boolean) as IDropdownMenuProps['items'];
|
||||
|
||||
return (
|
||||
<div
|
||||
data-u-comp="debugger-fab"
|
||||
className={`
|
||||
univer-fixed univer-bottom-12 univer-right-8 univer-z-[9999] univer-flex univer-select-none
|
||||
univer-flex-col univer-items-center univer-gap-1
|
||||
`}
|
||||
>
|
||||
<DropdownMenu align="end" items={items}>
|
||||
<button
|
||||
className={clsx(`
|
||||
univer-flex univer-size-9 univer-cursor-pointer univer-items-center univer-justify-center
|
||||
univer-rounded-full univer-bg-gray-0 univer-text-base univer-text-gray-900 univer-shadow
|
||||
univer-outline-none univer-transition-shadow
|
||||
hover:univer-ring-1 hover:univer-ring-primary-400
|
||||
dark:!univer-bg-gray-900 dark:!univer-text-gray-200
|
||||
`, borderClassName)}
|
||||
type="button"
|
||||
>
|
||||
🏗️
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
|
||||
{performanceMonitor?.enabled && (
|
||||
<span data-u-comp="debugger-fps" className="univer-text-xs univer-text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
common/debugger/src/views/test-editor/component-name.ts
Normal file
1
common/debugger/src/views/test-editor/component-name.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const TEST_EDITOR_CONTAINER_COMPONENT = 'TestEditorContainer';
|
||||
28
common/debugger/src/views/use-cell-content.ts
Normal file
28
common/debugger/src/views/use-cell-content.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ILogService, IUniverInstanceService, ObjectMatrix, Range, UniverInstanceType } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, SheetsSelectionsService } from '@univerjs/sheets';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
export function useCellContent(fabEntryUnitType?: UniverInstanceType) {
|
||||
const logService = useDependency(ILogService);
|
||||
const selectionManagerService = fabEntryUnitType === UniverInstanceType.UNIVER_SHEET ? useDependency(SheetsSelectionsService) : null;
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
const onSelect = () => {
|
||||
if (!selectionManagerService) return;
|
||||
const selections = selectionManagerService.getCurrentSelections();
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
const matrix = new ObjectMatrix();
|
||||
selections.forEach((selection) => {
|
||||
Range.foreach(selection.range, (row, col) => {
|
||||
matrix.setValue(row, col, target?.worksheet.getCell(row, col));
|
||||
});
|
||||
});
|
||||
logService.debug('cell-content', matrix);
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'item' as const,
|
||||
children: '🗒️ Console cell content',
|
||||
onSelect,
|
||||
};
|
||||
}
|
||||
31
common/debugger/src/views/use-dark-mode.ts
Normal file
31
common/debugger/src/views/use-dark-mode.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ThemeService } from '@univerjs/core';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useDarkMode() {
|
||||
const themeService = useDependency(ThemeService);
|
||||
|
||||
useEffect(() => {
|
||||
const darkMode = localStorage.getItem('local.darkMode');
|
||||
|
||||
if (darkMode === 'dark') {
|
||||
document.documentElement.classList.add('univer-dark');
|
||||
themeService.setDarkMode(true);
|
||||
} else if (darkMode === 'system') {
|
||||
themeService.setDarkMode(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSelect = () => {
|
||||
const darkMode = themeService.darkMode ? 'light' : 'dark';
|
||||
|
||||
themeService.setDarkMode(darkMode === 'dark');
|
||||
localStorage.setItem('local.darkMode', darkMode);
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'item' as const,
|
||||
children: '🌓 Toggle dark mode',
|
||||
onSelect,
|
||||
};
|
||||
}
|
||||
89
common/debugger/src/views/use-dialog.ts
Normal file
89
common/debugger/src/views/use-dialog.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { IConfirmService } from '@univerjs/core';
|
||||
import { IDialogService, useDependency } from '@univerjs/ui';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Open dialog',
|
||||
value: 'dialog',
|
||||
},
|
||||
{
|
||||
label: 'Draggable dialog',
|
||||
value: 'draggable',
|
||||
},
|
||||
{
|
||||
label: 'Open confirm',
|
||||
value: 'confirm',
|
||||
},
|
||||
];
|
||||
|
||||
export function useDialog() {
|
||||
const dialogService = useDependency(IDialogService);
|
||||
const confirmService = useDependency(IConfirmService);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value === 'draggable') {
|
||||
dialogService.open({
|
||||
id: 'draggable',
|
||||
children: { title: 'Draggable Dialog Content' },
|
||||
title: { title: 'Draggable Dialog' },
|
||||
draggable: true,
|
||||
preservePositionOnDestroy: true,
|
||||
width: 350,
|
||||
onClose() {
|
||||
// dialogService.close('draggable');
|
||||
},
|
||||
onOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
dialogService.close('draggable');
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (value === 'dialog') {
|
||||
dialogService.open({
|
||||
id: 'dialog1',
|
||||
children: { title: 'Dialog Content' },
|
||||
footer: { title: 'Dialog Footer' },
|
||||
title: { title: 'Dialog Title' },
|
||||
draggable: false,
|
||||
onClose() {
|
||||
// dialogService.close('dialog1');
|
||||
},
|
||||
onOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
dialogService.close('dialog1');
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (value === 'confirm') {
|
||||
confirmService.open({
|
||||
id: 'confirm1',
|
||||
children: { title: 'Confirm Content' },
|
||||
title: { title: 'Confirm Title' },
|
||||
confirmText: 'hello',
|
||||
cancelText: 'world',
|
||||
onClose() {
|
||||
confirmService.close('confirm1');
|
||||
},
|
||||
});
|
||||
|
||||
confirmService.open({
|
||||
id: 'confirm2',
|
||||
children: { title: 'Confirm2 Content' },
|
||||
title: { title: 'Confirm2 Title' },
|
||||
onClose() {
|
||||
confirmService.close('confirm2');
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '💬 Dialog & Confirm',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
42
common/debugger/src/views/use-dispose.ts
Normal file
42
common/debugger/src/views/use-dispose.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Univer } from '@univerjs/core';
|
||||
import { IUniverInstanceService } from '@univerjs/core';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Dispose Univer',
|
||||
value: 'univer',
|
||||
},
|
||||
{
|
||||
label: 'Dispose current unit',
|
||||
value: 'unit',
|
||||
},
|
||||
];
|
||||
|
||||
export function useDispose() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value === 'univer') {
|
||||
const debuggerWindow = window as typeof window & { univer?: Univer };
|
||||
debuggerWindow.univer?.dispose();
|
||||
Reflect.deleteProperty(debuggerWindow, 'univer');
|
||||
Reflect.deleteProperty(debuggerWindow, 'univerAPI');
|
||||
} else if (value === 'unit') {
|
||||
const focused = univerInstanceService.getFocusedUnit();
|
||||
if (!focused) return false;
|
||||
|
||||
return univerInstanceService.disposeUnit(focused.getUnitId());
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '🗑️ Dispose',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
48
common/debugger/src/views/use-editable.ts
Normal file
48
common/debugger/src/views/use-editable.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { IPermissionService, IUniverInstanceService } from '@univerjs/core';
|
||||
import { getSheetCommandTarget, WorkbookEditablePermission, WorksheetEditPermission } from '@univerjs/sheets';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Change workbook editable',
|
||||
value: 'univer',
|
||||
},
|
||||
{
|
||||
label: 'Change worksheet editable',
|
||||
value: 'sheet',
|
||||
},
|
||||
];
|
||||
|
||||
export function useEditable() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const permissionService = useDependency(IPermissionService);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
const target = getSheetCommandTarget(univerInstanceService);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
const { workbook, worksheet, unitId, subUnitId } = target;
|
||||
if (!workbook || !worksheet) {
|
||||
return false;
|
||||
}
|
||||
if (value === 'sheet') {
|
||||
const editable = permissionService.getPermissionPoint(new WorksheetEditPermission(unitId, subUnitId).id);
|
||||
permissionService.updatePermissionPoint(new WorksheetEditPermission(unitId, subUnitId).id, !editable);
|
||||
} else {
|
||||
const unitId = workbook!.getUnitId();
|
||||
const editable = permissionService.getPermissionPoint(new WorkbookEditablePermission(unitId).id);
|
||||
permissionService.updatePermissionPoint(new WorkbookEditablePermission(unitId).id, !editable);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '✍️ Editable',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
50
common/debugger/src/views/use-floating-dom.ts
Normal file
50
common/debugger/src/views/use-floating-dom.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { DocFloatDomController } from '@univerjs/docs-drawing-ui';
|
||||
import { SheetCanvasFloatDomManagerService } from '@univerjs/sheets-drawing-ui';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
export function useFloatingDom(entryUnitType?: UniverInstanceType) {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const floatDomService = entryUnitType === UniverInstanceType.UNIVER_SHEET ? useDependency(SheetCanvasFloatDomManagerService) : null;
|
||||
const floatDomController = entryUnitType === UniverInstanceType.UNIVER_DOC ? useDependency(DocFloatDomController) : null;
|
||||
|
||||
const onSelect = () => {
|
||||
if (entryUnitType === UniverInstanceType.UNIVER_SHEET) {
|
||||
const currentSheet = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_SHEET);
|
||||
if (!currentSheet) return;
|
||||
|
||||
floatDomService?.addFloatDomToPosition({
|
||||
allowTransform: true,
|
||||
initPosition: {
|
||||
startX: 200,
|
||||
endX: 400,
|
||||
startY: 200,
|
||||
endY: 400,
|
||||
},
|
||||
componentKey: 'ImageDemo',
|
||||
data: {
|
||||
aa: '128',
|
||||
},
|
||||
});
|
||||
} else if (entryUnitType === UniverInstanceType.UNIVER_DOC) {
|
||||
const currentDoc = univerInstanceService.getCurrentUnitOfType(UniverInstanceType.UNIVER_DOC);
|
||||
if (!currentDoc) return;
|
||||
|
||||
floatDomController?.insertFloatDom({
|
||||
allowTransform: true,
|
||||
componentKey: 'ImageDemo',
|
||||
data: {
|
||||
aa: '128',
|
||||
},
|
||||
}, {
|
||||
height: 300,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'item' as const,
|
||||
children: '☁️ Create floating DOM',
|
||||
onSelect,
|
||||
};
|
||||
}
|
||||
78
common/debugger/src/views/use-locale.ts
Normal file
78
common/debugger/src/views/use-locale.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { IUniverDebuggerConfig } from '../config/config';
|
||||
import { IConfigService, LocaleService, LocaleType } from '@univerjs/core';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { DEBUGGER_PLUGIN_CONFIG_KEY } from '../config/config';
|
||||
|
||||
const localeLabels: Record<LocaleType, string> = {
|
||||
[LocaleType.AR_SA]: 'العربية',
|
||||
[LocaleType.CA_ES]: 'Català',
|
||||
[LocaleType.DE_DE]: 'Deutsch',
|
||||
[LocaleType.EN_US]: 'English',
|
||||
[LocaleType.ES_ES]: 'Español',
|
||||
[LocaleType.FA_IR]: 'فارسی',
|
||||
[LocaleType.FR_FR]: 'Français',
|
||||
[LocaleType.ID_ID]: 'Bahasa Indonesia',
|
||||
[LocaleType.IT_IT]: 'Italiano',
|
||||
[LocaleType.JA_JP]: '日本語',
|
||||
[LocaleType.KO_KR]: '한국어',
|
||||
[LocaleType.PL_PL]: 'Polski',
|
||||
[LocaleType.PT_BR]: 'Português (Brasil)',
|
||||
[LocaleType.RU_RU]: 'Русский',
|
||||
[LocaleType.SK_SK]: 'Slovenčina',
|
||||
[LocaleType.VI_VN]: 'Tiếng Việt',
|
||||
[LocaleType.ZH_CN]: '简体中文',
|
||||
[LocaleType.ZH_HK]: '繁體中文(香港)',
|
||||
[LocaleType.ZH_TW]: '繁體中文(台灣)',
|
||||
};
|
||||
|
||||
export function useLocale() {
|
||||
const configService = useDependency(IConfigService);
|
||||
const localeService = useDependency(LocaleService);
|
||||
const configs = configService.getConfig<IUniverDebuggerConfig>(DEBUGGER_PLUGIN_CONFIG_KEY);
|
||||
const localeLoader = configs?.localeLoader;
|
||||
|
||||
const loadLocales = useCallback(async (value: string) => {
|
||||
const locale = value as LocaleType;
|
||||
if (!localeLoader) {
|
||||
throw new Error('[UniverDebuggerPlugin]: localeLoader is required.');
|
||||
}
|
||||
const localePack = await localeLoader(locale);
|
||||
|
||||
localeService.load({
|
||||
[locale]: localePack,
|
||||
});
|
||||
}, [localeLoader, localeService]);
|
||||
|
||||
useEffect(() => {
|
||||
const locale = localStorage.getItem('local.locale');
|
||||
|
||||
if (locale) {
|
||||
loadLocales(locale).then(() => {
|
||||
localeService.setLocale(locale as LocaleType);
|
||||
});
|
||||
}
|
||||
}, [loadLocales, localeService]);
|
||||
|
||||
const onSelect = async (value: string) => {
|
||||
await loadLocales(value);
|
||||
|
||||
localeService.setLocale(value as LocaleType);
|
||||
|
||||
localStorage.setItem('local.locale', value);
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '🌐 Languages',
|
||||
options: Object.values(LocaleType).map((locale) => ({
|
||||
type: 'radio' as const,
|
||||
value: localeService.getCurrentLocale(),
|
||||
options: [{
|
||||
label: localeLabels[locale],
|
||||
value: locale,
|
||||
}],
|
||||
onSelect,
|
||||
})),
|
||||
};
|
||||
}
|
||||
43
common/debugger/src/views/use-message.ts
Normal file
43
common/debugger/src/views/use-message.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { MessageType } from '@univerjs/design';
|
||||
import { IMessageService, useDependency } from '@univerjs/ui';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Open message',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: 'Open loading message',
|
||||
value: 'loading',
|
||||
},
|
||||
];
|
||||
|
||||
export function useMessage() {
|
||||
const messageService = useDependency(IMessageService);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value === 'loading') {
|
||||
messageService.show({
|
||||
type: MessageType.Loading,
|
||||
content: 'Loading message',
|
||||
duration: 3000,
|
||||
});
|
||||
} else {
|
||||
messageService.show({
|
||||
type: MessageType.Success,
|
||||
content: 'Demo message',
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '✉️ Message',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
43
common/debugger/src/views/use-notification.ts
Normal file
43
common/debugger/src/views/use-notification.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { INotificationOptions } from '@univerjs/ui';
|
||||
import { INotificationService, useDependency } from '@univerjs/ui';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Notification success',
|
||||
value: 'success',
|
||||
},
|
||||
{
|
||||
label: 'Notification info',
|
||||
value: 'info',
|
||||
},
|
||||
{
|
||||
label: 'Notification warning',
|
||||
value: 'warning',
|
||||
},
|
||||
{
|
||||
label: 'Notification error',
|
||||
value: 'error',
|
||||
},
|
||||
];
|
||||
|
||||
export function useNotification() {
|
||||
const notificationService = useDependency(INotificationService);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
notificationService.show({
|
||||
type: value as INotificationOptions['type'],
|
||||
content: 'Lorem Ipusm dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
title: 'Notification Title',
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '🔔 Notification',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
29
common/debugger/src/views/use-rtl.ts
Normal file
29
common/debugger/src/views/use-rtl.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { LocaleService } from '@univerjs/core';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useRTL() {
|
||||
const localeService = useDependency(LocaleService);
|
||||
|
||||
useEffect(() => {
|
||||
const dir = localStorage.getItem('local.direction');
|
||||
|
||||
if (dir === 'rtl' || dir === 'ltr') {
|
||||
localeService.setDirection(dir);
|
||||
}
|
||||
}, [localeService]);
|
||||
|
||||
const onSelect = () => {
|
||||
const current = localeService.getDirection();
|
||||
const nextDirection = current === 'rtl' ? 'ltr' : 'rtl';
|
||||
|
||||
localeService.setDirection(nextDirection);
|
||||
localStorage.setItem('local.direction', nextDirection);
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'item' as const,
|
||||
children: '↔️ Toggle RTL',
|
||||
onSelect,
|
||||
};
|
||||
}
|
||||
41
common/debugger/src/views/use-sidebar.ts
Normal file
41
common/debugger/src/views/use-sidebar.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ISidebarService, useDependency } from '@univerjs/ui';
|
||||
import { TEST_EDITOR_CONTAINER_COMPONENT } from './test-editor/component-name';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Open sidebar',
|
||||
value: 'open',
|
||||
},
|
||||
{
|
||||
label: 'Close sidebar',
|
||||
value: 'close',
|
||||
},
|
||||
];
|
||||
|
||||
export function useSidebar() {
|
||||
const sidebarService = useDependency(ISidebarService);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value === 'open') {
|
||||
sidebarService.open({
|
||||
header: { title: 'Sidebar title' },
|
||||
children: { label: TEST_EDITOR_CONTAINER_COMPONENT },
|
||||
footer: { title: 'Sidebar Footer' },
|
||||
onClose: () => {
|
||||
},
|
||||
});
|
||||
} else if (value === 'close') {
|
||||
sidebarService.close();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '🧩 Sidebar',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
128
common/debugger/src/views/use-snapshot.ts
Normal file
128
common/debugger/src/views/use-snapshot.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable node/prefer-global/process */
|
||||
import type { DocumentDataModel, IStyleData, IWorkbookData, Workbook } from '@univerjs/core';
|
||||
import { IResourceLoaderService, IUniverInstanceService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
|
||||
import { ILocalFileService, useDependency } from '@univerjs/ui';
|
||||
import { RecordController } from '../controllers/local-save/record.controller';
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Save workbook',
|
||||
value: 'workbook',
|
||||
},
|
||||
{
|
||||
label: 'Save worksheet',
|
||||
value: 'sheet',
|
||||
},
|
||||
{
|
||||
label: 'Record',
|
||||
value: 'record',
|
||||
},
|
||||
{
|
||||
label: 'Load snapshot',
|
||||
value: 'load',
|
||||
},
|
||||
];
|
||||
|
||||
export interface ISaveSnapshotParams {
|
||||
value: 'sheet' | 'workbook' | 'record';
|
||||
}
|
||||
|
||||
const filterStyle = (workbookData: IWorkbookData) => {
|
||||
const sheets = workbookData.sheets;
|
||||
const cacheStyle: Record<string, IStyleData> = {};
|
||||
Object.keys(sheets).forEach((sheetId) => {
|
||||
const sheet = sheets[sheetId];
|
||||
new ObjectMatrix(sheet.cellData).forValue((_r, _c, value) => {
|
||||
const s = value?.s;
|
||||
if (s && typeof s === 'string') {
|
||||
const style = workbookData.styles[s];
|
||||
if (style) {
|
||||
cacheStyle[s] = style;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
workbookData.styles = cacheStyle;
|
||||
return workbookData;
|
||||
};
|
||||
|
||||
export function useSnapshot() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const resourceLoaderService = useDependency(IResourceLoaderService);
|
||||
const localFileService = useDependency(ILocalFileService);
|
||||
const recordController = useDependency(RecordController);
|
||||
|
||||
const onSelect = async (value: string) => {
|
||||
const preName = new Date().toLocaleString();
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
if (!workbook) {
|
||||
const doc = univerInstanceService.getCurrentUnitOfType<DocumentDataModel>(UniverInstanceType.UNIVER_DOC)!;
|
||||
const snapshot = resourceLoaderService.saveUnit(doc.getUnitId());
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const gitHash = process.env.GIT_COMMIT_HASH;
|
||||
const gitBranch = process.env.GIT_REF_NAME;
|
||||
const buildTime = process.env.BUILD_TIME;
|
||||
(snapshot as any).__env__ = { gitHash, gitBranch, buildTime };
|
||||
}
|
||||
const text = JSON.stringify(snapshot, null, 2);
|
||||
localFileService.downloadFile(new Blob([text]), `${preName} snapshot.json`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const worksheet = workbook.getActiveSheet();
|
||||
if (!worksheet) {
|
||||
return false;
|
||||
}
|
||||
const snapshot = resourceLoaderService.saveUnit<ReturnType<typeof workbook.getSnapshot>>(workbook.getUnitId())!;
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const gitHash = process.env.GIT_COMMIT_HASH;
|
||||
const gitBranch = process.env.GIT_REF_NAME;
|
||||
const buildTime = process.env.BUILD_TIME;
|
||||
(snapshot as any).__env__ = { gitHash, gitBranch, buildTime };
|
||||
}
|
||||
|
||||
if (value === 'sheet') {
|
||||
const sheetId = worksheet.getSheetId();
|
||||
const sheet = snapshot.sheets[sheetId];
|
||||
snapshot.sheets = { [sheetId]: sheet };
|
||||
snapshot.sheetOrder = [sheetId];
|
||||
const text = JSON.stringify(filterStyle(snapshot), null, 2);
|
||||
localFileService.downloadFile(new Blob([text]), `${preName} snapshot.json`);
|
||||
} else if (value === 'workbook') {
|
||||
const text = JSON.stringify(filterStyle(snapshot), null, 2);
|
||||
localFileService.downloadFile(new Blob([text]), `${preName} snapshot.json`);
|
||||
} else if (value === 'record') {
|
||||
let endCommands = () => [];
|
||||
|
||||
recordController.record().subscribe((v) => {
|
||||
if (v.type === 'start') {
|
||||
endCommands = recordController.startSaveCommands() as any;
|
||||
}
|
||||
if (v.type === 'finish') {
|
||||
const commands = endCommands();
|
||||
localFileService.downloadFile(v.data, `${preName} video.webm`);
|
||||
localFileService.downloadFile(new Blob([JSON.stringify(commands, null, 2)]), `${preName} commands.json`);
|
||||
}
|
||||
});
|
||||
} else if (value === 'load') {
|
||||
const snapshotFile = await localFileService.openFile({ multiple: false, accept: '.json' });
|
||||
if (snapshotFile.length !== 1) return false;
|
||||
|
||||
const text = await snapshotFile[0].text();
|
||||
univerInstanceService.createUnit<IWorkbookData, Workbook>(UniverInstanceType.UNIVER_SHEET, JSON.parse(text));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '📷 Snapshot',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
77
common/debugger/src/views/use-theme.ts
Normal file
77
common/debugger/src/views/use-theme.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ThemeService } from '@univerjs/core';
|
||||
import {
|
||||
blueTheme,
|
||||
darkBlueTheme,
|
||||
greenTheme,
|
||||
orangeTheme,
|
||||
purpleTheme,
|
||||
redTheme,
|
||||
yellowTheme,
|
||||
} from '@univerjs/themes';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const themes = [
|
||||
{
|
||||
label: '🟢',
|
||||
value: greenTheme,
|
||||
},
|
||||
{
|
||||
label: '🔵',
|
||||
value: blueTheme,
|
||||
},
|
||||
{
|
||||
label: '🔵🌑',
|
||||
value: darkBlueTheme,
|
||||
},
|
||||
{
|
||||
label: '🟠',
|
||||
value: orangeTheme,
|
||||
},
|
||||
{
|
||||
label: '🔴',
|
||||
value: redTheme,
|
||||
},
|
||||
{
|
||||
label: '🟣',
|
||||
value: purpleTheme,
|
||||
},
|
||||
{
|
||||
label: '🟡',
|
||||
value: yellowTheme,
|
||||
},
|
||||
];
|
||||
|
||||
export function useTheme() {
|
||||
const themeService = useDependency(ThemeService);
|
||||
|
||||
useEffect(() => {
|
||||
const themeKey = localStorage.getItem('local.theme');
|
||||
|
||||
const theme = themes.find((theme) => theme.label === themeKey);
|
||||
|
||||
if (theme) {
|
||||
themeService.setTheme(theme.value);
|
||||
}
|
||||
}, [themeService]);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
localStorage.setItem('local.theme', value);
|
||||
|
||||
const theme = themes.find((theme) => theme.label === value);
|
||||
|
||||
if (theme) {
|
||||
themeService.setTheme(theme.value);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '🎨 Themes',
|
||||
options: themes.map((theme) => ({
|
||||
type: 'item' as const,
|
||||
children: theme.label,
|
||||
onSelect: () => onSelect(theme.label),
|
||||
})),
|
||||
};
|
||||
}
|
||||
50
common/debugger/src/views/use-units.ts
Normal file
50
common/debugger/src/views/use-units.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import { IUniverInstanceService, UniverInstanceType } from '@univerjs/core';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const defaultMenu = [
|
||||
{
|
||||
label: 'Create another sheet',
|
||||
value: 'create',
|
||||
},
|
||||
];
|
||||
|
||||
export function useUnits() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const [menu, setMenu] = useState<{ label: string; value: string }[]>([...defaultMenu]);
|
||||
const unitAdded = useObservable(univerInstanceService.unitAdded$);
|
||||
const unitDisposed = useObservable(univerInstanceService.unitDisposed$);
|
||||
|
||||
useEffect(() => {
|
||||
const sheets = univerInstanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET);
|
||||
const options = sheets.map((sheet) => ({
|
||||
label: sheet.name || sheet.getUnitId(),
|
||||
value: sheet.getUnitId(),
|
||||
}));
|
||||
|
||||
setMenu([
|
||||
...defaultMenu,
|
||||
...(options as any[]),
|
||||
]);
|
||||
}, [unitAdded, unitDisposed]);
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value === 'create') {
|
||||
univerInstanceService.createUnit(UniverInstanceType.UNIVER_SHEET, {});
|
||||
} else {
|
||||
if (!univerInstanceService.getUnit(value)) return false;
|
||||
univerInstanceService.setCurrentUnitForType(value);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '🪸 Units',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
54
common/debugger/src/views/use-user.ts
Normal file
54
common/debugger/src/views/use-user.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Workbook } from '@univerjs/core';
|
||||
import { createDefaultUser, IPermissionService, IUniverInstanceService, UniverInstanceType, UserManagerService } from '@univerjs/core';
|
||||
import { WorkbookManageCollaboratorPermission } from '@univerjs/sheets';
|
||||
import { useDependency } from '@univerjs/ui';
|
||||
|
||||
enum UnitRole {
|
||||
Reader = 0,
|
||||
Editor = 1,
|
||||
Owner = 2,
|
||||
UNRECOGNIZED = -1,
|
||||
}
|
||||
|
||||
const menu = [
|
||||
{
|
||||
label: 'Owner',
|
||||
value: UnitRole.Owner,
|
||||
},
|
||||
{
|
||||
label: 'Editor',
|
||||
value: UnitRole.Editor,
|
||||
},
|
||||
{
|
||||
label: 'Reader',
|
||||
value: UnitRole.Reader,
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export function useUser() {
|
||||
const univerInstanceService = useDependency(IUniverInstanceService);
|
||||
const userManagerService = useDependency(UserManagerService);
|
||||
const permissionService = useDependency(IPermissionService);
|
||||
|
||||
const onSelect = (value: UnitRole) => {
|
||||
userManagerService.setCurrentUser(createDefaultUser(value));
|
||||
const workbook = univerInstanceService.getCurrentUnitOfType<Workbook>(UniverInstanceType.UNIVER_SHEET)!;
|
||||
const unitId = workbook.getUnitId();
|
||||
if (value === UnitRole.Owner) {
|
||||
permissionService.updatePermissionPoint(new WorkbookManageCollaboratorPermission(unitId).id, true);
|
||||
} else {
|
||||
permissionService.updatePermissionPoint(new WorkbookManageCollaboratorPermission(unitId).id, false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'subItem' as const,
|
||||
children: '👥 Change user\'s role',
|
||||
options: menu.map((item) => ({
|
||||
type: 'item' as const,
|
||||
children: item.label,
|
||||
onSelect: () => onSelect(item.value),
|
||||
})),
|
||||
};
|
||||
}
|
||||
22
common/debugger/src/views/use-watermark.ts
Normal file
22
common/debugger/src/views/use-watermark.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ISidebarService, useDependency } from '@univerjs/ui';
|
||||
import { WATERMARK_PANEL, WATERMARK_PANEL_FOOTER } from '../controllers/components.controller';
|
||||
|
||||
export function useWatermark() {
|
||||
const sidebarService = useDependency(ISidebarService);
|
||||
|
||||
const onSelect = () => {
|
||||
sidebarService.open({
|
||||
header: { title: 'Watermark' },
|
||||
children: { label: WATERMARK_PANEL },
|
||||
footer: { label: WATERMARK_PANEL_FOOTER },
|
||||
onClose: () => { },
|
||||
width: 330,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'item' as const,
|
||||
children: '🌊 Watermark Settings',
|
||||
onSelect,
|
||||
};
|
||||
}
|
||||
224
common/debugger/src/views/watermark/WatermarkImageSetting.tsx
Normal file
224
common/debugger/src/views/watermark/WatermarkImageSetting.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import type { IImageWatermarkConfig } from '@univerjs/engine-render';
|
||||
import { Button, Checkbox, InputNumber } from '@univerjs/design';
|
||||
import { ILocalFileService, useDependency } from '@univerjs/ui';
|
||||
import { WATERMARK_IMAGE_ALLOW_IMAGE_LIST } from '@univerjs/watermark';
|
||||
|
||||
interface IWatermarkImageSettingProps {
|
||||
config?: IImageWatermarkConfig;
|
||||
onChange: (config: IImageWatermarkConfig) => void;
|
||||
}
|
||||
|
||||
export function WatermarkImageSetting({ config, onChange }: IWatermarkImageSettingProps) {
|
||||
const fileOpenService = useDependency(ILocalFileService);
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const handleUpdateImageUrl = async () => {
|
||||
const files = await fileOpenService.openFile({
|
||||
multiple: false,
|
||||
accept: WATERMARK_IMAGE_ALLOW_IMAGE_LIST.map((image) => `.${image.replace('image/', '')}`).join(','),
|
||||
});
|
||||
|
||||
const fileLength = files.length;
|
||||
if (fileLength === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (event) {
|
||||
if (event.target?.result) {
|
||||
const base64String = event.target.result;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
onChange({ ...config, url: base64String as string, width: Math.max(20, img.width), height: Math.max(img.height, 20), originRatio: img.width / img.height });
|
||||
};
|
||||
|
||||
img.src = base64String as string;
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="univer-grid univer-gap-2">
|
||||
<div className="univer-text-gray-400">Image</div>
|
||||
|
||||
<div className="univer-mb-4 univer-grid univer-gap-1">
|
||||
<Button
|
||||
className="univer-mb-2"
|
||||
onClick={handleUpdateImageUrl}
|
||||
>
|
||||
{config.url ? 'Replace Image' : 'Upload Image'}
|
||||
</Button>
|
||||
|
||||
<div className="univer-flex univer-gap-2 univer-text-center">
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Opacity</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.opacity}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.05}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, opacity: Number.parseFloat(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Keep Ratio</div>
|
||||
<Checkbox
|
||||
className="univer-justify-center univer-self-baseline"
|
||||
checked={config.maintainAspectRatio}
|
||||
onChange={(val) => {
|
||||
if (val === true) {
|
||||
onChange({ ...config, maintainAspectRatio: val as boolean, height: Math.round(config.width / config.originRatio) });
|
||||
} else {
|
||||
onChange({ ...config, maintainAspectRatio: val as boolean });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-2 univer-text-center">
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Width</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.width}
|
||||
min={20}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
const newWidth = Math.max(20, Number.parseInt(val.toString()));
|
||||
if (config.maintainAspectRatio) {
|
||||
onChange({ ...config, width: newWidth, height: Math.round(newWidth / config.originRatio) });
|
||||
} else {
|
||||
onChange({ ...config, width: newWidth });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Height</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.height}
|
||||
min={20}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
const newHeight = Math.max(20, Number.parseInt(val.toString()));
|
||||
if (config.maintainAspectRatio) {
|
||||
onChange({ ...config, height: newHeight, width: Math.round(newHeight * config.originRatio) });
|
||||
} else {
|
||||
onChange({ ...config, height: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="univer-text-gray-400">Layout Settings</div>
|
||||
|
||||
<div className="univer-grid univer-gap-2 univer-text-center">
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Rotate</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.rotate}
|
||||
max={360}
|
||||
min={-360}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, rotate: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Repeat</div>
|
||||
<Checkbox
|
||||
className="univer-justify-center univer-self-baseline"
|
||||
checked={config.repeat}
|
||||
onChange={(val) => onChange({ ...config, repeat: val as boolean })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Horizontal Spacing</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.spacingX}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, spacingX: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Vertical Spacing</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.spacingY}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, spacingY: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Horizontal Start Position</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.x}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, x: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Vertical Start Position</div>
|
||||
<InputNumber
|
||||
className="univer-box-border univer-h-7"
|
||||
value={config.y}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, y: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
65
common/debugger/src/views/watermark/WatermarkPanel.tsx
Normal file
65
common/debugger/src/views/watermark/WatermarkPanel.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { IWatermarkConfig, IWatermarkConfigWithType } from '@univerjs/engine-render';
|
||||
import { ILocalStorageService } from '@univerjs/core';
|
||||
import { Select } from '@univerjs/design';
|
||||
import { IWatermarkTypeEnum, UNIVER_WATERMARK_STORAGE_KEY } from '@univerjs/engine-render';
|
||||
import { useDependency, useObservable } from '@univerjs/ui';
|
||||
import { WatermarkImageBaseConfig, WatermarkService, WatermarkTextBaseConfig } from '@univerjs/watermark';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { WatermarkImageSetting } from './WatermarkImageSetting';
|
||||
import { WatermarkTextSetting } from './WatermarkTextSetting';
|
||||
|
||||
export function WatermarkPanel() {
|
||||
const [watermarkType, setWatermarkType] = useState<IWatermarkTypeEnum>(IWatermarkTypeEnum.Text);
|
||||
const [config, setConfig] = useState<IWatermarkConfig>();
|
||||
const watermarkService = useDependency(WatermarkService);
|
||||
const localStorageService = useDependency(ILocalStorageService);
|
||||
const _refresh = useObservable(watermarkService.refresh$);
|
||||
|
||||
function handleConfigChange(config: IWatermarkConfig, type?: IWatermarkTypeEnum) {
|
||||
setConfig(config);
|
||||
watermarkService.updateWatermarkConfig({ type: type ?? watermarkType, config });
|
||||
}
|
||||
|
||||
const getWatermarkConfig = useCallback(async () => {
|
||||
const watermarkConfig = await localStorageService.getItem<IWatermarkConfigWithType>(UNIVER_WATERMARK_STORAGE_KEY);
|
||||
if (watermarkConfig) {
|
||||
setWatermarkType(watermarkConfig.type);
|
||||
setConfig(watermarkConfig.config);
|
||||
} else {
|
||||
setConfig({ text: WatermarkTextBaseConfig });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getWatermarkConfig();
|
||||
}, [_refresh, getWatermarkConfig]);
|
||||
|
||||
return (
|
||||
<div className="univer-grid univer-gap-3 univer-text-sm">
|
||||
{/* Watermark type */}
|
||||
<div className="univer-grid univer-gap-2">
|
||||
<div className="univer-text-gray-400">Type</div>
|
||||
<Select
|
||||
value={watermarkType}
|
||||
options={[
|
||||
{ label: 'Text', value: IWatermarkTypeEnum.Text },
|
||||
{ label: 'Image', value: IWatermarkTypeEnum.Image },
|
||||
]}
|
||||
onChange={(v) => {
|
||||
setWatermarkType(v as IWatermarkTypeEnum);
|
||||
if (v === IWatermarkTypeEnum.Text) {
|
||||
handleConfigChange({ text: WatermarkTextBaseConfig }, IWatermarkTypeEnum.Text);
|
||||
} else if (v === IWatermarkTypeEnum.Image) {
|
||||
handleConfigChange({ image: WatermarkImageBaseConfig }, IWatermarkTypeEnum.Image);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-2">
|
||||
{watermarkType === IWatermarkTypeEnum.Text && <WatermarkTextSetting config={config?.text} onChange={(v) => handleConfigChange({ text: v })} />}
|
||||
{watermarkType === IWatermarkTypeEnum.Image && <WatermarkImageSetting config={config?.image} onChange={(v) => handleConfigChange({ image: v })} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
common/debugger/src/views/watermark/WatermarkPanelFooter.tsx
Normal file
57
common/debugger/src/views/watermark/WatermarkPanelFooter.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Button } from '@univerjs/design';
|
||||
import { IWatermarkTypeEnum } from '@univerjs/engine-render';
|
||||
import { IClipboardInterfaceService, ISidebarService, useDependency } from '@univerjs/ui';
|
||||
import { WatermarkService, WatermarkTextBaseConfig } from '@univerjs/watermark';
|
||||
|
||||
export function WatermarkPanelFooter() {
|
||||
const sidebarService = useDependency(ISidebarService);
|
||||
const watermarkService = useDependency(WatermarkService);
|
||||
const clipboardService = useDependency(IClipboardInterfaceService);
|
||||
|
||||
return (
|
||||
<div className="univer-flex univer-items-center univer-justify-between">
|
||||
<a
|
||||
className="univer-text-sm univer-text-primary-600 univer-underline"
|
||||
onClick={() => {
|
||||
watermarkService.updateWatermarkConfig({
|
||||
type: IWatermarkTypeEnum.Text,
|
||||
config: { text: WatermarkTextBaseConfig },
|
||||
});
|
||||
watermarkService.refresh();
|
||||
}}
|
||||
>
|
||||
Cancel Watermark
|
||||
</a>
|
||||
|
||||
<div className="univer-flex univer-items-center univer-gap-2">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const watermarkConfig = await watermarkService.getWatermarkConfig();
|
||||
let config;
|
||||
if (watermarkConfig?.type === IWatermarkTypeEnum.Text) {
|
||||
config = watermarkConfig.config.text;
|
||||
} else if (watermarkConfig?.type === IWatermarkTypeEnum.Image) {
|
||||
config = watermarkConfig.config.image;
|
||||
}
|
||||
clipboardService.writeText(JSON.stringify(config));
|
||||
}}
|
||||
>
|
||||
Copy Config
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const watermarkConfig = await watermarkService.getWatermarkConfig();
|
||||
if (watermarkConfig?.type === IWatermarkTypeEnum.Text && !watermarkConfig.config.text?.content) {
|
||||
watermarkService.deleteWatermarkConfig();
|
||||
} else if (watermarkConfig?.type === IWatermarkTypeEnum.Image && !watermarkConfig.config.image?.url) {
|
||||
watermarkService.deleteWatermarkConfig();
|
||||
}
|
||||
sidebarService.close();
|
||||
}}
|
||||
>
|
||||
Close Panel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
205
common/debugger/src/views/watermark/WatermarkTextSetting.tsx
Normal file
205
common/debugger/src/views/watermark/WatermarkTextSetting.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import type { ITextWatermarkConfig } from '@univerjs/engine-render';
|
||||
import { Checkbox, clsx, ColorPicker, Dropdown, Input, InputNumber, Select } from '@univerjs/design';
|
||||
import { BoldIcon, FontColorDoubleIcon, ItalicIcon } from '@univerjs/icons';
|
||||
|
||||
interface IWatermarkTextSettingProps {
|
||||
config?: ITextWatermarkConfig;
|
||||
onChange: (config: ITextWatermarkConfig) => void;
|
||||
}
|
||||
|
||||
export function WatermarkTextSetting(props: IWatermarkTextSettingProps) {
|
||||
const { config, onChange } = props;
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<div className="univer-grid univer-gap-2">
|
||||
<div className="univer-text-gray-400">Style Settings</div>
|
||||
|
||||
<div className="univer-mb-4 univer-grid univer-gap-1">
|
||||
<div>Content</div>
|
||||
<Input
|
||||
value={config.content}
|
||||
onChange={(val) => onChange({ ...config, content: val })}
|
||||
placeholder="Enter text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-4">
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Font Size</div>
|
||||
<InputNumber
|
||||
value={config.fontSize}
|
||||
max={72}
|
||||
min={12}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, fontSize: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Direction</div>
|
||||
<Select
|
||||
value={config.direction}
|
||||
options={[
|
||||
{ label: 'Left to Right', value: 'ltr' },
|
||||
{ label: 'Right to Left', value: 'rtl' },
|
||||
]}
|
||||
onChange={(v) => onChange({ ...config, direction: v as 'ltr' | 'rtl' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Opacity</div>
|
||||
<InputNumber
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.05}
|
||||
value={config.opacity}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, opacity: Number.parseFloat(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
univer-flex univer-justify-around univer-gap-4
|
||||
[&_a]:univer-flex [&_a]:univer-size-6 [&_a]:univer-items-center [&_a]:univer-justify-center
|
||||
[&_a]:univer-rounded
|
||||
`}
|
||||
>
|
||||
<Dropdown
|
||||
overlay={(
|
||||
<div className="univer-rounded-lg univer-p-4">
|
||||
<ColorPicker value={config.color} onChange={(val) => onChange({ ...config, color: val })} />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<a
|
||||
className={`
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FontColorDoubleIcon extend={{ colorChannel1: config.color ?? '#2c53f1' }} />
|
||||
</a>
|
||||
</Dropdown>
|
||||
<a
|
||||
className={clsx(`
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`, {
|
||||
'univer-bg-gray-200 dark:!univer-bg-gray-600': config.bold,
|
||||
})}
|
||||
onClick={() => { onChange({ ...config, bold: !config.bold }); }}
|
||||
>
|
||||
<BoldIcon />
|
||||
</a>
|
||||
<a
|
||||
className={clsx(`
|
||||
hover:univer-bg-gray-100
|
||||
dark:hover:!univer-bg-gray-700
|
||||
`, {
|
||||
'univer-bg-gray-200 dark:!univer-bg-gray-600': config.italic,
|
||||
})}
|
||||
onClick={() => { onChange({ ...config, italic: !config.italic }); }}
|
||||
>
|
||||
<ItalicIcon />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout */}
|
||||
<div className="univer-text-gray-400">Layout Settings</div>
|
||||
|
||||
<div className="univer-grid univer-gap-2 univer-text-center">
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Rotate</div>
|
||||
<InputNumber
|
||||
value={config.rotate}
|
||||
max={360}
|
||||
min={-360}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, rotate: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="univer-grid univer-flex-1 univer-gap-1">
|
||||
<div>Repeat</div>
|
||||
<Checkbox
|
||||
className="univer-justify-center univer-self-baseline"
|
||||
checked={config.repeat}
|
||||
onChange={(val) => onChange({ ...config, repeat: val as boolean })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Horizontal Spacing</div>
|
||||
<InputNumber
|
||||
value={config.spacingX}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, spacingX: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Vertical Spacing</div>
|
||||
<InputNumber
|
||||
value={config.spacingY}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, spacingY: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="univer-flex univer-gap-2">
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Horizontal Start Position</div>
|
||||
<InputNumber
|
||||
value={config.x}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, x: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="univer-grid univer-gap-1">
|
||||
<div>Vertical Start Position</div>
|
||||
<InputNumber
|
||||
value={config.y}
|
||||
min={0}
|
||||
onChange={(val) => {
|
||||
if (val != null) {
|
||||
onChange({ ...config, y: Number.parseInt(val.toString()) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
11
common/debugger/tailwind.config.ts
Normal file
11
common/debugger/tailwind.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import preset from '@univerjs-infra/shared/tailwind';
|
||||
|
||||
const config: Config = {
|
||||
presets: [preset],
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
8
common/debugger/tsconfig.json
Normal file
8
common/debugger/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@univerjs-infra/shared/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "lib/types"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
88
common/shared/bin/index.mts
Executable file
88
common/shared/bin/index.mts
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types
|
||||
|
||||
import type { IBuildOptions } from '../tsdown/types.ts';
|
||||
import process from 'node:process';
|
||||
import { buildPresetPackage, preparePresetPackage } from '../preset-build/index.ts';
|
||||
import { build } from '../tsdown/index.ts';
|
||||
|
||||
interface IPresetCliBuildOptions {
|
||||
cleanup?: boolean;
|
||||
tsdownConfigPath?: string;
|
||||
}
|
||||
|
||||
const argvs = process.argv.slice(2);
|
||||
const [command, ...args] = argvs;
|
||||
|
||||
function collectOptionValues(argv: string[], optionName: string) {
|
||||
const values: string[] = [];
|
||||
|
||||
for (let index = 0; index < argv.length; index++) {
|
||||
if (argv[index] !== optionName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith('--')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
values.push(...next.split(',').map((value) => value.trim()).filter(Boolean));
|
||||
index++;
|
||||
}
|
||||
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
if (command === 'preset') {
|
||||
const [presetCommand, ...presetArgs] = args;
|
||||
if (!presetCommand) {
|
||||
throw new Error('Missing preset subcommand. Expected "prepare" or "build".');
|
||||
}
|
||||
|
||||
if (presetCommand === 'prepare') {
|
||||
preparePresetPackage();
|
||||
} else if (presetCommand === 'build') {
|
||||
const options: IPresetCliBuildOptions = {};
|
||||
|
||||
if (presetArgs.includes('--cleanup')) {
|
||||
options.cleanup = true;
|
||||
}
|
||||
|
||||
const configIdx = presetArgs.indexOf('--config');
|
||||
if (configIdx !== -1 && presetArgs[configIdx + 1]) {
|
||||
options.tsdownConfigPath = presetArgs[configIdx + 1];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line antfu/no-top-level-await
|
||||
await buildPresetPackage(options);
|
||||
} else {
|
||||
throw new Error(`Unknown preset subcommand "${presetCommand}". Expected "prepare" or "build".`);
|
||||
}
|
||||
}
|
||||
|
||||
if (command === 'build') {
|
||||
const options: IBuildOptions = {};
|
||||
|
||||
if (args.includes('--skipUMD')) {
|
||||
options.skipUMD = true;
|
||||
}
|
||||
if (args.includes('--cleanup')) {
|
||||
options.cleanup = true;
|
||||
}
|
||||
if (args.includes('--nodeFirst')) {
|
||||
options.nodeFirst = true;
|
||||
}
|
||||
|
||||
const ignorePackages = collectOptionValues(args, '--ignorePackages');
|
||||
if (ignorePackages.length > 0) {
|
||||
options.ignorePackages = ignorePackages;
|
||||
}
|
||||
|
||||
const configIdx = args.indexOf('--config');
|
||||
if (configIdx !== -1 && args[configIdx + 1]) {
|
||||
options.tsdownConfigPath = args[configIdx + 1];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line antfu/no-top-level-await
|
||||
await build(options);
|
||||
}
|
||||
76
common/shared/esbuild/index.ts
Normal file
76
common/shared/esbuild/index.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
interface IResolveArgs {
|
||||
importer: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface ILoadArgs {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface IPluginBuild {
|
||||
onResolve(
|
||||
options: { filter: RegExp; namespace?: string },
|
||||
callback: (args: IResolveArgs) => unknown
|
||||
): void;
|
||||
onLoad(
|
||||
options: { filter: RegExp; namespace?: string },
|
||||
callback: (args: ILoadArgs) => unknown
|
||||
): void;
|
||||
}
|
||||
|
||||
interface IEsbuildPlugin {
|
||||
name: string;
|
||||
setup: (build: IPluginBuild) => void;
|
||||
}
|
||||
|
||||
export function ignoreGlobalCssPlugin(): IEsbuildPlugin {
|
||||
return {
|
||||
name: 'ignore-global-css',
|
||||
setup(build: IPluginBuild) {
|
||||
build.onResolve({ filter: /\/global\.css$/ }, (args: IResolveArgs) => {
|
||||
if (args.importer.includes('packages')) {
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: 'ignore-global-css',
|
||||
pluginData: {
|
||||
importer: args.importer,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
build.onLoad({ filter: /\/global\.css$/, namespace: 'ignore-global-css' }, () => {
|
||||
return { contents: '' };
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function removeClassnameNewlinesPlugin(): IEsbuildPlugin {
|
||||
return {
|
||||
name: 'remove-classname-newlines',
|
||||
setup(build: IPluginBuild) {
|
||||
build.onLoad({ filter: /\.(tsx)$/ }, (args: ILoadArgs) => {
|
||||
const source = fs.readFileSync(args.path, 'utf8');
|
||||
|
||||
const transformedSource = source.replace(
|
||||
/className\s*=\s*{([^}]*?)}/gs,
|
||||
(_match: string, classNameValue: string) => {
|
||||
const cleanedValue = classNameValue.replace(/`([^`]*?)`/gs, (_templateMatch: string, templateContent: string) => {
|
||||
return `\`${templateContent.replace(/\s*\n\s*/g, ' ').trim()}\``;
|
||||
});
|
||||
|
||||
return `className={${cleanedValue.trim()}}`;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
contents: transformedSource,
|
||||
loader: 'tsx',
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
80
common/shared/eslint/config.ts
Normal file
80
common/shared/eslint/config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Rules } from '@antfu/eslint-config';
|
||||
import type { ESLint, Linter } from 'eslint';
|
||||
import antfu from '@antfu/eslint-config';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import {
|
||||
facadePreset,
|
||||
headerPreset,
|
||||
indexEntryPreset,
|
||||
noBarrelImportPreset,
|
||||
penetratingPreset,
|
||||
specPreset,
|
||||
tailwindcssPreset,
|
||||
typescriptPreset,
|
||||
univerSourcePreset,
|
||||
} from './presets';
|
||||
import { baseRules } from './rules';
|
||||
|
||||
export interface IUniverEslintConfigOptions {
|
||||
ignores?: string[];
|
||||
header?: boolean;
|
||||
noFacadeImportsOutsideFacade?: {
|
||||
ignore?: string[];
|
||||
};
|
||||
rules?: Partial<Rules>;
|
||||
}
|
||||
|
||||
export const createUniverEslintConfig = (options: IUniverEslintConfigOptions = {}) => {
|
||||
const configs: Linter.Config[] = [
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks as unknown as ESLint.Plugin,
|
||||
},
|
||||
rules: {
|
||||
...baseRules,
|
||||
...options.rules,
|
||||
},
|
||||
},
|
||||
penetratingPreset(),
|
||||
typescriptPreset(),
|
||||
indexEntryPreset(),
|
||||
univerSourcePreset({
|
||||
noFacadeImportsOutsideFacade: options.noFacadeImportsOutsideFacade,
|
||||
}),
|
||||
facadePreset(),
|
||||
noBarrelImportPreset(),
|
||||
tailwindcssPreset(),
|
||||
specPreset(),
|
||||
];
|
||||
|
||||
if (options.header ?? true) {
|
||||
configs.push(headerPreset());
|
||||
}
|
||||
|
||||
return antfu(
|
||||
{
|
||||
ignores: options.ignores,
|
||||
stylistic: {
|
||||
indent: 4,
|
||||
semi: true,
|
||||
},
|
||||
e18e: false,
|
||||
regexp: false,
|
||||
react: true,
|
||||
pnpm: false,
|
||||
yaml: {
|
||||
overrides: {
|
||||
'yaml/indent': ['error', 4, { indicatorValueIndent: 2 }],
|
||||
},
|
||||
},
|
||||
markdown: false,
|
||||
typescript: true,
|
||||
formatters: {
|
||||
css: true,
|
||||
html: true,
|
||||
},
|
||||
},
|
||||
...configs
|
||||
);
|
||||
};
|
||||
14
common/shared/eslint/index.ts
Normal file
14
common/shared/eslint/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export { createUniverEslintConfig } from './config';
|
||||
export type { IUniverEslintConfigOptions } from './config';
|
||||
export {
|
||||
facadePreset,
|
||||
headerPreset,
|
||||
indexEntryPreset,
|
||||
noBarrelImportPreset,
|
||||
penetratingPreset,
|
||||
specPreset,
|
||||
tailwindcssPreset,
|
||||
typescriptPreset,
|
||||
univerSourcePreset,
|
||||
} from './presets';
|
||||
export { baseRules } from './rules';
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import { describe, it } from 'vitest';
|
||||
import noExternalImportsInFacade from './no-external-imports-in-facade';
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
});
|
||||
|
||||
describe('no-external-imports-in-facade', () => {
|
||||
it('explains the facade package-entry migration path', () => {
|
||||
ruleTester.run('no-external-imports-in-facade', noExternalImportsInFacade, {
|
||||
valid: [
|
||||
{
|
||||
code: 'import { PublicCommand } from "@univerjs/example";',
|
||||
filename: '/repo/packages/example/src/facade/f-univer.ts',
|
||||
},
|
||||
{
|
||||
code: 'import { FThing } from "./f-thing";',
|
||||
filename: '/repo/packages/example/src/facade/f-univer.ts',
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'import { PrivateCommand } from "../commands/private.command";',
|
||||
filename: '/repo/packages/example/src/facade/f-univer.ts',
|
||||
errors: [
|
||||
{
|
||||
message: 'Facade files are published as a separate package entry. Do not import package-internal runtime modules through "../commands/private.command"; it can duplicate module singletons when consumers import both the package root and the facade entry. Move the needed symbol to the package root export if necessary, then import it through the package name. Type-only imports should also use a public package entry.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
function getImportSourceValue(node: Rule.Node): string | null {
|
||||
if (!('source' in node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = (node as { source?: { value?: unknown } }).source;
|
||||
if (!source || typeof source.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.value;
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow imports from outside facade directory in facade files',
|
||||
},
|
||||
messages: {
|
||||
noExternalImports: 'Facade files are published as a separate package entry. Do not import package-internal runtime modules through "{{importPath}}"; it can duplicate module singletons when consumers import both the package root and the facade entry. Move the needed symbol to the package root export if necessary, then import it through the package name. Type-only imports should also use a public package entry.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = getRuleFilename(context);
|
||||
const normalizedPath = filename.split(path.sep).join('/');
|
||||
const isFacadeFile = normalizedPath.includes('/facade/');
|
||||
|
||||
if (!isFacadeFile) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const currentDir = path.dirname(filename);
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const importPath = getImportSourceValue(node);
|
||||
|
||||
if (!importPath || !importPath.startsWith('.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(currentDir, importPath).split(path.sep).join('/');
|
||||
|
||||
if (!resolvedPath.includes('/facade/')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noExternalImports',
|
||||
data: { importPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
137
common/shared/eslint/plugins/no-facade-imports-outside-facade.ts
Normal file
137
common/shared/eslint/plugins/no-facade-imports-outside-facade.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
interface IRuleOptions { ignore?: unknown[] }
|
||||
|
||||
function getImportSourceValue(node: Rule.Node): string | null {
|
||||
if (!('source' in node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = (node as { source?: { value?: unknown } }).source;
|
||||
if (!source || typeof source.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.value;
|
||||
}
|
||||
|
||||
function normalizePath(filePath: string): string {
|
||||
return filePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function shouldIgnoreFile(normalizedFilePath: string, ignorePaths: readonly unknown[]): boolean {
|
||||
return ignorePaths.some((ignorePath: unknown) => {
|
||||
if (typeof ignorePath !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedIgnorePath = normalizePath(ignorePath).replace(/^\.\//, '');
|
||||
|
||||
if (!normalizedIgnorePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalizedFilePath === normalizedIgnorePath
|
||||
|| normalizedFilePath.endsWith(`/${normalizedIgnorePath}`)
|
||||
|| normalizedFilePath.includes(normalizedIgnorePath);
|
||||
});
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow imports containing facade in non-facade files',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
ignore: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
noFacadeImports: 'Imports containing "facade" are not allowed in non-facade files: "{{importPath}}"',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = getRuleFilename(context);
|
||||
const normalizedPath = normalizePath(filename);
|
||||
const [ruleOptions = {} as IRuleOptions] = context.options as [IRuleOptions?];
|
||||
const ignorePaths = Array.isArray(ruleOptions.ignore) ? ruleOptions.ignore : [];
|
||||
|
||||
if (shouldIgnoreFile(normalizedPath, ignorePaths)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isInPackages = normalizedPath.includes('/packages/');
|
||||
const isInFacade = normalizedPath.includes('/facade/');
|
||||
|
||||
if (!isInPackages || isInFacade) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// get parent dir
|
||||
const parentDirMatch = normalizedPath.match(/\/([^/]+)\/packages\//);
|
||||
if (!parentDirMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const importPath = getImportSourceValue(node);
|
||||
|
||||
// Check if the import path contains 'facade'
|
||||
if (importPath?.includes('facade')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noFacadeImports',
|
||||
data: { importPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration(node: Rule.Node) {
|
||||
const exportPath = getImportSourceValue(node);
|
||||
if (exportPath?.includes('facade')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noFacadeImports',
|
||||
data: { importPath: exportPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportAllDeclaration(node: Rule.Node) {
|
||||
const exportPath = getImportSourceValue(node);
|
||||
if (exportPath?.includes('facade')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noFacadeImports',
|
||||
data: { importPath: exportPath },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { Rule, SourceCode } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
type ImportOrExportSpecifier = Rule.Node & {
|
||||
importKind?: 'type' | 'value';
|
||||
exportKind?: 'type' | 'value';
|
||||
};
|
||||
|
||||
function normalizePath(filePath: string): string {
|
||||
return filePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
function isSrcIndexFile(context: Rule.RuleContext): boolean {
|
||||
return normalizePath(getRuleFilename(context)).endsWith('/src/index.ts');
|
||||
}
|
||||
|
||||
function isInlineTypeSpecifier(specifier: ImportOrExportSpecifier, kind: 'import' | 'export'): boolean {
|
||||
return kind === 'import' ? specifier.importKind === 'type' : specifier.exportKind === 'type';
|
||||
}
|
||||
|
||||
function trimInlineTypeKeyword(specifierText: string): string {
|
||||
return specifierText.replace(/^type\s+/, '');
|
||||
}
|
||||
|
||||
function buildReplacement(
|
||||
sourceCode: SourceCode,
|
||||
node: Rule.Node,
|
||||
kind: 'import' | 'export'
|
||||
): string | null {
|
||||
const specifiers = (node as { specifiers?: ImportOrExportSpecifier[] }).specifiers;
|
||||
if (!specifiers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeSpecifiers = specifiers.filter((specifier) => isInlineTypeSpecifier(specifier, kind));
|
||||
const valueSpecifiers = specifiers.filter((specifier) => !isInlineTypeSpecifier(specifier, kind));
|
||||
if (typeSpecifiers.length === 0 || valueSpecifiers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeText = sourceCode.getText(node);
|
||||
const openingBraceIndex = nodeText.indexOf('{');
|
||||
const closingBraceIndex = nodeText.lastIndexOf('}');
|
||||
if (openingBraceIndex === -1 || closingBraceIndex === -1 || closingBraceIndex < openingBraceIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffixWithSemicolon = nodeText.slice(closingBraceIndex + 1);
|
||||
const hasSemicolon = suffixWithSemicolon.trimEnd().endsWith(';');
|
||||
const suffix = hasSemicolon ? suffixWithSemicolon.replace(/;\s*$/, '') : suffixWithSemicolon;
|
||||
const semicolon = hasSemicolon ? ';' : '';
|
||||
const valueSpecifierText = valueSpecifiers.map((specifier) => sourceCode.getText(specifier)).join(', ');
|
||||
const typeSpecifierText = typeSpecifiers
|
||||
.map((specifier) => trimInlineTypeKeyword(sourceCode.getText(specifier)))
|
||||
.join(', ');
|
||||
|
||||
return `${kind} { ${valueSpecifierText} }${suffix}${semicolon}\n${kind} type { ${typeSpecifierText} }${suffix}${semicolon}`;
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Disallow mixing inline type specifiers with value specifiers in src/index.ts',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
noMixedExportSpecifiers: 'Do not mix type and value export specifiers in src/index.ts.',
|
||||
noMixedImportSpecifiers: 'Do not mix type and value import specifiers in src/index.ts.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
if (!isSrcIndexFile(context)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const replacement = buildReplacement(context.sourceCode, node, 'import');
|
||||
if (!replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noMixedImportSpecifiers',
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, replacement);
|
||||
},
|
||||
});
|
||||
},
|
||||
ExportNamedDeclaration(node: Rule.Node) {
|
||||
const replacement = buildReplacement(context.sourceCode, node, 'export');
|
||||
if (!replacement) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noMixedExportSpecifiers',
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node, replacement);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
95
common/shared/eslint/plugins/no-self-package-imports.ts
Normal file
95
common/shared/eslint/plugins/no-self-package-imports.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import path from 'node:path';
|
||||
|
||||
function getImportSourceValue(node: Rule.Node): string | null {
|
||||
if (!('source' in node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = (node as { source?: { value?: unknown } }).source;
|
||||
if (!source || typeof source.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return source.value;
|
||||
}
|
||||
|
||||
function getRuleFilename(context: Rule.RuleContext): string {
|
||||
const filenameFromProperty = (context as { filename?: unknown }).filename;
|
||||
|
||||
if (typeof filenameFromProperty === 'string' && filenameFromProperty) {
|
||||
return filenameFromProperty;
|
||||
}
|
||||
|
||||
const getFilename = (context as { getFilename?: () => string }).getFilename;
|
||||
return typeof getFilename === 'function' ? getFilename.call(context) : '';
|
||||
}
|
||||
|
||||
const rule: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow self package imports in packages directory except facade',
|
||||
},
|
||||
messages: {
|
||||
noSelfImport: 'Package cannot import itself: "{{importPath}}" in {{packageName}}',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = getRuleFilename(context);
|
||||
const normalizedPath = filename.split(path.sep).join('/');
|
||||
|
||||
const isInPackages = normalizedPath.includes('/packages/');
|
||||
const isInFacade = normalizedPath.includes('/facade/');
|
||||
|
||||
if (!isInPackages || isInFacade) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// get parent dir
|
||||
const parentDirMatch = normalizedPath.match(/\/([^/]+)\/packages\//);
|
||||
if (!parentDirMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parentDir = parentDirMatch[1];
|
||||
const packagePrefix = parentDir === 'univer'
|
||||
? '@univerjs/' :
|
||||
parentDir === 'univer-pro'
|
||||
? '@univerjs-pro/' :
|
||||
null;
|
||||
|
||||
if (!packagePrefix) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// get package name
|
||||
const packageMatch = normalizedPath.match(/\/packages\/([^/]+)/);
|
||||
if (!packageMatch) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const packageName = packageMatch[1];
|
||||
const possiblePackageName = `${packagePrefix}${packageName}`;
|
||||
|
||||
return {
|
||||
ImportDeclaration(node: Rule.Node) {
|
||||
const importPath = getImportSourceValue(node);
|
||||
|
||||
if (importPath && importPath === possiblePackageName) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noSelfImport',
|
||||
data: {
|
||||
importPath,
|
||||
packageName: importPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
296
common/shared/eslint/presets.ts
Normal file
296
common/shared/eslint/presets.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import type { ESLint, Linter } from 'eslint';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { fixupPluginRules } from '@eslint/compat';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import eslintPluginBetterTailwindcss from 'eslint-plugin-better-tailwindcss';
|
||||
import header from 'eslint-plugin-header';
|
||||
import barrel from 'eslint-plugin-no-barrel-import';
|
||||
import penetrating from 'eslint-plugin-no-penetrating-import';
|
||||
import noExternalImportsInFacade from './plugins/no-external-imports-in-facade';
|
||||
import noFacadeImportsOutsideFacade from './plugins/no-facade-imports-outside-facade';
|
||||
import noMixedTypeSpecifiersInIndex from './plugins/no-mixed-type-specifiers-in-index';
|
||||
import noSelfPackageImports from './plugins/no-self-package-imports';
|
||||
|
||||
const univerPlugin = {
|
||||
rules: {
|
||||
'no-external-imports-in-facade': noExternalImportsInFacade,
|
||||
'no-self-package-imports': noSelfPackageImports,
|
||||
'no-facade-imports-outside-facade': noFacadeImportsOutsideFacade,
|
||||
'no-mixed-type-specifiers-in-index': noMixedTypeSpecifiersInIndex,
|
||||
},
|
||||
} satisfies ESLint.Plugin;
|
||||
|
||||
/**
|
||||
* TypeScript preset configuration for ESLint.
|
||||
* Applies TypeScript-specific rules and custom Univer rules.
|
||||
*
|
||||
* @returns ESLint configuration object for TypeScript files
|
||||
*/
|
||||
export const typescriptPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/*.{ts,tsx,mts,cts}'],
|
||||
rules: {
|
||||
'ts/naming-convention': [
|
||||
'warn',
|
||||
// Interfaces' names should start with a capital 'I'.
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: {
|
||||
regex: '^I[A-Z0-9]',
|
||||
match: true,
|
||||
},
|
||||
},
|
||||
// Private fields of a class should start with an underscore '_'.
|
||||
{
|
||||
selector: ['classMethod', 'classProperty'],
|
||||
modifiers: ['private'],
|
||||
format: ['camelCase'],
|
||||
leadingUnderscore: 'require',
|
||||
},
|
||||
],
|
||||
// 'ts/consistent-type-exports': 'warn',
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Index entry preset configuration for ESLint.
|
||||
* Keeps type-only imports and exports separate from value imports and exports.
|
||||
*
|
||||
* @returns ESLint configuration object for package entry files
|
||||
*/
|
||||
export const indexEntryPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/src/index.ts'],
|
||||
rules: {
|
||||
'univer/no-mixed-type-specifiers-in-index': 'error',
|
||||
},
|
||||
plugins: {
|
||||
univer: univerPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Univer source code preset configuration.
|
||||
* Enforces package structure rules for source files.
|
||||
*
|
||||
* @returns ESLint configuration object for Univer source files
|
||||
*/
|
||||
export const univerSourcePreset = (options: {
|
||||
noFacadeImportsOutsideFacade?: {
|
||||
ignore?: string[];
|
||||
};
|
||||
} = {}): Linter.Config => {
|
||||
return {
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
ignores: [
|
||||
'**/__tests__/**/*',
|
||||
'**/*.spec.ts',
|
||||
'**/*.test.ts',
|
||||
],
|
||||
rules: {
|
||||
'univer/no-self-package-imports': 'error',
|
||||
'univer/no-facade-imports-outside-facade': ['error', {
|
||||
ignore: options.noFacadeImportsOutsideFacade?.ignore ?? [],
|
||||
}],
|
||||
},
|
||||
plugins: {
|
||||
univer: univerPlugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Facade preset configuration for ESLint.
|
||||
* Enforces strict typing and import rules for facade files.
|
||||
*
|
||||
* @returns ESLint configuration object for facade files
|
||||
*/
|
||||
export const facadePreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/src/facade/**/*.ts'],
|
||||
ignores: [
|
||||
'**/__tests__/**/*',
|
||||
'**/*.spec.ts',
|
||||
'**/*.test.ts',
|
||||
],
|
||||
rules: {
|
||||
'ts/explicit-function-return-type': 'error',
|
||||
'univer/no-external-imports-in-facade': 'error',
|
||||
},
|
||||
plugins: {
|
||||
univer: univerPlugin,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Tailwind CSS preset configuration for ESLint.
|
||||
* Enforces Tailwind CSS class usage and formatting rules.
|
||||
*
|
||||
* @returns ESLint configuration object for files using Tailwind CSS
|
||||
*/
|
||||
export const tailwindcssPreset = (): Linter.Config => {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const lineBreakStyle = isWindows ? 'windows' : 'unix';
|
||||
|
||||
return {
|
||||
files: ['**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'better-tailwindcss': eslintPluginBetterTailwindcss,
|
||||
},
|
||||
settings: {
|
||||
'better-tailwindcss': {
|
||||
tailwindConfig: path.resolve(__dirname, '../tailwind/tailwind.config.ts'),
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// enable all recommended rules to error
|
||||
...eslintPluginBetterTailwindcss.configs['recommended-error'].rules,
|
||||
'better-tailwindcss/enforce-consistent-line-wrapping': ['error', {
|
||||
printWidth: 120,
|
||||
group: 'newLine',
|
||||
lineBreakStyle,
|
||||
}],
|
||||
'better-tailwindcss/no-unknown-classes': 'off',
|
||||
'better-tailwindcss/no-conflicting-classes': 'off',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Test/spec files preset configuration for ESLint.
|
||||
* Relaxed rules for test files to allow more flexible testing patterns.
|
||||
*
|
||||
* @returns ESLint configuration object for test files
|
||||
*/
|
||||
export const specPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: [
|
||||
'**/*.spec.ts',
|
||||
'**/__tests__/**/*.ts',
|
||||
],
|
||||
rules: {
|
||||
'ts/explicit-function-return-type': 'off',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Penetrating import preset configuration for ESLint.
|
||||
* Prevents direct imports from internal package directories.
|
||||
*
|
||||
* @returns ESLint configuration object for preventing penetrating imports
|
||||
*/
|
||||
export const penetratingPreset = (): Linter.Config => {
|
||||
return {
|
||||
// Not penetrating for source files
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
plugins: {
|
||||
penetrating: fixupPluginRules(penetrating),
|
||||
},
|
||||
ignores: [
|
||||
'**/__tests__/**/*',
|
||||
'**/__testing__/**/*',
|
||||
'examples/**/*',
|
||||
],
|
||||
rules: {
|
||||
'penetrating/no-penetrating-import': 2,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* No barrel import preset configuration for ESLint.
|
||||
* Prevents barrel imports and enforces code complexity limits.
|
||||
*
|
||||
* @returns ESLint configuration object for preventing barrel imports
|
||||
*/
|
||||
export const noBarrelImportPreset = (): Linter.Config => {
|
||||
return {
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
ignores: [
|
||||
'packages/engine-render/src/components/docs/**/*.ts',
|
||||
'**/*.tsx',
|
||||
'**/*.d.ts',
|
||||
'**/tsdown.config.ts',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.tsx',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.tsx',
|
||||
], // do not check test files
|
||||
plugins: {
|
||||
barrel: fixupPluginRules(barrel),
|
||||
},
|
||||
rules: {
|
||||
'barrel/no-barrel-import': 2,
|
||||
complexity: ['warn', { max: 20 }],
|
||||
'max-lines-per-function': ['warn', 80],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Header preset configuration for ESLint.
|
||||
* Enforces Apache License 2.0 header in package and preset source files.
|
||||
*
|
||||
* @returns ESLint configuration object for enforcing file headers
|
||||
*/
|
||||
export const headerPreset = (): Linter.Config => {
|
||||
header.rules.header.meta.schema = false;
|
||||
|
||||
return {
|
||||
files: ['{packages,presets}/**/*.{ts,tsx}'],
|
||||
ignores: [
|
||||
'**/*.d.ts',
|
||||
'**/vitest.config.ts',
|
||||
'**/vitest.workspace.ts',
|
||||
],
|
||||
plugins: {
|
||||
header: fixupPluginRules(header),
|
||||
},
|
||||
rules: {
|
||||
'header/header': [
|
||||
2,
|
||||
'block',
|
||||
[
|
||||
'*',
|
||||
' * Copyright 2023-present DreamNum Co., Ltd.',
|
||||
' *',
|
||||
' * Licensed under the Apache License, Version 2.0 (the "License");',
|
||||
' * you may not use this file except in compliance with the License.',
|
||||
' * You may obtain a copy of the License at',
|
||||
' *',
|
||||
' * http://www.apache.org/licenses/LICENSE-2.0',
|
||||
' *',
|
||||
' * Unless required by applicable law or agreed to in writing, software',
|
||||
' * distributed under the License is distributed on an "AS IS" BASIS,',
|
||||
' * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.',
|
||||
' * See the License for the specific language governing permissions and',
|
||||
' * limitations under the License.',
|
||||
' ',
|
||||
],
|
||||
2,
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
201
common/shared/eslint/rules.ts
Normal file
201
common/shared/eslint/rules.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import type { Rules } from '@antfu/eslint-config';
|
||||
|
||||
/**
|
||||
* Base ESLint rules configuration for Univer project.
|
||||
* These rules apply to all TypeScript and JavaScript files.
|
||||
*/
|
||||
const qualityRules: Partial<Rules> = {
|
||||
// Code style and formatting rules
|
||||
curly: ['error', 'multi-line'],
|
||||
'antfu/if-newline': 'off',
|
||||
'no-param-reassign': ['warn'],
|
||||
'eol-last': ['error', 'always'],
|
||||
'no-empty-function': 'off',
|
||||
};
|
||||
|
||||
const typescriptRules: Partial<Rules> = {
|
||||
// TypeScript specific rules
|
||||
'ts/no-explicit-any': 'warn',
|
||||
'ts/no-redeclare': 'off',
|
||||
'ts/method-signature-style': 'off',
|
||||
};
|
||||
|
||||
const styleRules: Partial<Rules> = {
|
||||
// Style and formatting rules
|
||||
'style/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
|
||||
'style/brace-style': ['warn', '1tbs', { allowSingleLine: true }],
|
||||
'style/jsx-first-prop-new-line': ['warn', 'multiline'],
|
||||
'style/arrow-parens': ['error', 'always'],
|
||||
'style/spaced-comment': 'off',
|
||||
'style/indent-binary-ops': 'off',
|
||||
'style/operator-linebreak': 'off',
|
||||
'style/indent': ['error', 4, {
|
||||
ObjectExpression: 'first',
|
||||
SwitchCase: 1,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
'style/quotes': ['warn', 'single', { avoidEscape: true }],
|
||||
'style/jsx-closing-tag-location': 'warn',
|
||||
'style/jsx-curly-newline': ['warn', { multiline: 'forbid', singleline: 'forbid' }],
|
||||
'style/jsx-wrap-multilines': 'warn',
|
||||
'style/quote-props': ['warn', 'as-needed'],
|
||||
'style/jsx-curly-brace-presence': 'warn',
|
||||
'style/multiline-ternary': 'warn',
|
||||
|
||||
// Comma and punctuation rules
|
||||
'style/comma-dangle': ['error', {
|
||||
arrays: 'always-multiline',
|
||||
objects: 'always-multiline',
|
||||
imports: 'always-multiline',
|
||||
exports: 'always-multiline',
|
||||
enums: 'always-multiline',
|
||||
functions: 'never',
|
||||
}],
|
||||
};
|
||||
|
||||
const filenameRules: Partial<Rules> = {
|
||||
// Filename conventions
|
||||
'unicorn/filename-case': [
|
||||
'error',
|
||||
{
|
||||
cases: {
|
||||
kebabCase: true,
|
||||
pascalCase: true,
|
||||
},
|
||||
ignore: [
|
||||
'^README-(\w+)?\\.md$',
|
||||
'^[a-z]{2}-[A-Z]{2}\.ts$',
|
||||
'^__tests__$',
|
||||
'^__testing__$',
|
||||
'^FUNDING.yml$',
|
||||
'^bug_report.yml$',
|
||||
'^bug_report.zh-CN.yml$',
|
||||
'^feature_request.yml$',
|
||||
'^feature_request.zh-CN.yml$',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const reactRules: Partial<Rules> = {
|
||||
// JSX and React rules
|
||||
'style/jsx-self-closing-comp': ['error', {
|
||||
component: true,
|
||||
html: true,
|
||||
}],
|
||||
'react-refresh/only-export-components': 'off',
|
||||
'react/no-unstable-context-value': 'warn',
|
||||
'react/no-unstable-default-props': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-create-ref': 'warn',
|
||||
'react/rules-of-hooks': 'off',
|
||||
'react/static-components': 'off',
|
||||
'react/dom-no-render': 'off',
|
||||
'react/unsupported-syntax': 'off',
|
||||
'react/jsx-no-key-after-spread': 'off',
|
||||
'react/use-memo': 'warn',
|
||||
'react-hooks/static-components': 'off',
|
||||
'react-hooks/preserve-manual-memoization': 'off',
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
'react-hooks/immutability': 'warn',
|
||||
'react-hooks/use-memo': 'warn',
|
||||
'react-hooks/purity': 'warn',
|
||||
'react-hooks/globals': 'warn',
|
||||
};
|
||||
|
||||
const importRules: Partial<Rules> = {
|
||||
// Import and export rules
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
allowSeparatedGroups: false,
|
||||
ignoreCase: true,
|
||||
ignoreDeclarationSort: true,
|
||||
ignoreMemberSort: false,
|
||||
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
|
||||
},
|
||||
],
|
||||
'perfectionist/sort-imports': 'warn',
|
||||
'perfectionist/sort-named-imports': 'off',
|
||||
'perfectionist/sort-named-exports': 'warn',
|
||||
|
||||
// Code quality rules
|
||||
'accessor-pairs': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'jsdoc/tag-lines': 'off',
|
||||
|
||||
// Restricted imports for consistency
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'clsx',
|
||||
message: 'Please use `import { clsx } from \'@univerjs/design\'` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const disabledDebatableRules: Partial<Rules> = {
|
||||
// IMPORTANT: To ensure compatibility, some features of React 19 will be disabled.
|
||||
'react/no-forward-ref': 'off',
|
||||
'react/no-context-provider': 'off',
|
||||
'react-dom/no-render': 'off',
|
||||
'react/no-use-context': 'off',
|
||||
|
||||
// Debatable rules - currently disabled
|
||||
'test/prefer-lowercase-title': 'off',
|
||||
'antfu/top-level-function': 'off',
|
||||
'unicorn/no-new-array': 'off',
|
||||
'unicorn/prefer-includes': 'off',
|
||||
'prefer-arrow-callback': 'off',
|
||||
'no-restricted-globals': 'off',
|
||||
'unicorn/prefer-string-starts-ends-with': 'warn',
|
||||
};
|
||||
|
||||
const migrationWarningRules: Partial<Rules> = {
|
||||
// Compatibility rules for legacy code - set to warn for gradual migration
|
||||
'unused-imports/no-unused-vars': 'warn',
|
||||
'ts/no-restricted-types': 'warn',
|
||||
'ts/no-wrapper-object-types': 'warn',
|
||||
'ts/no-empty-object-type': 'warn',
|
||||
'ts/no-unsafe-function-type': 'warn',
|
||||
'ts/no-unused-expressions': 'warn',
|
||||
'unicorn/prefer-dom-node-text-content': 'warn',
|
||||
'unicorn/prefer-number-properties': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'eslint-comments/no-unlimited-disable': 'off',
|
||||
'ts/prefer-ts-expect-error': 'off',
|
||||
'ts/ban-ts-comment': 'off',
|
||||
'ts/no-duplicate-enum-values': 'off',
|
||||
'no-cond-assign': 'warn',
|
||||
'ts/no-use-before-define': 'warn',
|
||||
'test/no-identical-title': 'warn',
|
||||
'ts/no-non-null-asserted-optional-chain': 'warn',
|
||||
'no-restricted-syntax': 'warn',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'ts/no-this-alias': 'warn',
|
||||
'prefer-promise-reject-errors': 'warn',
|
||||
'no-new': 'warn',
|
||||
'unicorn/error-message': 'warn',
|
||||
'ts/prefer-literal-enum-member': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'ts/no-import-type-side-effects': 'warn',
|
||||
'unicorn/number-literal-case': 'warn',
|
||||
'unicorn/prefer-type-error': 'warn',
|
||||
};
|
||||
|
||||
export const baseRules: Partial<Rules> = {
|
||||
...qualityRules,
|
||||
...typescriptRules,
|
||||
...styleRules,
|
||||
...filenameRules,
|
||||
...reactRules,
|
||||
...importRules,
|
||||
...disabledDebatableRules,
|
||||
...migrationWarningRules,
|
||||
};
|
||||
3
common/shared/eslint/third-party.d.ts
vendored
Normal file
3
common/shared/eslint/third-party.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module 'eslint-plugin-header';
|
||||
declare module 'eslint-plugin-no-barrel-import';
|
||||
declare module 'eslint-plugin-no-penetrating-import';
|
||||
61
common/shared/locale/index.ts
Normal file
61
common/shared/locale/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const LOCALE_FILE_NAME_PATTERN = /^[a-z]{2}-[A-Z]{2}\.ts$/;
|
||||
|
||||
export interface IDiscoverUniverUiLocalesOptions {
|
||||
rootDir?: string;
|
||||
uiPackageDir?: string;
|
||||
}
|
||||
|
||||
function getCandidateUiPackageDirs(rootDir: string): string[] {
|
||||
const dirs: string[] = [];
|
||||
let current = path.resolve(rootDir);
|
||||
|
||||
while (true) {
|
||||
dirs.push(
|
||||
path.join(current, 'node_modules/@univerjs/ui'),
|
||||
path.join(current, 'packages/ui'),
|
||||
path.join(current, 'submodules/univer/packages/ui')
|
||||
);
|
||||
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function resolveUniverUiPackageDir(options: IDiscoverUniverUiLocalesOptions): string {
|
||||
if (options.uiPackageDir) {
|
||||
return path.resolve(options.uiPackageDir);
|
||||
}
|
||||
|
||||
const rootDir = options.rootDir ?? process.cwd();
|
||||
const uiPackageDir = getCandidateUiPackageDirs(rootDir).find((candidate) => existsSync(path.join(candidate, 'src/locale')));
|
||||
|
||||
if (!uiPackageDir) {
|
||||
throw new Error(`Cannot find @univerjs/ui src/locale from ${path.resolve(rootDir)}`);
|
||||
}
|
||||
|
||||
return uiPackageDir;
|
||||
}
|
||||
|
||||
export function discoverUniverUiLocales(options: IDiscoverUniverUiLocalesOptions = {}): string[] {
|
||||
const uiPackageDir = resolveUniverUiPackageDir(options);
|
||||
const localeDir = path.join(uiPackageDir, 'src/locale');
|
||||
|
||||
if (!existsSync(localeDir)) {
|
||||
throw new Error(`Cannot find @univerjs/ui locale directory: ${localeDir}`);
|
||||
}
|
||||
|
||||
return readdirSync(localeDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && LOCALE_FILE_NAME_PATTERN.test(entry.name))
|
||||
.map((entry) => path.basename(entry.name, '.ts'))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
59
common/shared/package.json
Normal file
59
common/shared/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@univerjs-infra/shared",
|
||||
"type": "module",
|
||||
"version": "1.0.0-rc.0",
|
||||
"private": true,
|
||||
"description": "Some infrastructures for univerjs",
|
||||
"author": "DreamNum <developer@univer.ai>",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/dream-num/univer",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dream-num/univer.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"exports": {
|
||||
"./eslint": "./eslint/index.ts",
|
||||
"./tsconfigs/*": "./tsconfigs/*.json",
|
||||
"./vitest": "./vitest/index.ts",
|
||||
"./esbuild": "./esbuild/index.ts",
|
||||
"./tailwind": "./tailwind/tailwind.config.ts",
|
||||
"./postcss": "./postcss/postcss.config.mjs",
|
||||
"./tsdown": "./tsdown/index.ts",
|
||||
"./locale": "./locale/index.ts",
|
||||
"./preset-build": "./preset-build/index.ts"
|
||||
},
|
||||
"main": "bin/index.mts",
|
||||
"bin": {
|
||||
"univer-cli": "bin/index.mts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsdown/css": "^0.23.0",
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@vitest/coverage-istanbul": "^5.0.0",
|
||||
"autoprefixer": "^10.6.0",
|
||||
"eslint-plugin-better-tailwindcss": "^4.7.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-no-barrel-import": "^0.0.2",
|
||||
"eslint-plugin-no-penetrating-import": "^0.0.1",
|
||||
"happy-dom": "20.14.5",
|
||||
"javascript-obfuscator": "^5.7.0",
|
||||
"postcss-preset-env": "^11.5.3",
|
||||
"postcss-replace": "^2.0.1",
|
||||
"sort-keys": "^6.0.1",
|
||||
"tailwind-scrollbar": "^3",
|
||||
"tailwindcss": "3.4.18",
|
||||
"tsdown": "^0.23.0",
|
||||
"unplugin-vue": "^7.2.0",
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@univerjs/icons": "1.38.0",
|
||||
"@univerjs/icons-svg": "^1.38.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vue-tsc": "^3.3.11"
|
||||
}
|
||||
}
|
||||
26
common/shared/postcss/postcss.config.mjs
Normal file
26
common/shared/postcss/postcss.config.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import env from 'postcss-preset-env';
|
||||
import replace from 'postcss-replace';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: [
|
||||
tailwindcss,
|
||||
autoprefixer,
|
||||
env({
|
||||
features: {
|
||||
'color-functional-notation': true,
|
||||
'hexadecimal-alpha-notation': true,
|
||||
},
|
||||
}),
|
||||
replace({
|
||||
pattern: /(--tw|\*)/g,
|
||||
data: {
|
||||
'--tw': '--univer-tw',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
common/shared/preset-build/constants.ts
Normal file
1
common/shared/preset-build/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const PRESET_LOCALE_SOURCE_DIR = 'src/locales';
|
||||
49
common/shared/preset-build/entries.ts
Normal file
49
common/shared/preset-build/entries.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { IEntryConfig } from '../tsdown/types.ts';
|
||||
import { existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { PRESET_LOCALE_SOURCE_DIR } from './constants.ts';
|
||||
|
||||
function getPresetLocaleEntries(packageDir: string): IEntryConfig[] {
|
||||
const localeDir = path.join(packageDir, PRESET_LOCALE_SOURCE_DIR);
|
||||
|
||||
if (!existsSync(localeDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const entries: IEntryConfig[] = [];
|
||||
|
||||
for (const fileName of readdirSync(localeDir).sort((left, right) => left.localeCompare(right))) {
|
||||
const fullPath = path.join(localeDir, fileName);
|
||||
|
||||
if (statSync(fullPath).isDirectory() || !fileName.endsWith('.ts') || !fileName.includes('-')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
key: `locales/${fileName.replace(/\.ts$/, '')}`,
|
||||
path: fullPath,
|
||||
type: 'locale',
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function getPresetModuleEntries(packageDir: string): IEntryConfig[] {
|
||||
const entries: IEntryConfig[] = [{
|
||||
key: 'index',
|
||||
path: path.join(packageDir, 'src/index.ts'),
|
||||
type: 'index',
|
||||
}];
|
||||
|
||||
const workerEntry = path.join(packageDir, 'src/worker.ts');
|
||||
if (existsSync(workerEntry)) {
|
||||
entries.push({
|
||||
key: 'worker',
|
||||
path: workerEntry,
|
||||
type: 'worker',
|
||||
});
|
||||
}
|
||||
|
||||
return [...entries, ...getPresetLocaleEntries(packageDir)];
|
||||
}
|
||||
130
common/shared/preset-build/index.ts
Normal file
130
common/shared/preset-build/index.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { TModuleFormat } from '../tsdown/configs/module.ts';
|
||||
import type { IPackageJson } from '../tsdown/types.ts';
|
||||
import type { IPresetBuildOptions, IPresetPackageJson } from './types.ts';
|
||||
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import { mergeConfig, build as tsdownBuild } from 'tsdown';
|
||||
import { createModuleConfig } from '../tsdown/configs/module.ts';
|
||||
import { BUILD_OUTPUT_DIRECTORIES, CLEANUP_DIRECTORIES } from '../tsdown/constants.ts';
|
||||
import { createBaseConfig, createInputPlugins } from '../tsdown/utils/base-config.ts';
|
||||
import { getPresetModuleEntries } from './entries.ts';
|
||||
import { generatePresetLocales } from './locale.ts';
|
||||
import { buildPresetStyles } from './style.ts';
|
||||
|
||||
export { generatePresetLocales } from './locale.ts';
|
||||
export type { IPresetBuildOptions } from './types.ts';
|
||||
|
||||
function readPackageJson(packageDir: string): IPresetPackageJson {
|
||||
return JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as IPresetPackageJson;
|
||||
}
|
||||
|
||||
function getExternalPackages(packageJson: IPresetPackageJson) {
|
||||
return [...new Set([
|
||||
...Object.keys(packageJson.dependencies ?? {}),
|
||||
...Object.keys(packageJson.peerDependencies ?? {}),
|
||||
'react',
|
||||
'react-dom',
|
||||
'rxjs',
|
||||
])]
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.flatMap((packageName) => [packageName, `${packageName}/*`]);
|
||||
}
|
||||
|
||||
async function loadUserConfig(options: IPresetBuildOptions, packageDir: string) {
|
||||
if (!options.tsdownConfigPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configPath = path.resolve(packageDir, options.tsdownConfigPath);
|
||||
let userConfig = (await import(pathToFileURL(configPath).href)).default;
|
||||
|
||||
if (typeof userConfig === 'function') {
|
||||
userConfig = await userConfig();
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
}
|
||||
|
||||
function mergeUserConfig(configs: any[], userConfig: any) {
|
||||
if (!userConfig) {
|
||||
return configs;
|
||||
}
|
||||
|
||||
return configs.map((config) => mergeConfig(config, userConfig));
|
||||
}
|
||||
|
||||
export function createPresetModuleEntryGroups(entries: ReturnType<typeof getPresetModuleEntries>) {
|
||||
const primaryEntries = entries.filter((entry) => entry.type === 'index' || entry.type === 'locale');
|
||||
const isolatedEntries = entries.filter((entry) => entry.type !== 'index' && entry.type !== 'locale');
|
||||
|
||||
return [
|
||||
...(primaryEntries.length > 0 ? [primaryEntries] : []),
|
||||
...isolatedEntries.map((entry) => [entry]),
|
||||
];
|
||||
}
|
||||
|
||||
export function removePresetOutputs(packageDir = process.cwd()) {
|
||||
for (const dir of CLEANUP_DIRECTORIES) {
|
||||
const targetDir = path.resolve(packageDir, dir);
|
||||
|
||||
if (existsSync(targetDir)) {
|
||||
rmSync(targetDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function preparePresetPackage(packageDir = process.cwd()) {
|
||||
const packageJson = readPackageJson(packageDir);
|
||||
|
||||
if (packageJson.name === '@univerjs/presets') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return generatePresetLocales({ packageDir });
|
||||
}
|
||||
|
||||
export async function buildPresetPackage(options: IPresetBuildOptions = {}) {
|
||||
const packageDir = process.cwd();
|
||||
const packageJson = readPackageJson(packageDir);
|
||||
|
||||
if (options.cleanup) {
|
||||
removePresetOutputs(packageDir);
|
||||
}
|
||||
|
||||
const externalPackages = getExternalPackages(packageJson);
|
||||
const baseConfig = createBaseConfig({
|
||||
entries: [],
|
||||
externalPackages,
|
||||
facadeExternalPackages: externalPackages,
|
||||
packageDir,
|
||||
packageJson: packageJson as IPackageJson,
|
||||
plugins: [],
|
||||
});
|
||||
const plugins = createInputPlugins(packageDir);
|
||||
const userConfig = await loadUserConfig(options, packageDir);
|
||||
const moduleFormats: TModuleFormat[] = ['esm', 'cjs'];
|
||||
const moduleConfigs = createPresetModuleEntryGroups(getPresetModuleEntries(packageDir)).flatMap((entries) => {
|
||||
return moduleFormats.map((format) => createModuleConfig({
|
||||
baseConfig,
|
||||
enableObfuscation: false,
|
||||
entries,
|
||||
externalPackages,
|
||||
facadeExternalPackages: externalPackages,
|
||||
format,
|
||||
outDir: BUILD_OUTPUT_DIRECTORIES[format],
|
||||
packageDir,
|
||||
plugins,
|
||||
}));
|
||||
});
|
||||
|
||||
for (const config of mergeUserConfig(moduleConfigs, userConfig)) {
|
||||
await tsdownBuild(config);
|
||||
}
|
||||
|
||||
await buildPresetStyles({
|
||||
packageDir,
|
||||
packageJson,
|
||||
});
|
||||
}
|
||||
98
common/shared/preset-build/locale.ts
Normal file
98
common/shared/preset-build/locale.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { IGeneratePresetLocalesOptions, IPresetPackageJson } from './types.ts';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { discoverUniverUiLocales } from '../locale/index.ts';
|
||||
import { PRESET_LOCALE_SOURCE_DIR } from './constants.ts';
|
||||
|
||||
function readPackageJson(packageDir: string): IPresetPackageJson {
|
||||
return JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as IPresetPackageJson;
|
||||
}
|
||||
|
||||
function convertImportNameFromPackageName(name: string) {
|
||||
return name
|
||||
.replace(/^@univerjs(?:-[^/]+)?\//, 'univerjs')
|
||||
.replace(/[^a-zA-Z0-9_$]/g, '');
|
||||
}
|
||||
|
||||
function compareStrings(left: string, right: string) {
|
||||
if (left < right) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (left > right) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getDependencyNames(pkg: IPresetPackageJson) {
|
||||
return Object.keys(pkg.dependencies ?? {})
|
||||
.filter((name) => name.startsWith('@univerjs'))
|
||||
.sort(compareStrings);
|
||||
}
|
||||
|
||||
function getDependencyLocaleSubpath(packageDir: string, dependencyName: string, locale: string) {
|
||||
const dependencyDir = path.join(packageDir, 'node_modules', dependencyName);
|
||||
|
||||
if (
|
||||
existsSync(path.join(dependencyDir, 'lib/es/locale', `${locale}.js`))
|
||||
|| existsSync(path.join(dependencyDir, 'src/locale', `${locale}.ts`))
|
||||
) {
|
||||
return 'locale';
|
||||
}
|
||||
|
||||
if (
|
||||
existsSync(path.join(dependencyDir, 'lib/es/locales', `${locale}.js`))
|
||||
|| existsSync(path.join(dependencyDir, 'src/locales', `${locale}.ts`))
|
||||
) {
|
||||
return 'locales';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function generatePresetLocales(options: IGeneratePresetLocalesOptions): string[] {
|
||||
const { packageDir } = options;
|
||||
const pkg = readPackageJson(packageDir);
|
||||
const dependencyNames = getDependencyNames(pkg);
|
||||
const localeDir = path.join(packageDir, PRESET_LOCALE_SOURCE_DIR);
|
||||
|
||||
rmSync(localeDir, { force: true, recursive: true });
|
||||
mkdirSync(localeDir, { recursive: true });
|
||||
|
||||
const generated: string[] = [];
|
||||
|
||||
for (const locale of discoverUniverUiLocales({ rootDir: packageDir })) {
|
||||
const dependenciesWithLocale = dependencyNames
|
||||
.map((dependencyName) => ({
|
||||
dependencyName,
|
||||
localeSubpath: getDependencyLocaleSubpath(packageDir, dependencyName, locale),
|
||||
}))
|
||||
.filter((dependency): dependency is { dependencyName: string; localeSubpath: 'locale' | 'locales' } => dependency.localeSubpath !== null);
|
||||
|
||||
if (dependenciesWithLocale.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
for (const { dependencyName, localeSubpath } of dependenciesWithLocale) {
|
||||
content += `import ${convertImportNameFromPackageName(dependencyName)} from '${dependencyName}/${localeSubpath}/${locale}';\n`;
|
||||
}
|
||||
|
||||
content += '\nexport default Object.assign(\n';
|
||||
content += ' {},\n';
|
||||
|
||||
for (const { dependencyName } of dependenciesWithLocale) {
|
||||
content += ` ${convertImportNameFromPackageName(dependencyName)},\n`;
|
||||
}
|
||||
|
||||
content += ');\n';
|
||||
|
||||
writeFileSync(path.join(localeDir, `${locale}.ts`), content);
|
||||
generated.push(locale);
|
||||
}
|
||||
|
||||
return generated;
|
||||
}
|
||||
58
common/shared/preset-build/style.ts
Normal file
58
common/shared/preset-build/style.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { IPresetPackageJson } from './types.ts';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
function getDependencyDir(packageDir: string, dependencyName: string) {
|
||||
return path.join(packageDir, 'node_modules', dependencyName);
|
||||
}
|
||||
|
||||
function getBuiltStyleDependencyCssFiles(packageDir: string, packageJson: IPresetPackageJson) {
|
||||
return Object.keys(packageJson.dependencies ?? {})
|
||||
.flatMap((dependencyName) => {
|
||||
if (!dependencyName.startsWith('@univerjs')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dependencyDir = getDependencyDir(packageDir, dependencyName);
|
||||
const sourceGlobalCss = path.join(dependencyDir, 'src/global.css');
|
||||
const builtCss = path.join(dependencyDir, 'lib/index.css');
|
||||
|
||||
if (!existsSync(sourceGlobalCss)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!existsSync(builtCss)) {
|
||||
throw new Error(`Missing built CSS for ${dependencyName}. Build dependency packages before building preset CSS.`);
|
||||
}
|
||||
|
||||
return [builtCss];
|
||||
});
|
||||
}
|
||||
|
||||
function writePresetCss(packageDir: string, cssFiles: string[]) {
|
||||
const outputFile = path.join(packageDir, 'lib/index.css');
|
||||
|
||||
mkdirSync(path.dirname(outputFile), { recursive: true });
|
||||
|
||||
const content = cssFiles
|
||||
.map((cssFile) => readFileSync(cssFile, 'utf8').trimEnd())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
writeFileSync(outputFile, `${content}\n`);
|
||||
}
|
||||
|
||||
export async function buildPresetStyles(options: {
|
||||
packageDir: string;
|
||||
packageJson: IPresetPackageJson;
|
||||
}) {
|
||||
const { packageDir, packageJson } = options;
|
||||
const cssFiles = getBuiltStyleDependencyCssFiles(packageDir, packageJson);
|
||||
|
||||
if (cssFiles.length === 0) {
|
||||
rmSync(path.join(packageDir, 'lib/index.css'), { force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
writePresetCss(packageDir, cssFiles);
|
||||
}
|
||||
15
common/shared/preset-build/types.ts
Normal file
15
common/shared/preset-build/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface IPresetBuildOptions {
|
||||
cleanup?: boolean;
|
||||
tsdownConfigPath?: string;
|
||||
}
|
||||
|
||||
export interface IPresetPackageJson {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
name: string;
|
||||
peerDependencies?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface IGeneratePresetLocalesOptions {
|
||||
packageDir: string;
|
||||
}
|
||||
90
common/shared/react-polyfill/react-polyfill.js
vendored
Normal file
90
common/shared/react-polyfill/react-polyfill.js
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Starting from Univer 0.6.0, with the support for React 19[^1], UMD users may need additional adaptation.
|
||||
* It is recommended to migrate to module script[^2] and a more modern build system.
|
||||
*
|
||||
* [^1]: [Module Script] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#applying_the_module_to_your_html
|
||||
* [^2]: [support for React 19] https://github.com/dream-num/univer/pull/4247
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fix `Uncaught TypeError: client.createRoot is not a function`
|
||||
* If using UMD React < 18, you might need the following code.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
if (!global.ReactDOM) {
|
||||
throw new Error('ReactDOM must be loaded before ReactCreateRoot.');
|
||||
}
|
||||
const ReactDOM = global.ReactDOM;
|
||||
if (!ReactDOM.createRoot) {
|
||||
ReactDOM.createRoot = function (container) {
|
||||
return {
|
||||
render: (element) => {
|
||||
ReactDOM.render(element, container);
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
})(this);
|
||||
|
||||
/**
|
||||
* Fix `Uncaught TypeError: jsxRuntime.jsx is not a function`
|
||||
* If using UMD React, you might need the following code.
|
||||
* Reference: https://unpkg.com/react@18.3.1/cjs/react-jsx-runtime.production.min.js
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
if (!global.React) {
|
||||
throw new Error('React must be loaded before ReactJSXRuntime.');
|
||||
}
|
||||
const React = global.React;
|
||||
if (!React.jsx || !React.jsxs) {
|
||||
const REACT_ELEMENT_TYPE = Symbol.for('react.element');
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const RESERVED_PROPS = {
|
||||
key: true,
|
||||
ref: true,
|
||||
__self: true,
|
||||
__source: true,
|
||||
};
|
||||
const ReactCurrentOwner = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
|
||||
function createReactElement(type, config, maybeKey) {
|
||||
const props = {};
|
||||
let key = null;
|
||||
let ref = null;
|
||||
if (maybeKey !== undefined) {
|
||||
key = `${maybeKey}`;
|
||||
}
|
||||
if (config.key !== undefined) {
|
||||
key = `${config.key}`;
|
||||
}
|
||||
if (config.ref !== undefined) {
|
||||
ref = config.ref;
|
||||
}
|
||||
for (var propName in config) {
|
||||
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
|
||||
props[propName] = config[propName];
|
||||
}
|
||||
}
|
||||
if (type && type.defaultProps) {
|
||||
const defaultProps = type.defaultProps;
|
||||
for (var propName in defaultProps) {
|
||||
if (props[propName] === undefined) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
$$typeof: REACT_ELEMENT_TYPE,
|
||||
type,
|
||||
key,
|
||||
ref,
|
||||
props,
|
||||
_owner: ReactCurrentOwner.current,
|
||||
};
|
||||
}
|
||||
React.jsx = createReactElement;
|
||||
React.jsxs = createReactElement;
|
||||
}
|
||||
})(this);
|
||||
78
common/shared/tailwind/__tests__/content.spec.ts
Normal file
78
common/shared/tailwind/__tests__/content.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import tailwindConfig, { createTailwindContent } from '../tailwind.config';
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
function makePackageRoot() {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), 'univer-tailwind-content-'));
|
||||
roots.push(root);
|
||||
writeFileSync(path.join(root, 'tailwind.config.ts'), '');
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeJson(filePath: string, value: unknown) {
|
||||
mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, `${JSON.stringify(value, null, 4)}\n`);
|
||||
}
|
||||
|
||||
function writeGlobalCss(packageRoot: string, dependencyName: string) {
|
||||
const cssPath = path.join(packageRoot, 'node_modules', dependencyName, 'src/global.css');
|
||||
mkdirSync(path.dirname(cssPath), { recursive: true });
|
||||
writeFileSync(cssPath, '');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) {
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('createTailwindContent', () => {
|
||||
it('limits hover styles to devices that support hover', () => {
|
||||
expect(tailwindConfig.future).toMatchObject({ hoverOnlyWhenSupported: true });
|
||||
});
|
||||
|
||||
it('returns current package source content by default', () => {
|
||||
const packageRoot = makePackageRoot();
|
||||
writeJson(path.join(packageRoot, 'package.json'), {
|
||||
name: '@univerjs/preset-demo',
|
||||
dependencies: {
|
||||
'@univerjs/design': 'workspace:*',
|
||||
},
|
||||
});
|
||||
writeGlobalCss(packageRoot, '@univerjs/design');
|
||||
|
||||
expect(createTailwindContent(path.join(packageRoot, 'tailwind.config.ts'))).toEqual([
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes Univer dependencies with global styles when requested', () => {
|
||||
const packageRoot = makePackageRoot();
|
||||
writeJson(path.join(packageRoot, 'package.json'), {
|
||||
name: '@univerjs/preset-demo',
|
||||
dependencies: {
|
||||
'@univerjs/sheets-ui': 'workspace:*',
|
||||
'@univerjs/design': 'workspace:*',
|
||||
'@univerjs/core': 'workspace:*',
|
||||
'@univerjs-pro/sheets-chart-ui': 'workspace:*',
|
||||
react: '^18.3.1',
|
||||
},
|
||||
});
|
||||
writeGlobalCss(packageRoot, '@univerjs/sheets-ui');
|
||||
writeGlobalCss(packageRoot, '@univerjs/design');
|
||||
writeGlobalCss(packageRoot, '@univerjs-pro/sheets-chart-ui');
|
||||
|
||||
expect(createTailwindContent(path.join(packageRoot, 'tailwind.config.ts'), {
|
||||
includeStyleDependencies: true,
|
||||
})).toEqual([
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
'./node_modules/@univerjs/design/src/**/*.{js,ts,jsx,tsx}',
|
||||
'./node_modules/@univerjs/sheets-ui/src/**/*.{js,ts,jsx,tsx}',
|
||||
'./node_modules/@univerjs-pro/sheets-chart-ui/src/**/*.{js,ts,jsx,tsx}',
|
||||
]);
|
||||
});
|
||||
});
|
||||
237
common/shared/tailwind/tailwind.config.ts
Normal file
237
common/shared/tailwind/tailwind.config.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import scrollbar from 'tailwind-scrollbar';
|
||||
|
||||
const SOURCE_CONTENT = './src/**/*.{js,ts,jsx,tsx}';
|
||||
const DEPENDENCY_SOURCE_CONTENT = './node_modules/{name}/src/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
interface ITailwindContentOptions {
|
||||
includeStyleDependencies?: boolean;
|
||||
}
|
||||
|
||||
interface IPackageJson {
|
||||
dependencies?: Record<string, string>;
|
||||
}
|
||||
|
||||
function getPackageDir(configUrl: string) {
|
||||
const configPath = configUrl.startsWith('file:')
|
||||
? fileURLToPath(configUrl)
|
||||
: configUrl;
|
||||
|
||||
return path.dirname(configPath);
|
||||
}
|
||||
|
||||
function getPackageJson(packageDir: string): IPackageJson {
|
||||
const packageJsonPath = path.join(packageDir, 'package.json');
|
||||
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return JSON.parse(readFileSync(packageJsonPath, 'utf8')) as IPackageJson;
|
||||
}
|
||||
|
||||
function getDependencyDir(packageDir: string, dependencyName: string) {
|
||||
return path.join(packageDir, 'node_modules', dependencyName);
|
||||
}
|
||||
|
||||
function compareDependencyName(left: string, right: string) {
|
||||
const leftScope = left.startsWith('@univerjs-pro/') ? 1 : 0;
|
||||
const rightScope = right.startsWith('@univerjs-pro/') ? 1 : 0;
|
||||
|
||||
if (leftScope !== rightScope) {
|
||||
return leftScope - rightScope;
|
||||
}
|
||||
|
||||
return left.localeCompare(right);
|
||||
}
|
||||
|
||||
export function createTailwindContent(configUrl: string, options: ITailwindContentOptions = {}) {
|
||||
const content = [SOURCE_CONTENT];
|
||||
|
||||
if (!options.includeStyleDependencies) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const packageDir = getPackageDir(configUrl);
|
||||
const packageJson = getPackageJson(packageDir);
|
||||
const dependencyNames = Object.keys(packageJson.dependencies ?? {})
|
||||
.filter((dependencyName) => {
|
||||
if (!dependencyName.startsWith('@univerjs')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return existsSync(path.join(getDependencyDir(packageDir, dependencyName), 'src/global.css'));
|
||||
})
|
||||
.sort(compareDependencyName);
|
||||
|
||||
return [
|
||||
...content,
|
||||
...dependencyNames.map((dependencyName) => DEPENDENCY_SOURCE_CONTENT.replace('{name}', dependencyName)),
|
||||
];
|
||||
}
|
||||
|
||||
const config: Omit<Config, 'content'> = {
|
||||
prefix: 'univer-',
|
||||
darkMode: 'selector',
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true,
|
||||
},
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: 'var(--univer-primary-50)',
|
||||
100: 'var(--univer-primary-100)',
|
||||
200: 'var(--univer-primary-200)',
|
||||
300: 'var(--univer-primary-300)',
|
||||
400: 'var(--univer-primary-400)',
|
||||
500: 'var(--univer-primary-500)',
|
||||
600: 'var(--univer-primary-600)',
|
||||
700: 'var(--univer-primary-700)',
|
||||
800: 'var(--univer-primary-800)',
|
||||
900: 'var(--univer-primary-900)',
|
||||
},
|
||||
gray: {
|
||||
0: 'var(--univer-gray-0)',
|
||||
50: 'var(--univer-gray-50)',
|
||||
100: 'var(--univer-gray-100)',
|
||||
200: 'var(--univer-gray-200)',
|
||||
300: 'var(--univer-gray-300)',
|
||||
400: 'var(--univer-gray-400)',
|
||||
500: 'var(--univer-gray-500)',
|
||||
600: 'var(--univer-gray-600)',
|
||||
700: 'var(--univer-gray-700)',
|
||||
800: 'var(--univer-gray-800)',
|
||||
900: 'var(--univer-gray-900)',
|
||||
1000: 'var(--univer-gray-1000)',
|
||||
},
|
||||
blue: {
|
||||
50: 'var(--univer-blue-50)',
|
||||
100: 'var(--univer-blue-100)',
|
||||
200: 'var(--univer-blue-200)',
|
||||
300: 'var(--univer-blue-300)',
|
||||
400: 'var(--univer-blue-400)',
|
||||
500: 'var(--univer-blue-500)',
|
||||
600: 'var(--univer-blue-600)',
|
||||
700: 'var(--univer-blue-700)',
|
||||
800: 'var(--univer-blue-800)',
|
||||
900: 'var(--univer-blue-900)',
|
||||
},
|
||||
red: {
|
||||
50: 'var(--univer-red-50)',
|
||||
100: 'var(--univer-red-100)',
|
||||
200: 'var(--univer-red-200)',
|
||||
300: 'var(--univer-red-300)',
|
||||
400: 'var(--univer-red-400)',
|
||||
500: 'var(--univer-red-500)',
|
||||
600: 'var(--univer-red-600)',
|
||||
700: 'var(--univer-red-700)',
|
||||
800: 'var(--univer-red-800)',
|
||||
900: 'var(--univer-red-900)',
|
||||
},
|
||||
orange: {
|
||||
50: 'var(--univer-orange-50)',
|
||||
100: 'var(--univer-orange-100)',
|
||||
200: 'var(--univer-orange-200)',
|
||||
300: 'var(--univer-orange-300)',
|
||||
400: 'var(--univer-orange-400)',
|
||||
500: 'var(--univer-orange-500)',
|
||||
600: 'var(--univer-orange-600)',
|
||||
700: 'var(--univer-orange-700)',
|
||||
800: 'var(--univer-orange-800)',
|
||||
900: 'var(--univer-orange-900)',
|
||||
},
|
||||
yellow: {
|
||||
50: 'var(--univer-yellow-50)',
|
||||
100: 'var(--univer-yellow-100)',
|
||||
200: 'var(--univer-yellow-200)',
|
||||
300: 'var(--univer-yellow-300)',
|
||||
400: 'var(--univer-yellow-400)',
|
||||
500: 'var(--univer-yellow-500)',
|
||||
600: 'var(--univer-yellow-600)',
|
||||
700: 'var(--univer-yellow-700)',
|
||||
800: 'var(--univer-yellow-800)',
|
||||
900: 'var(--univer-yellow-900)',
|
||||
},
|
||||
green: {
|
||||
50: 'var(--univer-green-50)',
|
||||
100: 'var(--univer-green-100)',
|
||||
200: 'var(--univer-green-200)',
|
||||
300: 'var(--univer-green-300)',
|
||||
400: 'var(--univer-green-400)',
|
||||
500: 'var(--univer-green-500)',
|
||||
600: 'var(--univer-green-600)',
|
||||
700: 'var(--univer-green-700)',
|
||||
800: 'var(--univer-green-800)',
|
||||
900: 'var(--univer-green-900)',
|
||||
},
|
||||
jiqing: {
|
||||
50: 'var(--univer-jiqing-50)',
|
||||
100: 'var(--univer-jiqing-100)',
|
||||
200: 'var(--univer-jiqing-200)',
|
||||
300: 'var(--univer-jiqing-300)',
|
||||
400: 'var(--univer-jiqing-400)',
|
||||
500: 'var(--univer-jiqing-500)',
|
||||
600: 'var(--univer-jiqing-600)',
|
||||
700: 'var(--univer-jiqing-700)',
|
||||
800: 'var(--univer-jiqing-800)',
|
||||
900: 'var(--univer-jiqing-900)',
|
||||
},
|
||||
indigo: {
|
||||
50: 'var(--univer-indigo-50)',
|
||||
100: 'var(--univer-indigo-100)',
|
||||
200: 'var(--univer-indigo-200)',
|
||||
300: 'var(--univer-indigo-300)',
|
||||
400: 'var(--univer-indigo-400)',
|
||||
500: 'var(--univer-indigo-500)',
|
||||
600: 'var(--univer-indigo-600)',
|
||||
700: 'var(--univer-indigo-700)',
|
||||
800: 'var(--univer-indigo-800)',
|
||||
900: 'var(--univer-indigo-900)',
|
||||
},
|
||||
purple: {
|
||||
50: 'var(--univer-purple-50)',
|
||||
100: 'var(--univer-purple-100)',
|
||||
200: 'var(--univer-purple-200)',
|
||||
300: 'var(--univer-purple-300)',
|
||||
400: 'var(--univer-purple-400)',
|
||||
500: 'var(--univer-purple-500)',
|
||||
600: 'var(--univer-purple-600)',
|
||||
700: 'var(--univer-purple-700)',
|
||||
800: 'var(--univer-purple-800)',
|
||||
900: 'var(--univer-purple-900)',
|
||||
},
|
||||
pink: {
|
||||
50: 'var(--univer-pink-50)',
|
||||
100: 'var(--univer-pink-100)',
|
||||
200: 'var(--univer-pink-200)',
|
||||
300: 'var(--univer-pink-300)',
|
||||
400: 'var(--univer-pink-400)',
|
||||
500: 'var(--univer-pink-500)',
|
||||
600: 'var(--univer-pink-600)',
|
||||
700: 'var(--univer-pink-700)',
|
||||
800: 'var(--univer-pink-800)',
|
||||
900: 'var(--univer-pink-900)',
|
||||
},
|
||||
},
|
||||
boxShadow: {
|
||||
sm: '0px 1px 2px 0px rgba(30, 40, 77, 0.08)',
|
||||
md: '0px 1px 6px -2px rgba(30, 40, 77, 0.08), 0px 2px 6px -1px rgba(30, 40, 77, 0.10)',
|
||||
lg: '0px 4px 6px 0px rgba(30, 40, 77, 0.05), 0px 10px 15px -3px rgba(30, 40, 77, 0.10)',
|
||||
xl: '0px 10px 10px 0px rgba(30, 40, 77, 0.04), 0px 20px 24px -5px rgba(30, 40, 77, 0.10)',
|
||||
'2xl': '0px 24px 50px -12px rgba(30, 40, 77, 0.24)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
scrollbar,
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
20
common/shared/tsconfig.json
Normal file
20
common/shared/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfigs/base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"bin/**/*.mts",
|
||||
"esbuild/**/*.ts",
|
||||
"eslint/**/*.ts",
|
||||
"eslint/**/*.d.ts",
|
||||
"locale/**/*.ts",
|
||||
"tailwind/**/*.ts",
|
||||
"tsdown/**/*.ts",
|
||||
"vitest/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user