Update plugins installer component to TS, Fix TS bugs and Syncpack TypeScript (#34787)

This commit is contained in:
Joshua T Flowers 2022-09-29 14:59:07 -07:00 committed by GitHub
parent 4ccb2b478a
commit 3fd736c72f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 1645 additions and 2103 deletions

View File

@ -1,6 +1,6 @@
{
"dev": true,
"filter": "^(?:react|react-dom)$",
"filter": "^(?:react|react-dom|typescript)$",
"indent": "\t",
"overrides": true,
"peer": true,
@ -10,9 +10,23 @@
"workspace": true,
"versionGroups": [
{
"dependencies": [ "react", "react-dom" ],
"packages": [ "**" ],
"dependencies": [
"react",
"react-dom"
],
"packages": [
"**"
],
"pinVersion": "^17.0.2"
},
{
"dependencies": [
"typescript"
],
"packages": [
"**"
],
"pinVersion": "^4.8.3"
}
]
}

View File

@ -51,7 +51,7 @@
"sass-loader": "^10.2.1",
"syncpack": "^8.2.4",
"turbo": "^1.4.5",
"typescript": "4.2.4",
"typescript": "^4.8.3",
"url-loader": "^1.1.2",
"webpack": "^5.70.0"
},

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -50,7 +50,7 @@
"jest-mock-extended": "^1.0.18",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"publishConfig": {
"access": "public"

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -57,7 +57,7 @@
"eslint": "^8.2.0",
"jest": "^27",
"ts-jest": "^27",
"typescript": "^4.4.4"
"typescript": "^4.8.3"
},
"publishConfig": {
"access": "public"

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Update Plugin installer component to TS

View File

@ -107,6 +107,7 @@
"@types/react": "^17.0.0",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/wordpress__components": "^19.10.1",
"@types/wordpress__data": "^6.0.0",
"@types/wordpress__media-utils": "^3.0.0",
"@types/wordpress__viewport": "^2.5.4",
"@woocommerce/eslint-plugin": "workspace:*",
@ -123,7 +124,7 @@
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},

View File

@ -1,213 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { createElement, Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import PropTypes from 'prop-types';
import { withSelect, withDispatch } from '@wordpress/data';
import { PLUGINS_STORE_NAME } from '@woocommerce/data';
export class Plugins extends Component {
constructor() {
super( ...arguments );
this.state = {
hasErrors: false,
};
this.installAndActivate = this.installAndActivate.bind( this );
this.skipInstaller = this.skipInstaller.bind( this );
this.handleErrors = this.handleErrors.bind( this );
this.handleSuccess = this.handleSuccess.bind( this );
}
componentDidMount() {
const { autoInstall } = this.props;
if ( autoInstall ) {
this.installAndActivate();
}
}
async installAndActivate( event ) {
if ( event ) {
event.preventDefault();
}
const { installAndActivatePlugins, isRequesting, pluginSlugs } =
this.props;
// Avoid double activating.
if ( isRequesting ) {
return false;
}
installAndActivatePlugins( pluginSlugs )
.then( ( response ) => {
this.handleSuccess( response.data.activated, response );
} )
.catch( ( response ) => {
this.handleErrors( response.errors, response );
} );
}
handleErrors( errors, response ) {
const { onError } = this.props;
this.setState( { hasErrors: true } );
onError( errors, response );
}
handleSuccess( activePlugins, response ) {
const { onComplete } = this.props;
onComplete( activePlugins, response );
}
skipInstaller() {
if ( this.props.onSkip ) {
this.props.onSkip();
}
}
render() {
const {
isRequesting,
skipText,
autoInstall,
pluginSlugs,
onSkip,
onAbort,
abortText,
} = this.props;
const { hasErrors } = this.state;
if ( hasErrors ) {
return (
<Fragment>
<Button
isPrimary
isBusy={ isRequesting }
onClick={ this.installAndActivate }
>
{ __( 'Retry', 'woocommerce' ) }
</Button>
{ onSkip && (
<Button onClick={ this.skipInstaller }>
{ __(
'Continue without installing',
'woocommerce'
) }
</Button>
) }
</Fragment>
);
}
if ( autoInstall ) {
return null;
}
if ( pluginSlugs.length === 0 ) {
return (
<Fragment>
<Button
isPrimary
isBusy={ isRequesting }
onClick={ this.skipInstaller }
>
{ __( 'Continue', 'woocommerce' ) }
</Button>
</Fragment>
);
}
return (
<Fragment>
<Button
isBusy={ isRequesting }
isPrimary
onClick={ this.installAndActivate }
>
{ __( 'Install & enable', 'woocommerce' ) }
</Button>
{ onSkip && (
<Button isTertiary onClick={ this.skipInstaller }>
{ skipText || __( 'No thanks', 'woocommerce' ) }
</Button>
) }
{ onAbort && (
<Button isTertiary onClick={ onAbort }>
{ abortText || __( 'Abort', 'woocommerce' ) }
</Button>
) }
</Fragment>
);
}
}
Plugins.propTypes = {
/**
* Called when the plugin installer is successfully completed.
*/
onComplete: PropTypes.func.isRequired,
/**
* Called when the plugin installer completes with an error.
*/
onError: PropTypes.func,
/**
* Called when the plugin installer is skipped.
*/
onSkip: PropTypes.func,
/**
* Text used for the skip installer button.
*/
skipText: PropTypes.string,
/**
* If installation should happen automatically, or require user confirmation.
*/
autoInstall: PropTypes.bool,
/**
* An array of plugin slugs to install.
*/
pluginSlugs: PropTypes.arrayOf( PropTypes.string ),
/**
* Called when the plugin connection is aborted.
*/
onAbort: PropTypes.func,
/**
* Text used for the abort connection button.
*/
abortText: PropTypes.string,
};
Plugins.defaultProps = {
autoInstall: false,
onError: () => {},
pluginSlugs: [ 'jetpack', 'woocommerce-services' ],
};
export default compose(
withSelect( ( select ) => {
const { getActivePlugins, getInstalledPlugins, isPluginsRequesting } =
select( PLUGINS_STORE_NAME );
const isRequesting =
isPluginsRequesting( 'activatePlugins' ) ||
isPluginsRequesting( 'installPlugins' );
return {
isRequesting,
activePlugins: getActivePlugins(),
installedPlugins: getInstalledPlugins(),
};
} ),
withDispatch( ( dispatch ) => {
const { installAndActivatePlugins } = dispatch( PLUGINS_STORE_NAME );
return {
installAndActivatePlugins,
};
} )
)( Plugins );

View File

@ -0,0 +1,154 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import {
createElement,
Fragment,
useState,
useEffect,
} from '@wordpress/element';
import { SyntheticEvent } from 'react';
import { useDispatch, useSelect } from '@wordpress/data';
import { PLUGINS_STORE_NAME, InstallPluginsResponse } from '@woocommerce/data';
type PluginsProps = {
onComplete: (
activePlugins: string[],
response: InstallPluginsResponse
) => void;
onError: ( errors: unknown, response: InstallPluginsResponse ) => void;
onSkip?: () => void;
skipText?: string;
autoInstall?: boolean;
pluginSlugs?: string[];
onAbort?: () => void;
abortText?: string;
};
export const Plugins = ( {
autoInstall = false,
onAbort,
onComplete,
onError = () => null,
pluginSlugs = [ 'jetpack', 'woocommerce-services' ],
onSkip,
skipText = __( 'No thanks', 'woocommerce' ),
abortText = __( 'Abort', 'woocommerce' ),
}: PluginsProps ) => {
const [ hasErrors, setHasErrors ] = useState( false );
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
const { isRequesting } = useSelect( ( select ) => {
const { getActivePlugins, getInstalledPlugins, isPluginsRequesting } =
select( PLUGINS_STORE_NAME );
return {
isRequesting:
isPluginsRequesting( 'activatePlugins' ) ||
isPluginsRequesting( 'installPlugins' ),
activePlugins: getActivePlugins(),
installedPlugins: getInstalledPlugins(),
};
} );
const handleErrors = (
errors: unknown,
response: InstallPluginsResponse
) => {
setHasErrors( true );
onError( errors, response );
};
const handleSuccess = (
plugins: string[],
response: InstallPluginsResponse
) => {
onComplete( plugins, response );
};
const installAndActivate = async (
event?: SyntheticEvent< HTMLAnchorElement >
) => {
if ( event ) {
event.preventDefault();
}
// Avoid double activating.
if ( isRequesting ) {
return false;
}
installAndActivatePlugins( pluginSlugs )
.then( ( response ) => {
handleSuccess( response.data.activated, response );
} )
.catch( ( response ) => {
handleErrors( response.errors, response );
} );
};
useEffect( () => {
if ( autoInstall ) {
installAndActivate();
}
}, [] );
if ( hasErrors ) {
return (
<>
<Button
isPrimary
isBusy={ isRequesting }
onClick={ installAndActivate }
>
{ __( 'Retry', 'woocommerce' ) }
</Button>
{ onSkip && (
<Button onClick={ onSkip }>
{ __( 'Continue without installing', 'woocommerce' ) }
</Button>
) }
</>
);
}
if ( autoInstall ) {
return null;
}
if ( ! pluginSlugs.length ) {
return (
<Fragment>
<Button isPrimary isBusy={ isRequesting } onClick={ onSkip }>
{ __( 'Continue', 'woocommerce' ) }
</Button>
</Fragment>
);
}
return (
<>
<Button
isBusy={ isRequesting }
isPrimary
onClick={ installAndActivate }
>
{ __( 'Install & enable', 'woocommerce' ) }
</Button>
{ onSkip && (
<Button isTertiary onClick={ onSkip }>
{ skipText }
</Button>
) }
{ onAbort && (
<Button isTertiary onClick={ onAbort }>
{ abortText }
</Button>
) }
</>
);
};
export default Plugins;

View File

@ -4,16 +4,31 @@
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createElement } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { Plugins } from '../index.js';
import { Plugins } from '../index';
jest.mock( '@wordpress/data', () => ( {
...jest.requireActual( '@wordpress/data' ),
useDispatch: jest
.fn()
.mockReturnValue( { installAndActivatePlugins: jest.fn() } ),
useSelect: jest.fn().mockReturnValue( false ),
} ) );
describe( 'Rendering', () => {
afterAll( () => {
jest.restoreAllMocks();
} );
it( 'should render nothing when autoInstalling', async () => {
const installAndActivatePlugins = jest.fn().mockResolvedValue( {
const { installAndActivatePlugins } = useDispatch();
installAndActivatePlugins.mockResolvedValue( {
success: true,
data: {
activated: [ 'jetpack' ],
@ -26,7 +41,6 @@ describe( 'Rendering', () => {
autoInstall
pluginSlugs={ [ 'jetpack' ] }
onComplete={ onComplete }
installAndActivatePlugins={ installAndActivatePlugins }
/>
);
@ -93,21 +107,18 @@ describe( 'Installing and activating', () => {
activated: [ 'jetpack' ],
},
};
const installAndActivatePlugins = jest
.fn()
.mockResolvedValue( response );
const onComplete = jest.fn();
const { getByRole } = render(
<Plugins
pluginSlugs={ [ 'jetpack' ] }
onComplete={ onComplete }
installAndActivatePlugins={ installAndActivatePlugins }
/>
<Plugins pluginSlugs={ [ 'jetpack' ] } onComplete={ onComplete } />
);
userEvent.click( getByRole( 'button', { name: 'Install & enable' } ) );
// Get the mocked installAndActivatePlugins function.
const { installAndActivatePlugins } = useDispatch();
installAndActivatePlugins.mockResolvedValue( response );
expect( installAndActivatePlugins ).toHaveBeenCalledWith( [
'jetpack',
] );
@ -119,15 +130,17 @@ describe( 'Installing and activating', () => {
} );
describe( 'Installing and activating errors', () => {
it( 'should call installAndActivatePlugins and onComplete', async () => {
it( 'should call installAndActivatePlugins and onError', async () => {
const response = {
errors: {
'failed-plugin': [ 'error message' ],
},
};
const installAndActivatePlugins = jest
.fn()
.mockRejectedValue( response );
// Get the mocked installAndActivatePlugins function.
const { installAndActivatePlugins } = useDispatch();
installAndActivatePlugins.mockRejectedValue( response );
const onComplete = jest.fn();
const onError = jest.fn();
@ -135,7 +148,6 @@ describe( 'Installing and activating errors', () => {
<Plugins
pluginSlugs={ [ 'jetpack' ] }
onComplete={ onComplete }
installAndActivatePlugins={ installAndActivatePlugins }
onError={ onError }
/>
);

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -55,7 +55,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -58,7 +58,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -59,7 +59,7 @@
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Add types to plugin response data

View File

@ -69,7 +69,7 @@
"redux": "^4.1.0",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"peerDependencies": {
"@wordpress/core-data": "^4.1.0",

View File

@ -31,6 +31,8 @@ export const controls = {
setTimeout( function () {
if (
fetches.hasOwnProperty( optionName ) &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO - this type bug needs to be fixed.
fetches[ optionName ]
) {
return fetches[ optionName ].then( ( result ) => {

View File

@ -76,6 +76,7 @@ export type InstallPluginsResponse = PluginsResponse< {
installed: string[];
results: Record< string, boolean >;
install_time?: Record< string, number >;
activated: string[];
} >;
export type ActivatePluginsResponse = PluginsResponse< {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -45,7 +45,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"peerDependencies": {
"lodash": "^4.17.0"

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -35,7 +35,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -52,7 +52,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -72,7 +72,7 @@
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -50,7 +50,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"scripts": {
"turbo:build": "tsc --build ./tsconfig.json ./tsconfig-cjs.json",

View File

@ -45,7 +45,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -45,7 +45,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"webpack": "^5.70.0"
},
"lint-staged": {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -68,7 +68,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -62,7 +62,7 @@
"redux": "^4.2.0",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -55,7 +55,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -55,7 +55,7 @@
"rimraf": "^3.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},

View File

@ -20,9 +20,8 @@ export const trackView = async ( taskId: string, variant?: string ) => {
.select( 'wc/admin/plugins' )
.getInstalledPlugins();
const isJetpackConnected: boolean = wp.data
.select( 'wc/admin/plugins' )
.isJetpackConnected();
const isJetpackConnected: boolean =
wp.data.select( 'wc/admin/plugins' ).isJetpackConnected() || false;
recordEvent( 'task_view', {
task_name: taskId,

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -51,7 +51,7 @@
"jest-cli": "^27.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2"
"typescript": "^4.8.3"
},
"lint-staged": {
"*.(t|j)s?(x)": [

View File

@ -23,6 +23,9 @@ jest.mock( '@wordpress/data', () => ( {
...jest.requireActual( '@wordpress/data' ),
useSelect: jest.fn().mockImplementation( ( fn ) =>
fn( () => ( {
getActivePlugins: jest.fn().mockReturnValue( [] ),
getInstalledPlugins: jest.fn().mockReturnValue( [] ),
isPluginsRequesting: jest.fn().mockReturnValue( false ),
getSettings: () => ( {
general: {
woocommerce_default_country: 'US',

View File

@ -21,6 +21,8 @@ export const createOrderedChildren = (
if ( typeof children === 'function' ) {
return cloneElement( children( props ), { order } );
} else if ( isValidElement( children ) ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return cloneElement( children, { ...props, order } );
}
throw Error( 'Invalid children type' );

View File

@ -204,7 +204,7 @@
"style-loader": "^0.23.1",
"stylelint": "^14.5.3",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"typescript": "^4.8.3",
"url-loader": "^1.1.2",
"webpack": "^5.70.0",
"webpack-bundle-analyzer": "^3.9.0",

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Match TypeScript version with syncpack

View File

@ -83,7 +83,7 @@
"mocha": "7.2.0",
"prettier": "npm:wp-prettier@2.0.5",
"stylelint": "^13.8.0",
"typescript": "3.9.7",
"typescript": "^4.8.3",
"uuid": "^8.3.2",
"webpack": "5.70.0",
"webpack-cli": "3.3.12",

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
},
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
"typescript": "^4.7.4"
"typescript": "^4.8.3"
},
"dependencies": {
"chalk": "^4.1.2",

View File

@ -21,7 +21,7 @@
"eslint": "^7.32.0",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typescript": "^4.4.3"
"typescript": "^4.8.3"
},
"scripts": {
"lint": "eslint . --ext .ts --config .eslintrc",

View File

@ -32,7 +32,7 @@
"shx": "^0.3.3",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typescript": "^4.4.3"
"typescript": "^4.8.3"
},
"oclif": {
"bin": "monorepo-merge",

View File

@ -31,7 +31,7 @@
"shx": "^0.3.3",
"ts-node": "^10.2.1",
"tslib": "^2.3.1",
"typescript": "^4.4.3"
"typescript": "^4.8.3"
},
"oclif": {
"bin": "package-release",

View File

@ -15,7 +15,7 @@
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
"@types/express": "^4.17.13",
"typescript": "^4.7.4"
"typescript": "^4.8.3"
},
"dependencies": {
"@commander-js/extra-typings": "^0.1.0",

View File

@ -44,7 +44,7 @@
"@storybook/react": "^6.4.19",
"@storybook/theming": "^6.4.19",
"@woocommerce/eslint-plugin": "workspace:*",
"typescript": "4.2.4",
"typescript": "^4.8.3",
"webpack": "^5.70.0"
},
"dependencies": {

View File

@ -12,7 +12,7 @@
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
"@types/express": "^4.17.13",
"typescript": "^4.7.4"
"typescript": "^4.8.3"
},
"dependencies": {
"@commander-js/extra-typings": "^0.1.0",