mirror of
https://github.com/strapi/strapi.git
synced 2025-12-12 15:32:42 +00:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
58c6e40d2b
5
.gitattributes
vendored
5
.gitattributes
vendored
@ -104,3 +104,8 @@ AUTHORS text
|
||||
*.woff binary
|
||||
*.pyc binary
|
||||
*.pdf binary
|
||||
|
||||
# yarn
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
File diff suppressed because one or more lines are too long
180
.github/actions/check-pr-status/yarn.lock
vendored
180
.github/actions/check-pr-status/yarn.lock
vendored
@ -1,180 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@actions/core@1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.6.0.tgz#0568e47039bfb6a9170393a73f3b7eb3b22462cb"
|
||||
integrity sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==
|
||||
dependencies:
|
||||
"@actions/http-client" "^1.0.11"
|
||||
|
||||
"@actions/github@5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.0.0.tgz#1754127976c50bd88b2e905f10d204d76d1472f8"
|
||||
integrity sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==
|
||||
dependencies:
|
||||
"@actions/http-client" "^1.0.11"
|
||||
"@octokit/core" "^3.4.0"
|
||||
"@octokit/plugin-paginate-rest" "^2.13.3"
|
||||
"@octokit/plugin-rest-endpoint-methods" "^5.1.1"
|
||||
|
||||
"@actions/http-client@^1.0.11":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-1.0.11.tgz#c58b12e9aa8b159ee39e7dd6cbd0e91d905633c0"
|
||||
integrity sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==
|
||||
dependencies:
|
||||
tunnel "0.0.6"
|
||||
|
||||
"@octokit/auth-token@^2.4.4":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36"
|
||||
integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.0.3"
|
||||
|
||||
"@octokit/core@^3.4.0":
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
|
||||
integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
|
||||
dependencies:
|
||||
"@octokit/auth-token" "^2.4.4"
|
||||
"@octokit/graphql" "^4.5.8"
|
||||
"@octokit/request" "^5.6.0"
|
||||
"@octokit/request-error" "^2.0.5"
|
||||
"@octokit/types" "^6.0.3"
|
||||
before-after-hook "^2.2.0"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/endpoint@^6.0.1":
|
||||
version "6.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658"
|
||||
integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.0.3"
|
||||
is-plain-object "^5.0.0"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/graphql@^4.5.8":
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3"
|
||||
integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==
|
||||
dependencies:
|
||||
"@octokit/request" "^5.6.0"
|
||||
"@octokit/types" "^6.0.3"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/openapi-types@^11.2.0":
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6"
|
||||
integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==
|
||||
|
||||
"@octokit/plugin-paginate-rest@^2.13.3":
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7"
|
||||
integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.34.0"
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@^5.1.1":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba"
|
||||
integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.34.0"
|
||||
deprecation "^2.3.1"
|
||||
|
||||
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
|
||||
integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.0.3"
|
||||
deprecation "^2.0.0"
|
||||
once "^1.4.0"
|
||||
|
||||
"@octokit/request@^5.6.0":
|
||||
version "5.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0"
|
||||
integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^6.0.1"
|
||||
"@octokit/request-error" "^2.1.0"
|
||||
"@octokit/types" "^6.16.1"
|
||||
is-plain-object "^5.0.0"
|
||||
node-fetch "^2.6.7"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0":
|
||||
version "6.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218"
|
||||
integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==
|
||||
dependencies:
|
||||
"@octokit/openapi-types" "^11.2.0"
|
||||
|
||||
"@vercel/ncc@0.33.1":
|
||||
version "0.33.1"
|
||||
resolved "https://registry.yarnpkg.com/@vercel/ncc/-/ncc-0.33.1.tgz#b240080a3c1ded9446a30955a06a79851bb38f71"
|
||||
integrity sha512-Mlsps/P0PLZwsCFtSol23FGqT3FhBGb4B1AuGQ52JTAtXhak+b0Fh/4T55r0/SVQPeRiX9pNItOEHwakGPmZYA==
|
||||
|
||||
before-after-hook@^2.2.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
|
||||
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
|
||||
|
||||
deprecation@^2.0.0, deprecation@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
|
||||
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
|
||||
|
||||
is-plain-object@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
|
||||
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
|
||||
|
||||
node-fetch@^2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
tunnel@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
|
||||
|
||||
universal-user-agent@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
|
||||
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
6
.github/actions/run-api-tests/script.sh
vendored
6
.github/actions/run-api-tests/script.sh
vendored
@ -10,6 +10,6 @@ export JWT_SECRET="aSecret"
|
||||
|
||||
opts=($DB_OPTIONS)
|
||||
|
||||
yarn run -s build:ts
|
||||
yarn run -s test:generate-app "${opts[@]}"
|
||||
yarn run -s test:api --no-generate-app
|
||||
yarn nx run-many --target=build:ts --nx-ignore-cycles --skip-nx-cache
|
||||
yarn run test:generate-app "${opts[@]}"
|
||||
yarn run test:api --no-generate-app
|
||||
|
||||
6
.github/actions/security/lockfile/script.sh
vendored
6
.github/actions/security/lockfile/script.sh
vendored
@ -1,7 +1,5 @@
|
||||
yarn global add lockfile-lint
|
||||
|
||||
lockfile-lint \
|
||||
yarn dlx lockfile-lint \
|
||||
--type $LOCKFILE_TYPE \
|
||||
--path $LOCKFILE_PATH \
|
||||
--allowed-hosts $LOCKFILE_ALLOWED_HOSTS \
|
||||
--validate-https
|
||||
--allowed-schemes "npm:" "workspace:" "patch:"
|
||||
|
||||
3
.github/workflows/adminBundleSize.yml
vendored
3
.github/workflows/adminBundleSize.yml
vendored
@ -27,7 +27,8 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: yarn
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
|
||||
3
.github/workflows/checks.yml
vendored
3
.github/workflows/checks.yml
vendored
@ -24,5 +24,6 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: ./.github/actions/security/lockfile
|
||||
|
||||
5
.github/workflows/contributor-doc.yml
vendored
5
.github/workflows/contributor-doc.yml
vendored
@ -33,10 +33,11 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: yarn
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build website
|
||||
run: yarn build
|
||||
|
||||
25
.github/workflows/experimental.yml
vendored
Normal file
25
.github/workflows/experimental.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: 'Experimental Releases'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: 'Publish'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'strapi/strapi'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup npmrc
|
||||
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- run: yarn
|
||||
- run: ./scripts/pre-publish.sh --yes
|
||||
env:
|
||||
VERSION: '0.0.0-experimental.${{ github.sha }}'
|
||||
DIST_TAG: experimental
|
||||
5
.github/workflows/nightly.yml
vendored
5
.github/workflows/nightly.yml
vendored
@ -3,7 +3,6 @@ name: 'Nightly Releases'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 2-6'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
@ -23,5 +22,5 @@ jobs:
|
||||
- run: yarn
|
||||
- run: ./scripts/pre-publish.sh --yes
|
||||
env:
|
||||
VERSION: '0.0.0-${{ github.sha }}'
|
||||
DIST_TAG: experimental
|
||||
VERSION: '0.0.0-next.${{ github.sha }}'
|
||||
DIST_TAG: next
|
||||
|
||||
26
.github/workflows/tests.yml
vendored
26
.github/workflows/tests.yml
vendored
@ -36,9 +36,9 @@ jobs:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: nrwl/nx-set-shas@v3
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- name: Run build:ts
|
||||
run: yarn nx run-many --target=build:ts --nx-ignore-cycles
|
||||
run: yarn nx run-many --target=build:ts --nx-ignore-cycles --skip-nx-cache
|
||||
- name: Run lint
|
||||
run: yarn nx affected --target=lint --parallel --nx-ignore-cycles
|
||||
|
||||
@ -61,9 +61,9 @@ jobs:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: nrwl/nx-set-shas@v3
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- name: Run build:ts
|
||||
run: yarn nx run-many --target=build:ts --nx-ignore-cycles
|
||||
run: yarn nx run-many --target=build:ts --nx-ignore-cycles --skip-nx-cache
|
||||
- name: Run tests
|
||||
run: yarn nx affected --target=test:unit --nx-ignore-cycles
|
||||
|
||||
@ -86,7 +86,7 @@ jobs:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- uses: nrwl/nx-set-shas@v3
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- name: Run test
|
||||
run: yarn nx affected --target=test:front --nx-ignore-cycles
|
||||
|
||||
@ -106,7 +106,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- name: Build
|
||||
run: yarn build --projects=@strapi/admin,@strapi/helper-plugin
|
||||
|
||||
@ -145,7 +145,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
@ -183,7 +183,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=mysql --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
@ -220,7 +220,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=mysql --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
@ -242,7 +242,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
env:
|
||||
SQLITE_PKG: ${{ matrix.sqlite_pkg }}
|
||||
@ -288,7 +288,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=postgres --dbhost=localhost --dbport=5432 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
@ -330,7 +330,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
with:
|
||||
dbOptions: '--dbclient=mysql --dbhost=localhost --dbport=3306 --dbname=strapi_test --dbusername=strapi --dbpassword=strapi'
|
||||
@ -356,7 +356,7 @@ jobs:
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn install --immutable
|
||||
- uses: ./.github/actions/run-api-tests
|
||||
env:
|
||||
SQLITE_PKG: ${{ matrix.sqlite_pkg }}
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@ -137,5 +137,15 @@ schema.graphql
|
||||
.vscode/
|
||||
!.vscode/settings.json
|
||||
front-workspace.code-workspace
|
||||
.yarn
|
||||
.yarnrc
|
||||
|
||||
############################
|
||||
# Yarn
|
||||
############################
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
541
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
873
.yarn/releases/yarn-3.5.0.cjs
vendored
Executable file
873
.yarn/releases/yarn-3.5.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
11
.yarnrc.yml
Normal file
11
.yarnrc.yml
Normal file
@ -0,0 +1,11 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: '@yarnpkg/plugin-interactive-tools'
|
||||
|
||||
defaultSemverRangePrefix: ''
|
||||
|
||||
preferInteractive: true
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.5.0.cjs
|
||||
28
docs/docs/core/upload/backend/providers/intro.md
Normal file
28
docs/docs/core/upload/backend/providers/intro.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Introduction
|
||||
slug: /upload
|
||||
tags:
|
||||
- upload
|
||||
---
|
||||
|
||||
# Provider
|
||||
|
||||
Extends the upload plugin, to connect to different external services or applications, such as Amazon S3 buckets, Cloudinary, etc.
|
||||
|
||||
**In the case of the Upload plugin, the provider should be able to upload files to a remote server and delete them.**
|
||||
|
||||
# Using a provider
|
||||
|
||||
To use a provider, you need to install it and configure it in the `./config/plugins.js` file.
|
||||
|
||||
More info about installing providers on [strapi docs](https://docs.strapi.io/developer-docs/latest/development/providers.html#installing-providers).
|
||||
|
||||
# Provider development
|
||||
|
||||
To create a provider, you need to create a package that exports a function that returns an object with the following methods:
|
||||
|
||||
- `isPrivate()` (optional) - Returns a boolean indicating if the provider is private or not. If it is, the `getSignedUrl` method will be used to get the URL of the file. (default: `false`)
|
||||
- `getSignedUrl(file)`. (optional) - Returns a signed URL to access the file if it requires authentication
|
||||
- `upload(file)` - Uploads a file to the provider
|
||||
- `uploadStream(stream)` (optional) - Uploads a stream to the provider
|
||||
- `delete(file)` - Deletes a file from the provider
|
||||
17
docs/docs/core/upload/intro.md
Normal file
17
docs/docs/core/upload/intro.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Introduction
|
||||
slug: /upload
|
||||
tags:
|
||||
- upload
|
||||
---
|
||||
|
||||
# Content Manager
|
||||
|
||||
This section is an overview of all the features related to the Upload core plugin:
|
||||
|
||||
```mdx-code-block
|
||||
import DocCardList from '@theme/DocCardList';
|
||||
import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
|
||||
|
||||
<DocCardList items={useCurrentSidebarCategory().items} />
|
||||
```
|
||||
@ -1,9 +1,6 @@
|
||||
{
|
||||
"version": "4.8.2",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
],
|
||||
"packages": ["packages/*", "examples/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"useNx": true
|
||||
|
||||
10
package.json
10
package.json
@ -40,9 +40,9 @@
|
||||
"lint:css": "stylelint packages/**/admin/src/**/*.js",
|
||||
"lint:fix": "nx run-many --target=lint --nx-ignore-cycles -- --fix",
|
||||
"lint:other": "npm run prettier:other -- --check",
|
||||
"format": "npm-run-all -p format:*",
|
||||
"format:code": "npm run prettier:code -- --write",
|
||||
"format:other": "npm run prettier:other -- --write",
|
||||
"format": "yarn format:code && yarn format:other",
|
||||
"format:code": "yarn prettier:code --write",
|
||||
"format:other": "yarn prettier:other --write",
|
||||
"prettier:code": "prettier --cache --cache-strategy content \"**/*.{js,ts}\"",
|
||||
"prettier:other": "prettier --cache --cache-strategy content \"**/*.{md,css,scss,yaml,yml}\"",
|
||||
"test:front": "cross-env IS_EE=true nx run-many --target=test:front --nx-ignore-cycles",
|
||||
@ -87,7 +87,6 @@
|
||||
"lerna": "6.5.1",
|
||||
"lint-staged": "13.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"npm-run-all": "4.1.5",
|
||||
"nx": "15.8.3",
|
||||
"plop": "2.7.6",
|
||||
"prettier": "2.8.4",
|
||||
@ -106,5 +105,6 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1 <=18.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@3.5.0"
|
||||
}
|
||||
|
||||
@ -36,9 +36,7 @@
|
||||
}
|
||||
],
|
||||
"main": "./index.js",
|
||||
"bin": {
|
||||
"create-strapi-app": "./index.js"
|
||||
},
|
||||
"bin": "./index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint ."
|
||||
},
|
||||
|
||||
@ -31,9 +31,7 @@
|
||||
}
|
||||
],
|
||||
"main": "./index.js",
|
||||
"bin": {
|
||||
"create-strapi-starter": "./index.js"
|
||||
},
|
||||
"bin": "./index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint ."
|
||||
},
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
export default function useLocalesProvider() {
|
||||
return {
|
||||
changeLocale() {},
|
||||
localeNames: { en: 'English' },
|
||||
messages: ['test'],
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export default function () {
|
||||
return {
|
||||
logos: {
|
||||
auth: { custom: 'customAuthLogo.png', default: 'defaultAuthLogo.png' },
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import get from 'lodash/get';
|
||||
import omit from 'lodash/omit';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -14,6 +13,7 @@ import {
|
||||
useTracking,
|
||||
getYupInnerErrors,
|
||||
Link,
|
||||
useAPIErrorHandler,
|
||||
} from '@strapi/helper-plugin';
|
||||
import {
|
||||
Box,
|
||||
@ -27,17 +27,10 @@ import {
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { EyeStriked, Eye } from '@strapi/icons';
|
||||
import UnauthenticatedLayout, {
|
||||
Column,
|
||||
LayoutContent,
|
||||
} from '../../../../layouts/UnauthenticatedLayout';
|
||||
import UnauthenticatedLayout, { LayoutContent } from '../../../../layouts/UnauthenticatedLayout';
|
||||
import Logo from '../../../../components/UnauthenticatedLogo';
|
||||
import FieldActionWrapper from '../FieldActionWrapper';
|
||||
|
||||
const CenteredBox = styled(Box)`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const A = styled.a`
|
||||
color: ${({ theme }) => theme.colors.primary600};
|
||||
`;
|
||||
@ -58,6 +51,8 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
const { trackUsage } = useTracking();
|
||||
const { formatMessage } = useIntl();
|
||||
const query = useQuery();
|
||||
const { formatAPIError } = useAPIErrorHandler();
|
||||
|
||||
const registrationToken = query.get('registrationToken');
|
||||
|
||||
useEffect(() => {
|
||||
@ -73,17 +68,17 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
if (data) {
|
||||
setUserInfo(data);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = get(err, ['response', 'data', 'message'], 'An error occurred');
|
||||
} catch (error) {
|
||||
const message = formatAPIError(error);
|
||||
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: errorMessage,
|
||||
message,
|
||||
});
|
||||
|
||||
// Redirect to the oops page in case of an invalid token
|
||||
// @alexandrebodin @JAB I am not sure it is the wanted behavior
|
||||
push(`/auth/oops?info=${encodeURIComponent(errorMessage)}`);
|
||||
push(`/auth/oops?info=${encodeURIComponent(message)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -92,6 +87,20 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [registrationToken]);
|
||||
|
||||
function normalizeData(data) {
|
||||
return Object.entries(data).reduce((acc, [key, value]) => {
|
||||
let normalizedvalue = value;
|
||||
|
||||
if (!['password', 'confirmPassword'].includes(key) && typeof value === 'string') {
|
||||
normalizedvalue = normalizedvalue.trim();
|
||||
}
|
||||
|
||||
acc[key] = normalizedvalue;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return (
|
||||
<UnauthenticatedLayout>
|
||||
<LayoutContent>
|
||||
@ -107,8 +116,10 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
news: false,
|
||||
}}
|
||||
onSubmit={async (data, formik) => {
|
||||
const normalizedData = normalizeData(data);
|
||||
|
||||
try {
|
||||
await schema.validate(data, { abortEarly: false });
|
||||
await schema.validate(normalizedData, { abortEarly: false });
|
||||
|
||||
if (submitCount > 0 && authType === 'register-admin') {
|
||||
trackUsage('didSubmitWithErrorsFirstAdmin', { count: submitCount.toString() });
|
||||
@ -117,11 +128,11 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
if (registrationToken) {
|
||||
// We need to pass the registration token in the url param to the api in order to submit another admin user
|
||||
onSubmit(
|
||||
{ userInfo: omit(data, ['registrationToken']), registrationToken },
|
||||
{ userInfo: omit(normalizedData, ['registrationToken']), registrationToken },
|
||||
formik
|
||||
);
|
||||
} else {
|
||||
onSubmit(data, formik);
|
||||
onSubmit(normalizedData, formik);
|
||||
}
|
||||
} catch (err) {
|
||||
const errors = getYupInnerErrors(err);
|
||||
@ -138,27 +149,26 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
return (
|
||||
<Form noValidate>
|
||||
<Main>
|
||||
<Column>
|
||||
<Flex direction="column" alignItems="stretch" gap={3}>
|
||||
<Logo />
|
||||
<Box paddingTop={6} paddingBottom={1}>
|
||||
<Typography as="h1" variant="alpha">
|
||||
{formatMessage({
|
||||
id: 'Auth.form.welcome.title',
|
||||
defaultMessage: 'Welcome to Strapi!',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<CenteredBox paddingBottom={7}>
|
||||
<Typography variant="epsilon" textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: 'Auth.form.register.subtitle',
|
||||
defaultMessage:
|
||||
'Credentials are only used to authenticate in Strapi. All saved data will be stored in your database.',
|
||||
})}
|
||||
</Typography>
|
||||
</CenteredBox>
|
||||
</Column>
|
||||
<Flex direction="column" alignItems="stretch" gap={6}>
|
||||
|
||||
<Typography as="h1" variant="alpha" textAlign="center">
|
||||
{formatMessage({
|
||||
id: 'Auth.form.welcome.title',
|
||||
defaultMessage: 'Welcome to Strapi!',
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="epsilon" textColor="neutral600" textAlign="center">
|
||||
{formatMessage({
|
||||
id: 'Auth.form.register.subtitle',
|
||||
defaultMessage:
|
||||
'Credentials are only used to authenticate in Strapi. All saved data will be stored in your database.',
|
||||
})}
|
||||
</Typography>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" alignItems="stretch" gap={6} marginTop={7}>
|
||||
<Grid gap={4}>
|
||||
<GridItem col={6}>
|
||||
<TextInput
|
||||
@ -204,7 +214,6 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
value={values.password}
|
||||
error={errors.password ? formatMessage(errors.password) : undefined}
|
||||
endAction={
|
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@ -245,7 +254,6 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
errors.confirmPassword ? formatMessage(errors.confirmPassword) : undefined
|
||||
}
|
||||
endAction={
|
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
<FieldActionWrapper
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@
|
||||
|
||||
const { assoc, has, prop, omit } = require('lodash/fp');
|
||||
const strapiUtils = require('@strapi/utils');
|
||||
const { mapAsync } = require('@strapi/utils');
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const { getDeepPopulate, getDeepPopulateDraftCount } = require('./utils/populate');
|
||||
const { getDeepRelationsCount } = require('./utils/count');
|
||||
@ -72,40 +73,77 @@ module.exports = ({ strapi }) => ({
|
||||
return assoc(`${CREATED_BY_ATTRIBUTE}.roles`, roles, entity);
|
||||
},
|
||||
|
||||
find(opts, uid) {
|
||||
/**
|
||||
* Extend this function from other plugins to add custom mapping of entity
|
||||
* responses
|
||||
* @param {Object} entity
|
||||
* @returns
|
||||
*/
|
||||
mapEntity(entity) {
|
||||
return entity;
|
||||
},
|
||||
|
||||
/**
|
||||
* Some entity manager functions may return multiple entities or one entity.
|
||||
* This function maps the response in both cases
|
||||
* @param {Array|Object|null} entities
|
||||
* @param {string} uid
|
||||
*/
|
||||
async mapEntitiesResponse(entities, uid) {
|
||||
if (entities?.results) {
|
||||
const mappedResults = await mapAsync(entities.results, (entity) =>
|
||||
this.mapEntity(entity, uid)
|
||||
);
|
||||
return { ...entities, results: mappedResults };
|
||||
}
|
||||
// if entity is single type
|
||||
return this.mapEntity(entities, uid);
|
||||
},
|
||||
|
||||
async find(opts, uid) {
|
||||
const params = { ...opts, populate: getDeepPopulate(uid) };
|
||||
|
||||
return strapi.entityService.findMany(uid, params);
|
||||
const entities = await strapi.entityService.findMany(uid, params);
|
||||
|
||||
return this.mapEntitiesResponse(entities, uid);
|
||||
},
|
||||
|
||||
findPage(opts, uid) {
|
||||
async findPage(opts, uid) {
|
||||
const params = { ...opts, populate: getDeepPopulate(uid, { maxLevel: 1 }) };
|
||||
|
||||
return strapi.entityService.findPage(uid, params);
|
||||
const entities = await strapi.entityService.findPage(uid, params);
|
||||
|
||||
return this.mapEntitiesResponse(entities, uid);
|
||||
},
|
||||
|
||||
findWithRelationCountsPage(opts, uid) {
|
||||
async findWithRelationCountsPage(opts, uid) {
|
||||
const counterPopulate = getDeepPopulate(uid, { countMany: true, maxLevel: 1 });
|
||||
const params = { ...opts, populate: addCreatedByRolesPopulate(counterPopulate) };
|
||||
|
||||
return strapi.entityService.findWithRelationCountsPage(uid, params);
|
||||
const entities = await strapi.entityService.findWithRelationCountsPage(uid, params);
|
||||
|
||||
return this.mapEntitiesResponse(entities, uid);
|
||||
},
|
||||
|
||||
findOneWithCreatorRolesAndCount(id, uid) {
|
||||
async findOneWithCreatorRolesAndCount(id, uid) {
|
||||
const counterPopulate = getDeepPopulate(uid, { countMany: true, countOne: true });
|
||||
const params = { populate: addCreatedByRolesPopulate(counterPopulate) };
|
||||
|
||||
return strapi.entityService.findOne(uid, id, params);
|
||||
return strapi.entityService
|
||||
.findOne(uid, id, params)
|
||||
.then((entity) => this.mapEntity(entity, uid));
|
||||
},
|
||||
|
||||
async findOne(id, uid) {
|
||||
const params = { populate: getDeepPopulate(uid) };
|
||||
|
||||
return strapi.entityService.findOne(uid, id, params);
|
||||
return strapi.entityService
|
||||
.findOne(uid, id, params)
|
||||
.then((entity) => this.mapEntity(entity, uid));
|
||||
},
|
||||
|
||||
async findOneWithCreatorRoles(id, uid) {
|
||||
const entity = await this.findOne(id, uid);
|
||||
const entity = await this.findOne(id, uid).then((entity) => this.mapEntity(entity, uid));
|
||||
|
||||
if (!entity) {
|
||||
return entity;
|
||||
@ -130,7 +168,9 @@ module.exports = ({ strapi }) => ({
|
||||
: getDeepPopulate(uid, { countMany: true, countOne: true }),
|
||||
};
|
||||
|
||||
const entity = await strapi.entityService.create(uid, params);
|
||||
const entity = await strapi.entityService
|
||||
.create(uid, params)
|
||||
.then((entity) => this.mapEntity(entity, uid));
|
||||
|
||||
// If relations were populated, relations count will be returned instead of the array of relations.
|
||||
if (populateRelations) {
|
||||
@ -151,7 +191,9 @@ module.exports = ({ strapi }) => ({
|
||||
: getDeepPopulate(uid, { countMany: true, countOne: true }),
|
||||
};
|
||||
|
||||
const updatedEntity = await strapi.entityService.update(uid, entity.id, params);
|
||||
const updatedEntity = await strapi.entityService
|
||||
.update(uid, entity.id, params)
|
||||
.then((entity) => this.mapEntity(entity, uid));
|
||||
|
||||
// If relations were populated, relations count will be returned instead of the array of relations.
|
||||
if (populateRelations) {
|
||||
@ -214,12 +256,14 @@ module.exports = ({ strapi }) => ({
|
||||
|
||||
await emitEvent(ENTRY_PUBLISH, updatedEntity, uid);
|
||||
|
||||
const mappedEntity = await this.mapEntity(updatedEntity, uid);
|
||||
|
||||
// If relations were populated, relations count will be returned instead of the array of relations.
|
||||
if (isRelationsPopulateEnabled(uid)) {
|
||||
return getDeepRelationsCount(updatedEntity, uid);
|
||||
return getDeepRelationsCount(mappedEntity, uid);
|
||||
}
|
||||
|
||||
return updatedEntity;
|
||||
return mappedEntity;
|
||||
},
|
||||
|
||||
async unpublish(entity, body = {}, uid) {
|
||||
@ -241,12 +285,14 @@ module.exports = ({ strapi }) => ({
|
||||
|
||||
await emitEvent(ENTRY_UNPUBLISH, updatedEntity, uid);
|
||||
|
||||
const mappedEntity = await this.mapEntity(updatedEntity, uid);
|
||||
|
||||
// If relations were populated, relations count will be returned instead of the array of relations.
|
||||
if (isRelationsPopulateEnabled(uid)) {
|
||||
return getDeepRelationsCount(updatedEntity, uid);
|
||||
return getDeepRelationsCount(mappedEntity, uid);
|
||||
}
|
||||
|
||||
return updatedEntity;
|
||||
return mappedEntity;
|
||||
},
|
||||
|
||||
async getNumberOfDraftRelations(id, uid) {
|
||||
|
||||
@ -62,7 +62,7 @@ describe('Remote Strapi Destination', () => {
|
||||
// ignore ws connection error
|
||||
}
|
||||
|
||||
expect(WebSocket).toHaveBeenCalledWith(`ws://strapi.com/admin${TRANSFER_PATH}`);
|
||||
expect(WebSocket).toHaveBeenCalledWith(`ws://strapi.com/admin${TRANSFER_PATH}`, undefined);
|
||||
});
|
||||
|
||||
test('Should use wss protocol for https urls', async () => {
|
||||
@ -76,7 +76,7 @@ describe('Remote Strapi Destination', () => {
|
||||
// ignore ws connection error
|
||||
}
|
||||
|
||||
expect(WebSocket).toHaveBeenCalledWith(`wss://strapi.com/admin${TRANSFER_PATH}`);
|
||||
expect(WebSocket).toHaveBeenCalledWith(`wss://strapi.com/admin${TRANSFER_PATH}`, undefined);
|
||||
});
|
||||
|
||||
test('Should throw on invalid protocol', async () => {
|
||||
|
||||
@ -16,6 +16,10 @@ interface ITransferTokenAuth {
|
||||
token: string;
|
||||
}
|
||||
|
||||
type WebsocketParams = ConstructorParameters<typeof WebSocket>;
|
||||
type Address = WebsocketParams[0];
|
||||
type Options = WebsocketParams[2];
|
||||
|
||||
export interface IRemoteStrapiDestinationProviderOptions
|
||||
extends Pick<ILocalStrapiDestinationProviderOptions, 'restore' | 'strategy'> {
|
||||
url: URL;
|
||||
@ -47,31 +51,16 @@ class RemoteStrapiDestinationProvider implements IDestinationProvider {
|
||||
async initTransfer(): Promise<string> {
|
||||
const { strategy, restore } = this.options;
|
||||
|
||||
// Wait for the connection to be made to the server, then init the transfer
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.ws
|
||||
?.once('open', async () => {
|
||||
try {
|
||||
const query = this.dispatcher?.dispatchCommand({
|
||||
command: 'init',
|
||||
params: { options: { strategy, restore }, transfer: 'push' },
|
||||
});
|
||||
|
||||
const res = (await query) as server.Payload<server.InitMessage>;
|
||||
|
||||
if (!res?.transferID) {
|
||||
throw new ProviderTransferError('Init failed, invalid response from the server');
|
||||
}
|
||||
|
||||
resolve(res.transferID);
|
||||
} catch (e: unknown) {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
.once('error', (message) => {
|
||||
reject(message);
|
||||
});
|
||||
const query = this.dispatcher?.dispatchCommand({
|
||||
command: 'init',
|
||||
params: { options: { strategy, restore }, transfer: 'push' },
|
||||
});
|
||||
|
||||
const res = (await query) as server.Payload<server.InitMessage>;
|
||||
if (!res?.transferID) {
|
||||
throw new ProviderTransferError('Init failed, invalid response from the server');
|
||||
}
|
||||
return res.transferID;
|
||||
}
|
||||
|
||||
#startStepOnce(stage: client.TransferPushStep) {
|
||||
@ -186,6 +175,25 @@ class RemoteStrapiDestinationProvider implements IDestinationProvider {
|
||||
});
|
||||
}
|
||||
|
||||
async #connectToWebsocket(address: Address, options?: Options): Promise<WebSocket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = new WebSocket(address, options);
|
||||
server.once('open', () => {
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.once('error', (err) => {
|
||||
reject(
|
||||
new ProviderTransferError(err.message, {
|
||||
details: {
|
||||
error: err.message,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async bootstrap(): Promise<void> {
|
||||
const { url, auth } = this.options;
|
||||
const validProtocols = ['https:', 'http:'];
|
||||
@ -205,13 +213,13 @@ class RemoteStrapiDestinationProvider implements IDestinationProvider {
|
||||
const wsUrl = `${wsProtocol}//${url.host}${url.pathname}${TRANSFER_PATH}`;
|
||||
// No auth defined, trying public access for transfer
|
||||
if (!auth) {
|
||||
ws = new WebSocket(wsUrl);
|
||||
ws = await this.#connectToWebsocket(wsUrl);
|
||||
}
|
||||
|
||||
// Common token auth, this should be the main auth method
|
||||
else if (auth.type === 'token') {
|
||||
const headers = { Authorization: `Bearer ${auth.token}` };
|
||||
ws = new WebSocket(wsUrl, { headers });
|
||||
ws = await this.#connectToWebsocket(wsUrl, { headers });
|
||||
}
|
||||
|
||||
// Invalid auth method provided
|
||||
|
||||
@ -1,21 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
|
||||
const chalk = require('chalk');
|
||||
|
||||
const codeToColor = (code) => {
|
||||
return code >= 500
|
||||
? chalk.red(code)
|
||||
: code >= 400
|
||||
? chalk.yellow(code)
|
||||
: code >= 300
|
||||
? chalk.cyan(code)
|
||||
: code >= 200
|
||||
? chalk.green(code)
|
||||
: code;
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import('./').MiddlewareFactory}
|
||||
*/
|
||||
@ -25,6 +9,6 @@ module.exports = (_, { strapi }) => {
|
||||
await next();
|
||||
const delta = Math.ceil(Date.now() - start);
|
||||
|
||||
strapi.log.http(`${ctx.method} ${ctx.url} (${delta} ms) ${codeToColor(ctx.status)}`);
|
||||
strapi.log.http(`${ctx.method} ${ctx.url} (${delta} ms) ${ctx.status}`);
|
||||
};
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ const LIMITED_EVENTS = [
|
||||
'didSaveMediaWithCaption',
|
||||
'didDisableResponsiveDimensions',
|
||||
'didEnableResponsiveDimensions',
|
||||
'didInitializePluginUpload',
|
||||
];
|
||||
|
||||
const createTelemetryInstance = (strapi) => {
|
||||
|
||||
@ -66,9 +66,7 @@
|
||||
],
|
||||
"main": "./lib",
|
||||
"types": "./lib/index.d.ts",
|
||||
"bin": {
|
||||
"strapi": "./bin/strapi.js"
|
||||
},
|
||||
"bin": "./bin/strapi.js",
|
||||
"directories": {
|
||||
"lib": "./lib",
|
||||
"bin": "./bin",
|
||||
|
||||
@ -7,12 +7,10 @@ import { AssetCardBase } from './AssetCardBase';
|
||||
export const ImageAssetCard = ({ height, width, thumbnail, size, alt, ...props }) => {
|
||||
// Prevents the browser from caching the URL for all sizes and allow react-query to make a smooth update
|
||||
// instead of a full refresh
|
||||
const optimizedCachingThumbnail =
|
||||
width && height ? `${thumbnail}?width=${width}&height=${height}` : thumbnail;
|
||||
|
||||
return (
|
||||
<AssetCardBase {...props} subtitle={height && width && ` - ${width}✕${height}`} variant="Image">
|
||||
<CardAsset src={optimizedCachingThumbnail} size={size} alt={alt} />
|
||||
<CardAsset src={thumbnail} size={size} alt={alt} />
|
||||
</AssetCardBase>
|
||||
);
|
||||
};
|
||||
|
||||
@ -480,7 +480,7 @@ describe('ImageAssetCard', () => {
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="c19"
|
||||
src="http://somewhere.com/hello.png?width=40&height=40"
|
||||
src="http://somewhere.com/hello.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -390,7 +390,7 @@ exports[`MediaLibrary / AssetList snapshots the asset list 1`] = `
|
||||
alt="strapi-cover_1fabc982ce.png"
|
||||
aria-hidden="true"
|
||||
class="c14"
|
||||
src="http://localhost:1337/uploads/thumbnail_strapi_cover_1fabc982ce_5b43615ed5.png?width=1066&height=551"
|
||||
src="http://localhost:1337/uploads/thumbnail_strapi_cover_1fabc982ce_5b43615ed5.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -490,7 +490,7 @@ exports[`MediaLibrary / AssetList snapshots the asset list 1`] = `
|
||||
>
|
||||
<video
|
||||
crossorigin="anonymous"
|
||||
src="http://localhost:1337/uploads/mov_bbb_2f3907f7aa.mp4?updated_at=2021-09-14T07:48:30.882Z"
|
||||
src="http://localhost:1337/uploads/mov_bbb_2f3907f7aa.mp4"
|
||||
>
|
||||
<source
|
||||
type="video/mp4"
|
||||
|
||||
@ -219,7 +219,7 @@ describe('<EditAssetDialog />', () => {
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Download'));
|
||||
expect(downloadFile).toHaveBeenCalledWith(
|
||||
'http://localhost:1337/uploads/Screenshot_2_5d4a574d61.png?updated_at=2021-10-04T09:42:31.670Z',
|
||||
'http://localhost:1337/uploads/Screenshot_2_5d4a574d61.png',
|
||||
'Screenshot 2.png'
|
||||
);
|
||||
});
|
||||
|
||||
@ -901,7 +901,7 @@ exports[`<EditAssetDialog /> renders and matches the snapshot 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Screenshot 2.png"
|
||||
src="http://localhost:1337/uploads/thumbnail_Screenshot_2_5d4a574d61.png?updated_at=2021-10-04T09:42:31.670Z"
|
||||
src="http://localhost:1337/uploads/thumbnail_Screenshot_2_5d4a574d61.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -901,7 +901,7 @@ exports[`<EditAssetDialog /> renders and matches the snapshot 1`] = `
|
||||
>
|
||||
<img
|
||||
alt="Screenshot 2.png"
|
||||
src="http://localhost:1337/uploads/thumbnail_Screenshot_2_5d4a574d61.png?updated_at=2021-10-04T09:42:31.670Z"
|
||||
src="http://localhost:1337/uploads/thumbnail_Screenshot_2_5d4a574d61.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@ -165,7 +165,7 @@ describe('<EditAssetDialog />', () => {
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Download'));
|
||||
expect(downloadFile).toHaveBeenCalledWith(
|
||||
'http://localhost:1337/uploads/Screenshot_2_5d4a574d61.png?updated_at=2021-10-04T09:42:31.670Z',
|
||||
'http://localhost:1337/uploads/Screenshot_2_5d4a574d61.png',
|
||||
'Screenshot 2.png'
|
||||
);
|
||||
});
|
||||
|
||||
@ -13,9 +13,8 @@ const createAssetUrl = (asset, forThumbnail = true) => {
|
||||
}
|
||||
|
||||
const assetUrl = forThumbnail ? asset?.formats?.thumbnail?.url || asset.url : asset.url;
|
||||
const backendUrl = prefixFileUrlWithBackendUrl(assetUrl);
|
||||
|
||||
return `${backendUrl}?updated_at=${asset.updatedAt}`;
|
||||
return prefixFileUrlWithBackendUrl(assetUrl);
|
||||
};
|
||||
|
||||
export default createAssetUrl;
|
||||
|
||||
@ -42,6 +42,9 @@ describe('Upload plugin bootstrap function', () => {
|
||||
upload: {
|
||||
services: {
|
||||
metrics: {
|
||||
sendUploadPluginMetrics() {},
|
||||
},
|
||||
weeklyMetrics: {
|
||||
registerCron() {},
|
||||
},
|
||||
},
|
||||
|
||||
@ -32,6 +32,13 @@ jest.mock('@strapi/provider-upload-local', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
...jest.requireActual('../utils'),
|
||||
getService: () => ({
|
||||
contentManager: { entityManager: { addSignedFileUrlsToAdmin: jest.fn() } },
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Upload plugin register function', () => {
|
||||
test('The upload plugin registers the /upload route', async () => {
|
||||
const registerRoute = jest.fn();
|
||||
|
||||
3
packages/core/upload/server/bootstrap.js
vendored
3
packages/core/upload/server/bootstrap.js
vendored
@ -37,7 +37,8 @@ module.exports = async ({ strapi }) => {
|
||||
|
||||
await registerPermissionActions();
|
||||
|
||||
await getService('metrics').registerCron();
|
||||
await getService('weeklyMetrics').registerCron();
|
||||
getService('metrics').sendUploadPluginMetrics();
|
||||
};
|
||||
|
||||
const registerPermissionActions = async () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { merge } = require('lodash/fp');
|
||||
const { mapAsync } = require('@strapi/utils');
|
||||
const { getService } = require('../utils');
|
||||
const { ACTIONS, FILE_MODEL_UID } = require('../constants');
|
||||
const { findEntityAndCheckPermissions } = require('./utils/find-entity-and-check-permissions');
|
||||
@ -26,11 +27,14 @@ module.exports = {
|
||||
const pmQuery = pm.addPermissionsQueryTo(merge(defaultQuery, ctx.query));
|
||||
const query = await pm.sanitizeQuery(pmQuery);
|
||||
|
||||
const { results, pagination } = await getService('upload').findPage(query);
|
||||
const { results: files, pagination } = await getService('upload').findPage(query);
|
||||
|
||||
const sanitizedResults = await pm.sanitizeOutput(results);
|
||||
// Sign file urls for private providers
|
||||
const signedFiles = await mapAsync(files, getService('file').signFileUrls);
|
||||
|
||||
return { results: sanitizedResults, pagination };
|
||||
const sanitizedFiles = await pm.sanitizeOutput(signedFiles);
|
||||
|
||||
return { results: sanitizedFiles, pagination };
|
||||
},
|
||||
|
||||
async findOne(ctx) {
|
||||
@ -46,7 +50,8 @@ module.exports = {
|
||||
id
|
||||
);
|
||||
|
||||
ctx.body = await pm.sanitizeOutput(file);
|
||||
const signedFile = await getService('file').signFileUrls(file);
|
||||
ctx.body = await pm.sanitizeOutput(signedFile);
|
||||
},
|
||||
|
||||
async destroy(ctx) {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const { mapAsync } = require('@strapi/utils');
|
||||
const { getService } = require('../utils');
|
||||
const { ACTIONS, FILE_MODEL_UID } = require('../constants');
|
||||
const validateUploadBody = require('./validation/admin/upload');
|
||||
@ -49,9 +50,12 @@ module.exports = {
|
||||
}
|
||||
|
||||
const data = await validateUploadBody(body);
|
||||
const replacedFiles = await uploadService.replace(id, { data, file: files }, { user });
|
||||
const replacedFile = await uploadService.replace(id, { data, file: files }, { user });
|
||||
|
||||
ctx.body = await pm.sanitizeOutput(replacedFiles, { action: ACTIONS.read });
|
||||
// Sign file urls for private providers
|
||||
const signedFile = await getService('file').signFileUrls(replacedFile);
|
||||
|
||||
ctx.body = await pm.sanitizeOutput(signedFile, { action: ACTIONS.read });
|
||||
},
|
||||
|
||||
async uploadFiles(ctx) {
|
||||
@ -74,7 +78,10 @@ module.exports = {
|
||||
const data = await validateUploadBody(body);
|
||||
const uploadedFiles = await uploadService.upload({ data, files }, { user });
|
||||
|
||||
ctx.body = await pm.sanitizeOutput(uploadedFiles, { action: ACTIONS.read });
|
||||
// Sign file urls for private providers
|
||||
const signedFiles = await mapAsync(uploadedFiles, getService('file').signFileUrls);
|
||||
|
||||
ctx.body = await pm.sanitizeOutput(signedFiles, { action: ACTIONS.read });
|
||||
},
|
||||
|
||||
async upload(ctx) {
|
||||
|
||||
@ -6,6 +6,7 @@ const {
|
||||
} = require('@strapi/utils');
|
||||
const _ = require('lodash');
|
||||
const registerUploadMiddleware = require('./middlewares/upload');
|
||||
const { getService } = require('./utils');
|
||||
|
||||
/**
|
||||
* Register upload plugin
|
||||
@ -16,6 +17,8 @@ module.exports = async ({ strapi }) => {
|
||||
|
||||
await registerUploadMiddleware({ strapi });
|
||||
|
||||
getService('extensions').contentManager.entityManager.addSignedFileUrlsToAdmin();
|
||||
|
||||
if (strapi.plugin('graphql')) {
|
||||
require('./graphql')({ strapi });
|
||||
}
|
||||
@ -83,4 +86,10 @@ const baseProvider = {
|
||||
);
|
||||
}
|
||||
},
|
||||
getSignedUrl(file) {
|
||||
return file;
|
||||
},
|
||||
isPrivate() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const metricsService = require('../metrics');
|
||||
const metricsService = require('../metrics/weekly-metrics');
|
||||
|
||||
describe('metrics', () => {
|
||||
describe('computeMetrics', () => {
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
'use strict';
|
||||
|
||||
const { signEntityMedia } = require('../entity-manager');
|
||||
|
||||
const { getService } = require('../../../../utils');
|
||||
|
||||
jest.mock('../../../../utils');
|
||||
|
||||
describe('Upload | extensions | entity-manager', () => {
|
||||
const modelUID = 'model';
|
||||
const componentUID = 'component';
|
||||
|
||||
const models = {
|
||||
[modelUID]: {
|
||||
attributes: {
|
||||
media: {
|
||||
type: 'media',
|
||||
multiple: false,
|
||||
},
|
||||
media_repeatable: {
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
},
|
||||
compo_media_repeatable: {
|
||||
type: 'component',
|
||||
repeatable: true,
|
||||
component: componentUID,
|
||||
},
|
||||
compo_media: {
|
||||
type: 'component',
|
||||
component: componentUID,
|
||||
},
|
||||
dynamicZone: {
|
||||
type: 'dynamiczone',
|
||||
components: [componentUID],
|
||||
},
|
||||
},
|
||||
},
|
||||
[componentUID]: {
|
||||
attributes: {
|
||||
media_repeatable: {
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
},
|
||||
media: {
|
||||
type: 'media',
|
||||
multiple: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const media = ['media', 'media_1'].map((entry) => ({
|
||||
formats: {
|
||||
thumbnail: {
|
||||
url: `${entry}_thumb`,
|
||||
},
|
||||
large: {
|
||||
url: `${entry}_large`,
|
||||
},
|
||||
small: {
|
||||
url: `${entry}_small`,
|
||||
},
|
||||
medium: {
|
||||
url: `${entry}_medium`,
|
||||
},
|
||||
},
|
||||
url: `${entry}_url`,
|
||||
}));
|
||||
|
||||
describe('signEntityMedia', () => {
|
||||
let spySignFileUrls;
|
||||
beforeEach(() => {
|
||||
spySignFileUrls = jest.fn();
|
||||
getService.mockImplementation(() => ({
|
||||
signFileUrls: spySignFileUrls,
|
||||
}));
|
||||
|
||||
global.strapi = {
|
||||
plugins: {
|
||||
upload: {},
|
||||
},
|
||||
getModel: jest.fn((uid) => models[uid]),
|
||||
};
|
||||
});
|
||||
|
||||
test('makes correct calls for media attribute', async () => {
|
||||
const entity = {
|
||||
media: media[0],
|
||||
};
|
||||
|
||||
await signEntityMedia(entity, modelUID);
|
||||
expect(getService).toBeCalledWith('file');
|
||||
expect(spySignFileUrls).toBeCalledWith(entity.media);
|
||||
});
|
||||
|
||||
test('makes correct calls for repeatable media', async () => {
|
||||
const entity = {
|
||||
media_repeatable: media,
|
||||
};
|
||||
|
||||
await signEntityMedia(entity, modelUID);
|
||||
expect(getService).toBeCalledWith('file');
|
||||
expect(spySignFileUrls).toBeCalledTimes(2);
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
});
|
||||
|
||||
test('makes correct calls for components', async () => {
|
||||
const entity = {
|
||||
compo_media: {
|
||||
media: media[0],
|
||||
media_repeatable: media,
|
||||
},
|
||||
};
|
||||
|
||||
await signEntityMedia(entity, modelUID);
|
||||
expect(getService).toBeCalledWith('file');
|
||||
expect(spySignFileUrls).toBeCalledTimes(3);
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
});
|
||||
|
||||
test('makes correct calls for repeatable components', async () => {
|
||||
const entity = {
|
||||
compo_media_repeatable: [
|
||||
{
|
||||
media: media[0],
|
||||
media_repeatable: media,
|
||||
},
|
||||
{
|
||||
media: media[1],
|
||||
media_repeatable: media,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await signEntityMedia(entity, modelUID);
|
||||
expect(getService).toBeCalledWith('file');
|
||||
expect(spySignFileUrls).toBeCalledTimes(6);
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
});
|
||||
|
||||
test('makes correct calls for dynamic zones', async () => {
|
||||
const entity = {
|
||||
dynamicZone: [
|
||||
{
|
||||
__component: componentUID,
|
||||
media_repeatable: media,
|
||||
media: media[1],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await signEntityMedia(entity, modelUID);
|
||||
expect(getService).toBeCalledWith('file');
|
||||
expect(spySignFileUrls).toBeCalledTimes(3);
|
||||
expect(spySignFileUrls).toBeCalledWith(media[0], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
expect(spySignFileUrls).toBeCalledWith(media[1], expect.anything());
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const { mapAsync, traverseEntity } = require('@strapi/utils');
|
||||
const { getService } = require('../../../utils');
|
||||
|
||||
/**
|
||||
* Visitor function to sign media URLs
|
||||
* @param {Object} schema
|
||||
* @param {string} schema.key - The key of the attribute
|
||||
* @param {string} schema.value - The value of the attribute
|
||||
* @param {Object} schema.attribute - The attribute definition
|
||||
* @param {Object} entry
|
||||
* @param {Function} entry.set - The set function to update the value
|
||||
*/
|
||||
const signEntityMediaVisitor = async ({ key, value, attribute }, { set }) => {
|
||||
const { signFileUrls } = getService('file');
|
||||
|
||||
if (!value || attribute.type !== 'media') {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the attribute is repeatable sign each file
|
||||
if (attribute.multiple) {
|
||||
const signedFiles = await mapAsync(value, signFileUrls);
|
||||
set(key, signedFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the attribute is not repeatable only sign a single file
|
||||
const signedFile = await signFileUrls(value);
|
||||
set(key, signedFile);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Iterate through an entity manager result
|
||||
* Check which modelAttributes are media and pre sign the image URLs
|
||||
* if they are from the current upload provider
|
||||
*
|
||||
* @param {Object} entity
|
||||
* @param {Object} modelAttributes
|
||||
* @returns
|
||||
*/
|
||||
const signEntityMedia = async (entity, uid) => {
|
||||
const model = strapi.getModel(uid);
|
||||
return traverseEntity(signEntityMediaVisitor, { schema: model }, entity);
|
||||
};
|
||||
|
||||
const addSignedFileUrlsToAdmin = async () => {
|
||||
const { provider } = strapi.plugins.upload;
|
||||
const isPrivate = await provider.isPrivate();
|
||||
|
||||
// We only need to sign the file urls if the provider is private
|
||||
if (!isPrivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
strapi.container
|
||||
.get('services')
|
||||
.extend('plugin::content-manager.entity-manager', (entityManager) => {
|
||||
/**
|
||||
* Map entity manager responses to sign private media URLs
|
||||
* @param {Object} entity
|
||||
* @param {string} uid
|
||||
* @returns
|
||||
*/
|
||||
const mapEntity = async (entity, uid) => {
|
||||
const mappedEntity = await entityManager.mapEntity(entity, uid);
|
||||
return signEntityMedia(mappedEntity, uid);
|
||||
};
|
||||
|
||||
return { ...entityManager, mapEntity };
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
addSignedFileUrlsToAdmin,
|
||||
signEntityMedia,
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { addSignedFileUrlsToAdmin } = require('./entity-manager');
|
||||
|
||||
module.exports = {
|
||||
entityManager: { addSignedFileUrlsToAdmin },
|
||||
};
|
||||
7
packages/core/upload/server/services/extensions/index.js
Normal file
7
packages/core/upload/server/services/extensions/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const contentManagerExtensions = require('./content-manager');
|
||||
|
||||
module.exports = {
|
||||
contentManager: contentManagerExtensions,
|
||||
};
|
||||
@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const { cloneDeep } = require('lodash/fp');
|
||||
const { mapAsync } = require('@strapi/utils');
|
||||
const { FOLDER_MODEL_UID, FILE_MODEL_UID } = require('../constants');
|
||||
|
||||
const { getService } = require('../utils');
|
||||
|
||||
const getFolderPath = async (folderId) => {
|
||||
@ -22,7 +23,34 @@ const deleteByIds = async (ids = []) => {
|
||||
return filesToDelete;
|
||||
};
|
||||
|
||||
const signFileUrls = async (file) => {
|
||||
const { provider } = strapi.plugins.upload;
|
||||
const { provider: providerConfig } = strapi.config.get('plugin.upload');
|
||||
const isPrivate = await provider.isPrivate();
|
||||
|
||||
// Check file provider and if provider is private
|
||||
if (file.provider !== providerConfig || !isPrivate) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const signUrl = async (file) => {
|
||||
const signedUrl = await provider.getSignedUrl(file);
|
||||
file.url = signedUrl.url;
|
||||
};
|
||||
|
||||
const signedFile = cloneDeep(file);
|
||||
|
||||
// Sign each file format
|
||||
await signUrl(signedFile);
|
||||
if (file.formats) {
|
||||
await mapAsync(Object.values(signedFile.formats), signUrl);
|
||||
}
|
||||
|
||||
return signedFile;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getFolderPath,
|
||||
deleteByIds,
|
||||
signFileUrls,
|
||||
};
|
||||
|
||||
@ -5,15 +5,19 @@ const upload = require('./upload');
|
||||
const imageManipulation = require('./image-manipulation');
|
||||
const folder = require('./folder');
|
||||
const file = require('./file');
|
||||
const weeklyMetrics = require('./metrics/weekly-metrics');
|
||||
const metrics = require('./metrics');
|
||||
const apiUploadFolder = require('./api-upload-folder');
|
||||
const extensions = require('./extensions');
|
||||
|
||||
module.exports = {
|
||||
provider,
|
||||
upload,
|
||||
folder,
|
||||
file,
|
||||
weeklyMetrics,
|
||||
metrics,
|
||||
'image-manipulation': imageManipulation,
|
||||
'api-upload-folder': apiUploadFolder,
|
||||
extensions,
|
||||
};
|
||||
|
||||
18
packages/core/upload/server/services/metrics/index.js
Normal file
18
packages/core/upload/server/services/metrics/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
const getProviderName = () => strapi.config.get('plugin.upload.provider', 'local');
|
||||
const isProviderPrivate = async () => strapi.plugin('upload').provider.isPrivate();
|
||||
|
||||
module.exports = ({ strapi }) => ({
|
||||
async sendUploadPluginMetrics() {
|
||||
const uploadProvider = getProviderName();
|
||||
const privateProvider = await isProviderPrivate();
|
||||
|
||||
strapi.telemetry.send('didInitializePluginUpload', {
|
||||
groupProperties: {
|
||||
uploadProvider,
|
||||
privateProvider,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
const { defaultTo } = require('lodash/fp');
|
||||
const { add } = require('date-fns');
|
||||
const { FOLDER_MODEL_UID, FILE_MODEL_UID } = require('../constants');
|
||||
const { getWeeklyCronScheduleAt } = require('../utils/cron');
|
||||
const { FOLDER_MODEL_UID, FILE_MODEL_UID } = require('../../constants');
|
||||
const { getWeeklyCronScheduleAt } = require('../../utils/cron');
|
||||
|
||||
const ONE_WEEK = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@ -0,0 +1,187 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { createTestBuilder } = require('../../../../../test/helpers/builder');
|
||||
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
||||
const { createAuthRequest } = require('../../../../../test/helpers/request');
|
||||
|
||||
const builder = createTestBuilder();
|
||||
let strapi;
|
||||
let baseRequest;
|
||||
let rq;
|
||||
|
||||
const modelUID = 'api::model.model';
|
||||
const componentUID = 'default.component';
|
||||
|
||||
const models = {
|
||||
[modelUID]: {
|
||||
displayName: 'Model',
|
||||
singularName: 'model',
|
||||
pluralName: 'models',
|
||||
kind: 'collectionType',
|
||||
attributes: {
|
||||
name: {
|
||||
type: 'text',
|
||||
},
|
||||
media: {
|
||||
type: 'media',
|
||||
},
|
||||
media_repeatable: {
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
},
|
||||
compo_media: {
|
||||
type: 'component',
|
||||
component: componentUID,
|
||||
},
|
||||
compo_media_repeatable: {
|
||||
type: 'component',
|
||||
repeatable: true,
|
||||
component: componentUID,
|
||||
},
|
||||
dynamicZone: {
|
||||
type: 'dynamiczone',
|
||||
components: [componentUID],
|
||||
},
|
||||
},
|
||||
},
|
||||
[componentUID]: {
|
||||
displayName: 'component',
|
||||
attributes: {
|
||||
media_repeatable: {
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
},
|
||||
media: {
|
||||
type: 'media',
|
||||
multiple: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockProvider = (signUrl = true) => ({
|
||||
init() {
|
||||
return {
|
||||
isPrivate() {
|
||||
return signUrl;
|
||||
},
|
||||
getSignedUrl() {
|
||||
return { url: 'signedUrl' };
|
||||
},
|
||||
uploadStream() {},
|
||||
upload() {},
|
||||
delete() {},
|
||||
checkFileSize() {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const uploadImg = (fileName) => {
|
||||
return baseRequest({
|
||||
method: 'POST',
|
||||
url: '/upload',
|
||||
formData: {
|
||||
files: fs.createReadStream(path.join(__dirname, `../utils/${fileName}`)),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
let repeatable;
|
||||
let singleMedia;
|
||||
let mediaEntry = {};
|
||||
|
||||
describe('Upload Plugin url signing', () => {
|
||||
const responseExpectations = (result) => {
|
||||
expect(result.statusCode).toBe(200);
|
||||
|
||||
expect(result.body.media.url).toEqual('signedUrl');
|
||||
|
||||
for (const media of result.body.media_repeatable) {
|
||||
expect(media.url).toEqual('signedUrl');
|
||||
}
|
||||
|
||||
expect(result.body.compo_media.media.url).toEqual('signedUrl');
|
||||
for (const media of result.body.compo_media.media_repeatable) {
|
||||
expect(media.url).toEqual('signedUrl');
|
||||
}
|
||||
|
||||
for (const component of result.body.compo_media_repeatable) {
|
||||
expect(component.media.url).toEqual('signedUrl');
|
||||
for (const media of component.media_repeatable) {
|
||||
expect(media.url).toEqual('signedUrl');
|
||||
}
|
||||
}
|
||||
|
||||
for (const component of result.body.dynamicZone) {
|
||||
expect(component.media.url).toEqual('signedUrl');
|
||||
for (const media of component.media_repeatable) {
|
||||
expect(media.url).toEqual('signedUrl');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const localProviderPath = require.resolve('@strapi/provider-upload-local');
|
||||
jest.mock(localProviderPath, () => mockProvider(true));
|
||||
|
||||
// Create builder
|
||||
await builder.addComponent(models[componentUID]).addContentType(models[modelUID]).build();
|
||||
|
||||
// Create api instance
|
||||
strapi = await createStrapiInstance();
|
||||
|
||||
baseRequest = await createAuthRequest({ strapi });
|
||||
|
||||
rq = await createAuthRequest({ strapi });
|
||||
rq.setURLPrefix(`/content-manager/collection-types/${modelUID}`);
|
||||
|
||||
const imgRes = [await uploadImg('rec.jpg'), await uploadImg('strapi.jpg')];
|
||||
|
||||
repeatable = imgRes.map((img) => img.body[0].id);
|
||||
singleMedia = imgRes[0].body[0].id;
|
||||
mediaEntry = {
|
||||
media: singleMedia,
|
||||
media_repeatable: repeatable,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await strapi.destroy();
|
||||
await builder.cleanup();
|
||||
});
|
||||
|
||||
test('returns signed media URLs on content creation', async () => {
|
||||
const creationResult = await rq.post('/', {
|
||||
body: {
|
||||
name: 'name',
|
||||
media: singleMedia,
|
||||
media_repeatable: repeatable,
|
||||
compo_media: mediaEntry,
|
||||
compo_media_repeatable: [mediaEntry, mediaEntry],
|
||||
dynamicZone: [
|
||||
{
|
||||
__component: componentUID,
|
||||
...mediaEntry,
|
||||
},
|
||||
],
|
||||
},
|
||||
qs: {
|
||||
populate: ['name'],
|
||||
},
|
||||
});
|
||||
|
||||
responseExpectations(creationResult);
|
||||
});
|
||||
|
||||
test('returns signed media URLs when we GET content', async () => {
|
||||
const result = await baseRequest({
|
||||
method: 'GET',
|
||||
url: `/content-manager/collection-types/${modelUID}/1`,
|
||||
});
|
||||
|
||||
responseExpectations(result);
|
||||
});
|
||||
});
|
||||
@ -25,6 +25,10 @@ npm install @strapi/provider-upload-aws-s3 --save
|
||||
|
||||
- `provider` defines the name of the provider
|
||||
- `providerOptions` is passed down during the construction of the provider. (ex: `new AWS.S3(config)`). [Complete list of options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property)
|
||||
- `providerOptions.params` is passed directly to the parameters to each method respectively.
|
||||
- `ACL` is the access control list for the object. Defaults to `public-read`.
|
||||
- `signedUrlExpires` is the number of seconds before a signed URL expires. (See [how signed URLs work](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html)). Defaults to 15 minutes and URLs are only signed when ACL is set to `private`.
|
||||
- `Bucket` is the name of the bucket to upload to.
|
||||
- `actionOptions` is passed directly to the parameters to each method respectively. You can find the complete list of [upload/ uploadStream options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property) and [delete options](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#deleteObject-property)
|
||||
|
||||
See the [documentation about using a provider](https://docs.strapi.io/developer-docs/latest/plugins/upload.html#using-a-provider) for information on installing and using a provider. To understand how environment variables are used in Strapi, please refer to the [documentation about environment variables](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/environment.html#environment-variables).
|
||||
@ -49,9 +53,46 @@ module.exports = ({ env }) => ({
|
||||
secretAccessKey: env('AWS_ACCESS_SECRET'),
|
||||
region: env('AWS_REGION'),
|
||||
params: {
|
||||
ACL: env('AWS_ACL', 'public-read'),
|
||||
signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60),
|
||||
Bucket: env('AWS_BUCKET'),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
actionOptions: {
|
||||
upload: {},
|
||||
uploadStream: {},
|
||||
delete: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Configuration for a private S3 bucket
|
||||
|
||||
If your bucket is configured to be private, you will need to set the `ACL` option to `private` in the `params` object. This will ensure that the signed URL is generated with the correct permissions.
|
||||
|
||||
You can also define the expiration time of the signed URL by setting the `signedUrlExpires` option in the `params` object. The default value is 7 days.
|
||||
|
||||
`./config/plugins.js`
|
||||
|
||||
```js
|
||||
module.exports = ({ env }) => ({
|
||||
// ...
|
||||
upload: {
|
||||
config: {
|
||||
provider: 'aws-s3',
|
||||
providerOptions: {
|
||||
accessKeyId: env('AWS_ACCESS_KEY_ID'),
|
||||
secretAccessKey: env('AWS_ACCESS_SECRET'),
|
||||
region: env('AWS_REGION'),
|
||||
params: {
|
||||
ACL: 'private', // <== set ACL to private
|
||||
signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 60 * 60 * 24 * 7),
|
||||
Bucket: env('AWS_BUCKET'),
|
||||
},
|
||||
},
|
||||
actionOptions: {
|
||||
upload: {},
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
const { getBucketFromUrl } = require('../utils');
|
||||
|
||||
describe('Test for URLs', () => {
|
||||
test('Virtual hosted style', async () => {
|
||||
const url = 'https://bucket.s3.us-east-1.amazonaws.com/img.png';
|
||||
const { bucket } = getBucketFromUrl(url);
|
||||
expect(bucket).toEqual('bucket');
|
||||
});
|
||||
|
||||
describe('Path style', () => {
|
||||
test('No key', async () => {
|
||||
const url = 'https://s3.us-east-1.amazonaws.com/bucket';
|
||||
const { bucket } = getBucketFromUrl(url);
|
||||
expect(bucket).toEqual('bucket');
|
||||
});
|
||||
|
||||
test('With trailing slash', async () => {
|
||||
const url = 'https://s3.us-east-1.amazonaws.com/bucket/';
|
||||
const { bucket } = getBucketFromUrl(url);
|
||||
expect(bucket).toEqual('bucket');
|
||||
});
|
||||
|
||||
test('With key', async () => {
|
||||
const url = 'https://s3.us-east-1.amazonaws.com/bucket/img.png';
|
||||
const { bucket } = getBucketFromUrl(url);
|
||||
expect(bucket).toEqual('bucket');
|
||||
});
|
||||
});
|
||||
|
||||
test('S3 access point', async () => {
|
||||
const url = 'https://bucket.s3-accesspoint.us-east-1.amazonaws.com';
|
||||
const { bucket } = getBucketFromUrl(url);
|
||||
expect(bucket).toEqual('bucket');
|
||||
});
|
||||
|
||||
test('S3://', async () => {
|
||||
const url = 'S3://bucket/img.png';
|
||||
const { bucket } = getBucketFromUrl(url);
|
||||
expect(bucket).toEqual('bucket');
|
||||
});
|
||||
});
|
||||
@ -6,8 +6,9 @@
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
// Public node modules.
|
||||
const _ = require('lodash');
|
||||
const { getOr } = require('lodash/fp');
|
||||
const AWS = require('aws-sdk');
|
||||
const { getBucketFromUrl } = require('./utils');
|
||||
|
||||
function assertUrlProtocol(url) {
|
||||
// Regex to test protocol like "http://", "https://"
|
||||
@ -22,10 +23,11 @@ module.exports = {
|
||||
);
|
||||
}
|
||||
|
||||
const config = { ...s3Options, ...legacyS3Options };
|
||||
|
||||
const S3 = new AWS.S3({
|
||||
apiVersion: '2006-03-01',
|
||||
...s3Options,
|
||||
...legacyS3Options,
|
||||
...config,
|
||||
});
|
||||
|
||||
const filePrefix = rootPath ? `${rootPath.replace(/\/+$/, '')}/` : '';
|
||||
@ -36,6 +38,8 @@ module.exports = {
|
||||
return `${filePrefix}${path}${file.hash}${file.ext}`;
|
||||
};
|
||||
|
||||
const ACL = getOr('public-read', ['params', 'ACL'], config);
|
||||
|
||||
const upload = (file, customParams = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
// upload file on S3 bucket
|
||||
@ -44,7 +48,7 @@ module.exports = {
|
||||
{
|
||||
Key: fileKey,
|
||||
Body: file.stream || Buffer.from(file.buffer, 'binary'),
|
||||
ACL: 'public-read',
|
||||
ACL,
|
||||
ContentType: file.mime,
|
||||
...customParams,
|
||||
},
|
||||
@ -60,13 +64,49 @@ module.exports = {
|
||||
// Default protocol to https protocol
|
||||
file.url = `https://${data.Location}`;
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
isPrivate() {
|
||||
return ACL === 'private';
|
||||
},
|
||||
/**
|
||||
* @param {Object} file
|
||||
* @param {string} file.path
|
||||
* @param {string} file.hash
|
||||
* @param {string} file.ext
|
||||
* @param {Object} customParams
|
||||
* @returns {Promise<{url: string}>}
|
||||
*/
|
||||
getSignedUrl(file, customParams = {}) {
|
||||
// Do not sign the url if it does not come from the same bucket.
|
||||
const { bucket } = getBucketFromUrl(file.url);
|
||||
if (bucket !== config.params.Bucket) {
|
||||
return { url: file.url };
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileKey = getFileKey(file);
|
||||
|
||||
S3.getSignedUrl(
|
||||
'getObject',
|
||||
{
|
||||
Bucket: config.params.Bucket,
|
||||
Key: fileKey,
|
||||
Expires: getOr(15 * 60, ['params', 'signedUrlExpires'], config), // 15 minutes
|
||||
},
|
||||
(err, url) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve({ url });
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
uploadStream(file, customParams = {}) {
|
||||
return upload(file, customParams);
|
||||
},
|
||||
|
||||
63
packages/providers/upload-aws-s3/lib/utils.js
Normal file
63
packages/providers/upload-aws-s3/lib/utils.js
Normal file
@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const ENDPOINT_PATTERN = /^(.+\.)?s3[.-]([a-z0-9-]+)\./;
|
||||
|
||||
/**
|
||||
* Parse the bucket name from a URL.
|
||||
* See all URL formats in https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html
|
||||
*
|
||||
* @param {string} fileUrl - the URL to parse
|
||||
* @returns {object} result
|
||||
* @returns {string} result.bucket - the bucket name
|
||||
* @returns {string} result.error - if any
|
||||
*/
|
||||
function getBucketFromUrl(fileUrl) {
|
||||
const uri = new URL(fileUrl);
|
||||
|
||||
// S3://<bucket-name>/<key>
|
||||
if (uri.protocol === 's3:') {
|
||||
const bucket = uri.host;
|
||||
|
||||
if (!bucket) {
|
||||
return { err: `Invalid S3 URI: no bucket: ${uri}` };
|
||||
}
|
||||
return { bucket };
|
||||
}
|
||||
|
||||
if (!uri.host) {
|
||||
return { err: `Invalid S3 URI: no hostname: ${uri}` };
|
||||
}
|
||||
|
||||
const matches = uri.host.match(ENDPOINT_PATTERN);
|
||||
if (!matches) {
|
||||
return { err: `Invalid S3 URI: hostname does not appear to be a valid S3 endpoint: ${uri}` };
|
||||
}
|
||||
|
||||
const prefix = matches[1];
|
||||
// https://s3.amazonaws.com/<bucket-name>
|
||||
if (!prefix) {
|
||||
if (uri.pathname === '/') {
|
||||
return { bucket: null };
|
||||
}
|
||||
|
||||
const index = uri.pathname.indexOf('/', 1);
|
||||
|
||||
// https://s3.amazonaws.com/<bucket-name>
|
||||
if (index === -1) {
|
||||
return { bucket: uri.pathname.substring(1) };
|
||||
}
|
||||
|
||||
// https://s3.amazonaws.com/<bucket-name>/
|
||||
if (index === uri.pathname.length - 1) {
|
||||
return { bucket: uri.pathname.substring(1, index) };
|
||||
}
|
||||
|
||||
// https://s3.amazonaws.com/<bucket-name>/key
|
||||
return { bucket: uri.pathname.substring(1, index) };
|
||||
}
|
||||
|
||||
// https://<bucket-name>.s3.amazonaws.com/
|
||||
return { bucket: prefix.substring(0, prefix.length - 1) };
|
||||
}
|
||||
|
||||
module.exports = { getBucketFromUrl };
|
||||
@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@strapi/babel-plugin-switch-ee-ce",
|
||||
"version": "4.8.2",
|
||||
"private": false,
|
||||
"description": "Babel plugin to switch from CE to EE at runtime",
|
||||
"repository": "git://github.com/strapi/strapi.git",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
@ -6,8 +6,19 @@
|
||||
"dependencies": {
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@strapi/eslint-config": "0.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "8.27.0",
|
||||
"babel-eslint": "10.1.0"
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.7.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
}
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"./dist"
|
||||
],
|
||||
|
||||
@ -2,4 +2,4 @@
|
||||
"name": "tsconfig",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"silent": true,
|
||||
"autoJobCancelation": true
|
||||
},
|
||||
"installCommand": "yarn setup",
|
||||
"installCommand": "yarn install --immutable",
|
||||
"ignoreCommand": "git diff HEAD^ HEAD --quiet './packages/core/helper-plugin'",
|
||||
"outputDirectory": "packages/core/helper-plugin/storybook-static"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user