mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-29 19:29:36 +02:00
Add 'foundations/core/' from commit '4f31d1b32637d2f124f555531ee12be8af3fd4fc'
git-subtree-dir: foundations/core git-subtree-mainline:16b1109180git-subtree-split:4f31d1b326
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Don't allow people to merge changes to these generated files, because the result
|
||||
# may be invalid. You need to run "rush update" again.
|
||||
pnpm-lock.yaml merge=text
|
||||
shrinkwrap.yaml merge=binary
|
||||
npm-shrinkwrap.json merge=binary
|
||||
yarn.lock merge=binary
|
||||
|
||||
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
|
||||
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
|
||||
# may also require a special configuration to allow comments in JSON.
|
||||
#
|
||||
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
|
||||
#
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
@@ -0,0 +1,69 @@
|
||||
# GitHub Copilot Instructions - Huly Core
|
||||
|
||||
**Type**: Rush monorepo (TypeScript)
|
||||
**License**: EPL-2.0
|
||||
**Build System**: Rush v5.158.1 + pnpm v10.15.1
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Always use Rush commands**, never npm/pnpm directly: `rush install`, `rush build`, `rush test`, `rush update`
|
||||
2. **Internal dependencies must use `workspace:^` protocol** in package.json
|
||||
3. **All packages extend `@hcengineering/platform-rig`** for tsconfig/eslint - don't override configs
|
||||
4. **Co-locate tests** in `src/__tests__/` directories (Jest + ts-jest)
|
||||
5. **Named exports only** - avoid default exports
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
rush install && rush build # Initial setup
|
||||
rush build:watch # Development mode
|
||||
rush rebuild # Force clean rebuild (clears cache)
|
||||
rush test # Run all tests
|
||||
```
|
||||
|
||||
## Package Structure (All packages follow this)
|
||||
|
||||
```
|
||||
packages/<name>/
|
||||
├── src/index.ts # Main entry (exports)
|
||||
├── lib/ # Compiled JS (gitignored)
|
||||
├── types/ # TS declarations (gitignored)
|
||||
├── package.json # Scope: @hcengineering/, main: lib/index.js
|
||||
├── tsconfig.json # Extends platform-rig
|
||||
└── jest.config.js # preset: 'ts-jest', roots: ['./src']
|
||||
```
|
||||
|
||||
## Key Architecture Patterns
|
||||
|
||||
**Plugin System**: `@hcengineering/platform` provides dependency injection. Packages register resources/services via plugin manifests. Example: `packages/core/src/plugin.ts`
|
||||
|
||||
**Data Flow**: `core` → abstract models (Doc, Ref, Class) → `client` → concrete implementations → WebSocket/REST via `api-client`
|
||||
|
||||
**Text Processing**: Modular `text-*` packages (core/html/markdown/ydoc) support extensible rich-text editing with Yjs collaboration
|
||||
|
||||
**Storage Abstraction**: `storage` package defines interfaces; `storage-client` provides implementations; backends are pluggable
|
||||
|
||||
## Common Tasks
|
||||
|
||||
**Add a package**: Create under `packages/`, add standard files (see structure above), register in `rush.json` if needed, run `rush update`
|
||||
|
||||
**Debug build failures**: Check `<package>/rush-logs/` and `.build/build.tsbuildinfo`; use `rush rebuild` to clear incremental state
|
||||
|
||||
**Run single package tests**: `cd packages/<name> && rushx test`
|
||||
|
||||
**Update dependencies**: Edit package.json, run `rush update`, verify with `rush build`
|
||||
|
||||
## Where to Look
|
||||
|
||||
- **Rush config**: `common/config/rush/`, `rush.json`
|
||||
- **Shared scripts**: `common/scripts/` (coverage merging, install helpers)
|
||||
- **Core abstractions**: `packages/core/src/` (Doc, Hierarchy, TxOperations)
|
||||
- **Platform runtime**: `packages/platform/src/` (plugin loader, resources)
|
||||
- **Client layer**: `packages/client/src/` (LiveQuery, TxOperations client wrapper)
|
||||
|
||||
## Watch Out For
|
||||
|
||||
- Rush uses **incremental builds** - cached artifacts in `.build/` and `common/temp/`
|
||||
- **TypeScript strict mode** enabled - type safety enforced
|
||||
- **platform-rig** centralizes tooling configs (ESLint, Prettier, tsconfig)
|
||||
- Check `common/temp/rush-recycler/` for moved files during dependency updates
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
tags:
|
||||
- 'v0.7.*'
|
||||
- 's0.7.*'
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Verify Change Logs
|
||||
run: node common/scripts/install-run-rush.js change --verify
|
||||
- name: Rush Install
|
||||
run: node common/scripts/install-run-rush.js install
|
||||
- name: Rush check
|
||||
run: node common/scripts/install-run-rush.js check --verbose
|
||||
- name: Rush validate
|
||||
run: node common/scripts/install-run-rush.js validate --verbose
|
||||
- name: Rush test
|
||||
run: node common/scripts/install-run-rush.js test --verbose
|
||||
- name: Formatting...
|
||||
run: node common/scripts/install-run-rush.js format --force
|
||||
- name: Check files formatting
|
||||
run: |
|
||||
echo '================================================================'
|
||||
echo 'Checking for diff files'
|
||||
echo '================================================================'
|
||||
git diff '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat
|
||||
[ -z "$(git diff --name-only '*.js' '*.ts' '*.svelte' '*.json' '*.yaml' | cat)" ]
|
||||
echo '================================================================'
|
||||
- name: Publish packages
|
||||
if: startsWith(github.ref, 'refs/tags/v0.7.') || startsWith(github.ref, 'refs/tags/s0.7.')
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: node common/scripts/install-run-rush.js publish --include-all --publish
|
||||
@@ -0,0 +1,115 @@
|
||||
.heft/
|
||||
lib/
|
||||
_api-extractor-temp/
|
||||
temp/
|
||||
.idea
|
||||
pods/workspace/init/
|
||||
pods/workspace/init-scripts/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*./rush-logs
|
||||
*tests/sanity/screenshots
|
||||
|
||||
# Runtime data
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# VS Code settings
|
||||
.vscode/settings.json
|
||||
|
||||
# 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/
|
||||
|
||||
# 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
|
||||
|
||||
# OS X temporary files
|
||||
.DS_Store
|
||||
|
||||
# Rush temporary files
|
||||
common/deploy/
|
||||
common/temp/
|
||||
common/autoinstallers/*/.npmrc
|
||||
**/.rush/temp/
|
||||
bundle.js
|
||||
bundle/*.js
|
||||
dist
|
||||
.build
|
||||
typings
|
||||
types
|
||||
.validate
|
||||
tsconfig.tsbuildinfo
|
||||
ingest-attachment-*.zip
|
||||
tsdoc-metadata.json
|
||||
pods/front/dist
|
||||
*.cpuprofile
|
||||
*.pyc
|
||||
metrics.txt
|
||||
dev/tool/report*.csv
|
||||
tests/db_dump
|
||||
.build
|
||||
.format
|
||||
tools/apm/apm.js
|
||||
deploy
|
||||
metrics.txt
|
||||
services/github/pod-github/src/github.graphql
|
||||
.build
|
||||
.format
|
||||
dev/tool/report.csv
|
||||
bundle/*
|
||||
bundle.js.map
|
||||
tests/profiles
|
||||
**/bundle/model.json
|
||||
.wrangler
|
||||
dump
|
||||
**/logs/**
|
||||
dev/tool/history.json
|
||||
.aider*
|
||||
/combined_dependencies
|
||||
.tmp
|
||||
ws-tests/docker-compose.override.yml
|
||||
@@ -0,0 +1 @@
|
||||
v22
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"proseWrap": "preserve",
|
||||
"plugins": [],
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.svelte",
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"svelte.svelte-vscode",
|
||||
"esbenp.prettier-vscode",
|
||||
"firsttris.vscode-jest-runner"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
Eclipse Public License - v 2.0
|
||||
|
||||
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION
|
||||
OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
|
||||
|
||||
1. DEFINITIONS
|
||||
|
||||
"Contribution" means:
|
||||
|
||||
a) in the case of the initial Contributor, the initial content
|
||||
Distributed under this Agreement, and
|
||||
|
||||
b) in the case of each subsequent Contributor:
|
||||
i) changes to the Program, and
|
||||
ii) additions to the Program;
|
||||
where such changes and/or additions to the Program originate from
|
||||
and are Distributed by that particular Contributor. A Contribution
|
||||
"originates" from a Contributor if it was added to the Program by
|
||||
such Contributor itself or anyone acting on such Contributor's behalf.
|
||||
Contributions do not include changes or additions to the Program that
|
||||
are not Modified Works.
|
||||
|
||||
"Contributor" means any person or entity that Distributes the Program.
|
||||
|
||||
"Licensed Patents" mean patent claims licensable by a Contributor which
|
||||
are necessarily infringed by the use or sale of its Contribution alone
|
||||
or when combined with the Program.
|
||||
|
||||
"Program" means the Contributions Distributed in accordance with this
|
||||
Agreement.
|
||||
|
||||
"Recipient" means anyone who receives the Program under this Agreement
|
||||
or any Secondary License (as applicable), including Contributors.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source Code or other
|
||||
form, that is based on (or derived from) the Program and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship.
|
||||
|
||||
"Modified Works" shall mean any work in Source Code or other form that
|
||||
results from an addition to, deletion from, or modification of the
|
||||
contents of the Program, including, for purposes of clarity any new file
|
||||
in Source Code form that contains any contents of the Program. Modified
|
||||
Works shall not include works that contain only declarations,
|
||||
interfaces, types, classes, structures, or files of the Program solely
|
||||
in each case in order to link to, bind by name, or subclass the Program
|
||||
or Modified Works thereof.
|
||||
|
||||
"Distribute" means the acts of a) distributing or b) making available
|
||||
in any manner that enables the transfer of a copy.
|
||||
|
||||
"Source Code" means the form of a Program preferred for making
|
||||
modifications, including but not limited to software source code,
|
||||
documentation source, and configuration files.
|
||||
|
||||
"Secondary License" means either the GNU General Public License,
|
||||
Version 2.0, or any later versions of that license, including any
|
||||
exceptions or additional permissions as identified by the initial
|
||||
Contributor.
|
||||
|
||||
2. GRANT OF RIGHTS
|
||||
|
||||
a) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free copyright
|
||||
license to reproduce, prepare Derivative Works of, publicly display,
|
||||
publicly perform, Distribute and sublicense the Contribution of such
|
||||
Contributor, if any, and such Derivative Works.
|
||||
|
||||
b) Subject to the terms of this Agreement, each Contributor hereby
|
||||
grants Recipient a non-exclusive, worldwide, royalty-free patent
|
||||
license under Licensed Patents to make, use, sell, offer to sell,
|
||||
import and otherwise transfer the Contribution of such Contributor,
|
||||
if any, in Source Code or other form. This patent license shall
|
||||
apply to the combination of the Contribution and the Program if, at
|
||||
the time the Contribution is added by the Contributor, such addition
|
||||
of the Contribution causes such combination to be covered by the
|
||||
Licensed Patents. The patent license shall not apply to any other
|
||||
combinations which include the Contribution. No hardware per se is
|
||||
licensed hereunder.
|
||||
|
||||
c) Recipient understands that although each Contributor grants the
|
||||
licenses to its Contributions set forth herein, no assurances are
|
||||
provided by any Contributor that the Program does not infringe the
|
||||
patent or other intellectual property rights of any other entity.
|
||||
Each Contributor disclaims any liability to Recipient for claims
|
||||
brought by any other entity based on infringement of intellectual
|
||||
property rights or otherwise. As a condition to exercising the
|
||||
rights and licenses granted hereunder, each Recipient hereby
|
||||
assumes sole responsibility to secure any other intellectual
|
||||
property rights needed, if any. For example, if a third party
|
||||
patent license is required to allow Recipient to Distribute the
|
||||
Program, it is Recipient's responsibility to acquire that license
|
||||
before distributing the Program.
|
||||
|
||||
d) Each Contributor represents that to its knowledge it has
|
||||
sufficient copyright rights in its Contribution, if any, to grant
|
||||
the copyright license set forth in this Agreement.
|
||||
|
||||
e) Notwithstanding the terms of any Secondary License, no
|
||||
Contributor makes additional grants to any Recipient (other than
|
||||
those set forth in this Agreement) as a result of such Recipient's
|
||||
receipt of the Program under the terms of a Secondary License
|
||||
(if permitted under the terms of Section 3).
|
||||
|
||||
3. REQUIREMENTS
|
||||
|
||||
3.1 If a Contributor Distributes the Program in any form, then:
|
||||
|
||||
a) the Program must also be made available as Source Code, in
|
||||
accordance with section 3.2, and the Contributor must accompany
|
||||
the Program with a statement that the Source Code for the Program
|
||||
is available under this Agreement, and informs Recipients how to
|
||||
obtain it in a reasonable manner on or through a medium customarily
|
||||
used for software exchange; and
|
||||
|
||||
b) the Contributor may Distribute the Program under a license
|
||||
different than this Agreement, provided that such license:
|
||||
i) effectively disclaims on behalf of all other Contributors all
|
||||
warranties and conditions, express and implied, including
|
||||
warranties or conditions of title and non-infringement, and
|
||||
implied warranties or conditions of merchantability and fitness
|
||||
for a particular purpose;
|
||||
|
||||
ii) effectively excludes on behalf of all other Contributors all
|
||||
liability for damages, including direct, indirect, special,
|
||||
incidental and consequential damages, such as lost profits;
|
||||
|
||||
iii) does not attempt to limit or alter the recipients' rights
|
||||
in the Source Code under section 3.2; and
|
||||
|
||||
iv) requires any subsequent distribution of the Program by any
|
||||
party to be under a license that satisfies the requirements
|
||||
of this section 3.
|
||||
|
||||
3.2 When the Program is Distributed as Source Code:
|
||||
|
||||
a) it must be made available under this Agreement, or if the
|
||||
Program (i) is combined with other material in a separate file or
|
||||
files made available under a Secondary License, and (ii) the initial
|
||||
Contributor attached to the Source Code the notice described in
|
||||
Exhibit A of this Agreement, then the Program may be made available
|
||||
under the terms of such Secondary Licenses, and
|
||||
|
||||
b) a copy of this Agreement must be included with each copy of
|
||||
the Program.
|
||||
|
||||
3.3 Contributors may not remove or alter any copyright, patent,
|
||||
trademark, attribution notices, disclaimers of warranty, or limitations
|
||||
of liability ("notices") contained within the Program from any copy of
|
||||
the Program which they Distribute, provided that Contributors may add
|
||||
their own appropriate notices.
|
||||
|
||||
4. COMMERCIAL DISTRIBUTION
|
||||
|
||||
Commercial distributors of software may accept certain responsibilities
|
||||
with respect to end users, business partners and the like. While this
|
||||
license is intended to facilitate the commercial use of the Program,
|
||||
the Contributor who includes the Program in a commercial product
|
||||
offering should do so in a manner which does not create potential
|
||||
liability for other Contributors. Therefore, if a Contributor includes
|
||||
the Program in a commercial product offering, such Contributor
|
||||
("Commercial Contributor") hereby agrees to defend and indemnify every
|
||||
other Contributor ("Indemnified Contributor") against any losses,
|
||||
damages and costs (collectively "Losses") arising from claims, lawsuits
|
||||
and other legal actions brought by a third party against the Indemnified
|
||||
Contributor to the extent caused by the acts or omissions of such
|
||||
Commercial Contributor in connection with its distribution of the Program
|
||||
in a commercial product offering. The obligations in this section do not
|
||||
apply to any claims or Losses relating to any actual or alleged
|
||||
intellectual property infringement. In order to qualify, an Indemnified
|
||||
Contributor must: a) promptly notify the Commercial Contributor in
|
||||
writing of such claim, and b) allow the Commercial Contributor to control,
|
||||
and cooperate with the Commercial Contributor in, the defense and any
|
||||
related settlement negotiations. The Indemnified Contributor may
|
||||
participate in any such claim at its own expense.
|
||||
|
||||
For example, a Contributor might include the Program in a commercial
|
||||
product offering, Product X. That Contributor is then a Commercial
|
||||
Contributor. If that Commercial Contributor then makes performance
|
||||
claims, or offers warranties related to Product X, those performance
|
||||
claims and warranties are such Commercial Contributor's responsibility
|
||||
alone. Under this section, the Commercial Contributor would have to
|
||||
defend claims against the other Contributors related to those performance
|
||||
claims and warranties, and if a court requires any other Contributor to
|
||||
pay any damages as a result, the Commercial Contributor must pay
|
||||
those damages.
|
||||
|
||||
5. NO WARRANTY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED 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. Each Recipient is solely responsible for determining the
|
||||
appropriateness of using and distributing the Program and assumes all
|
||||
risks associated with its exercise of rights under this Agreement,
|
||||
including but not limited to the risks and costs of program errors,
|
||||
compliance with applicable laws, damage to or loss of data, programs
|
||||
or equipment, and unavailability or interruption of operations.
|
||||
|
||||
6. DISCLAIMER OF LIABILITY
|
||||
|
||||
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT
|
||||
PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS
|
||||
SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
|
||||
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
|
||||
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. GENERAL
|
||||
|
||||
If any provision of this Agreement is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this Agreement, and without further
|
||||
action by the parties hereto, such provision shall be reformed to the
|
||||
minimum extent necessary to make such provision valid and enforceable.
|
||||
|
||||
If Recipient institutes patent litigation against any entity
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||
Program itself (excluding combinations of the Program with other software
|
||||
or hardware) infringes such Recipient's patent(s), then such Recipient's
|
||||
rights granted under Section 2(b) shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
All Recipient's rights under this Agreement shall terminate if it
|
||||
fails to comply with any of the material terms or conditions of this
|
||||
Agreement and does not cure such failure in a reasonable period of
|
||||
time after becoming aware of such noncompliance. If all Recipient's
|
||||
rights under this Agreement terminate, Recipient agrees to cease use
|
||||
and distribution of the Program as soon as reasonably practicable.
|
||||
However, Recipient's obligations under this Agreement and any licenses
|
||||
granted by Recipient relating to the Program shall continue and survive.
|
||||
|
||||
Everyone is permitted to copy and distribute copies of this Agreement,
|
||||
but in order to avoid inconsistency the Agreement is copyrighted and
|
||||
may only be modified in the following manner. The Agreement Steward
|
||||
reserves the right to publish new versions (including revisions) of
|
||||
this Agreement from time to time. No one other than the Agreement
|
||||
Steward has the right to modify this Agreement. The Eclipse Foundation
|
||||
is the initial Agreement Steward. The Eclipse Foundation may assign the
|
||||
responsibility to serve as the Agreement Steward to a suitable separate
|
||||
entity. Each new version of the Agreement will be given a distinguishing
|
||||
version number. The Program (including Contributions) may always be
|
||||
Distributed subject to the version of the Agreement under which it was
|
||||
received. In addition, after a new version of the Agreement is published,
|
||||
Contributor may elect to Distribute the Program (including its
|
||||
Contributions) under the new version.
|
||||
|
||||
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient
|
||||
receives no rights or licenses to the intellectual property of any
|
||||
Contributor under this Agreement, whether expressly, by implication,
|
||||
estoppel or otherwise. All rights in the Program not expressly granted
|
||||
under this Agreement are reserved. Nothing in this Agreement is intended
|
||||
to be enforceable by any entity that is not a Contributor or Recipient.
|
||||
No third-party beneficiary rights are created under this Agreement.
|
||||
|
||||
Exhibit A - Form of Secondary Licenses Notice
|
||||
|
||||
"This Source Code may also be made available under the following
|
||||
Secondary Licenses when the conditions for such availability set forth
|
||||
in the Eclipse Public License, v. 2.0 are satisfied: {name license(s),
|
||||
version(s), and exceptions or additional permissions here}."
|
||||
|
||||
Simply including a copy of this Agreement, including this Exhibit A
|
||||
is not sufficient to license the Source Code under Secondary Licenses.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to
|
||||
look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
@@ -0,0 +1,167 @@
|
||||
# Huly Core
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
⭐️ Your star shines on us. Star us on GitHub!
|
||||
|
||||
## About
|
||||
|
||||
Huly Core is a collection of core packages extracted from the [Huly Platform](https://github.com/hcengineering/platform). This repository contains fundamental building blocks and libraries that power the Huly ecosystem, including core data models, client libraries, text processing engines, and platform utilities.
|
||||
|
||||
These packages are designed to be reusable, modular, and framework-agnostic, making them suitable for building custom applications on top of the Huly Platform or integrating Huly functionality into existing projects.
|
||||
|
||||
## Packages
|
||||
|
||||
This repository includes the following core packages:
|
||||
|
||||
### Core Packages
|
||||
|
||||
- **[@hcengineering/core](packages/core)** - Core data models, types, and fundamental platform abstractions
|
||||
- **[@hcengineering/platform](packages/platform)** - Platform runtime, plugin system, and dependency injection
|
||||
- **[@hcengineering/model](packages/model)** - Data model definitions and schema management
|
||||
|
||||
### Client Libraries
|
||||
|
||||
- **[@hcengineering/client](packages/client)** - Client-side data access and synchronization layer
|
||||
- **[@hcengineering/client-resources](packages/client-resources)** - Shared client resources and utilities
|
||||
- **[@hcengineering/api-client](packages/api-client)** - API client for programmatic access to Huly Platform (WebSocket and REST)
|
||||
- **[@hcengineering/account-client](packages/account-client)** - Account management client
|
||||
- **[@hcengineering/collaborator-client](packages/collaborator-client)** - Real-time collaboration client
|
||||
- **[@hcengineering/hulylake-client](packages/hulylake-client)** - HulyLake data warehouse client
|
||||
- **[@hcengineering/analytics](packages/analytics)** - Analytics and tracking
|
||||
- **[@hcengineering/analytics-service](packages/analytics-service)** - Analytics service implementation
|
||||
|
||||
### Text Processing
|
||||
|
||||
- **[@hcengineering/text](packages/text)** - High-level text processing utilities
|
||||
- **[@hcengineering/text-core](packages/text-core)** - Core text processing engine
|
||||
- **[@hcengineering/text-html](packages/text-html)** - HTML text rendering and parsing
|
||||
- **[@hcengineering/text-markdown](packages/text-markdown)** - Markdown support
|
||||
- **[@hcengineering/text-ydoc](packages/text-ydoc)** - Yjs document integration for collaborative editing
|
||||
|
||||
### Utilities
|
||||
|
||||
- **[@hcengineering/query](packages/query)** - Query language and execution engine
|
||||
- **[@hcengineering/storage](packages/storage)** - Storage abstractions and implementations
|
||||
- **[@hcengineering/rank](packages/rank)** - Ranking and ordering utilities
|
||||
- **[@hcengineering/retry](packages/retry)** - Retry logic and resilience patterns
|
||||
- **[@hcengineering/rpc](packages/rpc)** - RPC communication layer
|
||||
- **[@hcengineering/token](packages/token)** - Token management and authentication utilities
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
Before proceeding, ensure that your system meets the following requirements:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/) (v20.11.0 or higher is required)
|
||||
- [Rush](https://rushjs.io/) - Microsoft's scalable monorepo manager
|
||||
|
||||
## Installation
|
||||
|
||||
You need Microsoft's [rush](https://rushjs.io/) to install the application.
|
||||
|
||||
1. Install Rush globally using the command:
|
||||
|
||||
```bash
|
||||
npm install -g @microsoft/rush
|
||||
```
|
||||
|
||||
1. Navigate to the repository root and run the following commands:
|
||||
|
||||
```bash
|
||||
rush install
|
||||
rush build
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
To build all packages:
|
||||
|
||||
```bash
|
||||
rush build
|
||||
```
|
||||
|
||||
To rebuild (ignoring cache):
|
||||
|
||||
```bash
|
||||
rush rebuild
|
||||
```
|
||||
|
||||
## Build & Watch
|
||||
|
||||
For development purposes, `rush build:watch` action could be used:
|
||||
|
||||
```bash
|
||||
rush build:watch
|
||||
```
|
||||
|
||||
It includes build and validate phases in watch mode.
|
||||
|
||||
## Update project structure
|
||||
|
||||
If the project's structure is updated, it may be necessary to relink and rebuild the projects:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
rush build
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a build fails, but the code is correct, try to delete the [build cache](https://rushjs.io/pages/maintainer/build_cache/) and retry:
|
||||
|
||||
```bash
|
||||
rm -rf common/temp/build-cache
|
||||
rush rebuild
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
To execute all tests:
|
||||
|
||||
```bash
|
||||
rush test
|
||||
```
|
||||
|
||||
For individual test execution inside a package directory:
|
||||
|
||||
```bash
|
||||
rushx test
|
||||
```
|
||||
|
||||
## Package Publishing
|
||||
|
||||
To bump a package version:
|
||||
|
||||
```bash
|
||||
node ./common/scripts/bump.js -p projectName
|
||||
```
|
||||
|
||||
## API Client Usage
|
||||
|
||||
If you want to interact with Huly programmatically, check out the [API Client](packages/api-client/README.md) documentation. The API client provides a typed interface for all Huly operations and can be used to build integrations and custom applications.
|
||||
|
||||
You can find API usage examples in the [Huly examples](https://github.com/hcengineering/huly-examples) repository.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- **[Huly Platform](https://github.com/hcengineering/platform)** - The main Huly Platform repository
|
||||
- **[Huly Self-Host](https://github.com/hcengineering/huly-selfhost)** - Self-hosting solution for Huly
|
||||
- **[Huly Examples](https://github.com/hcengineering/huly-examples)** - API usage examples
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [EPL-2.0](LICENSE) license.
|
||||
|
||||
## Additional Links
|
||||
|
||||
- [Huly Website](https://huly.io/)
|
||||
- [Documentation](https://docs.huly.io/)
|
||||
- [Community](https://github.com/hcengineering/platform/discussions)
|
||||
|
||||
---
|
||||
|
||||
© 2025 [Hardcore Engineering Inc](https://hardcoreeng.com/).
|
||||
@@ -0,0 +1,33 @@
|
||||
# Rush uses this file to configure the NPM package registry during installation. It is applicable
|
||||
# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install",
|
||||
# "rush update", and the "install-run.js" scripts.
|
||||
#
|
||||
# NOTE: The "rush publish" command uses .npmrc-publish instead.
|
||||
#
|
||||
# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation
|
||||
# is performed. This generated file will omit any config lines that reference environment variables
|
||||
# that are undefined in that session; this avoids problems that would otherwise result due to
|
||||
# a missing variable being replaced by an empty string.
|
||||
#
|
||||
# If "subspacesEnabled" is true in subspaces.json, the generated file will merge settings from
|
||||
# "common/config/rush/.npmrc" and "common/config/subspaces/<name>/.npmrc", with the latter taking
|
||||
# precedence.
|
||||
#
|
||||
# * * * SECURITY WARNING * * *
|
||||
#
|
||||
# It is NOT recommended to store authentication tokens in a text file on a lab machine, because
|
||||
# other unrelated processes may be able to read that file. Also, the file may persist indefinitely,
|
||||
# for example if the machine loses power. A safer practice is to pass the token via an
|
||||
# environment variable, which can be referenced from .npmrc using ${} expansion. For example:
|
||||
#
|
||||
# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}
|
||||
#
|
||||
|
||||
# Explicitly specify the NPM registry that "rush install" and "rush update" will use by default:
|
||||
registry=https://registry.npmjs.org/
|
||||
|
||||
# Optionally provide an authentication token for the above registry URL (if it is a private registry):
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
|
||||
# Change this to "true" if your registry requires authentication for read-only operations:
|
||||
always-auth=false
|
||||
@@ -0,0 +1,29 @@
|
||||
# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish
|
||||
# is used by the "rush publish" command, as publishing often involves different credentials
|
||||
# and registries than other operations.
|
||||
#
|
||||
# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc"
|
||||
# and then temporarily map that folder as the "home directory" for the current user account.
|
||||
# This enables the same settings to apply for each project folder that gets published. The copied file
|
||||
# will omit any config lines that reference environment variables that are undefined in that session;
|
||||
# this avoids problems that would otherwise result due to a missing variable being replaced by
|
||||
# an empty string.
|
||||
#
|
||||
# * * * SECURITY WARNING * * *
|
||||
#
|
||||
# It is NOT recommended to store authentication tokens in a text file on a lab machine, because
|
||||
# other unrelated processes may be able to read the file. Also, the file may persist indefinitely,
|
||||
# for example if the machine loses power. A safer practice is to pass the token via an
|
||||
# environment variable, which can be referenced from .npmrc using ${} expansion. For example:
|
||||
#
|
||||
# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}
|
||||
#
|
||||
|
||||
# Explicitly specify the NPM registry that "rush publish" will use by default:
|
||||
registry=https://registry.npmjs.org/
|
||||
|
||||
# Optionally provide an authentication token for the above registry URL (if it is a private registry):
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
|
||||
# Change this to "true" if your registry requires authentication for read-only operations:
|
||||
always-auth=false
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* When using the PNPM package manager, you can use pnpmfile.js to workaround
|
||||
* dependencies that have mistakes in their package.json file. (This feature is
|
||||
* functionally similar to Yarn's "resolutions".)
|
||||
*
|
||||
* For details, see the PNPM documentation:
|
||||
* https://pnpm.io/pnpmfile#hooks
|
||||
*
|
||||
* IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE
|
||||
* ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run
|
||||
* "rush update --full" so that PNPM will recalculate all version selections.
|
||||
*/
|
||||
module.exports = {
|
||||
hooks: {
|
||||
readPackage
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This hook is invoked during installation before a package's dependencies
|
||||
* are selected.
|
||||
* The `packageJson` parameter is the deserialized package.json
|
||||
* contents for the package that is about to be installed.
|
||||
* The `context` parameter provides a log() function.
|
||||
* The return value is the updated object.
|
||||
*/
|
||||
function readPackage(packageJson, context) {
|
||||
|
||||
// // The karma types have a missing dependency on typings from the log4js package.
|
||||
// if (packageJson.name === '@types/karma') {
|
||||
// context.log('Fixed up dependencies for @types/karma');
|
||||
// packageJson.dependencies['log4js'] = '0.6.38';
|
||||
// }
|
||||
|
||||
return packageJson;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* This configuration file manages Rush integration with JFrog Artifactory services.
|
||||
* More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json",
|
||||
|
||||
"packageRegistry": {
|
||||
/**
|
||||
* (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry.
|
||||
* When enabled, "rush install" will automatically detect when the user's ~/.npmrc
|
||||
* authentication token is missing or expired. And "rush setup" will prompt the user to
|
||||
* renew their token.
|
||||
*
|
||||
* The default value is false.
|
||||
*/
|
||||
"enabled": false,
|
||||
|
||||
/**
|
||||
* (Required) Specify the URL of your NPM registry. This is the same URL that appears in
|
||||
* your .npmrc file. It should look something like this example:
|
||||
*
|
||||
* https://your-company.jfrog.io/your-project/api/npm/npm-private/
|
||||
*/
|
||||
"registryUrl": "",
|
||||
|
||||
/**
|
||||
* A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time
|
||||
* when the token is updated. This could be used for example to configure the company registry
|
||||
* to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush
|
||||
* operations like "rush add" and "rush install", which get their mappings from the monorepo's
|
||||
* common/config/rush/.npmrc file).
|
||||
*
|
||||
* NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful
|
||||
* about adding settings that may interfere with other work outside the monorepo.
|
||||
*/
|
||||
"userNpmrcLinesToAdd": [
|
||||
// "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/"
|
||||
],
|
||||
|
||||
/**
|
||||
* (Required) Specifies the URL of the Artifactory control panel where the user can generate
|
||||
* an API key. This URL is printed after the "visitWebsite" message.
|
||||
* It should look something like this example: https://your-company.jfrog.io/
|
||||
* Specify an empty string to suppress this line entirely.
|
||||
*/
|
||||
"artifactoryWebsiteUrl": "",
|
||||
|
||||
/**
|
||||
* Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file.
|
||||
* The default is "password", which means the user's API token will be traded in for an
|
||||
* npm password specific to that registry. Optionally you can specify "authToken", which
|
||||
* will save the user's API token as credentials instead.
|
||||
*/
|
||||
// "credentialType": "password",
|
||||
|
||||
/**
|
||||
* These settings allow the "rush setup" interactive prompts to be customized, for
|
||||
* example with messages specific to your team or configuration. Specify an empty string
|
||||
* to suppress that message entirely.
|
||||
*/
|
||||
"messageOverrides": {
|
||||
/**
|
||||
* Overrides the message that normally says:
|
||||
* "This monorepo consumes packages from an Artifactory private NPM registry."
|
||||
*/
|
||||
// "introduction": "",
|
||||
|
||||
/**
|
||||
* Overrides the message that normally says:
|
||||
* "Please contact the repository maintainers for help with setting up an Artifactory user account."
|
||||
*/
|
||||
// "obtainAnAccount": "",
|
||||
|
||||
/**
|
||||
* Overrides the message that normally says:
|
||||
* "Please open this URL in your web browser:"
|
||||
*
|
||||
* The "artifactoryWebsiteUrl" string is printed after this message.
|
||||
*/
|
||||
// "visitWebsite": "",
|
||||
|
||||
/**
|
||||
* Overrides the message that normally says:
|
||||
* "Your user name appears in the upper-right corner of the JFrog website."
|
||||
*/
|
||||
// "locateUserName": "",
|
||||
|
||||
/**
|
||||
* Overrides the message that normally says:
|
||||
* "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key'
|
||||
* button if you haven't already done so previously."
|
||||
*/
|
||||
// "locateApiKey": ""
|
||||
|
||||
/**
|
||||
* Overrides the message that normally prompts:
|
||||
* "What is your Artifactory user name?"
|
||||
*/
|
||||
// "userNamePrompt": ""
|
||||
|
||||
/**
|
||||
* Overrides the message that normally prompts:
|
||||
* "What is your Artifactory API key?"
|
||||
*/
|
||||
// "apiKeyPrompt": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* This configuration file manages Rush's build cache feature.
|
||||
* More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json",
|
||||
|
||||
/**
|
||||
* (Required) EXPERIMENTAL - Set this to true to enable the build cache feature.
|
||||
*
|
||||
* See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature.
|
||||
*/
|
||||
"buildCacheEnabled": false,
|
||||
|
||||
/**
|
||||
* (Required) Choose where project build outputs will be cached.
|
||||
*
|
||||
* Possible values: "local-only", "azure-blob-storage", "amazon-s3"
|
||||
*/
|
||||
"cacheProvider": "local-only",
|
||||
|
||||
/**
|
||||
* Setting this property overrides the cache entry ID. If this property is set, it must contain
|
||||
* a [hash] token.
|
||||
*
|
||||
* Other available tokens:
|
||||
* - [projectName] Example: "@my-scope/my-project"
|
||||
* - [projectName:normalize] Example: "my-scope+my-project"
|
||||
* - [phaseName] Example: "_phase:test/api"
|
||||
* - [phaseName:normalize] Example: "_phase:test+api"
|
||||
* - [phaseName:trimPrefix] Example: "test/api"
|
||||
* - [os] Example: "win32"
|
||||
* - [arch] Example: "x64"
|
||||
*/
|
||||
// "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]"
|
||||
|
||||
/**
|
||||
* (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes.
|
||||
*/
|
||||
// "cacheHashSalt": "1",
|
||||
|
||||
/**
|
||||
* Use this configuration with "cacheProvider"="azure-blob-storage"
|
||||
*/
|
||||
"azureBlobStorageConfiguration": {
|
||||
/**
|
||||
* (Required) The name of the the Azure storage account to use for build cache.
|
||||
*/
|
||||
// "storageAccountName": "example",
|
||||
|
||||
/**
|
||||
* (Required) The name of the container in the Azure storage account to use for build cache.
|
||||
*/
|
||||
// "storageContainerName": "my-container",
|
||||
|
||||
/**
|
||||
* The Azure environment the storage account exists in. Defaults to AzurePublicCloud.
|
||||
*
|
||||
* Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment"
|
||||
*/
|
||||
// "azureEnvironment": "AzurePublicCloud",
|
||||
|
||||
/**
|
||||
* An optional prefix for cache item blob names.
|
||||
*/
|
||||
// "blobPrefix": "my-prefix",
|
||||
|
||||
/**
|
||||
* If set to true, allow writing to the cache. Defaults to false.
|
||||
*/
|
||||
// "isCacheWriteAllowed": true,
|
||||
|
||||
/**
|
||||
* The Entra ID login flow to use. Defaults to 'AdoCodespacesAuth' on GitHub Codespaces, 'InteractiveBrowser' otherwise.
|
||||
*/
|
||||
// "loginFlow": "InteractiveBrowser",
|
||||
|
||||
/**
|
||||
* If set to true, reading the cache requires authentication. Defaults to false.
|
||||
*/
|
||||
// "readRequiresAuthentication": true
|
||||
},
|
||||
|
||||
/**
|
||||
* Use this configuration with "cacheProvider"="amazon-s3"
|
||||
*/
|
||||
"amazonS3Configuration": {
|
||||
/**
|
||||
* (Required unless s3Endpoint is specified) The name of the bucket to use for build cache.
|
||||
* Example: "my-bucket"
|
||||
*/
|
||||
// "s3Bucket": "my-bucket",
|
||||
|
||||
/**
|
||||
* (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache.
|
||||
* This should not include any path; use the s3Prefix to set the path.
|
||||
* Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000"
|
||||
*/
|
||||
// "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com",
|
||||
|
||||
/**
|
||||
* (Required) The Amazon S3 region of the bucket to use for build cache.
|
||||
* Example: "us-east-1"
|
||||
*/
|
||||
// "s3Region": "us-east-1",
|
||||
|
||||
/**
|
||||
* An optional prefix ("folder") for cache items. It should not start with "/".
|
||||
*/
|
||||
// "s3Prefix": "my-prefix",
|
||||
|
||||
/**
|
||||
* If set to true, allow writing to the cache. Defaults to false.
|
||||
*/
|
||||
// "isCacheWriteAllowed": true
|
||||
},
|
||||
|
||||
/**
|
||||
* Use this configuration with "cacheProvider"="http"
|
||||
*/
|
||||
"httpConfiguration": {
|
||||
/**
|
||||
* (Required) The URL of the server that stores the caches.
|
||||
* Example: "https://build-cacches.example.com/"
|
||||
*/
|
||||
// "url": "https://build-cacches.example.com/",
|
||||
|
||||
/**
|
||||
* (Optional) The HTTP method to use when writing to the cache (defaults to PUT).
|
||||
* Should be one of PUT, POST, or PATCH.
|
||||
* Example: "PUT"
|
||||
*/
|
||||
// "uploadMethod": "PUT",
|
||||
|
||||
/**
|
||||
* (Optional) HTTP headers to pass to the cache server.
|
||||
* Example: { "X-HTTP-Company-Id": "109283" }
|
||||
*/
|
||||
// "headers": {},
|
||||
|
||||
/**
|
||||
* (Optional) Shell command that prints the authorization token needed to communicate with the
|
||||
* cache server, and exits with exit code 0. This command will be executed from the root of
|
||||
* the monorepo.
|
||||
* Example: { "exec": "node", "args": ["common/scripts/auth.js"] }
|
||||
*/
|
||||
// "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] },
|
||||
|
||||
/**
|
||||
* (Optional) Prefix for cache keys.
|
||||
* Example: "my-company-"
|
||||
*/
|
||||
// "cacheKeyPrefix": "",
|
||||
|
||||
/**
|
||||
* (Optional) If set to true, allow writing to the cache. Defaults to false.
|
||||
*/
|
||||
// "isCacheWriteAllowed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* This configuration file manages Rush's cobuild feature.
|
||||
* More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json",
|
||||
|
||||
/**
|
||||
* (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature.
|
||||
* RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string,
|
||||
* otherwise the cobuild feature will be disabled.
|
||||
*/
|
||||
"cobuildFeatureEnabled": false,
|
||||
|
||||
/**
|
||||
* (Required) Choose where cobuild lock will be acquired.
|
||||
*
|
||||
* The lock provider is registered by the rush plugins.
|
||||
* For example, @rushstack/rush-redis-cobuild-plugin registers the "redis" lock provider.
|
||||
*/
|
||||
"cobuildLockProvider": "redis"
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
/**
|
||||
* This configuration file defines custom commands for the "rush" command-line.
|
||||
* More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",
|
||||
|
||||
"phases": [
|
||||
{
|
||||
"name": "_phase:build",
|
||||
"dependencies": {
|
||||
"upstream": ["_phase:build"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": false
|
||||
},
|
||||
{
|
||||
"name": "_phase:validate",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"],
|
||||
"upstream": ["_phase:validate", "_phase:build"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": false
|
||||
},
|
||||
{
|
||||
"name": "_phase:test",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"],
|
||||
"upstream": ["_phase:validate"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": true
|
||||
},
|
||||
{
|
||||
"name": "_phase:lint",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": false
|
||||
},
|
||||
{
|
||||
"name": "_phase:bundle",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": false
|
||||
},
|
||||
{
|
||||
"name": "_phase:format",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": false
|
||||
},
|
||||
{
|
||||
"name": "_phase:svelte-check",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": true
|
||||
},
|
||||
{
|
||||
"name": "_phase:package",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build"],
|
||||
"upstream": ["_phase:package"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": false
|
||||
},
|
||||
{
|
||||
"name": "_phase:docker-build",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build", "_phase:package", "_phase:bundle"],
|
||||
"upstream": ["_phase:build", "_phase:bundle", "_phase:package"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": true
|
||||
},
|
||||
{
|
||||
"name": "_phase:docker-staging",
|
||||
"dependencies": {
|
||||
"self": ["_phase:build", "_phase:package", "_phase:bundle"]
|
||||
},
|
||||
"ignoreMissingScript": true,
|
||||
"allowWarningsOnSuccess": true
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "coverage",
|
||||
"summary": "Run tests, merge LCOV and generate HTML coverage",
|
||||
"description": "Run 'rush test', then merge per-package LCOV files and generate HTML coverage in coverage/html",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "rush test && node scripts/merge-coverage.js && node scripts/generate-coverage-html.js coverage/lcov.info coverage/html"
|
||||
},
|
||||
{
|
||||
"commandKind": "bulk",
|
||||
"name": "format",
|
||||
"summary": "Format",
|
||||
"description": "Perform a formatting",
|
||||
"enableParallelism": true,
|
||||
"incremental": false,
|
||||
"ignoreMissingScript": true,
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"disableBuildCache": true
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "doformat",
|
||||
"summary": "Do a format and show errors after it",
|
||||
"description": "Do a format and show errors after it",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "(rush format && true) && ./common/scripts/format-show.sh"
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"name": "build:watch",
|
||||
"summary": "Build and watch",
|
||||
"phases": ["_phase:build", "_phase:validate"],
|
||||
"description": "Perform build with tsc and watch for changes with rush",
|
||||
"enableParallelism": true,
|
||||
"incremental": true,
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"watchOptions": {
|
||||
"alwaysWatch": true,
|
||||
"watchPhases": ["_phase:build", "_phase:validate"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"summary": "svelte-check",
|
||||
"commandKind": "phased",
|
||||
"name": "svelte-check",
|
||||
"phases": ["_phase:build", "_phase:svelte-check"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"name": "build",
|
||||
"summary": "build",
|
||||
"phases": ["_phase:build"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true,
|
||||
"watchOptions": {
|
||||
"alwaysWatch": false,
|
||||
"watchPhases": ["_phase:build"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"name": "validate",
|
||||
"phases": ["_phase:validate"],
|
||||
"summary": "validate",
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"name": "rebuild",
|
||||
"summary": "ReBuild and test all projects.",
|
||||
"phases": ["_phase:build"],
|
||||
"enableParallelism": true,
|
||||
"incremental": false
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"name": "dorevalidate",
|
||||
"phases": ["_phase:validate"],
|
||||
"summary": "dorevalidate",
|
||||
"enableParallelism": true,
|
||||
"incremental": false
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"summary": "Do testing",
|
||||
"name": "test",
|
||||
"phases": ["_phase:build", "_phase:test"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"name": "retest",
|
||||
"summary": "Build and test all projects.",
|
||||
"phases": ["_phase:build", "_phase:test"],
|
||||
"enableParallelism": true,
|
||||
"incremental": false
|
||||
},
|
||||
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"summary": "Do bundle",
|
||||
"name": "bundle",
|
||||
"phases": ["_phase:build", "_phase:bundle"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"commandKind": "phased",
|
||||
"summary": "Do packaging",
|
||||
"name": "package",
|
||||
"phases": ["_phase:build", "_phase:package"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
|
||||
{
|
||||
"summary": "docker:build",
|
||||
"commandKind": "phased",
|
||||
"name": "docker:build",
|
||||
"phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"summary": "docker:rebuild",
|
||||
"commandKind": "phased",
|
||||
"name": "docker:rebuild",
|
||||
"phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-build"],
|
||||
"enableParallelism": true,
|
||||
"incremental": false
|
||||
},
|
||||
{
|
||||
"summary": "docker:staging",
|
||||
"commandKind": "phased",
|
||||
"name": "docker:staging",
|
||||
"phases": ["_phase:build", "_phase:bundle", "_phase:package", "_phase:docker-staging"],
|
||||
"enableParallelism": true,
|
||||
"incremental": true
|
||||
},
|
||||
{
|
||||
"commandKind": "bulk",
|
||||
"name": "docker:push",
|
||||
"summary": "docker:push",
|
||||
"description": "Push docker release images",
|
||||
"enableParallelism": true,
|
||||
"incremental": false,
|
||||
"ignoreDependencyOrder": false,
|
||||
"ignoreMissingScript": true,
|
||||
"disableBuildCache": true,
|
||||
"allowWarningsInSuccessfulBuild": true
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "docker",
|
||||
"summary": "Build docker with platform",
|
||||
"description": "use to build all docker containers required for platform",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "./common/scripts/docker.sh"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "docker:up",
|
||||
"summary": "Up development build",
|
||||
"description": "Up development build",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "cd ./dev && docker compose up -d --force-recreate"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "docker:local",
|
||||
"summary": "Up development build",
|
||||
"description": "Up development build",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "cd ./dev/local-mongo && docker compose -p dev up -d --force-recreate"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "tool:upgrade",
|
||||
"summary": "Upgrade all local models",
|
||||
"description": "Upgrade all local models",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "cd ./dev/tool && rushx run-local upgrade -f"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "apply-templates",
|
||||
"summary": "Update all package.json according to templates matched from templates folder",
|
||||
"description": "Use to update all projects to templates",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "node templates/apply.js"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "ts-clean",
|
||||
"summary": "Clean tsconfig.tsbuildinfo",
|
||||
"description": "Clean typescript incremental cache",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "find .|grep tsconfig.tsbuildinfo | xargs rm | pwd"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "revalidate",
|
||||
"summary": "Clean Validate cache and to validate again",
|
||||
"description": "Clean typescript incremental cache",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "find .|grep tsBuildInfoFile.info | xargs rm | pwd && rush dorevalidate"
|
||||
},
|
||||
{
|
||||
"commandKind": "bulk",
|
||||
"name": "remove-ts-types",
|
||||
"summary": "Clean validate types",
|
||||
"description": "Clean validate types",
|
||||
"shellCommand": "rm -rf ./types",
|
||||
"enableParallelism": true,
|
||||
"incremental": false,
|
||||
"ignoreDependencyOrder": true,
|
||||
"ignoreMissingScript": true,
|
||||
"disableBuildCache": true,
|
||||
"allowWarningsInSuccessfulBuild": true
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "model-version",
|
||||
"summary": "show model version",
|
||||
"description": "show model version",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "npx node ./common/scripts/show_version.js"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "show-model",
|
||||
"summary": "show model",
|
||||
"description": "show model",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "cd ./models/all && rushx show-model"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "deps-clean",
|
||||
"summary": "Clean package-deps-*.json files",
|
||||
"description": "Clean package-deps-*.json files",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "find .|grep .rush/temp/package-deps_ | xargs rm"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "fast-format",
|
||||
"summary": "Format changed projects",
|
||||
"description": "Format and autofix linting issues in changed projects",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "./common/scripts/fast-format.sh"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
"name": "bump-changes",
|
||||
"summary": "Bump changes from tag",
|
||||
"description": "Bump changes from previous tag",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "./common/scripts/node_modules/.bin/bump-changes-from-tag"
|
||||
}
|
||||
],
|
||||
|
||||
/**
|
||||
* Custom "parameters" introduce new parameters for specified Rush command-line commands.
|
||||
* For example, you might define a "--production" parameter for the "rush build" command.
|
||||
*/
|
||||
"parameters": [
|
||||
{
|
||||
"parameterKind": "flag",
|
||||
"longName": "--lite",
|
||||
"shortName": "-l",
|
||||
"description": "Enable Heft lite building option, will skip some phases.",
|
||||
"associatedCommands": ["build", "rebuild"]
|
||||
},
|
||||
{
|
||||
"parameterKind": "flag",
|
||||
"longName": "--clean",
|
||||
"description": "Enable Heft clean building option",
|
||||
"associatedCommands": ["build", "rebuild"]
|
||||
},
|
||||
{
|
||||
"parameterKind": "flag",
|
||||
"longName": "--force",
|
||||
"shortName": "-f",
|
||||
"description": "Force formatting",
|
||||
"associatedCommands": ["format"]
|
||||
},
|
||||
{
|
||||
"parameterKind": "string",
|
||||
"argumentName": "BRANCH",
|
||||
"required": false,
|
||||
"associatedPhases": [],
|
||||
"shortName": "-b",
|
||||
"longName": "--branch",
|
||||
"description": "Force formatting of branch",
|
||||
"associatedCommands": ["fast-format"]
|
||||
}
|
||||
// {
|
||||
// /**
|
||||
// * (Required) Determines the type of custom parameter.
|
||||
// * A "flag" is a custom command-line parameter whose presence acts as an on/off switch.
|
||||
// */
|
||||
// "parameterKind": "flag",
|
||||
//
|
||||
// /**
|
||||
// * (Required) The long name of the parameter. It must be lower-case and use dash delimiters.
|
||||
// */
|
||||
// "longName": "--my-flag",
|
||||
//
|
||||
// /**
|
||||
// * An optional alternative short name for the parameter. It must be a dash followed by a single
|
||||
// * lower-case or upper-case letter, which is case-sensitive.
|
||||
// *
|
||||
// * NOTE: The Rush developers recommend that automation scripts should always use the long name
|
||||
// * to improve readability. The short name is only intended as a convenience for humans.
|
||||
// * The alphabet letters run out quickly, and are difficult to memorize, so *only* use
|
||||
// * a short name if you expect the parameter to be needed very often in everyday operations.
|
||||
// */
|
||||
// "shortName": "-m",
|
||||
//
|
||||
// /**
|
||||
// * (Required) A long description to be shown in the command-line help.
|
||||
// *
|
||||
// * Whenever you introduce commands/parameters, taking a little time to write meaningful
|
||||
// * documentation can make a big difference for the developer experience in your repo.
|
||||
// */
|
||||
// "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects",
|
||||
//
|
||||
// /**
|
||||
// * (Required) A list of custom commands and/or built-in Rush commands that this parameter may
|
||||
// * be used with. The parameter will be appended to the shell command that Rush invokes.
|
||||
// */
|
||||
// "associatedCommands": ["build", "rebuild"]
|
||||
// },
|
||||
//
|
||||
// {
|
||||
// /**
|
||||
// * (Required) Determines the type of custom parameter.
|
||||
// * A "string" is a custom command-line parameter whose value is a simple text string.
|
||||
// */
|
||||
// "parameterKind": "string",
|
||||
// "longName": "--my-string",
|
||||
// "description": "A custom string parameter for the \"my-global-command\" custom command",
|
||||
//
|
||||
// "associatedCommands": ["my-global-command"],
|
||||
//
|
||||
// /**
|
||||
// * The name of the argument, which will be shown in the command-line help.
|
||||
// *
|
||||
// * For example, if the parameter name is '--count" and the argument name is "NUMBER",
|
||||
// * then the command-line help would display "--count NUMBER". The argument name must
|
||||
// * be comprised of upper-case letters, numbers, and underscores. It should be kept short.
|
||||
// */
|
||||
// "argumentName": "SOME_TEXT",
|
||||
//
|
||||
// /**
|
||||
// * If true, this parameter must be included with the command. The default is false.
|
||||
// */
|
||||
// "required": false
|
||||
// },
|
||||
//
|
||||
// {
|
||||
// /**
|
||||
// * (Required) Determines the type of custom parameter.
|
||||
// * A "choice" is a custom command-line parameter whose argument must be chosen from a list of
|
||||
// * allowable alternatives.
|
||||
// */
|
||||
// "parameterKind": "choice",
|
||||
// "longName": "--my-choice",
|
||||
// "description": "A custom choice parameter for the \"my-global-command\" custom command",
|
||||
//
|
||||
// "associatedCommands": ["my-global-command"],
|
||||
//
|
||||
// /**
|
||||
// * If true, this parameter must be included with the command. The default is false.
|
||||
// */
|
||||
// "required": false,
|
||||
//
|
||||
// /**
|
||||
// * Normally if a parameter is omitted from the command line, it will not be passed
|
||||
// * to the shell command. this value will be inserted by default. Whereas if a "defaultValue"
|
||||
// * is defined, the parameter will always be passed to the shell command, and will use the
|
||||
// * default value if unspecified. The value must be one of the defined alternatives.
|
||||
// */
|
||||
// "defaultValue": "vanilla",
|
||||
//
|
||||
// /**
|
||||
// * (Required) A list of alternative argument values that can be chosen for this parameter.
|
||||
// */
|
||||
// "alternatives": [
|
||||
// {
|
||||
// /**
|
||||
// * A token that is one of the alternatives that can be used with the choice parameter,
|
||||
// * e.g. "vanilla" in "--flavor vanilla".
|
||||
// */
|
||||
// "name": "vanilla",
|
||||
//
|
||||
// /**
|
||||
// * A detailed description for the alternative that can be shown in the command-line help.
|
||||
// *
|
||||
// * Whenever you introduce commands/parameters, taking a little time to write meaningful
|
||||
// * documentation can make a big difference for the developer experience in your repo.
|
||||
// */
|
||||
// "description": "Use the vanilla flavor (the default)"
|
||||
// },
|
||||
//
|
||||
// {
|
||||
// "name": "chocolate",
|
||||
// "description": "Use the chocolate flavor"
|
||||
// },
|
||||
//
|
||||
// {
|
||||
// "name": "strawberry",
|
||||
// "description": "Use the strawberry flavor"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* This configuration file specifies NPM dependency version selections that affect all projects
|
||||
* in a Rush repo. More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json",
|
||||
|
||||
/**
|
||||
* A table that specifies a "preferred version" for a given NPM package. This feature is typically used
|
||||
* to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies.
|
||||
*
|
||||
* The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into
|
||||
* the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager
|
||||
* will calculate versions. The specific effect depends on your package manager. Generally it will have no
|
||||
* effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be
|
||||
* achieved using the pnpmfile.js hook. See the Rush documentation for more details.
|
||||
*
|
||||
* After modifying this field, it's recommended to run "rush update --full" so that the package manager
|
||||
* will recalculate all version selections.
|
||||
*/
|
||||
"preferredVersions": {
|
||||
/**
|
||||
* When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo,
|
||||
* instead of the latest version.
|
||||
*/
|
||||
// "some-library": "1.2.3"
|
||||
},
|
||||
|
||||
/**
|
||||
* When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions,
|
||||
* except in cases where different projects specify different version ranges for a given dependency. For older
|
||||
* package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause
|
||||
* trouble for indirect dependencies with incompatible peerDependencies ranges.
|
||||
*
|
||||
* The default value is true. If you're encountering installation errors related to peer dependencies,
|
||||
* it's recommended to set this to false.
|
||||
*
|
||||
* After modifying this field, it's recommended to run "rush update --full" so that the package manager
|
||||
* will recalculate all version selections.
|
||||
*/
|
||||
// "implicitlyPreferredVersions": false,
|
||||
|
||||
/**
|
||||
* If you would like the version specifiers for your dependencies to be consistent, then
|
||||
* uncomment this line. This is effectively similar to running "rush check" before any
|
||||
* of the following commands:
|
||||
*
|
||||
* rush install, rush update, rush link, rush version, rush publish
|
||||
*
|
||||
* In some cases you may want this turned on, but need to allow certain packages to use a different
|
||||
* version. In those cases, you will need to add an entry to the "allowedAlternativeVersions"
|
||||
* section of the common-versions.json.
|
||||
*
|
||||
* In the case that subspaces is enabled, this setting will take effect at a subspace level.
|
||||
*/
|
||||
// "ensureConsistentVersions": true,
|
||||
|
||||
/**
|
||||
* The "rush check" command can be used to enforce that every project in the repo must specify
|
||||
* the same SemVer range for a given dependency. However, sometimes exceptions are needed.
|
||||
* The allowedAlternativeVersions table allows you to list other SemVer ranges that will be
|
||||
* accepted by "rush check" for a given dependency.
|
||||
*
|
||||
* IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE
|
||||
* USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO).
|
||||
* This design avoids unnecessary churn in this file.
|
||||
*/
|
||||
"allowedAlternativeVersions": {
|
||||
/**
|
||||
* For example, allow some projects to use an older TypeScript compiler
|
||||
* (in addition to whatever "usual" version is being used by other projects in the repo):
|
||||
*/
|
||||
// "typescript": [
|
||||
// "~2.4.0"
|
||||
// ]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* This configuration file allows repo maintainers to configure extra details to be
|
||||
* printed alongside certain Rush messages. More documentation is available on the
|
||||
* Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json",
|
||||
|
||||
/**
|
||||
* Custom tips allow you to annotate Rush's console messages with advice tailored for
|
||||
* your specific monorepo.
|
||||
*/
|
||||
"customTips": [
|
||||
// {
|
||||
// /**
|
||||
// * (REQUIRED) An identifier indicating a message that may be printed by Rush.
|
||||
// * If that message is printed, then this custom tip will be shown.
|
||||
// * The list of available tip identifiers can be found on this page:
|
||||
// * https://rushjs.io/pages/maintainer/custom_tips/
|
||||
// */
|
||||
// "tipId": "TIP_RUSH_INCONSISTENT_VERSIONS",
|
||||
//
|
||||
// /**
|
||||
// * (REQUIRED) The message text to be displayed for this tip.
|
||||
// */
|
||||
// "message": "For additional troubleshooting information, refer this wiki article:\n\nhttps://intranet.contoso.com/docs/pnpm-mismatch"
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* This configuration file allows repo maintainers to enable and disable experimental
|
||||
* Rush features. More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json",
|
||||
|
||||
/**
|
||||
* By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'.
|
||||
* Set this option to true to pass '--frozen-lockfile' instead for faster installs.
|
||||
*/
|
||||
// "usePnpmFrozenLockfileForRushInstall": true,
|
||||
|
||||
/**
|
||||
* By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'.
|
||||
* Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes.
|
||||
*/
|
||||
// "usePnpmPreferFrozenLockfileForRushUpdate": true,
|
||||
|
||||
/**
|
||||
* By default, 'rush update' runs as a single operation.
|
||||
* Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install.
|
||||
* Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs.
|
||||
*/
|
||||
// "usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate": true,
|
||||
|
||||
/**
|
||||
* If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies.
|
||||
* Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not
|
||||
* cause hash changes.
|
||||
*/
|
||||
// "omitImportersFromPreventManualShrinkwrapChanges": true,
|
||||
|
||||
/**
|
||||
* If true, the chmod field in temporary project tar headers will not be normalized.
|
||||
* This normalization can help ensure consistent tarball integrity across platforms.
|
||||
*/
|
||||
// "noChmodFieldInTarHeaderNormalization": true,
|
||||
|
||||
/**
|
||||
* If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings.
|
||||
* This will not replay warnings from the cached build.
|
||||
*/
|
||||
// "buildCacheWithAllowWarningsInSuccessfulBuild": true,
|
||||
|
||||
/**
|
||||
* If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings.
|
||||
* This will not replay warnings from the skipped build.
|
||||
*/
|
||||
// "buildSkipWithAllowWarningsInSuccessfulBuild": true,
|
||||
|
||||
/**
|
||||
* If true, perform a clean install after when running `rush install` or `rush update` if the
|
||||
* `.npmrc` file has changed since the last install.
|
||||
*/
|
||||
// "cleanInstallAfterNpmrcChanges": true,
|
||||
|
||||
/**
|
||||
* If true, print the outputs of shell commands defined in event hooks to the console.
|
||||
*/
|
||||
// "printEventHooksOutputToConsole": true,
|
||||
|
||||
/**
|
||||
* If true, Rush will not allow node_modules in the repo folder or in parent folders.
|
||||
*/
|
||||
// "forbidPhantomResolvableNodeModulesFolders": true,
|
||||
|
||||
/**
|
||||
* (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot
|
||||
* correctly satisfy versioning requirements without installing duplicate copies of a package inside the
|
||||
* node_modules folder. This poses a problem for "workspace:*" dependencies, as they are normally
|
||||
* installed by making a symlink to the local project source folder. PNPM's "injected dependencies"
|
||||
* feature provides a model for copying the local project folder into node_modules, however copying
|
||||
* must occur AFTER the dependency project is built and BEFORE the consuming project starts to build.
|
||||
* The "pnpm-sync" tool manages this operation; see its documentation for details.
|
||||
* Enable this experiment if you want "rush" and "rushx" commands to resync injected dependencies
|
||||
* by invoking "pnpm-sync" during the build.
|
||||
*/
|
||||
// "usePnpmSyncForInjectedDependencies": true,
|
||||
|
||||
/**
|
||||
* If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`.
|
||||
*/
|
||||
// "generateProjectImpactGraphDuringRushUpdate": true,
|
||||
|
||||
/**
|
||||
* If true, when running in watch mode, Rush will check for phase scripts named `_phase:<name>:ipc` and run them instead
|
||||
* of `_phase:<name>` if they exist. The created child process will be provided with an IPC channel and expected to persist
|
||||
* across invocations.
|
||||
*/
|
||||
// "useIPCScriptsInWatchMode": true,
|
||||
|
||||
/**
|
||||
* (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers
|
||||
* working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands.
|
||||
* This ensures that important notices will be seen by anyone doing active development, since people often
|
||||
* ignore normal discussion group messages or don't know to subscribe.
|
||||
*/
|
||||
// "rushAlerts": true,
|
||||
|
||||
|
||||
/**
|
||||
* When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache.
|
||||
*/
|
||||
// "allowCobuildWithoutCache": true,
|
||||
|
||||
/**
|
||||
* By default, rush perform a full scan of the entire repository. For example, Rush runs `git status` to check for local file changes.
|
||||
* When this toggle is enabled, Rush will only scan specific paths, significantly speeding up Git operations.
|
||||
*/
|
||||
// "enableSubpathScan": true,
|
||||
|
||||
/**
|
||||
* Rush has a policy that normally requires Rush projects to specify `workspace:*` in package.json when depending
|
||||
* on other projects in the workspace, unless they are explicitly declared as `decoupledLocalDependencies`
|
||||
* in rush.json. Enabling this experiment will remove that requirement for dependencies belonging to a different
|
||||
* subspace. This is useful for large product groups who work in separate subspaces and generally prefer to consume
|
||||
* each other's packages via the NPM registry.
|
||||
*/
|
||||
// "exemptDecoupledDependenciesBetweenSubspaces": false
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* This configuration file provides settings specific to the PNPM package manager.
|
||||
* More documentation is available on the Rush website: https://rushjs.io
|
||||
*
|
||||
* Rush normally looks for this file in `common/config/rush/pnpm-config.json`. However,
|
||||
* if `subspacesEnabled` is true in subspaces.json, then Rush will instead first look
|
||||
* for `common/config/subspaces/<name>/pnpm-config.json`. (If the file exists in both places,
|
||||
* then the file under `common/config/rush` is ignored.)
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json",
|
||||
|
||||
/**
|
||||
* If true, then `rush install` and `rush update` will use the PNPM workspaces feature
|
||||
* to perform the install, instead of the old model where Rush generated the symlinks
|
||||
* for each projects's node_modules folder.
|
||||
*
|
||||
* When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing
|
||||
* all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements
|
||||
* Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs`
|
||||
* is invoked by the shim.
|
||||
*
|
||||
* This option is strongly recommended. The default value is false.
|
||||
*/
|
||||
"useWorkspaces": true,
|
||||
|
||||
/**
|
||||
* This setting determines how PNPM chooses version numbers during `rush update`.
|
||||
* For example, suppose `lib-x@3.0.0` depends on `"lib-y": "^1.2.3"` whose latest major
|
||||
* releases are `1.8.9` and `2.3.4`. The resolution mode `lowest-direct` might choose
|
||||
* `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the
|
||||
* highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring
|
||||
* that the version could have been tested by the maintainer of "lib-x"). For local workspace
|
||||
* projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless
|
||||
* they are explicitly requested. Although `time-based` is the most robust option, it may be
|
||||
* slightly slower with registries such as npmjs.com that have not implemented an optimization.
|
||||
*
|
||||
* IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of
|
||||
* `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users.
|
||||
* Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to
|
||||
* `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc,
|
||||
* regardless of your PNPM version.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/npmrc#resolution-mode
|
||||
*
|
||||
* Possible values are: `highest`, `time-based`, and `lowest-direct`.
|
||||
* The default is `highest`.
|
||||
*/
|
||||
// "resolutionMode": "time-based",
|
||||
|
||||
/**
|
||||
* This setting determines whether PNPM will automatically install (non-optional)
|
||||
* missing peer dependencies instead of reporting an error. Doing so conveniently
|
||||
* avoids the need to specify peer versions in package.json, but in a large monorepo
|
||||
* this often creates worse problems. The reason is that peer dependency behavior
|
||||
* is inherently complicated, and it is easier to troubleshoot consequences of an explicit
|
||||
* version than an invisible heuristic. The original NPM RFC discussion pointed out
|
||||
* some other problems with this feature: https://github.com/npm/rfcs/pull/43
|
||||
|
||||
* IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however,
|
||||
* as of Rush version 5.109.0 the default is always false unless `autoInstallPeers`
|
||||
* is specified in pnpm-config.json or .npmrc, regardless of your PNPM version.
|
||||
|
||||
* PNPM documentation: https://pnpm.io/npmrc#auto-install-peers
|
||||
|
||||
* The default value is false.
|
||||
*/
|
||||
// "autoInstallPeers": false,
|
||||
|
||||
/**
|
||||
* If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when
|
||||
* invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies,
|
||||
* which is an invalid state that can cause build failures or incompatible dependency versions.
|
||||
* (For historical reasons, JavaScript package managers generally do not treat this invalid
|
||||
* state as an error.)
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies
|
||||
*
|
||||
* The default value is false to avoid legacy compatibility issues.
|
||||
* It is strongly recommended to set `strictPeerDependencies=true`.
|
||||
*/
|
||||
"strictPeerDependencies": true,
|
||||
|
||||
/**
|
||||
* Environment variables that will be provided to PNPM.
|
||||
*/
|
||||
// "environmentVariables": {
|
||||
// "NODE_OPTIONS": {
|
||||
// "value": "--max-old-space-size=4096",
|
||||
// "override": false
|
||||
// }
|
||||
// },
|
||||
|
||||
/**
|
||||
* Specifies the location of the PNPM store. There are two possible values:
|
||||
*
|
||||
* - `local` - use the `pnpm-store` folder in the current configured temp folder:
|
||||
* `common/temp/pnpm-store` by default.
|
||||
* - `global` - use PNPM's global store, which has the benefit of being shared
|
||||
* across multiple repo folders, but the disadvantage of less isolation for builds
|
||||
* (for example, bugs or incompatibilities when two repos use different releases of PNPM)
|
||||
*
|
||||
* In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`.
|
||||
*
|
||||
* The default value is `local`.
|
||||
*/
|
||||
// "pnpmStore": "global",
|
||||
|
||||
/**
|
||||
* If true, then `rush install` will report an error if manual modifications
|
||||
* were made to the PNPM shrinkwrap file without running `rush update` afterwards.
|
||||
*
|
||||
* This feature protects against accidental inconsistencies that may be introduced
|
||||
* if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this
|
||||
* feature is enabled, `rush update` will append a hash to the file as a YAML comment,
|
||||
* and then `rush update` and `rush install` will validate the hash. Note that this
|
||||
* does not prohibit manual modifications, but merely requires `rush update` be run
|
||||
* afterwards, ensuring that PNPM can report or repair any potential inconsistencies.
|
||||
*
|
||||
* To temporarily disable this validation when invoking `rush install`, use the
|
||||
* `--bypass-policy` command-line parameter.
|
||||
*
|
||||
* The default value is false.
|
||||
*/
|
||||
// "preventManualShrinkwrapChanges": true,
|
||||
|
||||
/**
|
||||
* When a project uses `workspace:` to depend on another Rush project, PNPM normally installs
|
||||
* it by creating a symlink under `node_modules`. This generally works well, but in certain
|
||||
* cases such as differing `peerDependencies` versions, symlinking may cause trouble
|
||||
* such as incorrectly satisfied versions. For such cases, the dependency can be declared
|
||||
* as "injected", causing PNPM to copy its built output into `node_modules` like a real
|
||||
* install from a registry. Details here: https://rushjs.io/pages/advanced/injected_deps/
|
||||
*
|
||||
* When using Rush subspaces, these sorts of versioning problems are much more likely if
|
||||
* `workspace:` refers to a project from a different subspace. This is because the symlink
|
||||
* would point to a separate `node_modules` tree installed by a different PNPM lockfile.
|
||||
* A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`,
|
||||
* which automatically treats all projects from other subspaces as injected dependencies
|
||||
* without having to manually configure them.
|
||||
*
|
||||
* NOTE: Use carefully -- excessive file copying can slow down the `rush install` and
|
||||
* `pnpm-sync` operations if too many dependencies become injected.
|
||||
*
|
||||
* The default value is false.
|
||||
*/
|
||||
// "alwaysInjectDependenciesFromOtherSubspaces": false,
|
||||
|
||||
/**
|
||||
* Defines the policies to be checked for the `pnpm-lock.yaml` file.
|
||||
*/
|
||||
"pnpmLockfilePolicies": {
|
||||
/**
|
||||
* This policy will cause "rush update" to report an error if `pnpm-lock.yaml` contains
|
||||
* any SHA1 integrity hashes.
|
||||
*
|
||||
* For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash. Although
|
||||
* its main purpose is to detect corrupted or truncated network requests, this hash can also
|
||||
* serve as a security fingerprint to protect against attacks that would substitute a
|
||||
* malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally
|
||||
* download a matching package name+version from npmjs.com instead of the private NPM registry.
|
||||
* NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft
|
||||
* a tarball with a matching fingerprint. For this reason, NPM later deprecated SHA1 and
|
||||
* instead adopted a cryptographically strong SHA512 hash. Nonetheless, SHA1 hashes can
|
||||
* occasionally reappear during "rush update", for example due to missing metadata fallbacks
|
||||
* (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry.
|
||||
* The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts.
|
||||
*/
|
||||
// "disallowInsecureSha1": {
|
||||
// /**
|
||||
// * Enables the "disallowInsecureSha1" policy. The default value is false.
|
||||
// */
|
||||
// "enabled": true,
|
||||
//
|
||||
// /**
|
||||
// * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old
|
||||
// * package versions, perhaps due to a caching issue or database migration glitch. To avoid
|
||||
// * having to disable the "disallowInsecureSha1" policy for the entire monorepo, the problematic
|
||||
// * package versions can be individually ignored. The "exemptPackageVersions" key is the
|
||||
// * package name, and the array value lists exact version numbers to be ignored.
|
||||
// */
|
||||
// "exemptPackageVersions": {
|
||||
// "example1": ["1.0.0"],
|
||||
// "example2": ["2.0.0", "2.0.1"]
|
||||
// }
|
||||
// }
|
||||
},
|
||||
|
||||
/**
|
||||
* The "globalOverrides" setting provides a simple mechanism for overriding version selections
|
||||
* for all dependencies of all projects in the monorepo workspace. The settings are copied
|
||||
* into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated
|
||||
* by Rush during installation.
|
||||
*
|
||||
* Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by
|
||||
* `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,
|
||||
* and `globalOverrides` has lowest precedence.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/package_json#pnpmoverrides
|
||||
*/
|
||||
"globalOverrides": {
|
||||
// "example1": "^1.0.0",
|
||||
// "example2": "npm:@company/example2@^1.0.0"
|
||||
},
|
||||
|
||||
/**
|
||||
* The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors
|
||||
* that are reported during installation with `strictPeerDependencies=true`. The settings are copied
|
||||
* into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated
|
||||
* by Rush during installation.
|
||||
*
|
||||
* Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by
|
||||
* `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,
|
||||
* and `globalOverrides` has lowest precedence.
|
||||
*
|
||||
* https://pnpm.io/package_json#pnpmpeerdependencyrules
|
||||
*/
|
||||
"globalPeerDependencyRules": {
|
||||
// "ignoreMissing": ["@eslint/*"],
|
||||
// "allowedVersions": { "react": "17" },
|
||||
// "allowAny": ["@babel/*"]
|
||||
},
|
||||
|
||||
/**
|
||||
* The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields
|
||||
* for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions`
|
||||
* field of the `common/temp/package.json` file that is generated by Rush during installation.
|
||||
* The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without
|
||||
* the downsides of an executable script (nondeterminism, unreliable caching, performance concerns).
|
||||
*
|
||||
* Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by
|
||||
* `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,
|
||||
* and `globalOverrides` has lowest precedence.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions
|
||||
*/
|
||||
"globalPackageExtensions": {
|
||||
// "fork-ts-checker-webpack-plugin": {
|
||||
// "dependencies": {
|
||||
// "@babel/core": "1"
|
||||
// },
|
||||
// "peerDependencies": {
|
||||
// "eslint": ">= 6"
|
||||
// },
|
||||
// "peerDependenciesMeta": {
|
||||
// "eslint": {
|
||||
// "optional": true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
|
||||
/**
|
||||
* The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall`
|
||||
* lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices
|
||||
* such as downloading large binaries without retries or attempting to invoke OS tools such as
|
||||
* a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package;
|
||||
* it has nothing to do with build system operations such as `rush build` or `rushx build`.)
|
||||
* The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json`
|
||||
* file that is generated by Rush during installation.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies
|
||||
*/
|
||||
"globalNeverBuiltDependencies": [
|
||||
// "fsevents"
|
||||
],
|
||||
|
||||
/**
|
||||
* The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM
|
||||
* dependencies specified in the list. This is useful when certain optional dependencies are
|
||||
* not needed in your environment, such as platform-specific packages or dependencies that
|
||||
* fail during installation but are not critical to your project.
|
||||
* These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json`
|
||||
* file that is generated by Rush during installation, instructing PNPM to ignore the specified
|
||||
* optional dependencies.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies
|
||||
*/
|
||||
"globalIgnoredOptionalDependencies": [
|
||||
// "fsevents"
|
||||
],
|
||||
|
||||
/**
|
||||
* The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package
|
||||
* versions that the NPM registry reports as being deprecated. This is useful if the
|
||||
* deprecated package is an indirect dependency of an external package that has not released a fix.
|
||||
* The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json`
|
||||
* file that is generated by Rush during installation.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions
|
||||
*
|
||||
* If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions`
|
||||
* in the package.json file for individual Rush projects.
|
||||
*/
|
||||
"globalAllowedDeprecatedVersions": {
|
||||
// "request": "*"
|
||||
},
|
||||
|
||||
/**
|
||||
* (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically
|
||||
* by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name
|
||||
* and exact version, and the value is a relative path to the associated patch file.
|
||||
*
|
||||
* PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies
|
||||
*/
|
||||
"globalPatchedDependencies": {},
|
||||
|
||||
/**
|
||||
* (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into
|
||||
* the `common/temp/package.json` file that is generated by Rush during installation.
|
||||
* This provides a way to experiment with new PNPM features. These settings will override
|
||||
* any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`.
|
||||
*
|
||||
* USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH
|
||||
* TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should
|
||||
* be supported, please create a GitHub issue or PR. Note that Rush does not aim to
|
||||
* support every possible PNPM setting, but rather to promote a battle-tested installation
|
||||
* strategy that is known to provide a good experience for large teams with lots of projects.
|
||||
*/
|
||||
"unsupportedPackageJsonSettings": {
|
||||
// "dependencies": {
|
||||
// "not-a-good-practice": "*"
|
||||
// },
|
||||
// "scripts": {
|
||||
// "do-something": "echo Also not a good practice"
|
||||
// },
|
||||
// "pnpm": { "futurePnpmFeature": true }
|
||||
}
|
||||
}
|
||||
+10389
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||
{
|
||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* This configuration file manages Rush's plugin feature.
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json",
|
||||
"plugins": [
|
||||
/**
|
||||
* Each item configures a plugin to be loaded by Rush.
|
||||
*/
|
||||
// {
|
||||
// /**
|
||||
// * The name of the NPM package that provides the plugin.
|
||||
// */
|
||||
// "packageName": "@scope/my-rush-plugin",
|
||||
// /**
|
||||
// * The name of the plugin. This can be found in the "pluginName"
|
||||
// * field of the "rush-plugin-manifest.json" file in the NPM package folder.
|
||||
// */
|
||||
// "pluginName": "my-plugin-name",
|
||||
// /**
|
||||
// * The name of a Rush autoinstaller that will be used for installation, which
|
||||
// * can be created using "rush init-autoinstaller". Add the plugin's NPM package
|
||||
// * to the package.json "dependencies" of your autoinstaller, then run
|
||||
// * "rush update-autoinstaller".
|
||||
// */
|
||||
// "autoinstallerName": "rush-plugins"
|
||||
// }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* This configuration file manages the experimental "subspaces" feature for Rush,
|
||||
* which allows multiple PNPM lockfiles to be used in a single Rush workspace.
|
||||
* For full documentation, please see https://rushjs.io
|
||||
*/
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json",
|
||||
|
||||
/**
|
||||
* Set this flag to "true" to enable usage of subspaces.
|
||||
*/
|
||||
"subspacesEnabled": false,
|
||||
|
||||
/**
|
||||
* (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype
|
||||
* of this feature: https://github.com/microsoft/rushstack/pull/3481
|
||||
* It allows subspaces with only one project to store their config files in the project folder.
|
||||
*/
|
||||
"splitWorkspaceCompatibility": false,
|
||||
|
||||
/**
|
||||
* When a command such as "rush update" is invoked without the "--subspace" or "--to"
|
||||
* parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces,
|
||||
* this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this
|
||||
* mistake by always requiring selection parameters for commands such as "rush update".
|
||||
*/
|
||||
"preventSelectingAllSubspaces": false,
|
||||
|
||||
/**
|
||||
* The list of subspace names, which should be lowercase alphanumeric words separated by
|
||||
* hyphens, for example "my-subspace". The corresponding config files will have paths
|
||||
* such as "common/config/subspaces/my-subspace/package-lock.yaml".
|
||||
*/
|
||||
"subspaceNames": []
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* This is configuration file is used for advanced publishing configurations with Rush.
|
||||
* More documentation is available on the Rush website: https://rushjs.io
|
||||
*/
|
||||
|
||||
/**
|
||||
* A list of version policy definitions. A "version policy" is a custom package versioning
|
||||
* strategy that affects "rush change", "rush version", and "rush publish". The strategy applies
|
||||
* to a set of projects that are specified using the "versionPolicyName" field in rush.json.
|
||||
*/
|
||||
[
|
||||
// {
|
||||
// /**
|
||||
// * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion").
|
||||
// *
|
||||
// * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This
|
||||
// * strategy is appropriate for a set of packages that act as selectable components of a
|
||||
// * unified product. The entire set of packages are always published together, and always share
|
||||
// * the same NPM version number. When the packages depend on other packages in the set, the
|
||||
// * SemVer range is usually restricted to a single version.
|
||||
// */
|
||||
// "definitionName": "lockStepVersion",
|
||||
//
|
||||
// /**
|
||||
// * (Required) The name that will be used for the "versionPolicyName" field in rush.json.
|
||||
// * This name is also used command-line parameters such as "--version-policy"
|
||||
// * and "--to-version-policy".
|
||||
// */
|
||||
// "policyName": "MyBigFramework",
|
||||
//
|
||||
// /**
|
||||
// * (Required) The current version. All packages belonging to the set should have this version
|
||||
// * in the current branch. When bumping versions, Rush uses this to determine the next version.
|
||||
// * (The "version" field in package.json is NOT considered.)
|
||||
// */
|
||||
// "version": "1.0.0",
|
||||
//
|
||||
// /**
|
||||
// * (Required) The type of bump that will be performed when publishing the next release.
|
||||
// * When creating a release branch in Git, this field should be updated according to the
|
||||
// * type of release.
|
||||
// *
|
||||
// * Valid values are: "prerelease", "preminor", "minor", "patch", "major"
|
||||
// */
|
||||
// "nextBump": "prerelease",
|
||||
//
|
||||
// /**
|
||||
// * (Optional) If specified, all packages in the set share a common CHANGELOG.md file.
|
||||
// * This file is stored with the specified "main" project, which must be a member of the set.
|
||||
// *
|
||||
// * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each
|
||||
// * package in the set.
|
||||
// */
|
||||
// "mainProject": "my-app",
|
||||
//
|
||||
// /**
|
||||
// * (Optional) If enabled, the "rush change" command will prompt the user for their email address
|
||||
// * and include it in the JSON change files. If an organization maintains multiple repos, tracking
|
||||
// * this contact information may be useful for a service that automatically upgrades packages and
|
||||
// * needs to notify engineers whose change may be responsible for a downstream build break. It might
|
||||
// * also be useful for crediting contributors. Rush itself does not do anything with the collected
|
||||
// * email addresses. The default value is "false".
|
||||
// */
|
||||
// // "includeEmailInChangeFile": true
|
||||
// },
|
||||
//
|
||||
{
|
||||
/**
|
||||
* (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion").
|
||||
*
|
||||
* The "individualVersion" mode specifies that the projects will use "individual versioning".
|
||||
* This is the typical NPM model where each package has an independent version number
|
||||
* and CHANGELOG.md file. Although a single CI definition is responsible for publishing the
|
||||
* packages, they otherwise don't have any special relationship. The version bumping will
|
||||
* depend on how developers answer the "rush change" questions for each package that
|
||||
* is changed.
|
||||
*/
|
||||
"definitionName": "individualVersion",
|
||||
|
||||
"policyName": "Huly.core",
|
||||
|
||||
/**
|
||||
* (Optional) This can be used to enforce that all packages in the set must share a common
|
||||
* major version number, e.g. because they are from the same major release branch.
|
||||
* It can also be used to discourage people from accidentally making "MAJOR" SemVer changes
|
||||
* inappropriately. The minor/patch version parts will be bumped independently according
|
||||
* to the types of changes made to each project, according to the "rush change" command.
|
||||
*/
|
||||
"lockedMajor": 0.7,
|
||||
|
||||
/**
|
||||
* (Optional) When publishing is managed by Rush, by default the "rush change" command will
|
||||
* request changes for any projects that are modified by a pull request. These change entries
|
||||
* will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates
|
||||
* in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects
|
||||
* belonging to this version policy.
|
||||
*/
|
||||
"exemptFromRushChange": false
|
||||
|
||||
// "includeEmailInChangeFile": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# This is an example Git hook for use with Rush. To enable this hook, rename this file
|
||||
# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks
|
||||
# to the .git/hooks folder.
|
||||
#
|
||||
# TO LEARN MORE ABOUT GIT HOOKS
|
||||
#
|
||||
# The Git documentation is here: https://git-scm.com/docs/githooks
|
||||
# Some helpful resources: https://githooks.com
|
||||
#
|
||||
# ABOUT THIS EXAMPLE
|
||||
#
|
||||
# The commit-msg hook is called by "git commit" with one argument, the name of the file
|
||||
# that has the commit message. The hook should exit with non-zero status after issuing
|
||||
# an appropriate message if it wants to stop the commit. The hook is allowed to edit
|
||||
# the commit message file.
|
||||
|
||||
# This example enforces that commit message should contain a minimum amount of
|
||||
# description text.
|
||||
if [ `cat $1 | wc -w` -lt 3 ]; then
|
||||
echo ""
|
||||
echo "Invalid commit message: The message must contain at least 3 words."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const [, , inFile = 'coverage/lcov.info', outDir = 'coverage/html'] = process.argv
|
||||
|
||||
if (!fs.existsSync(inFile)) {
|
||||
console.error('Input lcov not found:', inFile)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const lcovParse = require('lcov-parse')
|
||||
const libCoverage = require('istanbul-lib-coverage')
|
||||
const reports = require('istanbul-reports')
|
||||
const libReport = require('istanbul-lib-report')
|
||||
|
||||
const data = fs.readFileSync(inFile, 'utf8')
|
||||
|
||||
// build repo file index to resolve source files
|
||||
const root = process.cwd()
|
||||
const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store'])
|
||||
const repoFiles = []
|
||||
function walk(dir) {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true })
|
||||
for (const it of items) {
|
||||
if (it.isDirectory()) {
|
||||
if (ignoreDirs.has(it.name)) continue
|
||||
if (it.name.startsWith('.')) continue
|
||||
try {
|
||||
walk(path.join(dir, it.name))
|
||||
} catch (e) {}
|
||||
} else if (it.isFile()) {
|
||||
repoFiles.push(path.join(dir, it.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
walk(root)
|
||||
} catch (e) {}
|
||||
|
||||
lcovParse(data, (err, parsed) => {
|
||||
if (err) {
|
||||
console.error('lcov-parse error:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const map = libCoverage.createCoverageMap({})
|
||||
for (const file of parsed) {
|
||||
// parsed entries include 'file', 'lines', 'functions', 'branches'
|
||||
const coverage = {
|
||||
path: file.file,
|
||||
statementMap: {},
|
||||
fnMap: {},
|
||||
branchMap: {},
|
||||
s: {},
|
||||
f: {},
|
||||
b: {}
|
||||
}
|
||||
|
||||
// The lcov parser gives line coverage data; create synthetic statement entries per line
|
||||
if (file.lines && file.lines.details) {
|
||||
let idx = 0
|
||||
for (const d of file.lines.details) {
|
||||
idx++
|
||||
const key = String(idx)
|
||||
coverage.statementMap[key] = { start: { line: d.line, column: 0 }, end: { line: d.line, column: 0 } }
|
||||
coverage.s[key] = d.hit
|
||||
}
|
||||
}
|
||||
|
||||
// functions and branches are ignored for more accurate tools; keep minimal
|
||||
map.addFileCoverage(coverage)
|
||||
}
|
||||
|
||||
// custom source finder: try absolute, repo-relative, and suffix matches
|
||||
const sourceFinder = (filePath) => {
|
||||
try {
|
||||
if (!global.__seenPaths) global.__seenPaths = []
|
||||
if (global.__seenPaths.length < 500) global.__seenPaths.push(filePath)
|
||||
if (global.__seenPaths.length === 500 && !global.__seenLogged) {
|
||||
console.error('sourceFinder seen paths (sample):\n', global.__seenPaths.join('\n'))
|
||||
global.__seenLogged = true
|
||||
}
|
||||
if (global.__seenPaths.length <= 200) console.error('sourceFinder request:', filePath)
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (path.isAbsolute(filePath) && fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8')
|
||||
const abs1 = path.resolve(root, filePath)
|
||||
if (fs.existsSync(abs1)) return fs.readFileSync(abs1, 'utf8')
|
||||
// try suffix match
|
||||
const found = repoFiles.find((p) => p.endsWith(path.sep + filePath) || p.endsWith(filePath))
|
||||
if (found) return fs.readFileSync(found, 'utf8')
|
||||
// debug unresolved
|
||||
if (!found) {
|
||||
try {
|
||||
if (!global.__unresolved) global.__unresolved = new Set()
|
||||
if (global.__unresolved.size < 200) global.__unresolved.add(filePath)
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore and return null below
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const context = libReport.createContext({ dir: outDir, coverageMap: map, sourceFinder })
|
||||
const report = reports.create('html', {})
|
||||
report.execute(context)
|
||||
if (global.__unresolved && global.__unresolved.size) {
|
||||
console.error('Unresolved filePath samples:\n', Array.from(global.__unresolved).slice(0, 50).join('\n'))
|
||||
}
|
||||
console.log('HTML report generated in', outDir)
|
||||
// Post-process HTML files: if any report page contains the 'Unable to lookup source' placeholder,
|
||||
// replace it with the actual source file contents when we can resolve it.
|
||||
try {
|
||||
for (const file of parsed) {
|
||||
const srcAbs = file.file
|
||||
// normalize key as used by report (from last '/src/' onward) if present
|
||||
let key
|
||||
const idx = srcAbs.lastIndexOf(path.sep + 'src' + path.sep)
|
||||
if (idx !== -1) key = srcAbs.slice(idx + 1)
|
||||
else key = path.basename(srcAbs)
|
||||
|
||||
const htmlPath = path.join(outDir, key + '.html')
|
||||
if (!fs.existsSync(htmlPath)) continue
|
||||
let html = fs.readFileSync(htmlPath, 'utf8')
|
||||
if (!html.includes('Unable to lookup source')) continue
|
||||
// read source
|
||||
let src
|
||||
try {
|
||||
src = fs.readFileSync(srcAbs, 'utf8')
|
||||
} catch (e) {
|
||||
src = null
|
||||
}
|
||||
if (!src) {
|
||||
// try suffix match in repoFiles
|
||||
const found = repoFiles.find((p) => p.endsWith(path.sep + key) || p.endsWith(key))
|
||||
if (found) src = fs.readFileSync(found, 'utf8')
|
||||
}
|
||||
if (!src) continue
|
||||
// escape HTML
|
||||
const esc = src.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
// replace the first prettyprint <pre>...</pre> block that contains 'Unable to lookup source'
|
||||
html = html.replace(
|
||||
/<pre class="prettyprint[\s\S]*?>[\s\S]*?Unable to lookup source:[\s\S]*?<\/pre>/,
|
||||
`<pre class="prettyprint lang-js">${esc}</pre>`
|
||||
)
|
||||
fs.writeFileSync(htmlPath, html, 'utf8')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('post-process html error', e)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
|
||||
//
|
||||
// This script is intended for usage in an automated build environment where the Rush command may not have
|
||||
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
|
||||
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
|
||||
// rush-pnpm command.
|
||||
//
|
||||
// An example usage would be:
|
||||
//
|
||||
// node common/scripts/install-run-rush-pnpm.js pnpm-command
|
||||
//
|
||||
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
|
||||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See the @microsoft/rush package's LICENSE file for details.
|
||||
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
var __webpack_exports__ = {};
|
||||
/*!*****************************************************!*\
|
||||
!*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***!
|
||||
\*****************************************************/
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
require('./install-run-rush');
|
||||
//# sourceMappingURL=install-run-rush-pnpm.js.map
|
||||
module.exports = __webpack_exports__;
|
||||
/******/ })()
|
||||
;
|
||||
//# sourceMappingURL=install-run-rush-pnpm.js.map
|
||||
@@ -0,0 +1,218 @@
|
||||
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
|
||||
//
|
||||
// This script is intended for usage in an automated build environment where the Rush command may not have
|
||||
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
|
||||
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.
|
||||
// An example usage would be:
|
||||
//
|
||||
// node common/scripts/install-run-rush.js install
|
||||
//
|
||||
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
|
||||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See the @microsoft/rush package's LICENSE file for details.
|
||||
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ 16928:
|
||||
/*!***********************!*\
|
||||
!*** external "path" ***!
|
||||
\***********************/
|
||||
/***/ ((module) => {
|
||||
|
||||
module.exports = require("path");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 179896:
|
||||
/*!*********************!*\
|
||||
!*** external "fs" ***!
|
||||
\*********************/
|
||||
/***/ ((module) => {
|
||||
|
||||
module.exports = require("fs");
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ // no module.id needed
|
||||
/******/ // no module.loaded needed
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ (() => {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = (module) => {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ () => (module['default']) :
|
||||
/******/ () => (module);
|
||||
/******/ __webpack_require__.d(getter, { a: getter });
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/define property getters */
|
||||
/******/ (() => {
|
||||
/******/ // define getter functions for harmony exports
|
||||
/******/ __webpack_require__.d = (exports, definition) => {
|
||||
/******/ for(var key in definition) {
|
||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/make namespace object */
|
||||
/******/ (() => {
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = (exports) => {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/************************************************************************/
|
||||
var __webpack_exports__ = {};
|
||||
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
|
||||
(() => {
|
||||
/*!************************************************!*\
|
||||
!*** ./lib-esnext/scripts/install-run-rush.js ***!
|
||||
\************************************************/
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928);
|
||||
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__);
|
||||
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896);
|
||||
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
/* eslint-disable no-console */
|
||||
|
||||
|
||||
const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run');
|
||||
const PACKAGE_NAME = '@microsoft/rush';
|
||||
const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION';
|
||||
const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH';
|
||||
function _getRushVersion(logger) {
|
||||
const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION];
|
||||
if (rushPreviewVersion !== undefined) {
|
||||
logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`);
|
||||
return rushPreviewVersion;
|
||||
}
|
||||
const rushJsonFolder = findRushJsonFolder();
|
||||
const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME);
|
||||
try {
|
||||
const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8');
|
||||
// Use a regular expression to parse out the rushVersion value because rush.json supports comments,
|
||||
// but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.
|
||||
const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/);
|
||||
return rushJsonMatches[1];
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` +
|
||||
`The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` +
|
||||
'using an unexpected syntax.');
|
||||
}
|
||||
}
|
||||
function _getBin(scriptName) {
|
||||
switch (scriptName.toLowerCase()) {
|
||||
case 'install-run-rush-pnpm.js':
|
||||
return 'rush-pnpm';
|
||||
case 'install-run-rushx.js':
|
||||
return 'rushx';
|
||||
default:
|
||||
return 'rush';
|
||||
}
|
||||
}
|
||||
function _run() {
|
||||
const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv;
|
||||
// Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the
|
||||
// appropriate binary inside the rush package to run
|
||||
const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath);
|
||||
const bin = _getBin(scriptName);
|
||||
if (!nodePath || !scriptPath) {
|
||||
throw new Error('Unexpected exception: could not detect node path or script path');
|
||||
}
|
||||
let commandFound = false;
|
||||
let logger = { info: console.log, error: console.error };
|
||||
for (const arg of packageBinArgs) {
|
||||
if (arg === '-q' || arg === '--quiet') {
|
||||
// The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress
|
||||
// any normal informational/diagnostic information printed during startup.
|
||||
//
|
||||
// To maintain the same user experience, the install-run* scripts pass along this
|
||||
// flag but also use it to suppress any diagnostic information normally printed
|
||||
// to stdout.
|
||||
logger = {
|
||||
info: () => { },
|
||||
error: console.error
|
||||
};
|
||||
}
|
||||
else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') {
|
||||
// We either found something that looks like a command (i.e. - doesn't start with a "-"),
|
||||
// or we found the -h/--help flag, which can be run without a command
|
||||
commandFound = true;
|
||||
}
|
||||
}
|
||||
if (!commandFound) {
|
||||
console.log(`Usage: ${scriptName} <command> [args...]`);
|
||||
if (scriptName === 'install-run-rush-pnpm.js') {
|
||||
console.log(`Example: ${scriptName} pnpm-command`);
|
||||
}
|
||||
else if (scriptName === 'install-run-rush.js') {
|
||||
console.log(`Example: ${scriptName} build --to myproject`);
|
||||
}
|
||||
else {
|
||||
console.log(`Example: ${scriptName} custom-command`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
runWithErrorAndStatusCode(logger, () => {
|
||||
const version = _getRushVersion(logger);
|
||||
logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`);
|
||||
const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE];
|
||||
if (lockFilePath) {
|
||||
logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}="${lockFilePath}", installing with lockfile.`);
|
||||
}
|
||||
return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath);
|
||||
});
|
||||
}
|
||||
_run();
|
||||
//# sourceMappingURL=install-run-rush.js.map
|
||||
})();
|
||||
|
||||
module.exports = __webpack_exports__;
|
||||
/******/ })()
|
||||
;
|
||||
//# sourceMappingURL=install-run-rush.js.map
|
||||
@@ -0,0 +1,31 @@
|
||||
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
|
||||
//
|
||||
// This script is intended for usage in an automated build environment where the Rush command may not have
|
||||
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
|
||||
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the
|
||||
// rushx command.
|
||||
//
|
||||
// An example usage would be:
|
||||
//
|
||||
// node common/scripts/install-run-rushx.js custom-command
|
||||
//
|
||||
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
|
||||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See the @microsoft/rush package's LICENSE file for details.
|
||||
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
var __webpack_exports__ = {};
|
||||
/*!*************************************************!*\
|
||||
!*** ./lib-esnext/scripts/install-run-rushx.js ***!
|
||||
\*************************************************/
|
||||
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
require('./install-run-rush');
|
||||
//# sourceMappingURL=install-run-rushx.js.map
|
||||
module.exports = __webpack_exports__;
|
||||
/******/ })()
|
||||
;
|
||||
//# sourceMappingURL=install-run-rushx.js.map
|
||||
@@ -0,0 +1,778 @@
|
||||
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
|
||||
//
|
||||
// This script is intended for usage in an automated build environment where a Node tool may not have
|
||||
// been preinstalled, or may have an unpredictable version. This script will automatically install the specified
|
||||
// version of the specified tool (if not already installed), and then pass a command-line to it.
|
||||
// An example usage would be:
|
||||
//
|
||||
// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
|
||||
//
|
||||
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
|
||||
//
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See the @microsoft/rush package's LICENSE file for details.
|
||||
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
/******/ var __webpack_modules__ = ({
|
||||
|
||||
/***/ 16928:
|
||||
/*!***********************!*\
|
||||
!*** external "path" ***!
|
||||
\***********************/
|
||||
/***/ ((module) => {
|
||||
|
||||
module.exports = require("path");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 179896:
|
||||
/*!*********************!*\
|
||||
!*** external "fs" ***!
|
||||
\*********************/
|
||||
/***/ ((module) => {
|
||||
|
||||
module.exports = require("fs");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 370857:
|
||||
/*!*********************!*\
|
||||
!*** external "os" ***!
|
||||
\*********************/
|
||||
/***/ ((module) => {
|
||||
|
||||
module.exports = require("os");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 535317:
|
||||
/*!********************************!*\
|
||||
!*** external "child_process" ***!
|
||||
\********************************/
|
||||
/***/ ((module) => {
|
||||
|
||||
module.exports = require("child_process");
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 832286:
|
||||
/*!************************************************!*\
|
||||
!*** ./lib-esnext/utilities/npmrcUtilities.js ***!
|
||||
\************************************************/
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile),
|
||||
/* harmony export */ syncNpmrc: () => (/* binding */ syncNpmrc),
|
||||
/* harmony export */ trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines)
|
||||
/* harmony export */ });
|
||||
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896);
|
||||
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__);
|
||||
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928);
|
||||
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__);
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
// IMPORTANT - do not use any non-built-in libraries in this file
|
||||
|
||||
|
||||
/**
|
||||
* This function reads the content for given .npmrc file path, and also trims
|
||||
* unusable lines from the .npmrc file.
|
||||
*
|
||||
* @returns
|
||||
* The text of the the .npmrc.
|
||||
*/
|
||||
// create a global _combinedNpmrc for cache purpose
|
||||
const _combinedNpmrcMap = new Map();
|
||||
function _trimNpmrcFile(options) {
|
||||
const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options;
|
||||
const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath);
|
||||
if (combinedNpmrcFromCache !== undefined) {
|
||||
return combinedNpmrcFromCache;
|
||||
}
|
||||
let npmrcFileLines = [];
|
||||
if (linesToPrepend) {
|
||||
npmrcFileLines.push(...linesToPrepend);
|
||||
}
|
||||
if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {
|
||||
npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\n'));
|
||||
}
|
||||
if (linesToAppend) {
|
||||
npmrcFileLines.push(...linesToAppend);
|
||||
}
|
||||
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
|
||||
const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax);
|
||||
const combinedNpmrc = resultLines.join('\n');
|
||||
//save the cache
|
||||
_combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc);
|
||||
return combinedNpmrc;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param npmrcFileLines The npmrc file's lines
|
||||
* @param env The environment variables object
|
||||
* @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}`
|
||||
* @returns
|
||||
*/
|
||||
function trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) {
|
||||
var _a;
|
||||
const resultLines = [];
|
||||
// This finds environment variable tokens that look like "${VAR_NAME}"
|
||||
const expansionRegExp = /\$\{([^\}]+)\}/g;
|
||||
// Comment lines start with "#" or ";"
|
||||
const commentRegExp = /^\s*[#;]/;
|
||||
// Trim out lines that reference environment variables that aren't defined
|
||||
for (let line of npmrcFileLines) {
|
||||
let lineShouldBeTrimmed = false;
|
||||
//remove spaces before or after key and value
|
||||
line = line
|
||||
.split('=')
|
||||
.map((lineToTrim) => lineToTrim.trim())
|
||||
.join('=');
|
||||
// Ignore comment lines
|
||||
if (!commentRegExp.test(line)) {
|
||||
const environmentVariables = line.match(expansionRegExp);
|
||||
if (environmentVariables) {
|
||||
for (const token of environmentVariables) {
|
||||
/**
|
||||
* Remove the leading "${" and the trailing "}" from the token
|
||||
*
|
||||
* ${nameString} -> nameString
|
||||
* ${nameString-fallbackString} -> name-fallbackString
|
||||
* ${nameString:-fallbackString} -> name:-fallbackString
|
||||
*/
|
||||
const nameWithFallback = token.substring(2, token.length - 1);
|
||||
let environmentVariableName;
|
||||
let fallback;
|
||||
if (supportEnvVarFallbackSyntax) {
|
||||
/**
|
||||
* Get the environment variable name and fallback value.
|
||||
*
|
||||
* name fallback
|
||||
* nameString -> nameString undefined
|
||||
* nameString-fallbackString -> nameString fallbackString
|
||||
* nameString:-fallbackString -> nameString fallbackString
|
||||
*/
|
||||
const matched = nameWithFallback.match(/^([^:-]+)(?:\:?-(.+))?$/);
|
||||
// matched: [originStr, variableName, fallback]
|
||||
environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback;
|
||||
fallback = matched === null || matched === void 0 ? void 0 : matched[2];
|
||||
}
|
||||
else {
|
||||
environmentVariableName = nameWithFallback;
|
||||
}
|
||||
// Is the environment variable and fallback value defined.
|
||||
if (!env[environmentVariableName] && !fallback) {
|
||||
// No, so trim this line
|
||||
lineShouldBeTrimmed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lineShouldBeTrimmed) {
|
||||
// Example output:
|
||||
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
|
||||
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
|
||||
}
|
||||
else {
|
||||
resultLines.push(line);
|
||||
}
|
||||
}
|
||||
return resultLines;
|
||||
}
|
||||
function _copyAndTrimNpmrcFile(options) {
|
||||
const { logger, sourceNpmrcPath, targetNpmrcPath } = options;
|
||||
logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose
|
||||
logger.info(` --> "${targetNpmrcPath}"`);
|
||||
const combinedNpmrc = _trimNpmrcFile(options);
|
||||
fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc);
|
||||
return combinedNpmrc;
|
||||
}
|
||||
function syncNpmrc(options) {
|
||||
const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = {
|
||||
// eslint-disable-next-line no-console
|
||||
info: console.log,
|
||||
// eslint-disable-next-line no-console
|
||||
error: console.error
|
||||
}, createIfMissing = false } = options;
|
||||
const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
|
||||
const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc');
|
||||
try {
|
||||
if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) {
|
||||
// Ensure the target folder exists
|
||||
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) {
|
||||
fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true });
|
||||
}
|
||||
return _copyAndTrimNpmrcFile({
|
||||
sourceNpmrcPath,
|
||||
targetNpmrcPath,
|
||||
logger,
|
||||
...options
|
||||
});
|
||||
}
|
||||
else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) {
|
||||
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
|
||||
logger.info(`Deleting ${targetNpmrcPath}`); // Verbose
|
||||
fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Error syncing .npmrc file: ${e}`);
|
||||
}
|
||||
}
|
||||
function isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) {
|
||||
const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`;
|
||||
//if .npmrc file does not exist, return false directly
|
||||
if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {
|
||||
return false;
|
||||
}
|
||||
const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax });
|
||||
const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm');
|
||||
return trimmedNpmrcFile.match(variableKeyRegExp) !== null;
|
||||
}
|
||||
//# sourceMappingURL=npmrcUtilities.js.map
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ });
|
||||
/************************************************************************/
|
||||
/******/ // The module cache
|
||||
/******/ var __webpack_module_cache__ = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/ // Check if module is in cache
|
||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
||||
/******/ if (cachedModule !== undefined) {
|
||||
/******/ return cachedModule.exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
||||
/******/ // no module.id needed
|
||||
/******/ // no module.loaded needed
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/************************************************************************/
|
||||
/******/ /* webpack/runtime/compat get default export */
|
||||
/******/ (() => {
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = (module) => {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ () => (module['default']) :
|
||||
/******/ () => (module);
|
||||
/******/ __webpack_require__.d(getter, { a: getter });
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/define property getters */
|
||||
/******/ (() => {
|
||||
/******/ // define getter functions for harmony exports
|
||||
/******/ __webpack_require__.d = (exports, definition) => {
|
||||
/******/ for(var key in definition) {
|
||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
||||
/******/ }
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/make namespace object */
|
||||
/******/ (() => {
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = (exports) => {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/ })();
|
||||
/******/
|
||||
/************************************************************************/
|
||||
var __webpack_exports__ = {};
|
||||
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
|
||||
(() => {
|
||||
/*!*******************************************!*\
|
||||
!*** ./lib-esnext/scripts/install-run.js ***!
|
||||
\*******************************************/
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME),
|
||||
/* harmony export */ findRushJsonFolder: () => (/* binding */ findRushJsonFolder),
|
||||
/* harmony export */ getNpmPath: () => (/* binding */ getNpmPath),
|
||||
/* harmony export */ installAndRun: () => (/* binding */ installAndRun),
|
||||
/* harmony export */ runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode)
|
||||
/* harmony export */ });
|
||||
/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317);
|
||||
/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__);
|
||||
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896);
|
||||
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);
|
||||
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857);
|
||||
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__);
|
||||
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928);
|
||||
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__);
|
||||
/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286);
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
/* eslint-disable no-console */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const RUSH_JSON_FILENAME = 'rush.json';
|
||||
const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';
|
||||
const INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';
|
||||
const INSTALLED_FLAG_FILENAME = 'installed.flag';
|
||||
const NODE_MODULES_FOLDER_NAME = 'node_modules';
|
||||
const PACKAGE_JSON_FILENAME = 'package.json';
|
||||
/**
|
||||
* Parse a package specifier (in the form of name\@version) into name and version parts.
|
||||
*/
|
||||
function _parsePackageSpecifier(rawPackageSpecifier) {
|
||||
rawPackageSpecifier = (rawPackageSpecifier || '').trim();
|
||||
const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
|
||||
let name;
|
||||
let version = undefined;
|
||||
if (separatorIndex === 0) {
|
||||
// The specifier starts with a scope and doesn't have a version specified
|
||||
name = rawPackageSpecifier;
|
||||
}
|
||||
else if (separatorIndex === -1) {
|
||||
// The specifier doesn't have a version
|
||||
name = rawPackageSpecifier;
|
||||
}
|
||||
else {
|
||||
name = rawPackageSpecifier.substring(0, separatorIndex);
|
||||
version = rawPackageSpecifier.substring(separatorIndex + 1);
|
||||
}
|
||||
if (!name) {
|
||||
throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
|
||||
}
|
||||
return { name, version };
|
||||
}
|
||||
let _npmPath = undefined;
|
||||
/**
|
||||
* Get the absolute path to the npm executable
|
||||
*/
|
||||
function getNpmPath() {
|
||||
if (!_npmPath) {
|
||||
try {
|
||||
if (_isWindows()) {
|
||||
// We're on Windows
|
||||
const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString();
|
||||
const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line);
|
||||
// take the last result, we are looking for a .cmd command
|
||||
// see https://github.com/microsoft/rushstack/issues/759
|
||||
_npmPath = lines[lines.length - 1];
|
||||
}
|
||||
else {
|
||||
// We aren't on Windows - assume we're on *NIX or Darwin
|
||||
_npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
|
||||
}
|
||||
_npmPath = _npmPath.trim();
|
||||
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) {
|
||||
throw new Error('The NPM executable does not exist');
|
||||
}
|
||||
}
|
||||
return _npmPath;
|
||||
}
|
||||
function _ensureFolder(folderPath) {
|
||||
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) {
|
||||
const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath);
|
||||
_ensureFolder(parentDir);
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create missing directories under the specified base directory, and return the resolved directory.
|
||||
*
|
||||
* Does not support "." or ".." path segments.
|
||||
* Assumes the baseFolder exists.
|
||||
*/
|
||||
function _ensureAndJoinPath(baseFolder, ...pathSegments) {
|
||||
let joinedPath = baseFolder;
|
||||
try {
|
||||
for (let pathSegment of pathSegments) {
|
||||
pathSegment = pathSegment.replace(/[\\\/]/g, '+');
|
||||
joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment);
|
||||
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) {
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`);
|
||||
}
|
||||
return joinedPath;
|
||||
}
|
||||
function _getRushTempFolder(rushCommonFolder) {
|
||||
const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME];
|
||||
if (rushTempFolder !== undefined) {
|
||||
_ensureFolder(rushTempFolder);
|
||||
return rushTempFolder;
|
||||
}
|
||||
else {
|
||||
return _ensureAndJoinPath(rushCommonFolder, 'temp');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Compare version strings according to semantic versioning.
|
||||
* Returns a positive integer if "a" is a later version than "b",
|
||||
* a negative integer if "b" is later than "a",
|
||||
* and 0 otherwise.
|
||||
*/
|
||||
function _compareVersionStrings(a, b) {
|
||||
const aParts = a.split(/[.-]/);
|
||||
const bParts = b.split(/[.-]/);
|
||||
const numberOfParts = Math.max(aParts.length, bParts.length);
|
||||
for (let i = 0; i < numberOfParts; i++) {
|
||||
if (aParts[i] !== bParts[i]) {
|
||||
return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/**
|
||||
* Resolve a package specifier to a static version
|
||||
*/
|
||||
function _resolvePackageVersion(logger, rushCommonFolder, { name, version }) {
|
||||
if (!version) {
|
||||
version = '*'; // If no version is specified, use the latest version
|
||||
}
|
||||
if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
|
||||
// If the version contains only characters that we recognize to be used in static version specifiers,
|
||||
// pass the version through
|
||||
return version;
|
||||
}
|
||||
else {
|
||||
// version resolves to
|
||||
try {
|
||||
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
|
||||
const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');
|
||||
(0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({
|
||||
sourceNpmrcFolder,
|
||||
targetNpmrcFolder: rushTempFolder,
|
||||
logger,
|
||||
supportEnvVarFallbackSyntax: false
|
||||
});
|
||||
const npmPath = getNpmPath();
|
||||
// This returns something that looks like:
|
||||
// ```
|
||||
// [
|
||||
// "3.0.0",
|
||||
// "3.0.1",
|
||||
// ...
|
||||
// "3.0.20"
|
||||
// ]
|
||||
// ```
|
||||
//
|
||||
// if multiple versions match the selector, or
|
||||
//
|
||||
// ```
|
||||
// "3.0.0"
|
||||
// ```
|
||||
//
|
||||
// if only a single version matches.
|
||||
const spawnSyncOptions = {
|
||||
cwd: rushTempFolder,
|
||||
stdio: [],
|
||||
shell: _isWindows()
|
||||
};
|
||||
const platformNpmPath = _getPlatformPath(npmPath);
|
||||
const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions);
|
||||
if (npmVersionSpawnResult.status !== 0) {
|
||||
throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
|
||||
}
|
||||
const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
|
||||
const parsedVersionOutput = JSON.parse(npmViewVersionOutput);
|
||||
const versions = Array.isArray(parsedVersionOutput)
|
||||
? parsedVersionOutput
|
||||
: [parsedVersionOutput];
|
||||
let latestVersion = versions[0];
|
||||
for (let i = 1; i < versions.length; i++) {
|
||||
const latestVersionCandidate = versions[i];
|
||||
if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) {
|
||||
latestVersion = latestVersionCandidate;
|
||||
}
|
||||
}
|
||||
if (!latestVersion) {
|
||||
throw new Error('No versions found for the specified version range.');
|
||||
}
|
||||
return latestVersion;
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _rushJsonFolder;
|
||||
/**
|
||||
* Find the absolute path to the folder containing rush.json
|
||||
*/
|
||||
function findRushJsonFolder() {
|
||||
if (!_rushJsonFolder) {
|
||||
let basePath = __dirname;
|
||||
let tempPath = __dirname;
|
||||
do {
|
||||
const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME);
|
||||
if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) {
|
||||
_rushJsonFolder = basePath;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
basePath = tempPath;
|
||||
}
|
||||
} while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root
|
||||
if (!_rushJsonFolder) {
|
||||
throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`);
|
||||
}
|
||||
}
|
||||
return _rushJsonFolder;
|
||||
}
|
||||
/**
|
||||
* Detects if the package in the specified directory is installed
|
||||
*/
|
||||
function _isPackageAlreadyInstalled(packageInstallFolder) {
|
||||
try {
|
||||
const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
|
||||
if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) {
|
||||
return false;
|
||||
}
|
||||
const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString();
|
||||
return fileContents.trim() === process.version;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Delete a file. Fail silently if it does not exist.
|
||||
*/
|
||||
function _deleteFile(file) {
|
||||
try {
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file);
|
||||
}
|
||||
catch (err) {
|
||||
if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Removes the following files and directories under the specified folder path:
|
||||
* - installed.flag
|
||||
* -
|
||||
* - node_modules
|
||||
*/
|
||||
function _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) {
|
||||
try {
|
||||
const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
|
||||
_deleteFile(flagFile);
|
||||
const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json');
|
||||
if (lockFilePath) {
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile);
|
||||
}
|
||||
else {
|
||||
// Not running `npm ci`, so need to cleanup
|
||||
_deleteFile(packageLockFile);
|
||||
const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
|
||||
if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) {
|
||||
const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler');
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
|
||||
}
|
||||
}
|
||||
function _createPackageJson(packageInstallFolder, name, version) {
|
||||
try {
|
||||
const packageJsonContents = {
|
||||
name: 'ci-rush',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
[name]: version
|
||||
},
|
||||
description: "DON'T WARN",
|
||||
repository: "DON'T WARN",
|
||||
license: 'MIT'
|
||||
};
|
||||
const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Unable to create package.json: ${e}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Run "npm install" in the package install folder.
|
||||
*/
|
||||
function _installPackage(logger, packageInstallFolder, name, version, command) {
|
||||
try {
|
||||
logger.info(`Installing ${name}...`);
|
||||
const npmPath = getNpmPath();
|
||||
const platformNpmPath = _getPlatformPath(npmPath);
|
||||
const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], {
|
||||
stdio: 'inherit',
|
||||
cwd: packageInstallFolder,
|
||||
env: process.env,
|
||||
shell: _isWindows()
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`"npm ${command}" encountered an error`);
|
||||
}
|
||||
logger.info(`Successfully installed ${name}@${version}`);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Unable to install package: ${e}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the ".bin" path for the package.
|
||||
*/
|
||||
function _getBinPath(packageInstallFolder, binName) {
|
||||
const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
|
||||
const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName;
|
||||
return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName);
|
||||
}
|
||||
/**
|
||||
* Returns a cross-platform path - windows must enclose any path containing spaces within double quotes.
|
||||
*/
|
||||
function _getPlatformPath(platformPath) {
|
||||
return _isWindows() && platformPath.includes(' ') ? `"${platformPath}"` : platformPath;
|
||||
}
|
||||
function _isWindows() {
|
||||
return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32';
|
||||
}
|
||||
/**
|
||||
* Write a flag file to the package's install directory, signifying that the install was successful.
|
||||
*/
|
||||
function _writeFlagFile(packageInstallFolder) {
|
||||
try {
|
||||
const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
|
||||
fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
|
||||
}
|
||||
}
|
||||
function installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) {
|
||||
const rushJsonFolder = findRushJsonFolder();
|
||||
const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common');
|
||||
const rushTempFolder = _getRushTempFolder(rushCommonFolder);
|
||||
const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);
|
||||
if (!_isPackageAlreadyInstalled(packageInstallFolder)) {
|
||||
// The package isn't already installed
|
||||
_cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath);
|
||||
const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');
|
||||
(0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({
|
||||
sourceNpmrcFolder,
|
||||
targetNpmrcFolder: packageInstallFolder,
|
||||
logger,
|
||||
supportEnvVarFallbackSyntax: false
|
||||
});
|
||||
_createPackageJson(packageInstallFolder, packageName, packageVersion);
|
||||
const command = lockFilePath ? 'ci' : 'install';
|
||||
_installPackage(logger, packageInstallFolder, packageName, packageVersion, command);
|
||||
_writeFlagFile(packageInstallFolder);
|
||||
}
|
||||
const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
|
||||
const statusMessageLine = new Array(statusMessage.length + 1).join('-');
|
||||
logger.info('\n' + statusMessage + '\n' + statusMessageLine + '\n');
|
||||
const binPath = _getBinPath(packageInstallFolder, packageBinName);
|
||||
const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
|
||||
// Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to
|
||||
// assign via the process.env proxy to ensure that we append to the right PATH key.
|
||||
const originalEnvPath = process.env.PATH || '';
|
||||
let result;
|
||||
try {
|
||||
// `npm` bin stubs on Windows are `.cmd` files
|
||||
// Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true`
|
||||
const platformBinPath = _getPlatformPath(binPath);
|
||||
process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter);
|
||||
result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, {
|
||||
stdio: 'inherit',
|
||||
windowsVerbatimArguments: false,
|
||||
shell: _isWindows(),
|
||||
cwd: process.cwd(),
|
||||
env: process.env
|
||||
});
|
||||
}
|
||||
finally {
|
||||
process.env.PATH = originalEnvPath;
|
||||
}
|
||||
if (result.status !== null) {
|
||||
return result.status;
|
||||
}
|
||||
else {
|
||||
throw result.error || new Error('An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
function runWithErrorAndStatusCode(logger, fn) {
|
||||
process.exitCode = 1;
|
||||
try {
|
||||
const exitCode = fn();
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('\n\n' + e.toString() + '\n\n');
|
||||
}
|
||||
}
|
||||
function _run() {
|
||||
const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
|
||||
if (!nodePath) {
|
||||
throw new Error('Unexpected exception: could not detect node path');
|
||||
}
|
||||
if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') {
|
||||
// If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
|
||||
// to the script that (presumably) imported this file
|
||||
return;
|
||||
}
|
||||
if (process.argv.length < 4) {
|
||||
console.log('Usage: install-run.js <package>@<version> <command> [args...]');
|
||||
console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
|
||||
process.exit(1);
|
||||
}
|
||||
const logger = { info: console.log, error: console.error };
|
||||
runWithErrorAndStatusCode(logger, () => {
|
||||
const rushJsonFolder = findRushJsonFolder();
|
||||
const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common');
|
||||
const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier);
|
||||
const name = packageSpecifier.name;
|
||||
const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier);
|
||||
if (packageSpecifier.version !== version) {
|
||||
console.log(`Resolved to ${name}@${version}`);
|
||||
}
|
||||
return installAndRun(logger, name, version, packageBinName, packageBinArgs);
|
||||
});
|
||||
}
|
||||
_run();
|
||||
//# sourceMappingURL=install-run.js.map
|
||||
})();
|
||||
|
||||
module.exports = __webpack_exports__;
|
||||
/******/ })()
|
||||
;
|
||||
//# sourceMappingURL=install-run.js.map
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = process.cwd()
|
||||
const patterns = ['packages', 'pods', 'tests']
|
||||
let files = []
|
||||
for (const p of patterns) {
|
||||
const dir = path.join(root, p)
|
||||
if (!fs.existsSync(dir)) continue
|
||||
const items = fs.readdirSync(dir)
|
||||
for (const it of items) {
|
||||
const lcov = path.join(dir, it, 'coverage', 'lcov.info')
|
||||
if (fs.existsSync(lcov)) files.push(lcov)
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error('No lcov files found in packages/pods/tests/*/coverage/lcov.info')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const outDir = path.join(root, 'coverage')
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true })
|
||||
const outFile = path.join(outDir, 'lcov.info')
|
||||
|
||||
let outData = ''
|
||||
let seenTN = false
|
||||
// Build a repo file index to help resolve SF entries that are ambiguous
|
||||
const ignoreDirs = new Set(['node_modules', '.git', 'coverage', 'lib', 'dist', 'types', '.rush', 'temp', 'pnpm-store'])
|
||||
const repoFiles = []
|
||||
function walk(dir) {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true })
|
||||
for (const it of items) {
|
||||
if (it.isDirectory()) {
|
||||
if (ignoreDirs.has(it.name)) continue
|
||||
// skip hidden folders except top-level .config maybe
|
||||
if (it.name.startsWith('.')) continue
|
||||
try {
|
||||
walk(path.join(dir, it.name))
|
||||
} catch (e) {
|
||||
// ignore permission errors
|
||||
}
|
||||
} else if (it.isFile()) {
|
||||
repoFiles.push(path.join(dir, it.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
walk(root)
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
for (const f of files) {
|
||||
const data = fs.readFileSync(f, 'utf8')
|
||||
const pkgDir = path.dirname(path.dirname(f))
|
||||
const lines = data.split(/\r?\n/)
|
||||
const outLines = []
|
||||
for (const line of lines) {
|
||||
if (!line) continue
|
||||
// skip duplicate TN: headers (test name)
|
||||
if (line.startsWith('TN:')) {
|
||||
if (seenTN) continue
|
||||
seenTN = true
|
||||
outLines.push(line)
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith('SF:')) {
|
||||
const orig = line.slice(3)
|
||||
// if path is absolute and exists, keep it; otherwise resolve from package dir
|
||||
if (path.isAbsolute(orig)) {
|
||||
outLines.push('SF:' + orig)
|
||||
continue
|
||||
}
|
||||
|
||||
const abs = path.resolve(pkgDir, orig)
|
||||
if (fs.existsSync(abs)) {
|
||||
outLines.push('SF:' + abs)
|
||||
} else {
|
||||
// try package/src/orig if orig is not already prefixed with src
|
||||
const alt = path.resolve(pkgDir, orig)
|
||||
if (fs.existsSync(alt)) {
|
||||
outLines.push('SF:' + path.relative(root, alt))
|
||||
} else {
|
||||
// try to find any file in repo that ends with the orig path
|
||||
const found = repoFiles.find((p) => p.endsWith(path.sep + orig) || p.endsWith(orig))
|
||||
if (found) {
|
||||
outLines.push('SF:' + found)
|
||||
} else {
|
||||
// keep original if we can't resolve
|
||||
outLines.push('SF:' + orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
outLines.push(line)
|
||||
}
|
||||
|
||||
outData += outLines.join('\n') + '\n'
|
||||
}
|
||||
fs.writeFileSync(outFile, outData, 'utf8')
|
||||
console.log('Merged', files.length, 'lcov files into', outFile)
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@hcengineering/scripts",
|
||||
"version": "0.7.17",
|
||||
"scripts": {
|
||||
"format": "echo \"No format specified\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=== FINAL COVERAGE REPORT ==="
|
||||
echo ""
|
||||
|
||||
# Iterate through each package directory
|
||||
for pkg in packages/*/; do
|
||||
pkgname=$(basename "$pkg")
|
||||
|
||||
echo "📦 Package: $pkgname"
|
||||
echo "---"
|
||||
|
||||
# Change to package directory
|
||||
cd "$pkg" || continue
|
||||
|
||||
# Run tests with coverage and extract summary
|
||||
npm test -- --coverage --silent 2>&1 | \
|
||||
grep -A 4 "Coverage summary" | \
|
||||
grep -E "Statements|Branches|Functions|Lines"
|
||||
|
||||
# Return to root directory
|
||||
cd ../.. || exit
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "=== END OF COVERAGE REPORT ==="
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"name": "@hcengineering/account-client",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.20",
|
||||
"tag": "@hcengineering/account-client_v0.7.20",
|
||||
"date": "Wed, 26 Nov 2025 15:28:11 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Add password aging"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.22` to `0.7.23`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.18` to `0.7.19`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.19",
|
||||
"tag": "@hcengineering/account-client_v0.7.19",
|
||||
"date": "Thu, 30 Oct 2025 08:41:42 GMT",
|
||||
"comments": {
|
||||
"none": [
|
||||
{
|
||||
"comment": "formatting"
|
||||
}
|
||||
],
|
||||
"patch": [
|
||||
{
|
||||
"comment": "add workspace usage info"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.19` to `0.7.20`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.18",
|
||||
"tag": "@hcengineering/account-client_v0.7.18",
|
||||
"date": "Tue, 28 Oct 2025 21:50:57 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "new sub methods"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.17",
|
||||
"tag": "@hcengineering/account-client_v0.7.17",
|
||||
"date": "Mon, 27 Oct 2025 13:27:12 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.7",
|
||||
"tag": "@hcengineering/account-client_v0.7.7",
|
||||
"date": "Wed, 22 Oct 2025 12:46:09 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "add subs methods"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.6",
|
||||
"tag": "@hcengineering/account-client_v0.7.6",
|
||||
"date": "Wed, 15 Oct 2025 18:01:30 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "added methods to work with profile"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.7` to `0.7.8`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.5",
|
||||
"tag": "@hcengineering/account-client_v0.7.5",
|
||||
"date": "Tue, 14 Oct 2025 04:58:17 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "update deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.4",
|
||||
"tag": "@hcengineering/account-client_v0.7.4",
|
||||
"date": "Sat, 11 Oct 2025 18:20:33 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Update to latest platform rig"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.3",
|
||||
"tag": "@hcengineering/account-client_v0.7.3",
|
||||
"date": "Wed, 08 Oct 2025 03:40:53 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# Change Log - @hcengineering/account-client
|
||||
|
||||
This log was last generated on Wed, 26 Nov 2025 15:28:11 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.20
|
||||
Wed, 26 Nov 2025 15:28:11 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Add password aging
|
||||
|
||||
## 0.7.19
|
||||
Thu, 30 Oct 2025 08:41:42 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- add workspace usage info
|
||||
|
||||
## 0.7.18
|
||||
Tue, 28 Oct 2025 21:50:57 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- new sub methods
|
||||
|
||||
## 0.7.17
|
||||
Mon, 27 Oct 2025 13:27:12 GMT
|
||||
|
||||
_Version update only_
|
||||
|
||||
## 0.7.7
|
||||
Wed, 22 Oct 2025 12:46:09 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- add subs methods
|
||||
|
||||
## 0.7.6
|
||||
Wed, 15 Oct 2025 18:01:30 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- added methods to work with profile
|
||||
|
||||
## 0.7.5
|
||||
Tue, 14 Oct 2025 04:58:17 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- update deps
|
||||
|
||||
## 0.7.4
|
||||
Sat, 11 Oct 2025 18:20:33 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Update to latest platform rig
|
||||
|
||||
## 0.7.3
|
||||
Wed, 08 Oct 2025 03:40:53 GMT
|
||||
|
||||
_Initial release_
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ['./src'],
|
||||
coverageReporters: ['text-summary', 'html', 'lcov']
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@hcengineering/account-client",
|
||||
"version": "0.7.20",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"format": "format src",
|
||||
"test": "jest --passWithNoTests --silent --coverage",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent --coverage",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "~7.0.3",
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@types/node": "^22.15.29",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "workspace:^0.7.23",
|
||||
"@hcengineering/platform": "workspace:^0.7.19"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.core",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export * from './client'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
type AccountUuid,
|
||||
PersonId,
|
||||
WorkspaceDataId,
|
||||
WorkspaceUuid,
|
||||
type AccountRole,
|
||||
type Timestamp,
|
||||
type SocialId as SocialIdBase,
|
||||
PersonUuid,
|
||||
type WorkspaceMode,
|
||||
Person,
|
||||
WorkspaceInfo,
|
||||
AccountInfo,
|
||||
IntegrationKind
|
||||
} from '@hcengineering/core'
|
||||
|
||||
export interface LoginInfo {
|
||||
account: AccountUuid
|
||||
name?: string
|
||||
socialId?: PersonId
|
||||
token?: string
|
||||
}
|
||||
|
||||
export interface EndpointInfo {
|
||||
internalUrl: string
|
||||
externalUrl: string
|
||||
region: string
|
||||
}
|
||||
export interface WorkspaceVersion {
|
||||
versionMajor: number
|
||||
versionMinor: number
|
||||
versionPatch: number
|
||||
}
|
||||
|
||||
export interface LoginInfoWorkspace {
|
||||
url: string
|
||||
dataId?: WorkspaceDataId
|
||||
mode: WorkspaceMode
|
||||
version: WorkspaceVersion
|
||||
endpoint: EndpointInfo
|
||||
role: AccountRole | null
|
||||
progress?: number
|
||||
branding?: string
|
||||
passwordAgingRule?: number // in days
|
||||
}
|
||||
|
||||
export interface LoginInfoWithWorkspaces extends LoginInfo {
|
||||
// Information necessary to handle user <--> transactor connectivity.
|
||||
workspaces: Record<WorkspaceUuid, LoginInfoWorkspace>
|
||||
socialIds: SocialId[]
|
||||
}
|
||||
|
||||
export type LoginInfoByToken = LoginInfo | WorkspaceLoginInfo | LoginInfoRequest | null
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface WorkspaceLoginInfo extends LoginInfo {
|
||||
workspace: WorkspaceUuid // worspace uuid
|
||||
workspaceDataId?: WorkspaceDataId
|
||||
workspaceUrl: string
|
||||
endpoint: string
|
||||
token: string
|
||||
role: AccountRole
|
||||
allowGuestSignUp?: boolean
|
||||
}
|
||||
|
||||
export interface LoginInfoRequestData {
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
}
|
||||
|
||||
export type LoginInfoRequest = {
|
||||
request: true
|
||||
} & LoginInfoRequestData
|
||||
|
||||
export interface WorkspaceInviteInfo {
|
||||
workspace: WorkspaceUuid
|
||||
email?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface OtpInfo {
|
||||
sent: boolean
|
||||
retryOn: Timestamp
|
||||
}
|
||||
|
||||
export interface RegionInfo {
|
||||
region: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type WorkspaceOperation = 'create' | 'upgrade' | 'all' | 'all+backup'
|
||||
|
||||
export interface MailboxOptions {
|
||||
availableDomains: string[]
|
||||
minNameLength: number
|
||||
maxNameLength: number
|
||||
maxMailboxCount: number
|
||||
}
|
||||
|
||||
export interface MailboxInfo {
|
||||
mailbox: string
|
||||
aliases: string[]
|
||||
appPasswords: string[]
|
||||
}
|
||||
|
||||
export interface MailboxSecret {
|
||||
mailbox: string
|
||||
app?: string
|
||||
secret: string
|
||||
}
|
||||
|
||||
export interface Integration {
|
||||
socialId: PersonId
|
||||
kind: IntegrationKind // Integration kind. E.g. 'github', 'mail', 'telegram-bot', 'telegram' etc.
|
||||
workspaceUuid: WorkspaceUuid | null
|
||||
data?: Record<string, any>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface SocialId extends SocialIdBase {
|
||||
personUuid: PersonUuid
|
||||
isDeleted?: boolean
|
||||
}
|
||||
|
||||
export type IntegrationKey = Omit<Integration, 'data'>
|
||||
|
||||
export interface IntegrationSecret {
|
||||
socialId: PersonId
|
||||
kind: IntegrationKind // Integration kind. E.g. 'github', 'mail', 'telegram-bot', 'telegram' etc.
|
||||
workspaceUuid: WorkspaceUuid | null
|
||||
key: string // Key for the secret in the integration. Different secrets for the same integration must have different keys. Can be any string. E.g. '', 'user_app_1' etc.
|
||||
secret: string
|
||||
}
|
||||
|
||||
export type IntegrationSecretKey = Omit<IntegrationSecret, 'secret'>
|
||||
|
||||
export interface ProviderInfo {
|
||||
name: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
export interface AccountAggregatedInfo extends AccountInfo, Person {
|
||||
uuid: AccountUuid
|
||||
integrations: Omit<Integration, 'data'>[]
|
||||
socialIds: SocialId[]
|
||||
workspaces: Omit<WorkspaceInfo, 'allowReadOnlyGuest' | 'allowGuestSignUp'>[]
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile with additional information for public sharing
|
||||
* Stored in accounts database (global, not workspace-specific)
|
||||
*/
|
||||
export interface UserProfile {
|
||||
personUuid: PersonUuid
|
||||
bio?: string // LinkedIn-style bio (up to ~2000 chars)
|
||||
city?: string
|
||||
country?: string
|
||||
website?: string // Personal website URL
|
||||
socialLinks?: Record<string, string> // Flexible storage for social links
|
||||
isPublic: boolean // Public visibility toggle (default: false)
|
||||
}
|
||||
|
||||
export type PersonWithProfile = Person & Omit<UserProfile, 'personUuid'>
|
||||
|
||||
/**
|
||||
* Subscription status enum
|
||||
* Reflects the subscription lifecycle from active to canceled/expired
|
||||
*/
|
||||
export enum SubscriptionStatus {
|
||||
Active = 'active', // Subscription is active and paid
|
||||
Trialing = 'trialing', // In trial period (free usage)
|
||||
PastDue = 'past_due', // Payment failed but subscription not yet canceled
|
||||
Canceled = 'canceled', // Subscription was canceled by user or admin
|
||||
Paused = 'paused', // Subscription is temporarily paused (some providers support this)
|
||||
Expired = 'expired' // Subscription or trial has expired
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription type/purpose
|
||||
* Allows multiple active subscriptions per workspace for different purposes
|
||||
*/
|
||||
export enum SubscriptionType {
|
||||
Tier = 'tier', // Main workspace tier (free, starter, pro, enterprise)
|
||||
Support = 'support' // Voluntary support/donation subscription
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace subscription information
|
||||
* Provider-agnostic subscription data managed by billing service
|
||||
* Multiple subscriptions can be active per workspace (tier + addons + support)
|
||||
* Historical subscriptions are preserved with status: canceled/expired
|
||||
*/
|
||||
export interface Subscription {
|
||||
id: string // Our internal unique subscription ID (UUID)
|
||||
workspaceUuid: WorkspaceUuid
|
||||
accountUuid: AccountUuid // Account that paid for the subscription
|
||||
|
||||
// Provider details
|
||||
provider: string // Payment provider identifier (e.g. 'polar', 'stripe', 'manual')
|
||||
providerSubscriptionId: string // External subscription ID from the provider
|
||||
providerCheckoutId?: string // External checkout/session ID that created this subscription
|
||||
|
||||
// Subscription classification
|
||||
type: SubscriptionType // What this subscription is for (tier, addon, support)
|
||||
status: SubscriptionStatus // Current status
|
||||
plan: string // Plan/product identifier (e.g. 'free', 'pro', 'storage-100gb', 'supporter')
|
||||
|
||||
// Amount paid (in cents, e.g. 9999 = $99.99)
|
||||
// Used primarily for pay-what-you-want/donation subscriptions to track actual payment
|
||||
amount?: number
|
||||
|
||||
// Billing period (optional - not set for free/manual plans)
|
||||
periodStart?: Timestamp
|
||||
periodEnd?: Timestamp
|
||||
|
||||
// Trial information (optional)
|
||||
trialEnd?: Timestamp
|
||||
|
||||
// Cancellation tracking (optional)
|
||||
canceledAt?: Timestamp
|
||||
|
||||
// Provider-specific data stored as JSONB (optional)
|
||||
providerData?: Record<string, any>
|
||||
|
||||
// Timestamps (managed by database)
|
||||
createdOn: Timestamp
|
||||
updatedOn: Timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription data for creating/updating subscriptions (without timestamps)
|
||||
* Used by billing service to upsert subscription data
|
||||
*/
|
||||
export type SubscriptionData = Omit<Subscription, 'createdOn' | 'updatedOn'>
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 type { LoginInfoByToken, LoginInfoRequest, WorkspaceLoginInfo } from './types'
|
||||
|
||||
export function isWorkspaceLoginInfo (loginInfo: LoginInfoByToken): loginInfo is WorkspaceLoginInfo {
|
||||
return !isLoginInfoRequest(loginInfo) && (loginInfo as WorkspaceLoginInfo)?.workspace != null
|
||||
}
|
||||
|
||||
export function isLoginInfoRequest (info: LoginInfoByToken): info is LoginInfoRequest {
|
||||
return (info as LoginInfoRequest)?.request
|
||||
}
|
||||
|
||||
export function getClientTimezone (): string | undefined {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
} catch (err: any) {
|
||||
console.error('Failed to get client timezone', err)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "@hcengineering/analytics-service",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.17",
|
||||
"tag": "@hcengineering/analytics-service_v0.7.17",
|
||||
"date": "Mon, 27 Oct 2025 13:27:12 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.5",
|
||||
"tag": "@hcengineering/analytics-service_v0.7.5",
|
||||
"date": "Tue, 14 Oct 2025 04:58:17 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "update deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.4` to `0.7.5`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.4",
|
||||
"tag": "@hcengineering/analytics-service_v0.7.4",
|
||||
"date": "Sat, 11 Oct 2025 18:20:33 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Update to latest platform rig"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/analytics\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.3",
|
||||
"tag": "@hcengineering/analytics-service_v0.7.3",
|
||||
"date": "Wed, 08 Oct 2025 03:40:53 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Change Log - @hcengineering/analytics-service
|
||||
|
||||
This log was last generated on Mon, 27 Oct 2025 13:27:12 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.17
|
||||
Mon, 27 Oct 2025 13:27:12 GMT
|
||||
|
||||
_Version update only_
|
||||
|
||||
## 0.7.5
|
||||
Tue, 14 Oct 2025 04:58:17 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- update deps
|
||||
|
||||
## 0.7.4
|
||||
Sat, 11 Oct 2025 18:20:33 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Update to latest platform rig
|
||||
|
||||
## 0.7.3
|
||||
Wed, 08 Oct 2025 03:40:53 GMT
|
||||
|
||||
_Initial release_
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ['./src'],
|
||||
coverageReporters: ['text-summary', 'html', 'lcov']
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@hcengineering/analytics-service",
|
||||
"version": "0.7.17",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent --coverage",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent --coverage",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "workspace:^0.7.19",
|
||||
"@hcengineering/core": "workspace:^0.7.23",
|
||||
"@hcengineering/analytics": "workspace:^0.7.17",
|
||||
"@hcengineering/measurements-otlp": "workspace:^0.7.17",
|
||||
"winston": "^3.11.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.core",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { AnalyticProvider, Analytics } from '@hcengineering/analytics'
|
||||
import { initOpenTelemetrySDK, reportOTELError } from '@hcengineering/measurements-otlp'
|
||||
|
||||
export * from '@hcengineering/measurements-otlp'
|
||||
export * from './logging'
|
||||
|
||||
class OTELAnalyticsProvider implements AnalyticProvider {
|
||||
init (config: Record<string, any>): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
setUser: (email: string, data: any) => void = (email, data) => {}
|
||||
|
||||
setAlias: (distinctId: string, alias: string) => void = (distinctId, alias) => {}
|
||||
|
||||
setTag: (key: string, value: string) => void = (key, value) => {}
|
||||
|
||||
setWorkspace: (ws: string, guest: boolean) => void = (ws, guest) => {}
|
||||
|
||||
handleEvent: (event: string, params: Record<string, string>) => void = (event, params) => {}
|
||||
|
||||
handleError (error: Error): void {
|
||||
reportOTELError(error)
|
||||
}
|
||||
|
||||
navigate (path: string): void {}
|
||||
|
||||
logout (): void {}
|
||||
}
|
||||
|
||||
export function configureAnalytics (serviceName: string, serviceVersion: string, config?: Record<string, any>): void {
|
||||
const providers: AnalyticProvider[] = [new OTELAnalyticsProvider()]
|
||||
|
||||
initOpenTelemetrySDK(serviceName, serviceVersion)
|
||||
for (const provider of providers) {
|
||||
Analytics.init(provider, config ?? {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
import { MeasureLogger, ParamsType } from '@hcengineering/core'
|
||||
import { basename, dirname, join } from 'path'
|
||||
import winston from 'winston'
|
||||
import DailyRotateFile from 'winston-daily-rotate-file'
|
||||
|
||||
export class SplitLogger implements MeasureLogger {
|
||||
logger: winston.Logger
|
||||
|
||||
constructor (
|
||||
readonly name: string,
|
||||
readonly opts: { root?: string, parent?: winston.Logger, pretty?: boolean, enableConsole?: boolean }
|
||||
) {
|
||||
const rootDir = this.opts.root ?? 'logs'
|
||||
|
||||
this.logger = winston.createLogger({
|
||||
level: 'info',
|
||||
exitOnError: false
|
||||
})
|
||||
const errorPrinter = ({ message, stack, ...rest }: Error): object => ({
|
||||
message,
|
||||
stack,
|
||||
...rest
|
||||
})
|
||||
const jsonOptions: winston.Logform.JsonOptions = {
|
||||
replacer: (key, value) => {
|
||||
return value instanceof Error ? errorPrinter(value) : value
|
||||
}
|
||||
}
|
||||
this.logger.add(
|
||||
new DailyRotateFile({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
opts.pretty === true ? winston.format.prettyPrint() : winston.format.json(jsonOptions)
|
||||
),
|
||||
filename: `${name}-combined-%DATE%.log`,
|
||||
auditFile: join(rootDir, `${basename(name)}-audit.log`),
|
||||
dirname: rootDir,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d'
|
||||
})
|
||||
)
|
||||
this.logger.add(
|
||||
new DailyRotateFile({
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.prettyPrint()),
|
||||
filename: `${name}-error-%DATE%.log`,
|
||||
auditFile: join(rootDir, `${basename(name)}-audit.log`),
|
||||
level: 'error',
|
||||
dirname: rootDir,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d'
|
||||
})
|
||||
)
|
||||
if (opts.parent === undefined && opts.enableConsole === true) {
|
||||
console.log('Logging also into console', process.env.NODE_ENV, opts.enableConsole)
|
||||
this.logger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json(jsonOptions),
|
||||
winston.format.colorize({ all: true })
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
this.logger.info(
|
||||
'####################################################################################################################'
|
||||
)
|
||||
this.logger.info(
|
||||
`########################SplitLogger ${this.name} initialized: ${new Date().toISOString()}###########################`
|
||||
)
|
||||
}
|
||||
|
||||
error (message: string, obj?: Record<string, any>): void {
|
||||
if (this.opts.parent !== undefined) {
|
||||
this.opts.parent.error({ message, ...obj })
|
||||
}
|
||||
this.logger.error({ message, ...obj })
|
||||
}
|
||||
|
||||
info (message: string, obj?: Record<string, any>): void {
|
||||
if (this.opts.parent !== undefined && this.opts.enableConsole === true) {
|
||||
// Only propogate if enable console is true
|
||||
this.opts.parent.info({ message, ...obj })
|
||||
}
|
||||
this.logger.info({ message, ...obj })
|
||||
}
|
||||
|
||||
warn (message: string, obj?: Record<string, any>): void {
|
||||
if (this.opts.parent !== undefined) {
|
||||
this.opts.parent.warn({ message, ...obj })
|
||||
}
|
||||
this.logger.warn({ message, ...obj })
|
||||
}
|
||||
|
||||
logOperation (operation: string, time: number, params: ParamsType): void {
|
||||
this.logger.info(operation, { time, ...params })
|
||||
}
|
||||
|
||||
childLogger (name: string, params: Record<string, string>): MeasureLogger {
|
||||
const dirName = dirname(name)
|
||||
const { enableConsole, ...otherParams } = params
|
||||
const child = this.logger.child({ name, ...otherParams })
|
||||
return new SplitLogger(name, {
|
||||
...this.opts,
|
||||
parent: child,
|
||||
root: join(this.opts.root ?? 'logs', dirName),
|
||||
enableConsole: enableConsole === 'true'
|
||||
})
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
this.logger.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@hcengineering/analytics",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.17",
|
||||
"tag": "@hcengineering/analytics_v0.7.17",
|
||||
"date": "Fri, 31 Oct 2025 20:11:13 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.17` to `0.7.18`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.5",
|
||||
"tag": "@hcengineering/analytics_v0.7.5",
|
||||
"date": "Tue, 14 Oct 2025 04:58:17 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "update deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.4",
|
||||
"tag": "@hcengineering/analytics_v0.7.4",
|
||||
"date": "Sat, 11 Oct 2025 18:20:33 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Update to latest platform rig"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Change Log - @hcengineering/analytics
|
||||
|
||||
This log was last generated on Fri, 31 Oct 2025 20:11:13 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.17
|
||||
Fri, 31 Oct 2025 20:11:13 GMT
|
||||
|
||||
_Version update only_
|
||||
|
||||
## 0.7.5
|
||||
Tue, 14 Oct 2025 04:58:17 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- update deps
|
||||
|
||||
## 0.7.4
|
||||
Sat, 11 Oct 2025 18:20:33 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Update to latest platform rig
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ['./src'],
|
||||
coverageReporters: ['text-summary', 'html', 'lcov']
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@hcengineering/analytics",
|
||||
"version": "0.7.17",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent --coverage",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent --coverage",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "workspace:^0.7.19"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.core",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc
|
||||
//
|
||||
|
||||
import { addEventListener, PlatformEvent, Severity, Status, translate } from '@hcengineering/platform'
|
||||
|
||||
export const providers: AnalyticProvider[] = []
|
||||
export interface AnalyticProvider {
|
||||
init: (config: Record<string, any>) => boolean
|
||||
setUser: (email: string, data: any) => void
|
||||
setAlias: (distinctId: string, alias: string) => void
|
||||
setTag: (key: string, value: string) => void
|
||||
setWorkspace: (ws: string, guest: boolean) => void
|
||||
handleEvent: (event: string, params: Record<string, string>) => void
|
||||
handleError: (error: Error) => void
|
||||
navigate: (path: string) => void
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
export const Analytics = {
|
||||
data: {},
|
||||
|
||||
init (provider: AnalyticProvider, config: Record<string, any>): void {
|
||||
const res = provider.init(config)
|
||||
if (res) {
|
||||
providers.push(provider)
|
||||
}
|
||||
},
|
||||
|
||||
setUser (email: string, data: any): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.setUser(email, data)
|
||||
})
|
||||
},
|
||||
|
||||
setAlias (distinctId: string, alias: string): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.setAlias(distinctId, alias)
|
||||
})
|
||||
},
|
||||
|
||||
setTag (key: string, value: string): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.setTag(key, value)
|
||||
})
|
||||
},
|
||||
|
||||
setWorkspace (ws: string, guest: boolean): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.setWorkspace(ws, guest)
|
||||
})
|
||||
},
|
||||
|
||||
handleEvent (event: string, params: Record<string, any> = {}): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.handleEvent(event, { ...this.data, ...params })
|
||||
})
|
||||
},
|
||||
|
||||
handleError (error: Error): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.handleError(error)
|
||||
})
|
||||
},
|
||||
|
||||
navigate (path: string): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.navigate(path)
|
||||
})
|
||||
},
|
||||
|
||||
logout (): void {
|
||||
providers.forEach((provider) => {
|
||||
provider.logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener(PlatformEvent, async (_event, _status: Status) => {
|
||||
if (_status.severity === Severity.ERROR) {
|
||||
const label = await translate(_status.code, _status.params, 'en')
|
||||
Analytics.handleError(new Error(label))
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"name": "@hcengineering/api-client",
|
||||
"entries": [
|
||||
{
|
||||
"version": "0.7.18",
|
||||
"tag": "@hcengineering/api-client_v0.7.18",
|
||||
"date": "Mon, 27 Oct 2025 17:09:21 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "add support for textColor and textStyle marks"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/text\" from `^0.7.17` to `0.7.18`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.17",
|
||||
"tag": "@hcengineering/api-client_v0.7.17",
|
||||
"date": "Mon, 27 Oct 2025 13:27:12 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.17` to `0.7.18`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.5",
|
||||
"tag": "@hcengineering/api-client_v0.7.5",
|
||||
"date": "Tue, 14 Oct 2025 04:58:17 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "update deps"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/client\" from `^0.7.5` to `0.7.6`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/client-resources\" from `^0.7.5` to `0.7.6`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/collaborator-client\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.6` to `0.7.7`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/text\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/text-markdown\" from `^0.7.4` to `0.7.5`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.4",
|
||||
"tag": "@hcengineering/api-client_v0.7.4",
|
||||
"date": "Sat, 11 Oct 2025 18:20:33 GMT",
|
||||
"comments": {
|
||||
"patch": [
|
||||
{
|
||||
"comment": "Update to latest platform rig"
|
||||
}
|
||||
],
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/account-client\" from `^0.7.3` to `0.7.4`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/client\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/client-resources\" from `^0.7.4` to `0.7.5`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/collaborator-client\" from `^0.7.3` to `0.7.4`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.5` to `0.7.6`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/platform\" from `^0.7.3` to `0.7.4`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/text\" from `^0.7.3` to `0.7.4`"
|
||||
},
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/text-markdown\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.7.3",
|
||||
"tag": "@hcengineering/api-client_v0.7.3",
|
||||
"date": "Wed, 08 Oct 2025 03:40:53 GMT",
|
||||
"comments": {
|
||||
"dependency": [
|
||||
{
|
||||
"comment": "Updating dependency \"@hcengineering/core\" from `^0.7.3` to `0.7.4`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Change Log - @hcengineering/api-client
|
||||
|
||||
This log was last generated on Mon, 27 Oct 2025 17:09:21 GMT and should not be manually modified.
|
||||
|
||||
## 0.7.18
|
||||
Mon, 27 Oct 2025 17:09:21 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- add support for textColor and textStyle marks
|
||||
|
||||
## 0.7.17
|
||||
Mon, 27 Oct 2025 13:27:12 GMT
|
||||
|
||||
_Version update only_
|
||||
|
||||
## 0.7.5
|
||||
Tue, 14 Oct 2025 04:58:17 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- update deps
|
||||
|
||||
## 0.7.4
|
||||
Sat, 11 Oct 2025 18:20:33 GMT
|
||||
|
||||
### Patches
|
||||
|
||||
- Update to latest platform rig
|
||||
|
||||
## 0.7.3
|
||||
Wed, 08 Oct 2025 03:40:53 GMT
|
||||
|
||||
_Initial release_
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
# Huly Platform API Client
|
||||
|
||||
A TypeScript client library for interacting with the Huly Platform API.
|
||||
|
||||
## Installation
|
||||
|
||||
In order to be able to install required packages, you will need to obtain GitHub access token. You can create a token by following the instructions [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token).
|
||||
|
||||
```bash
|
||||
npm install @hcengineering/api-client
|
||||
```
|
||||
|
||||
## WebSocket Client vs REST Client
|
||||
|
||||
The api client package provides two main client variants: a WebSocket client and a REST client. The WebSocket client holds persistent connection to the Huly Platform API. The REST client uses standard HTTP requests to perform operations.
|
||||
|
||||
### WebSocket Client
|
||||
|
||||
```ts
|
||||
import { connect } from '@hcengineering/api-client'
|
||||
|
||||
// Connect to Huly
|
||||
const client = await connect('https://huly.app', {
|
||||
email: 'johndoe@example.com',
|
||||
password: 'password',
|
||||
workspace: 'my-workspace',
|
||||
})
|
||||
|
||||
// Use the client to perform operations
|
||||
...
|
||||
|
||||
// Close the client when done
|
||||
await client.close()
|
||||
```
|
||||
|
||||
### REST Client
|
||||
|
||||
```ts
|
||||
import { connectRest } from '@hcengineering/api-client'
|
||||
|
||||
// Connect to Huly
|
||||
const client = await connectRest('https://huly.app', {
|
||||
email: 'johndoe@example.com',
|
||||
password: 'password',
|
||||
workspace: 'my-workspace'
|
||||
})
|
||||
|
||||
// Use the client to perform operations
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The client supports two authentication methods: using email and password, or using a token.
|
||||
When authenticated, the client will have access to the same resources as the user.
|
||||
|
||||
> Note: The examples below use the WebSocket client (`connect`). To use the REST client instead, import and call `connectRest` with the same options.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `url`: URL of the Huly instance, for Huly Cloud use `https://huly.app`
|
||||
- `options`: Connection options
|
||||
- `workspace`: Name of the workspace to connect to, the workspace name can be found in the URL of the workspace: `https://huly.app/workbench/<workspace-name>`
|
||||
- `token`: Optional authentication token
|
||||
- `email`: Optional user email
|
||||
- `password`: Optional user password
|
||||
|
||||
### Using Email and Password
|
||||
|
||||
```ts
|
||||
import { connect } from '@hcengineering/api-client'
|
||||
|
||||
const client = await connect('https://huly.app', {
|
||||
email: 'johndoe@example.com',
|
||||
password: 'password',
|
||||
workspace: 'my-workspace'
|
||||
})
|
||||
|
||||
...
|
||||
|
||||
await client.close()
|
||||
```
|
||||
|
||||
### Using Token
|
||||
|
||||
```ts
|
||||
import { connect } from '@hcengineering/api-client'
|
||||
|
||||
const client = await connect('https://huly.app', {
|
||||
token: '...',
|
||||
workspace: 'my-workspace'
|
||||
})
|
||||
|
||||
...
|
||||
|
||||
await client.close()
|
||||
```
|
||||
|
||||
## Client API
|
||||
|
||||
The client provides a set of methods for interacting with the Huly Platform API. This section describes the main methods available in the client.
|
||||
|
||||
### Fetch API
|
||||
|
||||
The client provides two main methods for retrieving documents: `findOne` and `findAll`.
|
||||
|
||||
#### findOne
|
||||
|
||||
Retrieves a single document matching the query criteria.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object to find, results will include all subclasses of the target class
|
||||
- `query`: Query criteria
|
||||
- `options`: Find options
|
||||
- `limit`: Limit the number of results returned
|
||||
- `sort`: Sorting criteria
|
||||
- `lookup`: Lookup criteria
|
||||
- `projection`: Projection criteria
|
||||
- `total`: If specified total will be returned
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
...
|
||||
|
||||
const person = await client.findOne(
|
||||
contact.class.Person,
|
||||
{
|
||||
_id: 'person-id'
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### findAll
|
||||
|
||||
Retrieves multiple document matching the query criteria.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object to find, results will include all subclasses of the target class
|
||||
- `query`: Query criteria
|
||||
- `options`: Find options
|
||||
- `limit`: Limit the number of results returned
|
||||
- `sort`: Sorting criteria
|
||||
- `lookup`: Lookup criteria
|
||||
- `projection`: Projection criteria
|
||||
- `total`: If specified total will be returned
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
const persons = await client.findAll(
|
||||
contact.class.Person,
|
||||
{
|
||||
city: 'New York'
|
||||
},
|
||||
{
|
||||
limit: 10,
|
||||
sort: {
|
||||
name: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Documents API
|
||||
|
||||
The client provides three main methods for managing documents: `createDoc`, `updateDoc`, and `removeDoc`. These methods allow you to perform CRUD operations on documents.
|
||||
|
||||
#### createDoc
|
||||
|
||||
Creates a new document in the specified space.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object
|
||||
- `space`: Space of the object
|
||||
- `attributes`: Attributes of the object
|
||||
- `id`: Optional id of the object, if not provided, a new id will be generated
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact, { AvatarType } from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
const personId = await client.createDoc(
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
{
|
||||
name: 'Doe,John',
|
||||
city: 'New York',
|
||||
avatarType: AvatarType.COLOR
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### updateDoc
|
||||
|
||||
Updates existing document.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object
|
||||
- `space`: Space of the object
|
||||
- `objectId`: Id of the object
|
||||
- `operations`: Attributes of the object to update
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
await client.updateDoc(
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
personId,
|
||||
{
|
||||
city: 'New York',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### removeDoc
|
||||
|
||||
Removes existing document.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object
|
||||
- `space`: Space of the object
|
||||
- `objectId`: Id of the object
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
await client.removeDoc(
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
personId
|
||||
)
|
||||
```
|
||||
|
||||
### Collections API
|
||||
|
||||
#### addCollection
|
||||
|
||||
Creates a new attached document in the specified collection.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object to create
|
||||
- `space`: Space of the object to create
|
||||
- `attachedTo`: Id of the object to attach to
|
||||
- `attachedToClass`: Class of the object to attach to
|
||||
- `collection`: Name of the collection containing attached documents
|
||||
- `attributes`: Attributes of the object
|
||||
- `id`: Optional id of the object, if not provided, a new id will be generated
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact, { AvatarType } from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
const personId = await client.createDoc(
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
{
|
||||
name: 'Doe,John',
|
||||
city: 'New York',
|
||||
avatarType: AvatarType.COLOR
|
||||
}
|
||||
)
|
||||
|
||||
await client.addCollection(
|
||||
contact.class.Channel,
|
||||
contact.space.Contacts,
|
||||
personId,
|
||||
contact.class.Person,
|
||||
'channels',
|
||||
{
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'john.doe@example.com'
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### updateCollection
|
||||
|
||||
Updates existing attached document in collection.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object to update
|
||||
- `space`: Space of the object to update
|
||||
- `objectId`: Space of the object to update
|
||||
- `attachedTo`: Id of the parent object
|
||||
- `attachedToClass`: Class of the parent object
|
||||
- `collection`: Name of the collection containing attached documents
|
||||
- `attributes`: Attributes of the object to update
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
await client.updateCollection(
|
||||
contact.class.Channel,
|
||||
contact.space.Contacts,
|
||||
channelId,
|
||||
personId,
|
||||
contact.class.Person,
|
||||
'channels',
|
||||
{
|
||||
city: 'New York',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### removeCollection
|
||||
|
||||
Removes existing attached document from collection.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `_class`: Class of the object to remove
|
||||
- `space`: Space of the object to remove
|
||||
- `objectId`: Space of the object to remove
|
||||
- `attachedTo`: Id of the parent object
|
||||
- `attachedToClass`: Class of the parent object
|
||||
- `collection`: Name of the collection containing attached documents
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
await client.removeCollection(
|
||||
contact.class.Channel,
|
||||
contact.space.Contacts,
|
||||
channelId,
|
||||
personId,
|
||||
contact.class.Person,
|
||||
'channels'
|
||||
)
|
||||
```
|
||||
|
||||
### Mixins API
|
||||
|
||||
The client provides two methods for managing mixins: `createMixin` and `updateMixin`.
|
||||
|
||||
#### createMixin
|
||||
|
||||
Creates a new mixin for a specified document.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `objectId`: Id of the object the mixin is attached to
|
||||
- `objectClass`: Class of the object the mixin is attached to
|
||||
- `objectSpace`: Space of the object the mixin is attached to
|
||||
- `mixin`: Id of the mixin type to update
|
||||
- `attributes`: Attributes of the mixin
|
||||
|
||||
```ts
|
||||
import contact, { AvatarType } from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
const personId = await client.createDoc(
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
{
|
||||
name: 'Doe,John',
|
||||
city: 'New York',
|
||||
avatarType: AvatarType.COLOR
|
||||
}
|
||||
)
|
||||
|
||||
await client.createMixin(
|
||||
personId,
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
contact.mixin.Employee,
|
||||
{
|
||||
active: true,
|
||||
position: 'CEO'
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### updateMixin
|
||||
|
||||
Updates an existing mixin.
|
||||
|
||||
Parameters:
|
||||
|
||||
- `objectId`: Id of the object the mixin is attached to
|
||||
- `objectClass`: Class of the object the mixin is attached to
|
||||
- `objectSpace`: Space of the object the mixin is attached to
|
||||
- `mixin`: Id of the mixin type to update
|
||||
- `attributes`: Attributes of the mixin to update
|
||||
|
||||
```ts
|
||||
import contact, { AvatarType } from '@hcengineering/contact'
|
||||
|
||||
..
|
||||
|
||||
const person = await client.findOne(
|
||||
contact.class.Person,
|
||||
{
|
||||
_id: 'person-id'
|
||||
}
|
||||
)
|
||||
|
||||
await client.updateMixin(
|
||||
personId,
|
||||
contact.class.Person,
|
||||
contact.space.Contacts,
|
||||
contact.mixin.Employee,
|
||||
{
|
||||
active: false
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ['./src'],
|
||||
coverageReporters: ['text-summary', 'html', 'lcov']
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@hcengineering/api-client",
|
||||
"version": "0.7.18",
|
||||
"main": "lib/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
"require": "./lib/index.js",
|
||||
"import": "./lib/index.js"
|
||||
}
|
||||
},
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"src/**/*",
|
||||
"!src/**/__test__/**",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"template": "@hcengineering/api-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent --coverage",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent --coverage",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hcengineering/platform-rig": "^0.7.19",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.8.0",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@types/snappyjs": "^0.7.1",
|
||||
"eslint-plugin-svelte": "^2.35.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/account-client": "workspace:^0.7.20",
|
||||
"@hcengineering/client": "workspace:^0.7.18",
|
||||
"@hcengineering/client-resources": "workspace:^0.7.18",
|
||||
"@hcengineering/collaborator-client": "workspace:^0.7.17",
|
||||
"@hcengineering/core": "workspace:^0.7.23",
|
||||
"@hcengineering/platform": "workspace:^0.7.19",
|
||||
"@hcengineering/text": "workspace:^0.7.18",
|
||||
"@hcengineering/text-markdown": "workspace:^0.7.20",
|
||||
"snappyjs": "^0.7.0"
|
||||
},
|
||||
"repository": "https://github.com/hcengineering/huly.core",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { loadServerConfig } from '../config'
|
||||
|
||||
describe('loadServerConfig', () => {
|
||||
const mockFetch = jest.fn()
|
||||
global.fetch = mockFetch as any
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockClear()
|
||||
})
|
||||
|
||||
it('should load server config successfully', async () => {
|
||||
const mockConfig = {
|
||||
ACCOUNTS_URL: 'https://accounts.example.com',
|
||||
COLLABORATOR_URL: 'https://collaborator.example.com',
|
||||
FILES_URL: 'https://files.example.com',
|
||||
UPLOAD_URL: 'https://upload.example.com'
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => mockConfig
|
||||
})
|
||||
|
||||
const config = await loadServerConfig('https://api.example.com')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true })
|
||||
expect(config).toEqual(mockConfig)
|
||||
})
|
||||
|
||||
it('should throw error when fetch fails', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404
|
||||
})
|
||||
|
||||
await expect(loadServerConfig('https://api.example.com')).rejects.toThrow('Failed to fetch config')
|
||||
})
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
await expect(loadServerConfig('https://api.example.com')).rejects.toThrow('Network error')
|
||||
})
|
||||
|
||||
it('should construct correct config URL', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ACCOUNTS_URL: '',
|
||||
COLLABORATOR_URL: '',
|
||||
FILES_URL: '',
|
||||
UPLOAD_URL: ''
|
||||
})
|
||||
})
|
||||
|
||||
await loadServerConfig('https://api.example.com/')
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true })
|
||||
})
|
||||
|
||||
it('should handle URL without trailing slash', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
ACCOUNTS_URL: '',
|
||||
COLLABORATOR_URL: '',
|
||||
FILES_URL: '',
|
||||
UPLOAD_URL: ''
|
||||
})
|
||||
})
|
||||
|
||||
await loadServerConfig('https://api.example.com')
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/config.json', { keepalive: true })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { createMarkupOperations } from '../markup/client'
|
||||
import { getClient } from '@hcengineering/collaborator-client'
|
||||
import { makeCollabId } from '@hcengineering/core'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@hcengineering/collaborator-client')
|
||||
jest.mock('@hcengineering/text', () => ({
|
||||
htmlToJSON: jest.fn((html) => ({ type: 'doc', content: [{ type: 'text', text: html }] })),
|
||||
jsonToHTML: jest.fn((json) => json.content?.[0]?.text ?? ''),
|
||||
jsonToMarkup: jest.fn((json) => json.content?.[0]?.text ?? ''),
|
||||
markupToJSON: jest.fn((markup) => ({ type: 'doc', content: [{ type: 'text', text: markup }] }))
|
||||
}))
|
||||
jest.mock('@hcengineering/text-markdown', () => ({
|
||||
markdownToMarkup: jest.fn((md) => md),
|
||||
markupToMarkdown: jest.fn((json) => json.content?.[0]?.text ?? '')
|
||||
}))
|
||||
|
||||
describe('MarkupOperations', () => {
|
||||
const mockConfig = {
|
||||
ACCOUNTS_URL: 'https://accounts.example.com',
|
||||
COLLABORATOR_URL: 'https://collaborator.example.com',
|
||||
FILES_URL: 'https://files.example.com',
|
||||
UPLOAD_URL: 'https://upload.example.com'
|
||||
}
|
||||
|
||||
const workspace = 'test-workspace' as any
|
||||
const token = 'test-token'
|
||||
const url = 'https://api.example.com'
|
||||
|
||||
let mockCollaborator: any
|
||||
let operations: any
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockCollaborator = {
|
||||
getMarkup: jest.fn(),
|
||||
createMarkup: jest.fn()
|
||||
}
|
||||
;(getClient as jest.Mock).mockReturnValue(mockCollaborator)
|
||||
|
||||
operations = createMarkupOperations(url, workspace, token, mockConfig)
|
||||
})
|
||||
|
||||
describe('fetchMarkup', () => {
|
||||
const objectClass = 'class:test.Doc' as any
|
||||
const objectId = 'doc-id-123' as any
|
||||
const objectAttr = 'content'
|
||||
const markupRef = 'markup-ref-456' as any
|
||||
|
||||
it('should fetch markup in markup format', async () => {
|
||||
const mockMarkup = 'Test markup content'
|
||||
mockCollaborator.getMarkup.mockResolvedValue(mockMarkup)
|
||||
|
||||
const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markup')
|
||||
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
expect(mockCollaborator.getMarkup).toHaveBeenCalledWith(collabId, markupRef)
|
||||
expect(result).toBe(mockMarkup)
|
||||
})
|
||||
|
||||
it('should fetch markup in HTML format', async () => {
|
||||
const mockMarkup = '<p>Test content</p>'
|
||||
mockCollaborator.getMarkup.mockResolvedValue(mockMarkup)
|
||||
|
||||
const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'html')
|
||||
|
||||
expect(mockCollaborator.getMarkup).toHaveBeenCalled()
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
it('should fetch markup in markdown format', async () => {
|
||||
const mockMarkup = '# Test heading'
|
||||
mockCollaborator.getMarkup.mockResolvedValue(mockMarkup)
|
||||
|
||||
const result = await operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markdown')
|
||||
|
||||
expect(mockCollaborator.getMarkup).toHaveBeenCalled()
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
|
||||
it('should throw error for unknown format', async () => {
|
||||
mockCollaborator.getMarkup.mockResolvedValue('content')
|
||||
|
||||
await expect(
|
||||
operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'unknown-format' as any)
|
||||
).rejects.toThrow('Unknown content format')
|
||||
})
|
||||
|
||||
it('should handle collaborator errors', async () => {
|
||||
mockCollaborator.getMarkup.mockRejectedValue(new Error('Collaborator error'))
|
||||
|
||||
await expect(operations.fetchMarkup(objectClass, objectId, objectAttr, markupRef, 'markup')).rejects.toThrow(
|
||||
'Collaborator error'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('uploadMarkup', () => {
|
||||
const objectClass = 'class:test.Doc' as any
|
||||
const objectId = 'doc-id-123' as any
|
||||
const objectAttr = 'content'
|
||||
const mockMarkupRef = 'new-markup-ref-789' as any
|
||||
|
||||
beforeEach(() => {
|
||||
mockCollaborator.createMarkup.mockResolvedValue(mockMarkupRef)
|
||||
})
|
||||
|
||||
it('should upload markup in markup format', async () => {
|
||||
const content = 'Test markup content'
|
||||
|
||||
const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'markup')
|
||||
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
expect(mockCollaborator.createMarkup).toHaveBeenCalledWith(collabId, content)
|
||||
expect(result).toBe(mockMarkupRef)
|
||||
})
|
||||
|
||||
it('should upload markup in HTML format', async () => {
|
||||
const content = '<p>Test HTML content</p>'
|
||||
|
||||
const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'html')
|
||||
|
||||
expect(mockCollaborator.createMarkup).toHaveBeenCalled()
|
||||
expect(result).toBe(mockMarkupRef)
|
||||
})
|
||||
|
||||
it('should upload markup in markdown format', async () => {
|
||||
const content = '# Test markdown'
|
||||
|
||||
const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, content, 'markdown')
|
||||
|
||||
expect(mockCollaborator.createMarkup).toHaveBeenCalled()
|
||||
expect(result).toBe(mockMarkupRef)
|
||||
})
|
||||
|
||||
it('should throw error for unknown format', async () => {
|
||||
await expect(
|
||||
operations.uploadMarkup(objectClass, objectId, objectAttr, 'content', 'unknown-format' as any)
|
||||
).rejects.toThrow('Unknown content format')
|
||||
})
|
||||
|
||||
it('should handle empty content', async () => {
|
||||
const result = await operations.uploadMarkup(objectClass, objectId, objectAttr, '', 'markup')
|
||||
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
expect(mockCollaborator.createMarkup).toHaveBeenCalledWith(collabId, '')
|
||||
expect(result).toBe(mockMarkupRef)
|
||||
})
|
||||
|
||||
it('should handle collaborator errors', async () => {
|
||||
mockCollaborator.createMarkup.mockRejectedValue(new Error('Upload failed'))
|
||||
|
||||
await expect(operations.uploadMarkup(objectClass, objectId, objectAttr, 'content', 'markup')).rejects.toThrow(
|
||||
'Upload failed'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize collaborator client with correct parameters', () => {
|
||||
expect(getClient).toHaveBeenCalledWith(workspace, token, mockConfig.COLLABORATOR_URL)
|
||||
})
|
||||
|
||||
it('should handle different workspace IDs', () => {
|
||||
const differentWorkspace = 'different-workspace' as any
|
||||
createMarkupOperations(url, differentWorkspace, token, mockConfig)
|
||||
|
||||
expect(getClient).toHaveBeenCalledWith(differentWorkspace, token, mockConfig.COLLABORATOR_URL)
|
||||
})
|
||||
|
||||
it('should handle different tokens', () => {
|
||||
const differentToken = 'different-token'
|
||||
createMarkupOperations(url, workspace, differentToken, mockConfig)
|
||||
|
||||
expect(getClient).toHaveBeenCalledWith(workspace, differentToken, mockConfig.COLLABORATOR_URL)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { html, markdown, MarkupContent } from '../markup/types'
|
||||
|
||||
describe('MarkupContent', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create MarkupContent with content and kind', () => {
|
||||
const content = '<p>Hello World</p>'
|
||||
const markup = new MarkupContent(content, 'html')
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('html')
|
||||
})
|
||||
|
||||
it('should create MarkupContent with markdown kind', () => {
|
||||
const content = '# Hello World'
|
||||
const markup = new MarkupContent(content, 'markdown')
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('markdown')
|
||||
})
|
||||
|
||||
it('should create MarkupContent with markup kind', () => {
|
||||
const content = 'plain markup content'
|
||||
const markup = new MarkupContent(content, 'markup')
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('markup')
|
||||
})
|
||||
})
|
||||
|
||||
describe('html helper', () => {
|
||||
it('should create HTML MarkupContent', () => {
|
||||
const content = '<h1>Title</h1><p>Content</p>'
|
||||
const markup = html(content)
|
||||
|
||||
expect(markup).toBeInstanceOf(MarkupContent)
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('html')
|
||||
})
|
||||
|
||||
it('should handle empty HTML', () => {
|
||||
const markup = html('')
|
||||
|
||||
expect(markup.content).toBe('')
|
||||
expect(markup.kind).toBe('html')
|
||||
})
|
||||
|
||||
it('should handle complex HTML with attributes', () => {
|
||||
const content = '<div class="container"><a href="https://example.com">Link</a></div>'
|
||||
const markup = html(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('html')
|
||||
})
|
||||
})
|
||||
|
||||
describe('markdown helper', () => {
|
||||
it('should create Markdown MarkupContent', () => {
|
||||
const content = '# Heading\n\n* List item 1\n* List item 2'
|
||||
const markup = markdown(content)
|
||||
|
||||
expect(markup).toBeInstanceOf(MarkupContent)
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('markdown')
|
||||
})
|
||||
|
||||
it('should handle empty markdown', () => {
|
||||
const markup = markdown('')
|
||||
|
||||
expect(markup.content).toBe('')
|
||||
expect(markup.kind).toBe('markdown')
|
||||
})
|
||||
|
||||
it('should handle markdown with code blocks', () => {
|
||||
const content = '```javascript\nconst x = 42;\n```'
|
||||
const markup = markdown(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('markdown')
|
||||
})
|
||||
|
||||
it('should handle markdown with links', () => {
|
||||
const content = '[Link text](https://example.com)'
|
||||
const markup = markdown(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.kind).toBe('markdown')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle special characters in content', () => {
|
||||
const content = '<p>Special chars: & < > " \'</p>'
|
||||
const markup = html(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
})
|
||||
|
||||
it('should handle Unicode characters', () => {
|
||||
const content = '# 你好世界 🌍'
|
||||
const markup = markdown(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
})
|
||||
|
||||
it('should handle very long content', () => {
|
||||
const content = 'a'.repeat(10000)
|
||||
const markup = html(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
expect(markup.content.length).toBe(10000)
|
||||
})
|
||||
|
||||
it('should handle multiline content', () => {
|
||||
const content = 'Line 1\nLine 2\nLine 3'
|
||||
const markup = markdown(content)
|
||||
|
||||
expect(markup.content).toBe(content)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { withRetry, extractJson } from '../rest/utils'
|
||||
|
||||
describe('withRetry', () => {
|
||||
it('should return result on first success', async () => {
|
||||
const fn = jest.fn().mockResolvedValue('success')
|
||||
|
||||
const result = await withRetry(fn)
|
||||
|
||||
expect(result).toBe('success')
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should retry on failure and eventually succeed', async () => {
|
||||
const fn = jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('fail 1'))
|
||||
.mockRejectedValueOnce(new Error('fail 2'))
|
||||
.mockResolvedValue('success')
|
||||
|
||||
const result = await withRetry(fn)
|
||||
|
||||
expect(result).toBe('success')
|
||||
expect(fn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should throw error after max retries', async () => {
|
||||
const error = new Error('persistent failure')
|
||||
const fn = jest.fn().mockRejectedValue(error)
|
||||
|
||||
await expect(withRetry(fn)).rejects.toThrow('persistent failure')
|
||||
expect(fn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should use exponential backoff', async () => {
|
||||
const delays: number[] = []
|
||||
const startTimes: number[] = []
|
||||
let lastTime = Date.now()
|
||||
|
||||
const fn = jest.fn(async () => {
|
||||
const now = Date.now()
|
||||
if (startTimes.length > 0) {
|
||||
delays.push(now - lastTime)
|
||||
}
|
||||
startTimes.push(now)
|
||||
lastTime = now
|
||||
throw new Error('fail')
|
||||
})
|
||||
|
||||
await expect(withRetry(fn)).rejects.toThrow('fail')
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(3)
|
||||
// Delays should be approximately 100ms and 200ms (with some tolerance)
|
||||
expect(delays.length).toBe(2)
|
||||
expect(delays[0]).toBeGreaterThanOrEqual(80)
|
||||
expect(delays[0]).toBeLessThan(150)
|
||||
expect(delays[1]).toBeGreaterThanOrEqual(180)
|
||||
expect(delays[1]).toBeLessThan(250)
|
||||
})
|
||||
|
||||
it('should not decrement attempt when ignoreAttemptCheck returns true', async () => {
|
||||
let callCount = 0
|
||||
const fn = jest.fn(async () => {
|
||||
callCount++
|
||||
if (callCount <= 5) {
|
||||
throw new Error('ignore')
|
||||
}
|
||||
throw new Error('real error')
|
||||
})
|
||||
|
||||
const ignoreCheck = jest.fn((err: any) => err.message === 'ignore')
|
||||
|
||||
await expect(withRetry(fn, ignoreCheck)).rejects.toThrow('real error')
|
||||
|
||||
// Should have tried more than 3 times because ignored errors don't count
|
||||
expect(fn.mock.calls.length).toBeGreaterThan(3)
|
||||
expect(ignoreCheck).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle promise rejection', async () => {
|
||||
const fn = jest.fn(async () => {
|
||||
throw new Error('async error')
|
||||
})
|
||||
|
||||
await expect(withRetry(fn)).rejects.toThrow('async error')
|
||||
expect(fn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should handle errors that bypass the check', async () => {
|
||||
const fn = jest.fn(async () => {
|
||||
// Always throw error
|
||||
throw new Error('persistent error')
|
||||
})
|
||||
|
||||
const ignoreCheck = jest.fn(() => false) // Never ignore
|
||||
|
||||
await expect(withRetry(fn, ignoreCheck)).rejects.toThrow('persistent error')
|
||||
expect(fn).toHaveBeenCalledTimes(3)
|
||||
expect(ignoreCheck).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractJson', () => {
|
||||
it('should extract plain JSON', async () => {
|
||||
const mockResponse = {
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue(null)
|
||||
},
|
||||
text: jest.fn().mockResolvedValue('{"key":"value"}')
|
||||
} as any
|
||||
|
||||
const result = await extractJson(mockResponse)
|
||||
|
||||
expect(result).toEqual({ key: 'value' })
|
||||
expect(mockResponse.headers.get).toHaveBeenCalledWith('content-encoding')
|
||||
})
|
||||
|
||||
it('should handle TotalArray dataType', async () => {
|
||||
const jsonString = JSON.stringify({
|
||||
dataType: 'TotalArray',
|
||||
value: [{ id: 1 }, { id: 2 }],
|
||||
total: 10,
|
||||
lookupMap: { key: 'value' }
|
||||
})
|
||||
|
||||
const mockResponse = {
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue(null)
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(jsonString)
|
||||
} as any
|
||||
|
||||
const result = await extractJson(mockResponse)
|
||||
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.total).toBe(10)
|
||||
expect(result.lookupMap).toEqual({ key: 'value' })
|
||||
expect(result[0]).toEqual({ id: 1 })
|
||||
})
|
||||
|
||||
it('should handle snappy encoding header', async () => {
|
||||
// For this test, we'll just verify the snappy path is attempted
|
||||
// Actual snappy compression/decompression would require valid compressed data
|
||||
const mockResponse = {
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue('snappy')
|
||||
},
|
||||
arrayBuffer: jest.fn().mockResolvedValue(new ArrayBuffer(0))
|
||||
} as any
|
||||
|
||||
// This will fail to decompress, but we're testing that the snappy path is taken
|
||||
await expect(extractJson(mockResponse)).rejects.toThrow()
|
||||
|
||||
expect(mockResponse.headers.get).toHaveBeenCalledWith('content-encoding')
|
||||
expect(mockResponse.arrayBuffer).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty JSON object', async () => {
|
||||
const mockResponse = {
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue(null)
|
||||
},
|
||||
text: jest.fn().mockResolvedValue('{}')
|
||||
} as any
|
||||
|
||||
const result = await extractJson(mockResponse)
|
||||
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle nested objects', async () => {
|
||||
const nestedData = {
|
||||
level1: {
|
||||
level2: {
|
||||
dataType: 'TotalArray',
|
||||
value: [1, 2, 3],
|
||||
total: 3,
|
||||
lookupMap: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockResponse = {
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue(null)
|
||||
},
|
||||
text: jest.fn().mockResolvedValue(JSON.stringify(nestedData))
|
||||
} as any
|
||||
|
||||
const result = await extractJson(mockResponse)
|
||||
|
||||
expect(result.level1.level2.total).toBe(3)
|
||||
expect(Array.isArray(result.level1.level2)).toBe(true)
|
||||
})
|
||||
|
||||
it('should throw error on invalid JSON', async () => {
|
||||
const mockResponse = {
|
||||
headers: {
|
||||
get: jest.fn().mockReturnValue(null)
|
||||
},
|
||||
text: jest.fn().mockResolvedValue('invalid json{')
|
||||
} as any
|
||||
|
||||
await expect(extractJson(mockResponse)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { getWorkspaceToken } from '../utils'
|
||||
import { loadServerConfig } from '../config'
|
||||
import { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../config')
|
||||
jest.mock('@hcengineering/account-client')
|
||||
|
||||
describe('getWorkspaceToken', () => {
|
||||
const mockConfig = {
|
||||
ACCOUNTS_URL: 'https://accounts.example.com',
|
||||
COLLABORATOR_URL: 'https://collaborator.example.com',
|
||||
FILES_URL: 'https://files.example.com',
|
||||
UPLOAD_URL: 'https://upload.example.com'
|
||||
}
|
||||
|
||||
const mockWorkspaceInfo = {
|
||||
endpoint: 'wss://workspace.example.com',
|
||||
token: 'workspace-token-123',
|
||||
workspace: 'workspace-id-123' as any,
|
||||
email: 'user@example.com',
|
||||
workspaceId: 'workspace-id-123',
|
||||
workspaceName: 'Test Workspace',
|
||||
workspaceUrl: 'https://workspace.example.com',
|
||||
createdOn: Date.now(),
|
||||
lastVisit: Date.now(),
|
||||
role: 0,
|
||||
account: 'account-id-123' as any
|
||||
}
|
||||
|
||||
const mockLoginInfo = {
|
||||
token: 'login-token-456',
|
||||
endpoint: 'wss://endpoint.example.com'
|
||||
}
|
||||
|
||||
let mockAccountClient: any
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockAccountClient = {
|
||||
login: jest.fn().mockResolvedValue(mockLoginInfo),
|
||||
selectWorkspace: jest.fn().mockResolvedValue(mockWorkspaceInfo)
|
||||
}
|
||||
;(getAccountClient as jest.Mock).mockReturnValue(mockAccountClient)
|
||||
;(loadServerConfig as jest.Mock).mockResolvedValue(mockConfig)
|
||||
})
|
||||
|
||||
describe('with email/password authentication', () => {
|
||||
it('should successfully get workspace token with credentials', async () => {
|
||||
const result = await getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
|
||||
expect(loadServerConfig).toHaveBeenCalledWith('https://api.example.com')
|
||||
expect(mockAccountClient.login).toHaveBeenCalledWith('user@example.com', 'password123')
|
||||
expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('test-workspace')
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: mockWorkspaceInfo.endpoint,
|
||||
token: mockWorkspaceInfo.token,
|
||||
workspaceId: mockWorkspaceInfo.workspace,
|
||||
info: mockWorkspaceInfo
|
||||
})
|
||||
})
|
||||
|
||||
it('should use provided config if available', async () => {
|
||||
await getWorkspaceToken(
|
||||
'https://api.example.com',
|
||||
{
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
workspace: 'test-workspace'
|
||||
},
|
||||
mockConfig
|
||||
)
|
||||
|
||||
expect(loadServerConfig).not.toHaveBeenCalled()
|
||||
expect(getAccountClient).toHaveBeenCalledWith(mockConfig.ACCOUNTS_URL)
|
||||
})
|
||||
|
||||
it('should throw error when login fails', async () => {
|
||||
mockAccountClient.login.mockResolvedValue({ token: undefined })
|
||||
|
||||
await expect(
|
||||
getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user@example.com',
|
||||
password: 'wrong-password',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
).rejects.toThrow('Login failed')
|
||||
})
|
||||
|
||||
it('should throw error when workspace not found', async () => {
|
||||
mockAccountClient.selectWorkspace.mockResolvedValue(undefined)
|
||||
|
||||
await expect(
|
||||
getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
workspace: 'non-existent-workspace'
|
||||
})
|
||||
).rejects.toThrow('Workspace not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with token authentication', () => {
|
||||
it('should successfully get workspace token with existing token', async () => {
|
||||
const result = await getWorkspaceToken('https://api.example.com', {
|
||||
token: 'existing-token-789',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
|
||||
expect(mockAccountClient.login).not.toHaveBeenCalled()
|
||||
expect(getAccountClient).toHaveBeenCalledWith(mockConfig.ACCOUNTS_URL, 'existing-token-789')
|
||||
expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('test-workspace')
|
||||
|
||||
expect(result).toEqual({
|
||||
endpoint: mockWorkspaceInfo.endpoint,
|
||||
token: mockWorkspaceInfo.token,
|
||||
workspaceId: mockWorkspaceInfo.workspace,
|
||||
info: mockWorkspaceInfo
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw error when workspace not found with token', async () => {
|
||||
mockAccountClient.selectWorkspace.mockResolvedValue(undefined)
|
||||
|
||||
await expect(
|
||||
getWorkspaceToken('https://api.example.com', {
|
||||
token: 'existing-token-789',
|
||||
workspace: 'non-existent-workspace'
|
||||
})
|
||||
).rejects.toThrow('Workspace not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should propagate config loading errors', async () => {
|
||||
;(loadServerConfig as jest.Mock).mockRejectedValue(new Error('Config load failed'))
|
||||
|
||||
await expect(
|
||||
getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
).rejects.toThrow('Config load failed')
|
||||
})
|
||||
|
||||
it('should propagate login errors', async () => {
|
||||
mockAccountClient.login.mockRejectedValue(new Error('Invalid credentials'))
|
||||
|
||||
await expect(
|
||||
getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user@example.com',
|
||||
password: 'wrong-password',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
).rejects.toThrow('Invalid credentials')
|
||||
})
|
||||
|
||||
it('should propagate workspace selection errors', async () => {
|
||||
mockAccountClient.selectWorkspace.mockRejectedValue(new Error('Access denied'))
|
||||
|
||||
await expect(
|
||||
getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
).rejects.toThrow('Access denied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty workspace name', async () => {
|
||||
await getWorkspaceToken('https://api.example.com', {
|
||||
token: 'token',
|
||||
workspace: ''
|
||||
})
|
||||
|
||||
expect(mockAccountClient.selectWorkspace).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should handle special characters in credentials', async () => {
|
||||
await getWorkspaceToken('https://api.example.com', {
|
||||
email: 'user+test@example.com',
|
||||
password: 'p@ssw0rd!#$%',
|
||||
workspace: 'test-workspace'
|
||||
})
|
||||
|
||||
expect(mockAccountClient.login).toHaveBeenCalledWith('user+test@example.com', 'p@ssw0rd!#$%')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,297 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
import client, { clientId } from '@hcengineering/client'
|
||||
import {
|
||||
type Account,
|
||||
type Class,
|
||||
type Client,
|
||||
type Data,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type Hierarchy,
|
||||
type ModelDb,
|
||||
type Ref,
|
||||
type Space,
|
||||
type TxResult,
|
||||
type WithLookup,
|
||||
AttachedData,
|
||||
AttachedDoc,
|
||||
DocumentUpdate,
|
||||
Mixin,
|
||||
MixinData,
|
||||
MixinUpdate,
|
||||
TxOperations,
|
||||
WorkspaceUuid,
|
||||
generateId,
|
||||
pickPrimarySocialId
|
||||
} from '@hcengineering/core'
|
||||
import { addLocation, getResource } from '@hcengineering/platform'
|
||||
|
||||
import { type ServerConfig, loadServerConfig } from './config'
|
||||
import {
|
||||
type MarkupFormat,
|
||||
type MarkupOperations,
|
||||
type MarkupRef,
|
||||
MarkupContent,
|
||||
createMarkupOperations
|
||||
} from './markup'
|
||||
import { type ConnectOptions, type PlatformClient, WithMarkup } from './types'
|
||||
import { getWorkspaceToken } from './utils'
|
||||
|
||||
/**
|
||||
* Create platform client
|
||||
* @public */
|
||||
export async function connect (url: string, options: ConnectOptions): Promise<PlatformClient> {
|
||||
const config = await loadServerConfig(url)
|
||||
|
||||
const { endpoint, token } = await getWorkspaceToken(url, options, config)
|
||||
const accountClient = getAccountClient(config.ACCOUNTS_URL, token)
|
||||
const socialIds = await accountClient.getSocialIds(true)
|
||||
const wsLoginInfo = await accountClient.selectWorkspace(options.workspace)
|
||||
|
||||
if (wsLoginInfo === undefined) {
|
||||
throw new Error(`Workspace ${options.workspace} not found`)
|
||||
}
|
||||
|
||||
const account: Account = {
|
||||
uuid: wsLoginInfo.account,
|
||||
role: wsLoginInfo.role,
|
||||
primarySocialId: pickPrimarySocialId(socialIds)._id,
|
||||
socialIds: socialIds.map((si) => si._id),
|
||||
fullSocialIds: socialIds
|
||||
}
|
||||
|
||||
return await createClient(url, endpoint, token, wsLoginInfo.workspace, account, config, options)
|
||||
}
|
||||
|
||||
async function createClient (
|
||||
url: string,
|
||||
endpoint: string,
|
||||
token: string,
|
||||
workspaceUuid: WorkspaceUuid,
|
||||
account: Account,
|
||||
config: ServerConfig,
|
||||
options: ConnectOptions
|
||||
): Promise<PlatformClient> {
|
||||
addLocation(clientId, () => import(/* webpackChunkName: "client" */ '@hcengineering/client-resources'))
|
||||
|
||||
const { socketFactory, connectionTimeout } = options
|
||||
|
||||
const clientFactory = await getResource(client.function.GetClient)
|
||||
const connection = await clientFactory(token, endpoint, {
|
||||
socketFactory,
|
||||
connectionTimeout
|
||||
})
|
||||
|
||||
return new PlatformClientImpl(url, workspaceUuid, token, config, connection, account)
|
||||
}
|
||||
|
||||
class PlatformClientImpl implements PlatformClient {
|
||||
private readonly client: TxOperations
|
||||
private readonly markup: MarkupOperations
|
||||
|
||||
constructor (
|
||||
private readonly url: string,
|
||||
private readonly workspace: WorkspaceUuid,
|
||||
private readonly token: string,
|
||||
private readonly config: ServerConfig,
|
||||
private readonly connection: Client,
|
||||
private readonly account: Account
|
||||
) {
|
||||
this.client = new TxOperations(connection, account.primarySocialId)
|
||||
this.markup = createMarkupOperations(url, workspace, token, config)
|
||||
}
|
||||
|
||||
// Client
|
||||
|
||||
getHierarchy (): Hierarchy {
|
||||
return this.client.getHierarchy()
|
||||
}
|
||||
|
||||
getModel (): ModelDb {
|
||||
return this.client.getModel()
|
||||
}
|
||||
|
||||
async getAccount (): Promise<Account> {
|
||||
return this.account
|
||||
}
|
||||
|
||||
async findOne<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
return await this.client.findOne(_class, query, options)
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
return await this.client.findAll(_class, query, options)
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.connection.close()
|
||||
}
|
||||
|
||||
private async processMarkup<T>(_class: Ref<Class<Doc>>, id: Ref<Doc>, data: WithMarkup<T>): Promise<T> {
|
||||
const result: any = {}
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (value instanceof MarkupContent) {
|
||||
result[key] = this.markup.uploadMarkup(_class, id, key, value.content, value.kind)
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result as T
|
||||
}
|
||||
|
||||
// DocOperations
|
||||
|
||||
async createDoc<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
space: Ref<Space>,
|
||||
attributes: WithMarkup<Data<T>>,
|
||||
id?: Ref<T>
|
||||
): Promise<Ref<T>> {
|
||||
id ??= generateId()
|
||||
const data = await this.processMarkup<Data<T>>(_class, id, attributes)
|
||||
return await this.client.createDoc(_class, space, data, id)
|
||||
}
|
||||
|
||||
async updateDoc<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<T>,
|
||||
operations: WithMarkup<DocumentUpdate<T>>,
|
||||
retrieve?: boolean
|
||||
): Promise<TxResult> {
|
||||
const update = await this.processMarkup<DocumentUpdate<T>>(_class, objectId, operations)
|
||||
return await this.client.updateDoc(_class, space, objectId, update, retrieve)
|
||||
}
|
||||
|
||||
async removeDoc<T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, objectId: Ref<T>): Promise<TxResult> {
|
||||
return await this.client.removeDoc(_class, space, objectId)
|
||||
}
|
||||
|
||||
// CollectionOperations
|
||||
|
||||
async addCollection<T extends Doc, P extends AttachedDoc>(
|
||||
_class: Ref<Class<P>>,
|
||||
space: Ref<Space>,
|
||||
attachedTo: Ref<T>,
|
||||
attachedToClass: Ref<Class<T>>,
|
||||
collection: Extract<keyof T, string> | string,
|
||||
attributes: WithMarkup<AttachedData<P>>,
|
||||
id?: Ref<P>
|
||||
): Promise<Ref<P>> {
|
||||
id ??= generateId()
|
||||
const data = await this.processMarkup<AttachedData<P>>(_class, id, attributes)
|
||||
return await this.client.addCollection(_class, space, attachedTo, attachedToClass, collection, data, id)
|
||||
}
|
||||
|
||||
async updateCollection<T extends Doc, P extends AttachedDoc>(
|
||||
_class: Ref<Class<P>>,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<P>,
|
||||
attachedTo: Ref<T>,
|
||||
attachedToClass: Ref<Class<T>>,
|
||||
collection: Extract<keyof T, string> | string,
|
||||
operations: WithMarkup<DocumentUpdate<P>>,
|
||||
retrieve?: boolean
|
||||
): Promise<Ref<T>> {
|
||||
const update = await this.processMarkup<DocumentUpdate<P>>(_class, objectId, operations)
|
||||
return await this.client.updateCollection(
|
||||
_class,
|
||||
space,
|
||||
objectId,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
collection,
|
||||
update,
|
||||
retrieve
|
||||
)
|
||||
}
|
||||
|
||||
async removeCollection<T extends Doc, P extends AttachedDoc>(
|
||||
_class: Ref<Class<P>>,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<P>,
|
||||
attachedTo: Ref<T>,
|
||||
attachedToClass: Ref<Class<T>>,
|
||||
collection: Extract<keyof T, string> | string
|
||||
): Promise<Ref<T>> {
|
||||
return await this.client.removeCollection(_class, space, objectId, attachedTo, attachedToClass, collection)
|
||||
}
|
||||
|
||||
// MixinOperations
|
||||
|
||||
async createMixin<D extends Doc, M extends D>(
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
objectSpace: Ref<Space>,
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: WithMarkup<MixinData<D, M>>
|
||||
): Promise<TxResult> {
|
||||
const data = await this.processMarkup<MixinData<D, M>>(objectClass, objectId, attributes)
|
||||
return await this.client.createMixin(objectId, objectClass, objectSpace, mixin, data)
|
||||
}
|
||||
|
||||
async updateMixin<D extends Doc, M extends D>(
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
objectSpace: Ref<Space>,
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: WithMarkup<MixinUpdate<D, M>>
|
||||
): Promise<TxResult> {
|
||||
const update = await this.processMarkup<MixinUpdate<D, M>>(objectClass, objectId, attributes)
|
||||
return await this.client.updateMixin(objectId, objectClass, objectSpace, mixin, update)
|
||||
}
|
||||
|
||||
// Markup
|
||||
|
||||
async fetchMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
markup: MarkupRef,
|
||||
format: MarkupFormat
|
||||
): Promise<string> {
|
||||
return await this.markup.fetchMarkup(objectClass, objectId, objectAttr, markup, format)
|
||||
}
|
||||
|
||||
async uploadMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
markup: string,
|
||||
format: MarkupFormat
|
||||
): Promise<MarkupRef> {
|
||||
return await this.markup.uploadMarkup(objectClass, objectId, objectAttr, markup, format)
|
||||
}
|
||||
|
||||
// AsyncDisposable
|
||||
|
||||
async [Symbol.asyncDispose] (): Promise<void> {
|
||||
await this.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { concatLink } from '@hcengineering/core'
|
||||
|
||||
export interface ServerConfig {
|
||||
ACCOUNTS_URL: string
|
||||
COLLABORATOR_URL: string
|
||||
FILES_URL: string
|
||||
UPLOAD_URL: string
|
||||
}
|
||||
|
||||
export async function loadServerConfig (url: string): Promise<ServerConfig> {
|
||||
const configUrl = concatLink(url, '/config.json')
|
||||
const res = await fetch(configUrl, { keepalive: true })
|
||||
if (res.ok) {
|
||||
return (await res.json()) as ServerConfig
|
||||
}
|
||||
throw new Error('Failed to fetch config')
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export * from './client'
|
||||
export * from './markup/types'
|
||||
export * from './socket'
|
||||
export * from './types'
|
||||
export * from './rest'
|
||||
export * from './config'
|
||||
export * from './utils'
|
||||
export * from './storage'
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 {
|
||||
type Class,
|
||||
type Doc,
|
||||
type Markup,
|
||||
type Ref,
|
||||
WorkspaceUuid,
|
||||
concatLink,
|
||||
makeCollabId
|
||||
} from '@hcengineering/core'
|
||||
import { type CollaboratorClient, getClient } from '@hcengineering/collaborator-client'
|
||||
import { htmlToJSON, jsonToHTML, jsonToMarkup, markupToJSON } from '@hcengineering/text'
|
||||
import { markdownToMarkup, markupToMarkdown } from '@hcengineering/text-markdown'
|
||||
|
||||
import { type ServerConfig } from '../config'
|
||||
import { type MarkupOperations, type MarkupFormat, type MarkupRef } from './types'
|
||||
|
||||
export function createMarkupOperations (
|
||||
url: string,
|
||||
workspace: WorkspaceUuid,
|
||||
token: string,
|
||||
config: ServerConfig
|
||||
): MarkupOperations {
|
||||
return new MarkupOperationsImpl(url, workspace, token, config)
|
||||
}
|
||||
|
||||
class MarkupOperationsImpl implements MarkupOperations {
|
||||
private readonly collaborator: CollaboratorClient
|
||||
private readonly imageUrl: string
|
||||
private readonly refUrl: string
|
||||
|
||||
constructor (
|
||||
private readonly url: string,
|
||||
private readonly workspace: WorkspaceUuid,
|
||||
private readonly token: string,
|
||||
private readonly config: ServerConfig
|
||||
) {
|
||||
this.refUrl = concatLink(this.url, `/browse?workspace=${workspace}`)
|
||||
this.imageUrl = concatLink(this.url, `/files?workspace=${workspace}&file=`)
|
||||
this.collaborator = getClient(workspace, token, config.COLLABORATOR_URL)
|
||||
}
|
||||
|
||||
async fetchMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
doc: MarkupRef,
|
||||
format: MarkupFormat
|
||||
): Promise<string> {
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
const markup = await this.collaborator.getMarkup(collabId, doc)
|
||||
const json = markupToJSON(markup)
|
||||
|
||||
switch (format) {
|
||||
case 'markup':
|
||||
return markup
|
||||
case 'html':
|
||||
return jsonToHTML(json)
|
||||
case 'markdown':
|
||||
return markupToMarkdown(json, { refUrl: this.refUrl, imageUrl: this.imageUrl })
|
||||
default:
|
||||
throw new Error('Unknown content format')
|
||||
}
|
||||
}
|
||||
|
||||
async uploadMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
value: string,
|
||||
format: MarkupFormat
|
||||
): Promise<MarkupRef> {
|
||||
let markup: Markup = ''
|
||||
|
||||
switch (format) {
|
||||
case 'markup':
|
||||
markup = value
|
||||
break
|
||||
case 'html':
|
||||
markup = jsonToMarkup(htmlToJSON(value))
|
||||
break
|
||||
case 'markdown':
|
||||
markup = jsonToMarkup(markdownToMarkup(value, { refUrl: this.refUrl, imageUrl: this.imageUrl }))
|
||||
break
|
||||
default:
|
||||
throw new Error('Unknown content format')
|
||||
}
|
||||
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
return await this.collaborator.createMarkup(collabId, markup)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export * from './client'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { Class, type Blob, type Doc, type Ref } from '@hcengineering/core'
|
||||
|
||||
/** @public */
|
||||
export type MarkupRef = Ref<Blob>
|
||||
|
||||
/** @public */
|
||||
export type MarkupFormat = 'markup' | 'html' | 'markdown'
|
||||
|
||||
/** @public */
|
||||
export class MarkupContent {
|
||||
constructor (
|
||||
readonly content: string,
|
||||
readonly kind: MarkupFormat
|
||||
) {}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function html (content: string): MarkupContent {
|
||||
return new MarkupContent(content, 'html')
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function markdown (content: string): MarkupContent {
|
||||
return new MarkupContent(content, 'markdown')
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides operations for managing markup (rich-text) content.
|
||||
* @public */
|
||||
export interface MarkupOperations {
|
||||
/**
|
||||
* Retrieves markup content for a specified document object
|
||||
* * @param objectClass - Reference to the class of the document containing the markup
|
||||
* @param objectId - Reference to the document containing the markup
|
||||
* @param objectAttr - The attribute/field name where the markup is stored
|
||||
* @param id - Unique reference identifying the specific markup content
|
||||
* @param format - The format of the markup (e.g., HTML, Markdown, etc.)
|
||||
* @returns Promise containing the markup content as a string
|
||||
*/
|
||||
fetchMarkup: (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
id: MarkupRef,
|
||||
format: MarkupFormat
|
||||
) => Promise<string>
|
||||
|
||||
/**
|
||||
* Saves markup content for a document object
|
||||
* @param objectClass - Reference to the class of the document where markup should be stored
|
||||
* @param objectId - Reference to the document where markup should be stored
|
||||
* @param objectAttr - The attribute/field name where markup should be saved
|
||||
* @param markup - The actual markup content to be uploaded
|
||||
* @param format - The format of the provided markup (e.g., HTML, Markdown, etc.)
|
||||
* @returns Promise containing a reference to the newly saved markup
|
||||
*/
|
||||
uploadMarkup: (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
markup: string,
|
||||
format: MarkupFormat
|
||||
) => Promise<MarkupRef>
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export { createRestClient, connectRest } from './rest'
|
||||
export { createRestTxOperations } from './tx'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,368 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 {
|
||||
type Account,
|
||||
buildModel,
|
||||
type Class,
|
||||
concatLink,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type DomainParams,
|
||||
type DomainRequestOptions,
|
||||
type DomainResult,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
Hierarchy,
|
||||
MeasureMetricsContext,
|
||||
ModelDb,
|
||||
OperationDomain,
|
||||
PersonId,
|
||||
PersonUuid,
|
||||
type Ref,
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
SocialIdType,
|
||||
type Tx,
|
||||
type TxResult,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { PlatformError, type Status, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import { AuthOptions } from '../types'
|
||||
import { getWorkspaceToken } from '../utils'
|
||||
import type { RestClient } from './types'
|
||||
import { extractJson, withRetry } from './utils'
|
||||
|
||||
export function createRestClient (endpoint: string, workspaceId: string, token: string): RestClient {
|
||||
return new RestClientImpl(endpoint, workspaceId, token)
|
||||
}
|
||||
|
||||
export async function connectRest (url: string, options: AuthOptions): Promise<RestClient> {
|
||||
const { endpoint, token, workspaceId } = await getWorkspaceToken(url, options)
|
||||
return createRestClient(endpoint, workspaceId, token)
|
||||
}
|
||||
|
||||
const rateLimitError = 'rate-limit'
|
||||
|
||||
function isRLE (err: any): boolean {
|
||||
return err.message === rateLimitError
|
||||
}
|
||||
|
||||
export class RestClientImpl implements RestClient {
|
||||
endpoint: string
|
||||
|
||||
slowDownTimer = 0
|
||||
currentRateLimit: { remaining: number, limit: number } = { remaining: 1000, limit: 1000 }
|
||||
|
||||
remaining: number = 1000
|
||||
limit: number = 1000
|
||||
constructor (
|
||||
endpoint: string,
|
||||
readonly workspace: string,
|
||||
readonly token: string
|
||||
) {
|
||||
this.endpoint = endpoint.replace('ws', 'http')
|
||||
}
|
||||
|
||||
jsonHeaders (): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + this.token,
|
||||
'accept-encoding': 'snappy, gzip'
|
||||
}
|
||||
}
|
||||
|
||||
requestInit (): RequestInit {
|
||||
return {
|
||||
method: 'GET',
|
||||
keepalive: true,
|
||||
headers: this.jsonHeaders()
|
||||
}
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const params = new URLSearchParams()
|
||||
params.append('class', _class)
|
||||
if (query !== undefined && Object.keys(query).length > 0) {
|
||||
params.append('query', JSON.stringify(query))
|
||||
}
|
||||
if (options !== undefined && Object.keys(options).length > 0) {
|
||||
params.append('options', JSON.stringify(options))
|
||||
}
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/find-all/${this.workspace}?${params.toString()}`)
|
||||
const result = await withRetry<FindResult<T> & { error?: Status }>(async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
return await extractJson<FindResult<T>>(response)
|
||||
}, isRLE)
|
||||
|
||||
if (result.error !== undefined) {
|
||||
throw new PlatformError(result.error)
|
||||
}
|
||||
|
||||
if (result.lookupMap !== undefined) {
|
||||
// We need to extract lookup map to document lookups
|
||||
for (const d of result) {
|
||||
if (d.$lookup !== undefined) {
|
||||
for (const [k, v] of Object.entries(d.$lookup)) {
|
||||
if (!Array.isArray(v)) {
|
||||
;(d as any).$lookup[k] = result.lookupMap[v]
|
||||
} else {
|
||||
;(d as any).$lookup[k] = v.map((it) => result.lookupMap?.[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete result.lookupMap
|
||||
}
|
||||
|
||||
// We need to revert deleted query simple values.
|
||||
// We need to get rid of simple query parameters matched in documents
|
||||
for (const doc of result) {
|
||||
if (doc._class == null) {
|
||||
doc._class = _class
|
||||
}
|
||||
for (const [k, v] of Object.entries(query)) {
|
||||
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
||||
if ((doc as any)[k] == null) {
|
||||
;(doc as any)[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async checkRate (): Promise<void> {
|
||||
if (this.currentRateLimit.remaining < this.currentRateLimit.limit / 3) {
|
||||
if (this.slowDownTimer < 50) {
|
||||
this.slowDownTimer += 50
|
||||
}
|
||||
this.slowDownTimer++
|
||||
} else if (this.slowDownTimer > 0) {
|
||||
this.slowDownTimer--
|
||||
}
|
||||
if (this.slowDownTimer > 0) {
|
||||
// We need to wait a bit to avoid ban.
|
||||
await new Promise((resolve) => setTimeout(resolve, this.slowDownTimer))
|
||||
}
|
||||
}
|
||||
|
||||
private updateRateLimit (response: Response): void {
|
||||
const rateLimitLimit: number = parseInt(response.headers.get('X-RateLimit-Limit') ?? '100')
|
||||
const remaining: number = parseInt(response.headers.get('X-RateLimit-Remaining') ?? '100')
|
||||
this.currentRateLimit = { remaining, limit: rateLimitLimit }
|
||||
}
|
||||
|
||||
private async checkRateLimits (response: Response): Promise<void> {
|
||||
if (response.status === 429) {
|
||||
// Extract rate limit information from headers
|
||||
const retryAfter = response.headers.get('Retry-After')
|
||||
const retryAfterMS = response.headers.get('Retry-After-ms')
|
||||
const rateLimitReset = response.headers.get('X-RateLimit-Reset')
|
||||
|
||||
this.updateRateLimit(response)
|
||||
const waitTime =
|
||||
(retryAfterMS != null ? parseInt(retryAfterMS) : undefined) ??
|
||||
(retryAfter != null
|
||||
? parseInt(retryAfter) * 1000
|
||||
: rateLimitReset != null
|
||||
? new Date(parseInt(rateLimitReset)).getTime() - Date.now()
|
||||
: 1000) // Default to 1 seconds if no headers are provided
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime))
|
||||
throw new Error(rateLimitError)
|
||||
}
|
||||
}
|
||||
|
||||
async getAccount (): Promise<Account> {
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/account/${this.workspace}`)
|
||||
await this.checkRate()
|
||||
const result = await withRetry<Account & { error?: Status }>(async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
return await extractJson<Account>(response)
|
||||
})
|
||||
if (result.error !== undefined) {
|
||||
throw new PlatformError(result.error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async getModel (full: boolean = false): Promise<{ hierarchy: Hierarchy, model: ModelDb }> {
|
||||
const requestUrl = new URL(concatLink(this.endpoint, `/api/v1/load-model/${this.workspace}`))
|
||||
if (full) {
|
||||
requestUrl.searchParams.append('full', 'true')
|
||||
}
|
||||
await this.checkRate()
|
||||
const result = await withRetry<{ hierarchy: Hierarchy, model: ModelDb, error?: Status }>(async () => {
|
||||
const response = await fetch(requestUrl, this.requestInit())
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
|
||||
const modelResponse: Tx[] = await extractJson<Tx[]>(response)
|
||||
|
||||
const hierarchy = new Hierarchy()
|
||||
const model = new ModelDb(hierarchy)
|
||||
|
||||
const ctx = new MeasureMetricsContext('loadModel', {})
|
||||
buildModel(ctx, modelResponse, undefined, hierarchy, model)
|
||||
|
||||
return { hierarchy, model }
|
||||
}, isRLE)
|
||||
if (result.error !== undefined) {
|
||||
throw new PlatformError(result.error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async findOne<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
return (await this.findAll(_class, query, { ...options, limit: 1 })).shift()
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/tx/${this.workspace}`)
|
||||
await this.checkRate()
|
||||
const result = await withRetry<TxResult & { error?: Status }>(async () => {
|
||||
const response = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
headers: this.jsonHeaders(),
|
||||
keepalive: true,
|
||||
body: JSON.stringify(tx)
|
||||
})
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
return await extractJson<TxResult>(response)
|
||||
}, isRLE)
|
||||
if (result.error !== undefined) {
|
||||
throw new PlatformError(result.error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||
const result = await withRetry<SearchResult & { error?: Status }>(async () => {
|
||||
const params = new URLSearchParams()
|
||||
params.append('query', query.query)
|
||||
if (query.classes != null && Object.keys(query.classes).length > 0) {
|
||||
params.append('classes', JSON.stringify(query.classes))
|
||||
}
|
||||
if (query.spaces != null && Object.keys(query.spaces).length > 0) {
|
||||
params.append('spaces', JSON.stringify(query.spaces))
|
||||
}
|
||||
if (options.limit != null) {
|
||||
params.append('limit', `${options.limit}`)
|
||||
}
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/search-fulltext/${this.workspace}?${params.toString()}`)
|
||||
const response = await fetch(requestUrl, {
|
||||
method: 'GET',
|
||||
headers: this.jsonHeaders(),
|
||||
keepalive: true
|
||||
})
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
return await extractJson<TxResult>(response)
|
||||
})
|
||||
if (result.error !== undefined) {
|
||||
throw new PlatformError(result.error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async domainRequest<T>(
|
||||
domain: OperationDomain,
|
||||
params: DomainParams,
|
||||
options?: DomainRequestOptions
|
||||
): Promise<DomainResult<T>> {
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/request/${domain}/${this.workspace}`)
|
||||
|
||||
await this.checkRate()
|
||||
return await withRetry(async () => {
|
||||
const response = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
headers: this.jsonHeaders(),
|
||||
keepalive: true,
|
||||
body: JSON.stringify(params)
|
||||
})
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
const value = await extractJson<T>(response)
|
||||
return { domain, value }
|
||||
}, isRLE)
|
||||
}
|
||||
|
||||
async ensurePerson (
|
||||
socialType: SocialIdType,
|
||||
socialValue: string,
|
||||
firstName: string,
|
||||
lastName: string
|
||||
): Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }> {
|
||||
const requestUrl = concatLink(this.endpoint, `/api/v1/ensure-person/${this.workspace}`)
|
||||
await this.checkRate()
|
||||
const result = await withRetry(async () => {
|
||||
const response = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
headers: this.jsonHeaders(),
|
||||
keepalive: true,
|
||||
body: JSON.stringify({
|
||||
socialType,
|
||||
socialValue,
|
||||
firstName,
|
||||
lastName
|
||||
})
|
||||
})
|
||||
if (!response.ok) {
|
||||
await this.checkRateLimits(response)
|
||||
throw new PlatformError(unknownError(response.statusText))
|
||||
}
|
||||
this.updateRateLimit(response)
|
||||
return await extractJson<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }>(response)
|
||||
}, isRLE)
|
||||
if (result.error !== undefined) {
|
||||
throw new PlatformError(result.error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 {
|
||||
type Account,
|
||||
type Class,
|
||||
type Client,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type DomainParams,
|
||||
type DomainRequestOptions,
|
||||
type DomainResult,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
Hierarchy,
|
||||
ModelDb,
|
||||
type OperationDomain,
|
||||
type Ref,
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
toFindResult,
|
||||
type Tx,
|
||||
TxOperations,
|
||||
type TxResult,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { RestClientImpl } from './rest'
|
||||
|
||||
export async function createRestTxOperations (
|
||||
endpoint: string,
|
||||
workspaceId: string,
|
||||
token: string,
|
||||
fullModel: boolean = false
|
||||
): Promise<TxOperations> {
|
||||
const restClient = new RestClientImpl(endpoint, workspaceId, token)
|
||||
|
||||
const account = await restClient.getAccount()
|
||||
const { hierarchy, model } = await restClient.getModel(fullModel)
|
||||
|
||||
return new TxOperations(new RestTxClient(restClient, hierarchy, model, account), account.socialIds[0])
|
||||
}
|
||||
|
||||
class RestTxClient implements Client {
|
||||
constructor (
|
||||
readonly client: RestClientImpl,
|
||||
readonly hierarchy: Hierarchy,
|
||||
readonly model: ModelDb,
|
||||
readonly account: Account
|
||||
) {}
|
||||
|
||||
close (): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
async findAll<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const data = await this.client.findAll(_class, query, options)
|
||||
const result = data.map((v) => {
|
||||
return this.hierarchy.updateLookupMixin(_class, v, options)
|
||||
})
|
||||
return toFindResult(result, data.total)
|
||||
}
|
||||
|
||||
async domainRequest<T>(
|
||||
domain: OperationDomain,
|
||||
params: DomainParams,
|
||||
options?: DomainRequestOptions
|
||||
): Promise<DomainResult<T>> {
|
||||
return await this.client.domainRequest(domain, params, options)
|
||||
}
|
||||
|
||||
async findOne<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<WithLookup<T> | undefined> {
|
||||
const v = await this.client.findOne(_class, query, options)
|
||||
if (v === undefined) {
|
||||
return
|
||||
}
|
||||
return this.hierarchy.updateLookupMixin(_class, v, options)
|
||||
}
|
||||
|
||||
getHierarchy: () => Hierarchy = () => this.hierarchy
|
||||
getModel: () => ModelDb = () => this.model
|
||||
|
||||
async getAccount (): Promise<Account> {
|
||||
return this.account
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<TxResult> {
|
||||
return await this.client.tx(tx)
|
||||
}
|
||||
|
||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||
return await this.client.searchFulltext(query, options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 {
|
||||
type Account,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type DomainParams,
|
||||
type DomainRequestOptions,
|
||||
type DomainResult,
|
||||
type FindOptions,
|
||||
type FulltextStorage,
|
||||
type Hierarchy,
|
||||
type ModelDb,
|
||||
type OperationDomain,
|
||||
type PersonId,
|
||||
type PersonUuid,
|
||||
type Ref,
|
||||
type SocialIdType,
|
||||
type Storage,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
|
||||
export interface RestClient extends Storage, FulltextStorage {
|
||||
getAccount: () => Promise<Account>
|
||||
|
||||
findOne: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<WithLookup<T> | undefined>
|
||||
|
||||
getModel: () => Promise<{ hierarchy: Hierarchy, model: ModelDb }>
|
||||
|
||||
domainRequest: <T>(
|
||||
domain: OperationDomain,
|
||||
params: DomainParams,
|
||||
options?: DomainRequestOptions
|
||||
) => Promise<DomainResult<T>>
|
||||
|
||||
ensurePerson: (
|
||||
socialType: SocialIdType,
|
||||
socialValue: string,
|
||||
firstName: string,
|
||||
lastName: string
|
||||
) => Promise<{ uuid: PersonUuid, socialId: PersonId, localPerson: string }>
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { uncompress } from 'snappyjs'
|
||||
|
||||
export async function withRetry<T> (fn: () => Promise<T>, ignoreAttemptCheck?: (err: any) => boolean): Promise<T> {
|
||||
const maxRetries = 3
|
||||
let lastError: any
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err: any) {
|
||||
if (ignoreAttemptCheck !== undefined && ignoreAttemptCheck(err)) {
|
||||
// Do not decrement attempt
|
||||
attempt--
|
||||
} else {
|
||||
lastError = err
|
||||
}
|
||||
if (attempt === maxRetries - 1) {
|
||||
throw lastError
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100))
|
||||
}
|
||||
}
|
||||
throw lastError
|
||||
}
|
||||
|
||||
function rpcJSONReceiver (key: string, value: any): any {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (value.dataType === 'TotalArray') {
|
||||
return Object.assign(value.value, { total: value.total, lookupMap: value.lookupMap })
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export async function extractJson<T> (response: Response): Promise<any> {
|
||||
const encoding = response.headers.get('content-encoding')
|
||||
if (encoding === 'snappy') {
|
||||
const buffer = await response.arrayBuffer()
|
||||
const decompressed = uncompress(buffer)
|
||||
const decoder = new TextDecoder()
|
||||
const jsonString = decoder.decode(decompressed)
|
||||
return JSON.parse(jsonString, rpcJSONReceiver) as T
|
||||
}
|
||||
const jsonString = await response.text()
|
||||
return JSON.parse(jsonString, rpcJSONReceiver) as T
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { type ClientSocket, type ClientSocketFactory } from '@hcengineering/client'
|
||||
|
||||
/** @public */
|
||||
export const BrowserWebSocketFactory: ClientSocketFactory = (url: string): ClientSocket => {
|
||||
const ws = new WebSocket(url)
|
||||
return ws as ClientSocket
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export * from './browser'
|
||||
export * from './node'
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { type ClientSocket, type ClientSocketFactory } from '@hcengineering/client'
|
||||
|
||||
/** @public */
|
||||
export const NodeWebSocketFactory: ClientSocketFactory = (url: string): ClientSocket => {
|
||||
// We need to override default factory with 'ws' one.
|
||||
// eslint-disable-next-line
|
||||
let WebSocket
|
||||
try {
|
||||
WebSocket = require('ws')
|
||||
} catch (error) {
|
||||
throw new Error('The "ws" package is required for NodeWebSocketFactory. ')
|
||||
}
|
||||
type WebSocketData = Parameters<typeof ws.on>[1]
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
|
||||
const client: ClientSocket = {
|
||||
get readyState (): number {
|
||||
return ws.readyState
|
||||
},
|
||||
|
||||
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView): void => {
|
||||
if (data instanceof Blob) {
|
||||
void data.arrayBuffer().then((buffer) => {
|
||||
ws.send(buffer)
|
||||
})
|
||||
} else {
|
||||
ws.send(data)
|
||||
}
|
||||
},
|
||||
|
||||
close: (code?: number): void => {
|
||||
ws.close(code)
|
||||
}
|
||||
}
|
||||
|
||||
ws.on('message', (data: WebSocketData) => {
|
||||
if (client.onmessage != null) {
|
||||
const event = {
|
||||
data,
|
||||
type: 'message',
|
||||
target: this
|
||||
} as unknown as MessageEvent
|
||||
|
||||
client.onmessage(event)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('close', (code: number, reason: string) => {
|
||||
if (client.onclose != null) {
|
||||
const closeEvent = {
|
||||
code,
|
||||
reason,
|
||||
wasClean: code === 1000,
|
||||
type: 'close',
|
||||
target: this
|
||||
} as unknown as CloseEvent
|
||||
|
||||
client.onclose(closeEvent)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('open', () => {
|
||||
if (client.onopen != null) {
|
||||
const event = {
|
||||
type: 'open',
|
||||
target: this
|
||||
} as unknown as Event
|
||||
|
||||
client.onopen(event)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('error', (error: Error) => {
|
||||
if (client.onerror != null) {
|
||||
const event = {
|
||||
type: 'error',
|
||||
target: this,
|
||||
error
|
||||
} as unknown as Event
|
||||
|
||||
client.onerror(event)
|
||||
}
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 core, { concatLink, WorkspaceUuid, Blob, Ref } from '@hcengineering/core'
|
||||
import { Readable } from 'stream'
|
||||
import { StorageClient } from './types'
|
||||
import { loadServerConfig, ServerConfig } from '../config'
|
||||
import { NetworkError, NotFoundError, StorageError } from './error'
|
||||
import { AuthOptions } from '../types'
|
||||
import { getWorkspaceToken } from '../utils'
|
||||
|
||||
interface ObjectMetadata {
|
||||
name: string
|
||||
etag: string
|
||||
size: number
|
||||
contentType: string
|
||||
lastModified: number
|
||||
cacheControl?: string
|
||||
}
|
||||
|
||||
interface BlobUploadSuccess {
|
||||
key: string
|
||||
id: string
|
||||
metadata: ObjectMetadata
|
||||
}
|
||||
|
||||
interface BlobUploadError {
|
||||
key: string
|
||||
error: string
|
||||
}
|
||||
|
||||
type BlobUploadResult = BlobUploadSuccess | BlobUploadError
|
||||
|
||||
export class StorageClientImpl implements StorageClient {
|
||||
private readonly headers: Record<string, string>
|
||||
constructor (
|
||||
readonly filesUrl: string,
|
||||
readonly uploadUrl: string,
|
||||
token: string,
|
||||
readonly workspace: WorkspaceUuid
|
||||
) {
|
||||
this.headers = {
|
||||
Authorization: 'Bearer ' + token
|
||||
}
|
||||
}
|
||||
|
||||
getObjectUrl (objectName: string): string {
|
||||
return this.filesUrl.replace(':filename', objectName).replace(':blobId', objectName)
|
||||
}
|
||||
|
||||
async stat (objectName: string): Promise<Blob | undefined> {
|
||||
const url = this.getObjectUrl(objectName)
|
||||
let response
|
||||
try {
|
||||
response = await wrappedFetch(url, { method: 'HEAD', headers: { ...this.headers } })
|
||||
} catch (error: any) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
const headers = response.headers
|
||||
const lastModified = Date.parse(headers.get('Last-Modified') ?? '')
|
||||
const size = parseInt(headers.get('Content-Length') ?? '0', 10)
|
||||
return {
|
||||
provider: '',
|
||||
_class: core.class.Blob,
|
||||
_id: objectName as Ref<Blob>,
|
||||
contentType: headers.get('Content-Type') ?? '',
|
||||
size: isNaN(size) ? 0 : (size ?? 0),
|
||||
etag: headers.get('ETag') ?? '',
|
||||
space: core.space.Configuration,
|
||||
modifiedBy: core.account.System,
|
||||
modifiedOn: isNaN(lastModified) ? 0 : lastModified,
|
||||
version: null
|
||||
}
|
||||
}
|
||||
|
||||
async get (objectName: string): Promise<Readable> {
|
||||
const url = this.getObjectUrl(objectName)
|
||||
|
||||
const response = await wrappedFetch(url, { headers: { ...this.headers } })
|
||||
|
||||
if (response.body == null) {
|
||||
throw new StorageError('Missing response body')
|
||||
}
|
||||
return Readable.from(response.body)
|
||||
}
|
||||
|
||||
async put (objectName: string, stream: Readable | Buffer | string, contentType: string, size?: number): Promise<Blob> {
|
||||
const buffer = await toBuffer(stream)
|
||||
const file = new File([new Uint8Array(buffer)], objectName, { type: contentType })
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
let response
|
||||
try {
|
||||
response = await fetch(this.uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { ...this.headers }
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new NetworkError(`Network error ${error}`)
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new StorageError(await response.text())
|
||||
}
|
||||
const result = (await response.json()) as BlobUploadResult[]
|
||||
if (Object.hasOwn(result[0], 'id')) {
|
||||
const fileResult = result[0] as BlobUploadSuccess
|
||||
return {
|
||||
_class: core.class.Blob,
|
||||
_id: fileResult.id as Ref<Blob>,
|
||||
space: core.space.Configuration,
|
||||
modifiedOn: fileResult.metadata.lastModified,
|
||||
modifiedBy: core.account.System,
|
||||
provider: '',
|
||||
contentType: fileResult.metadata.contentType,
|
||||
etag: fileResult.metadata.etag,
|
||||
version: null,
|
||||
size: fileResult.metadata.size
|
||||
}
|
||||
} else {
|
||||
const error = (result[0] as BlobUploadError) ?? 'Unknown error'
|
||||
throw new StorageError(`Storage error ${error.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async partial (objectName: string, offset: number, length?: number): Promise<Readable> {
|
||||
const url = this.getObjectUrl(objectName)
|
||||
|
||||
const response = await wrappedFetch(url, {
|
||||
headers: {
|
||||
...this.headers,
|
||||
Range: length !== undefined ? `bytes=${offset}-${offset + length - 1}` : `bytes=${offset}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.body == null) {
|
||||
throw new StorageError('Missing response body')
|
||||
}
|
||||
return Readable.from(response.body)
|
||||
}
|
||||
|
||||
async remove (objectName: string): Promise<void> {
|
||||
const url = this.getObjectUrl(objectName)
|
||||
await wrappedFetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: { ...this.headers }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function toBuffer (data: Buffer | string | Readable): Promise<Buffer> {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
return data
|
||||
} else if (typeof data === 'string') {
|
||||
return Buffer.from(data)
|
||||
} else if (data instanceof Readable) {
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of data) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
return Buffer.concat(chunks as any)
|
||||
} else {
|
||||
throw new TypeError('Unsupported data type')
|
||||
}
|
||||
}
|
||||
|
||||
async function wrappedFetch (url: string | URL, init?: RequestInit): Promise<Response> {
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(url, init)
|
||||
} catch (error: any) {
|
||||
throw new NetworkError(`Network error ${error}`)
|
||||
}
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
if (response.status === 404) {
|
||||
throw new NotFoundError(text)
|
||||
} else {
|
||||
throw new StorageError(text)
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export function createStorageClient (
|
||||
filesUrl: string,
|
||||
uploadUrl: string,
|
||||
token: string,
|
||||
workspace: WorkspaceUuid
|
||||
): StorageClient {
|
||||
return new StorageClientImpl(filesUrl, uploadUrl, token, workspace)
|
||||
}
|
||||
|
||||
export async function connectStorage (url: string, options: AuthOptions, config?: ServerConfig): Promise<StorageClient> {
|
||||
config ??= await loadServerConfig(url)
|
||||
const token = await getWorkspaceToken(url, options, config)
|
||||
const filesUrl = (config.FILES_URL.startsWith('/') ? concatLink(url, config.FILES_URL) : config.FILES_URL).replace(
|
||||
':workspace',
|
||||
token.workspaceId
|
||||
)
|
||||
const uploadUrl = (
|
||||
config.UPLOAD_URL.startsWith('/') ? concatLink(url, config.UPLOAD_URL) : config.UPLOAD_URL
|
||||
).replace(':workspace', token.workspaceId)
|
||||
return new StorageClientImpl(filesUrl, uploadUrl, token.token, token.workspaceId)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export class NetworkError extends Error {
|
||||
constructor (message: string) {
|
||||
super(message)
|
||||
this.name = 'NetworkError'
|
||||
}
|
||||
}
|
||||
|
||||
export class StorageError extends Error {
|
||||
constructor (message: string) {
|
||||
super(message)
|
||||
this.name = 'StorageError'
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends StorageError {
|
||||
constructor (message = 'Not Found') {
|
||||
super(message)
|
||||
this.name = 'NotFoundError'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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.
|
||||
//
|
||||
|
||||
export { createStorageClient, connectStorage } from './client'
|
||||
export * from './error'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { Blob } from '@hcengineering/core'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
export interface StorageClient {
|
||||
stat: (objectName: string) => Promise<Blob | undefined>
|
||||
get: (objectName: string) => Promise<Readable>
|
||||
put: (objectName: string, stream: Readable | Buffer | string, contentType: string, size?: number) => Promise<Blob>
|
||||
partial: (objectName: string, offset: number, length?: number) => Promise<Readable>
|
||||
remove: (objectName: string) => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { type ClientSocketFactory } from '@hcengineering/client'
|
||||
import {
|
||||
CollaborativeDoc,
|
||||
type Account,
|
||||
type AttachedData,
|
||||
type AttachedDoc,
|
||||
type Class,
|
||||
type Data,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type DocumentUpdate,
|
||||
type FindOptions,
|
||||
type FindResult,
|
||||
type Hierarchy,
|
||||
type Mixin,
|
||||
type MixinData,
|
||||
type MixinUpdate,
|
||||
type ModelDb,
|
||||
type Ref,
|
||||
type Space,
|
||||
type TxResult,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { type MarkupContent, type MarkupOperations } from './markup'
|
||||
|
||||
type WithPropertyType<T, X, Y> = {
|
||||
[P in keyof T]: T[P] extends X ? Y : T[P]
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type WithMarkup<T> = WithPropertyType<
|
||||
WithPropertyType<T, CollaborativeDoc | undefined, MarkupContent | undefined>,
|
||||
CollaborativeDoc,
|
||||
MarkupContent
|
||||
>
|
||||
|
||||
/**
|
||||
* Platform API client
|
||||
* @public
|
||||
* */
|
||||
export type PlatformClient = {
|
||||
getHierarchy: () => Hierarchy
|
||||
|
||||
getModel: () => ModelDb
|
||||
|
||||
getAccount: () => Promise<Account>
|
||||
|
||||
close: () => Promise<void>
|
||||
} & FindOperations &
|
||||
DocOperations &
|
||||
CollectionOperations &
|
||||
MixinOperations &
|
||||
MarkupOperations &
|
||||
AsyncDisposable
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface FindOperations {
|
||||
findAll: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> | undefined
|
||||
) => Promise<FindResult<T>>
|
||||
|
||||
findOne: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> | undefined
|
||||
) => Promise<WithLookup<T> | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DocOperations {
|
||||
createDoc: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
space: Ref<Space>,
|
||||
attributes: WithMarkup<Data<T>>,
|
||||
id?: Ref<T>
|
||||
) => Promise<Ref<T>>
|
||||
|
||||
updateDoc: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<T>,
|
||||
operations: WithMarkup<DocumentUpdate<T>>,
|
||||
retrieve?: boolean
|
||||
) => Promise<TxResult>
|
||||
|
||||
removeDoc: <T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, objectId: Ref<T>) => Promise<TxResult>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface CollectionOperations {
|
||||
addCollection: <T extends Doc, P extends AttachedDoc>(
|
||||
_class: Ref<Class<P>>,
|
||||
space: Ref<Space>,
|
||||
attachedTo: Ref<T>,
|
||||
attachedToClass: Ref<Class<T>>,
|
||||
collection: Extract<keyof T, string> | string,
|
||||
attributes: WithMarkup<AttachedData<P>>,
|
||||
id?: Ref<P>
|
||||
) => Promise<Ref<P>>
|
||||
|
||||
updateCollection: <T extends Doc, P extends AttachedDoc>(
|
||||
_class: Ref<Class<P>>,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<P>,
|
||||
attachedTo: Ref<T>,
|
||||
attachedToClass: Ref<Class<T>>,
|
||||
collection: Extract<keyof T, string> | string,
|
||||
operations: WithMarkup<DocumentUpdate<P>>,
|
||||
retrieve?: boolean
|
||||
) => Promise<Ref<T>>
|
||||
|
||||
removeCollection: <T extends Doc, P extends AttachedDoc>(
|
||||
_class: Ref<Class<P>>,
|
||||
space: Ref<Space>,
|
||||
objectId: Ref<P>,
|
||||
attachedTo: Ref<T>,
|
||||
attachedToClass: Ref<Class<T>>,
|
||||
collection: Extract<keyof T, string> | string
|
||||
) => Promise<Ref<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface MixinOperations {
|
||||
createMixin: <D extends Doc, M extends D>(
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
objectSpace: Ref<Space>,
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: WithMarkup<MixinData<D, M>>
|
||||
) => Promise<TxResult>
|
||||
|
||||
updateMixin: <D extends Doc, M extends D>(
|
||||
objectId: Ref<D>,
|
||||
objectClass: Ref<Class<D>>,
|
||||
objectSpace: Ref<Space>,
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: WithMarkup<MixinUpdate<D, M>>
|
||||
) => Promise<TxResult>
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for password-based authentication
|
||||
* @public
|
||||
*/
|
||||
export interface PasswordAuthOptions {
|
||||
/** User's email address */
|
||||
email: string
|
||||
|
||||
/** User's password */
|
||||
password: string
|
||||
|
||||
/** Workspace URL name */
|
||||
workspace: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for token-based authentication
|
||||
* @public
|
||||
*/
|
||||
export interface TokenAuthOptions {
|
||||
/** Authentication token */
|
||||
token: string
|
||||
|
||||
/** Workspace URL name */
|
||||
workspace: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type representing all authentication options
|
||||
* Can be either password-based or token-based authentication
|
||||
* @public
|
||||
*/
|
||||
export type AuthOptions = PasswordAuthOptions | TokenAuthOptions
|
||||
|
||||
/**
|
||||
* Configuration options for socket connection
|
||||
* @public
|
||||
*/
|
||||
export interface ConnectSocketOptions {
|
||||
/**
|
||||
* Optional factory for creating custom WebSocket implementations
|
||||
* Particularly useful in Node.js environments where you might need
|
||||
* to provide a specific WebSocket client implementation
|
||||
* If not provided, a default WebSocket implementation will be used
|
||||
*/
|
||||
socketFactory?: ClientSocketFactory
|
||||
|
||||
/**
|
||||
* Optional timeout duration for the connection attempt in milliseconds
|
||||
* Specifies how long to wait for a connection before timing out
|
||||
*/
|
||||
connectionTimeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* API connect options
|
||||
* @public
|
||||
*/
|
||||
export type ConnectOptions = ConnectSocketOptions & AuthOptions
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public 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 https://www.eclipse.org/legal/epl-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 { type WorkspaceLoginInfo, getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
import { WorkspaceUuid } from '@hcengineering/core'
|
||||
import { AuthOptions } from './types'
|
||||
import { loadServerConfig, ServerConfig } from './config'
|
||||
|
||||
export interface WorkspaceToken {
|
||||
endpoint: string
|
||||
token: string
|
||||
workspaceId: WorkspaceUuid
|
||||
info: WorkspaceLoginInfo
|
||||
}
|
||||
|
||||
export async function getWorkspaceToken (
|
||||
url: string,
|
||||
options: AuthOptions,
|
||||
config?: ServerConfig
|
||||
): Promise<WorkspaceToken> {
|
||||
config ??= await loadServerConfig(url)
|
||||
|
||||
let token: string | undefined
|
||||
|
||||
if ('token' in options) {
|
||||
token = options.token
|
||||
} else {
|
||||
const { email, password } = options
|
||||
const loginInfo = await getAccountClient(config.ACCOUNTS_URL).login(email, password)
|
||||
token = loginInfo.token
|
||||
}
|
||||
|
||||
if (token === undefined) {
|
||||
throw new Error('Login failed')
|
||||
}
|
||||
|
||||
const ws = await getAccountClient(config.ACCOUNTS_URL, token).selectWorkspace(options.workspace)
|
||||
if (ws === undefined) {
|
||||
throw new Error('Workspace not found')
|
||||
}
|
||||
|
||||
return { endpoint: ws.endpoint, token: ws.token, workspaceId: ws.workspace, info: ws }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo",
|
||||
"types": ["node", "jest"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user