Compare commits

..

No commits in common. "master" and "v4.3.4" have entirely different histories.

8 changed files with 69 additions and 97 deletions

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 📂 Sync files - name: 📂 Sync files
uses: ./ uses: ./

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 📂 Sync files - name: 📂 Sync files
uses: ./ uses: ./

View File

@ -20,10 +20,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 📂 Sync files - name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with: with:
server: ftp.samkirkland.com server: ftp.samkirkland.com
username: myFtpUserName username: myFtpUserName
@ -86,7 +86,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Use Node.js 16 - name: Use Node.js 16
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -99,7 +99,7 @@ jobs:
npm run build npm run build
- name: 📂 Sync files - name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with: with:
server: ftp.samkirkland.com server: ftp.samkirkland.com
username: myFtpUserName username: myFtpUserName
@ -116,10 +116,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 📂 Sync files - name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with: with:
server: ftp.samkirkland.com server: ftp.samkirkland.com
username: myFtpUserName username: myFtpUserName
@ -139,10 +139,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 📂 Sync files - name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with: with:
server: ftp.samkirkland.com server: ftp.samkirkland.com
username: myFtpUserName username: myFtpUserName
@ -161,10 +161,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: 📂 Sync files - name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.5 uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with: with:
server: ftp.samkirkland.com server: ftp.samkirkland.com
username: myFtpUserName username: myFtpUserName

View File

@ -45,7 +45,7 @@ inputs:
required: false required: false
description: "Timeout in milliseconds for FTP operations" description: "Timeout in milliseconds for FTP operations"
runs: runs:
using: "node20" using: "node16"
main: "dist/index.js" main: "dist/index.js"
branding: branding:
icon: "upload-cloud" icon: "upload-cloud"

56
dist/index.js vendored
View File

@ -4122,8 +4122,8 @@ const fsStat = (0, util_1.promisify)(fs_1.stat);
const fsOpen = (0, util_1.promisify)(fs_1.open); const fsOpen = (0, util_1.promisify)(fs_1.open);
const fsClose = (0, util_1.promisify)(fs_1.close); const fsClose = (0, util_1.promisify)(fs_1.close);
const fsUnlink = (0, util_1.promisify)(fs_1.unlink); const fsUnlink = (0, util_1.promisify)(fs_1.unlink);
const LIST_COMMANDS_DEFAULT = () => ["LIST -a", "LIST"]; const LIST_COMMANDS_DEFAULT = ["LIST -a", "LIST"];
const LIST_COMMANDS_MLSD = () => ["MLSD", "LIST -a", "LIST"]; const LIST_COMMANDS_MLSD = ["MLSD", "LIST -a", "LIST"];
/** /**
* High-level API to interact with an FTP server. * High-level API to interact with an FTP server.
*/ */
@ -4134,7 +4134,7 @@ class Client {
* @param timeout Timeout in milliseconds, use 0 for no timeout. Optional, default is 30 seconds. * @param timeout Timeout in milliseconds, use 0 for no timeout. Optional, default is 30 seconds.
*/ */
constructor(timeout = 30000) { constructor(timeout = 30000) {
this.availableListCommands = LIST_COMMANDS_DEFAULT(); this.availableListCommands = LIST_COMMANDS_DEFAULT;
this.ftp = new FtpContext_1.FTPContext(timeout); this.ftp = new FtpContext_1.FTPContext(timeout);
this.prepareTransfer = this._enterFirstCompatibleMode([transfer_1.enterPassiveModeIPv6, transfer_1.enterPassiveModeIPv4]); this.prepareTransfer = this._enterFirstCompatibleMode([transfer_1.enterPassiveModeIPv6, transfer_1.enterPassiveModeIPv4]);
this.parseList = parseList_1.parseList; this.parseList = parseList_1.parseList;
@ -4284,10 +4284,10 @@ class Client {
// Use MLSD directory listing if possible. See https://tools.ietf.org/html/rfc3659#section-7.8: // Use MLSD directory listing if possible. See https://tools.ietf.org/html/rfc3659#section-7.8:
// "The presence of the MLST feature indicates that both MLST and MLSD are supported." // "The presence of the MLST feature indicates that both MLST and MLSD are supported."
const supportsMLSD = features.has("MLST"); const supportsMLSD = features.has("MLST");
this.availableListCommands = supportsMLSD ? LIST_COMMANDS_MLSD() : LIST_COMMANDS_DEFAULT(); this.availableListCommands = supportsMLSD ? LIST_COMMANDS_MLSD : LIST_COMMANDS_DEFAULT;
await this.send("TYPE I"); // Binary mode await this.send("TYPE I"); // Binary mode
await this.sendIgnoringError("STRU F"); // Use file structure await this.sendIgnoringError("STRU F"); // Use file structure
await this.sendIgnoringError("OPTS UTF8 ON"); // Some servers expect UTF-8 to be enabled explicitly and setting before login might not have worked. await this.sendIgnoringError("OPTS UTF8 ON"); // Some servers expect UTF-8 to be enabled explicitly
if (supportsMLSD) { if (supportsMLSD) {
await this.sendIgnoringError("OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;"); // Make sure MLSD listings include all we can parse await this.sendIgnoringError("OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;"); // Make sure MLSD listings include all we can parse
} }
@ -4322,9 +4322,6 @@ class Client {
secureOptions.host = (_b = secureOptions.host) !== null && _b !== void 0 ? _b : options.host; secureOptions.host = (_b = secureOptions.host) !== null && _b !== void 0 ? _b : options.host;
await this.useTLS(secureOptions); await this.useTLS(secureOptions);
} }
// Set UTF-8 on before login in case there are non-ascii characters in user or password.
// Note that this might not work before login depending on server.
await this.sendIgnoringError("OPTS UTF8 ON");
await this.login(options.user, options.password); await this.login(options.user, options.password);
await this.useDefaultSettings(); await this.useDefaultSettings();
return welcome; return welcome;
@ -4422,10 +4419,7 @@ class Client {
*/ */
async remove(path, ignoreErrorCodes = false) { async remove(path, ignoreErrorCodes = false) {
const validPath = await this.protectWhitespace(path); const validPath = await this.protectWhitespace(path);
if (ignoreErrorCodes) { return this.send(`DELE ${validPath}`, ignoreErrorCodes);
return this.sendIgnoringError(`DELE ${validPath}`);
}
return this.send(`DELE ${validPath}`);
} }
/** /**
* Report transfer progress for any upload or download to a given handler. * Report transfer progress for any upload or download to a given handler.
@ -4634,13 +4628,10 @@ class Client {
async removeDir(remoteDirPath) { async removeDir(remoteDirPath) {
return this._exitAtCurrentDirectory(async () => { return this._exitAtCurrentDirectory(async () => {
await this.cd(remoteDirPath); await this.cd(remoteDirPath);
// Get the absolute path of the target because remoteDirPath might be a relative path, even `../` is possible.
const absoluteDirPath = await this.pwd();
await this.clearWorkingDir(); await this.clearWorkingDir();
const dirIsRoot = absoluteDirPath === "/"; if (remoteDirPath !== "/") {
if (!dirIsRoot) {
await this.cdup(); await this.cdup();
await this.removeEmptyDir(absoluteDirPath); await this.removeEmptyDir(remoteDirPath);
} }
}); });
} }
@ -4887,7 +4878,7 @@ var FileType;
FileType[FileType["File"] = 1] = "File"; FileType[FileType["File"] = 1] = "File";
FileType[FileType["Directory"] = 2] = "Directory"; FileType[FileType["Directory"] = 2] = "Directory";
FileType[FileType["SymbolicLink"] = 3] = "SymbolicLink"; FileType[FileType["SymbolicLink"] = 3] = "SymbolicLink";
})(FileType || (exports.FileType = FileType = {})); })(FileType = exports.FileType || (exports.FileType = {}));
/** /**
* Describes a file, directory or symbolic link. * Describes a file, directory or symbolic link.
*/ */
@ -4994,9 +4985,6 @@ class FTPError extends Error {
} }
} }
exports.FTPError = FTPError; exports.FTPError = FTPError;
function doNothing() {
/** Do nothing */
}
/** /**
* FTPContext holds the control and data sockets of an FTP connection and provides a * FTPContext holds the control and data sockets of an FTP connection and provides a
* simplified way to interact with an FTP server, handle responses, errors and timeouts. * simplified way to interact with an FTP server, handle responses, errors and timeouts.
@ -5049,8 +5037,9 @@ class FTPContext {
return; return;
} }
this._closingError = err; this._closingError = err;
this.send("QUIT"); // Don't wait for an answer
// Close the sockets but don't fully reset this context to preserve `this._closingError`. // Close the sockets but don't fully reset this context to preserve `this._closingError`.
this._closeControlSocket(); this._closeSocket(this._socket);
this._closeSocket(this._dataSocket); this._closeSocket(this._dataSocket);
// Give the user's task a chance to react, maybe cleanup resources. // Give the user's task a chance to react, maybe cleanup resources.
this._passToHandler(err); this._passToHandler(err);
@ -5091,7 +5080,7 @@ class FTPContext {
this._removeSocketListeners(this.socket); this._removeSocketListeners(this.socket);
} }
else { else {
this._closeControlSocket(); this._closeSocket(this.socket);
} }
} }
if (socket) { if (socket) {
@ -5300,23 +5289,16 @@ class FTPContext {
}); });
} }
/** /**
* Close the control socket. Sends QUIT, then FIN, and ignores any response or error. * Close a socket.
*/
_closeControlSocket() {
this._removeSocketListeners(this._socket);
this._socket.on("error", doNothing);
this.send("QUIT");
this._closeSocket(this._socket);
}
/**
* Close a socket, ignores any error.
* @protected * @protected
*/ */
_closeSocket(socket) { _closeSocket(socket) {
if (socket) { if (socket) {
this._removeSocketListeners(socket); this._removeSocketListeners(socket);
socket.on("error", doNothing); socket.on("error", () => { });
socket.destroy(); socket.on("timeout", () => socket.destroy());
socket.setTimeout(this.timeout);
socket.end();
} }
} }
/** /**
@ -5808,8 +5790,8 @@ function parseSize(value, info) {
* Parsers for MLSD facts. * Parsers for MLSD facts.
*/ */
const factHandlersByName = { const factHandlersByName = {
"size": parseSize, // File size "size": parseSize,
"sizd": parseSize, // Directory size "sizd": parseSize,
"unique": (value, info) => { "unique": (value, info) => {
info.uniqueID = value; info.uniqueID = value;
}, },

View File

@ -1,9 +1,8 @@
# How to migrate between versions # Migrating from v4.1.0 to v4.2.0
## Migrating from v4.1.0 to v4.2.0 `v4.2.0` parses the `exclude` option in a more standard way.
`v4.2.0` parses the `exclude` option in a more standard way. Going forward the `exclude` option **must** be in the following format:
Going forward the `exclude` option **must** be in the following format
```yml ```yml
exclude: | exclude: |
**/.git* **/.git*
@ -12,29 +11,28 @@ exclude: |
fileToExclude.txt fileToExclude.txt
``` ```
## Migrating from v3 to v4 ---
# Migrating from v3 to v4
Migrating from v3 to v4 should be fairly straightforward. Version 4 was designed with speed and ease of initial setup in mind. Going forward version 4 will be the only supported version. Migrating from v3 to v4 should be fairly straightforward. Version 4 was designed with speed and ease of initial setup in mind. Going forward version 4 will be the only supported version.
### Those who can't upgrade #### Those who can't upgrade
Most features have been carried forward and improved upon. However, some features did not make the cut.
Most features have been carried forward and improved upon. However, some features did not make the cut:
- **`sftp` is no longer supported**. If you have `sftp` access you are using `ssh`, that means you have access to a much more modern and capable protocol. I plan on releasing a separate github action that will deploy over `sftp`/`ssh` using `rsync`. Until then you can continue using version 3. - **`sftp` is no longer supported**. If you have `sftp` access you are using `ssh`, that means you have access to a much more modern and capable protocol. I plan on releasing a separate github action that will deploy over `sftp`/`ssh` using `rsync`. Until then you can continue using version 3.
- The `include` argument has been removed. I didn't see much need for it in the initial release. If you need this feature please create a support ticket. - The `include` argument has been removed. I didn't see much need for it in the initial release. If you need this feature please create a support ticket.
---
### How to upgrade ### How to upgrade
1. Remove `with: fetch-depth: 2`. It is no longer needed and removing it will _slightly_ speed up deployments. 1) Remove `with: fetch-depth: 2`. It is no longer needed and removing it will _slightly_ speed up deployments.
2. Change the version to `v4.X.X`, for example `SamKirkland/FTP-Deploy-Action@v4.3.5` (please check the [README](https://github.com/SamKirkland/FTP-Deploy-Action/blob/master/README.md) or the [releases page](https://github.com/SamKirkland/FTP-Deploy-Action/releases/latest) for the latest version). 2) Change the version to `4.X.X`, for example `SamKirkland/FTP-Deploy-Action@v4.3.4` (please check readme for latest version)
3. If you have a `.git-ftp-include` file you should delete it. Version 4 tracks files differently and no longer needs this config file. 3) If you have a `.git-ftp-include` file you should delete it. Version 4 tracks files differently and no longer needs this config file.
4. If you have a `.git-ftp-ignore` file, you should transfer the options to the new `exclude` argument. **Note:** version 4 excludes any `.git*` and `node_modules/` files / folders by default. 4) If you have a `.git-ftp-ignore` file, you should transfer the options to the new `exclude` argument. _Note: Version 4 excludes any `.git*` and `node_modules/` files/folders by default_
5. Update your arguments to reflect the following changes: 5) Update your arguments to reflect the following changes
- `ftp-server` was split into 4 arguments: * `ftp-server` was split into 4 arguments. `server`, `port`, `protocol`, and `server-dir`. Transfer your config to these options as needed.
- `server` * `ftp-username` was renamed to `username`
- `port` * `ftp-password` was renamed to `password`
- `protocol` * `local-dir` and `server-dir` now **must** end with `/`
- `server-dir` * `git-ftp-args` and `known-hosts` arguments were removed
- `ftp-username` was renamed to `username`.
- `ftp-password` was renamed to `password`.
- `local-dir` and `server-dir` now **must** end with `/`.
- `git-ftp-args` and `known-hosts` arguments were removed.

36
package-lock.json generated
View File

@ -1,23 +1,23 @@
{ {
"name": "ftp-deploy-action", "name": "ftp-deploy-action",
"version": "4.3.5", "version": "4.3.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ftp-deploy-action", "name": "ftp-deploy-action",
"version": "4.3.5", "version": "4.3.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.9.1", "@actions/core": "^1.9.1",
"@samkirkland/ftp-deploy": "^1.2.4", "@samkirkland/ftp-deploy": "^1.2.3",
"@types/jest": "^29.4.1", "@types/jest": "^29.4.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"ts-jest": "^29.0.5", "ts-jest": "^29.0.5",
"ts-node-dev": "^2.0.0" "ts-node-dev": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.24", "@types/node": "^14.0.27",
"@typescript-eslint/eslint-plugin": "^5.33.1", "@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1", "@typescript-eslint/parser": "^5.33.1",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
@ -1097,11 +1097,11 @@
} }
}, },
"node_modules/@samkirkland/ftp-deploy": { "node_modules/@samkirkland/ftp-deploy": {
"version": "1.2.4", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@samkirkland/ftp-deploy/-/ftp-deploy-1.2.4.tgz", "resolved": "https://registry.npmjs.org/@samkirkland/ftp-deploy/-/ftp-deploy-1.2.3.tgz",
"integrity": "sha512-MF5BoP1mQS3HBdx2pTMX/sIxSt9S4IgdL6ttrcGpUWC9U/IIPcUnyVEQiiBQJePLXpHI93dT9b8VDoS+cUknNg==", "integrity": "sha512-7DZEnyNlg78FdJ86Lm2BgBccI31mrMUnUxDt+Le/j0DCQpOA4Wl0u+jH2Mt6i6xOYtrVADX/LUeiZ8x+Epr/UA==",
"dependencies": { "dependencies": {
"basic-ftp": "^5.0.5", "basic-ftp": "^5.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multimatch": "^5.0.0", "multimatch": "^5.0.0",
"pretty-bytes": "^5.6.0", "pretty-bytes": "^5.6.0",
@ -1241,12 +1241,9 @@
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==" "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.24", "version": "14.18.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.25.tgz",
"integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "integrity": "sha512-9pLfceRSrKIsv/MISN6RoFWTIzka36Uk2Uuf5a8cHyDYhEgl5Hm5dXoe621KULeBjt+cFsY18mILsWWtJeG80w=="
"dependencies": {
"undici-types": "~5.26.4"
}
}, },
"node_modules/@types/prettier": { "node_modules/@types/prettier": {
"version": "2.7.2", "version": "2.7.2",
@ -1725,9 +1722,9 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"node_modules/basic-ftp": { "node_modules/basic-ftp": {
"version": "5.0.5", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.2.tgz",
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", "integrity": "sha512-NgkBwqp7rkhIUBaxLwL601lvUuBUvShJocrLYdiyTsH1WeP/wofMdooZ4p6hz+4hqiU59PBOj0EkaqELwFJLuQ==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
} }
@ -4732,11 +4729,6 @@
"node": ">=4.2.0" "node": ">=4.2.0"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "ftp-deploy-action", "name": "ftp-deploy-action",
"version": "4.3.5", "version": "4.3.4",
"private": true, "private": true,
"description": "Automate deploying websites and more with this GitHub action", "description": "Automate deploying websites and more with this GitHub action",
"main": "dist/index.js", "main": "dist/index.js",
@ -23,14 +23,14 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.9.1", "@actions/core": "^1.9.1",
"@samkirkland/ftp-deploy": "^1.2.4", "@samkirkland/ftp-deploy": "^1.2.3",
"@types/jest": "^29.4.1", "@types/jest": "^29.4.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"ts-jest": "^29.0.5", "ts-jest": "^29.0.5",
"ts-node-dev": "^2.0.0" "ts-node-dev": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.24", "@types/node": "^14.0.27",
"@typescript-eslint/eslint-plugin": "^5.33.1", "@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1", "@typescript-eslint/parser": "^5.33.1",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",