Merge branch 'main' into fix/clean-test-warnings

This commit is contained in:
Simone Taeggi 2022-10-18 10:24:07 +02:00
commit 1039e207c0
16 changed files with 342 additions and 124 deletions

View File

@ -110,6 +110,7 @@ testApp
license.txt license.txt
exports exports
*.cache *.cache
dist
build build
documentation documentation
.strapi-updater.json .strapi-updater.json

View File

@ -110,5 +110,6 @@ coverage
license.txt license.txt
exports exports
*.cache *.cache
dist
build build
.strapi-updater.json .strapi-updater.json

View File

@ -110,5 +110,6 @@ coverage
license.txt license.txt
exports exports
*.cache *.cache
dist
build build
.strapi-updater.json .strapi-updater.json

View File

@ -62,6 +62,15 @@ async function initProject(projectName, program) {
await checkInstallPath(resolve(projectName)); await checkInstallPath(resolve(projectName));
} }
const programFlags = program.options
.reduce((acc, { short, long }) => [...acc, short, long], [])
.filter(Boolean);
if (program.template && programFlags.includes(program.template)) {
console.error(`${program.template} is not a valid template`);
process.exit(1);
}
const hasDatabaseOptions = databaseOptions.some((opt) => program[opt]); const hasDatabaseOptions = databaseOptions.some((opt) => program[opt]);
if (program.quickstart && hasDatabaseOptions) { if (program.quickstart && hasDatabaseOptions) {

View File

@ -110,5 +110,6 @@ coverage
license.txt license.txt
exports exports
*.cache *.cache
dist
build build
.strapi-updater.json .strapi-updater.json

View File

@ -0,0 +1,110 @@
import React from 'react';
import semver from 'semver';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Tooltip } from '@strapi/design-system/Tooltip';
import { Button } from '@strapi/design-system/Button';
import { Box } from '@strapi/design-system/Box';
import Duplicate from '@strapi/icons/Duplicate';
const TooltipButton = ({ description, installMessage, disabled, handleCopy, pluginName }) => (
<Tooltip data-testid={`tooltip-${pluginName}`} description={description}>
<Box>
<Button
size="S"
startIcon={<Duplicate />}
variant="secondary"
disabled={disabled}
onClick={handleCopy}
>
{installMessage}
</Button>
</Box>
</Tooltip>
);
const CardButton = ({ strapiPeerDepVersion, strapiAppVersion, handleCopy, pluginName }) => {
const { formatMessage } = useIntl();
const versionRange = semver.validRange(strapiPeerDepVersion);
const isCompatible = semver.satisfies(strapiAppVersion, versionRange);
const installMessage = formatMessage({
id: 'admin.pages.MarketPlacePage.plugin.copy',
defaultMessage: 'Copy install command',
});
// Only plugins receive a strapiAppVersion
if (strapiAppVersion) {
if (!versionRange) {
return (
<TooltipButton
installMessage={installMessage}
pluginName={pluginName}
description={formatMessage(
{
id: 'admin.pages.MarketPlacePage.plugin.version.null',
defaultMessage:
'Unable to verify compatibility with your Strapi version: "{strapiAppVersion}"',
},
{ strapiAppVersion }
)}
handleCopy={handleCopy}
/>
);
}
if (!isCompatible) {
return (
<TooltipButton
installMessage={installMessage}
pluginName={pluginName}
description={formatMessage(
{
id: 'admin.pages.MarketPlacePage.plugin.version',
defaultMessage:
'Update your Strapi version: "{strapiAppVersion}" to: "{versionRange}"',
},
{
strapiAppVersion,
versionRange,
}
)}
disabled
/>
);
}
}
return (
<Button size="S" startIcon={<Duplicate />} variant="secondary" onClick={handleCopy}>
{installMessage}
</Button>
);
};
TooltipButton.defaultProps = {
disabled: false,
handleCopy: null,
};
TooltipButton.propTypes = {
description: PropTypes.string.isRequired,
installMessage: PropTypes.string.isRequired,
disabled: PropTypes.bool,
handleCopy: PropTypes.func,
pluginName: PropTypes.string.isRequired,
};
CardButton.defaultProps = {
strapiAppVersion: null,
strapiPeerDepVersion: null,
};
CardButton.propTypes = {
strapiAppVersion: PropTypes.string,
strapiPeerDepVersion: PropTypes.string,
handleCopy: PropTypes.func.isRequired,
pluginName: PropTypes.string.isRequired,
};
export default CardButton;

View File

@ -1,20 +1,34 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { useNotification, useTracking } from '@strapi/helper-plugin'; import { useNotification, useTracking } from '@strapi/helper-plugin';
import { Box } from '@strapi/design-system/Box'; import { Box } from '@strapi/design-system/Box';
import { Icon } from '@strapi/design-system/Icon'; import { Icon } from '@strapi/design-system/Icon';
import { Typography } from '@strapi/design-system/Typography'; import { Typography } from '@strapi/design-system/Typography';
import Check from '@strapi/icons/Check'; import Check from '@strapi/icons/Check';
import Duplicate from '@strapi/icons/Duplicate'; import CardButton from './CardButton';
import { Button } from '@strapi/design-system/Button';
const InstallPluginButton = ({ isInstalled, isInDevelopmentMode, commandToCopy }) => { const InstallPluginButton = ({
isInstalled,
isInDevelopmentMode,
commandToCopy,
strapiAppVersion,
strapiPeerDepVersion,
pluginName,
}) => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { trackUsage } = useTracking(); const { trackUsage } = useTracking();
const handleCopy = () => {
navigator.clipboard.writeText(commandToCopy);
trackUsage('willInstallPlugin');
toggleNotification({
type: 'success',
message: { id: 'admin.pages.MarketPlacePage.plugin.copy.success' },
});
};
// Already installed // Already installed
if (isInstalled) { if (isInstalled) {
return ( return (
@ -33,23 +47,12 @@ const InstallPluginButton = ({ isInstalled, isInDevelopmentMode, commandToCopy }
// In development, show install button // In development, show install button
if (isInDevelopmentMode) { if (isInDevelopmentMode) {
return ( return (
<CopyToClipboard <CardButton
onCopy={() => { strapiAppVersion={strapiAppVersion}
trackUsage('willInstallPlugin'); strapiPeerDepVersion={strapiPeerDepVersion}
toggleNotification({ handleCopy={handleCopy}
type: 'success', pluginName={pluginName}
message: { id: 'admin.pages.MarketPlacePage.plugin.copy.success' }, />
});
}}
text={commandToCopy}
>
<Button size="S" startIcon={<Duplicate />} variant="secondary">
{formatMessage({
id: 'admin.pages.MarketPlacePage.plugin.copy',
defaultMessage: 'Copy install command',
})}
</Button>
</CopyToClipboard>
); );
} }
@ -57,10 +60,18 @@ const InstallPluginButton = ({ isInstalled, isInDevelopmentMode, commandToCopy }
return null; return null;
}; };
InstallPluginButton.defaultProps = {
strapiAppVersion: null,
strapiPeerDepVersion: null,
};
InstallPluginButton.propTypes = { InstallPluginButton.propTypes = {
isInstalled: PropTypes.bool.isRequired, isInstalled: PropTypes.bool.isRequired,
isInDevelopmentMode: PropTypes.bool.isRequired, isInDevelopmentMode: PropTypes.bool.isRequired,
commandToCopy: PropTypes.string.isRequired, commandToCopy: PropTypes.string.isRequired,
strapiAppVersion: PropTypes.string,
strapiPeerDepVersion: PropTypes.string,
pluginName: PropTypes.string.isRequired,
}; };
export default InstallPluginButton; export default InstallPluginButton;

View File

@ -32,6 +32,7 @@ const NpmPackageCard = ({
useYarn, useYarn,
isInDevelopmentMode, isInDevelopmentMode,
npmPackageType, npmPackageType,
strapiAppVersion,
}) => { }) => {
const { attributes } = npmPackage; const { attributes } = npmPackage;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -139,6 +140,9 @@ const NpmPackageCard = ({
isInstalled={isInstalled} isInstalled={isInstalled}
isInDevelopmentMode={isInDevelopmentMode} isInDevelopmentMode={isInDevelopmentMode}
commandToCopy={commandToCopy} commandToCopy={commandToCopy}
strapiAppVersion={strapiAppVersion}
strapiPeerDepVersion={attributes.strapiVersion}
pluginName={attributes.name}
/> />
</Stack> </Stack>
</Flex> </Flex>
@ -147,6 +151,7 @@ const NpmPackageCard = ({
NpmPackageCard.defaultProps = { NpmPackageCard.defaultProps = {
isInDevelopmentMode: false, isInDevelopmentMode: false,
strapiAppVersion: null,
}; };
NpmPackageCard.propTypes = { NpmPackageCard.propTypes = {
@ -164,12 +169,14 @@ NpmPackageCard.propTypes = {
validated: PropTypes.bool.isRequired, validated: PropTypes.bool.isRequired,
madeByStrapi: PropTypes.bool.isRequired, madeByStrapi: PropTypes.bool.isRequired,
strapiCompatibility: PropTypes.oneOf(['v3', 'v4']), strapiCompatibility: PropTypes.oneOf(['v3', 'v4']),
strapiVersion: PropTypes.string,
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
isInstalled: PropTypes.bool.isRequired, isInstalled: PropTypes.bool.isRequired,
useYarn: PropTypes.bool.isRequired, useYarn: PropTypes.bool.isRequired,
isInDevelopmentMode: PropTypes.bool, isInDevelopmentMode: PropTypes.bool,
npmPackageType: PropTypes.string.isRequired, npmPackageType: PropTypes.string.isRequired,
strapiAppVersion: PropTypes.string,
}; };
export default NpmPackageCard; export default NpmPackageCard;

View File

@ -9,6 +9,7 @@ const NpmPackagesGrid = ({
useYarn, useYarn,
isInDevelopmentMode, isInDevelopmentMode,
npmPackageType, npmPackageType,
strapiAppVersion,
}) => { }) => {
// Check if an individual package is in the dependencies // Check if an individual package is in the dependencies
const isAlreadyInstalled = useCallback( const isAlreadyInstalled = useCallback(
@ -26,6 +27,7 @@ const NpmPackagesGrid = ({
useYarn={useYarn} useYarn={useYarn}
isInDevelopmentMode={isInDevelopmentMode} isInDevelopmentMode={isInDevelopmentMode}
npmPackageType={npmPackageType} npmPackageType={npmPackageType}
strapiAppVersion={strapiAppVersion}
/> />
</GridItem> </GridItem>
))} ))}
@ -35,6 +37,7 @@ const NpmPackagesGrid = ({
NpmPackagesGrid.defaultProps = { NpmPackagesGrid.defaultProps = {
installedPackageNames: [], installedPackageNames: [],
strapiAppVersion: null,
}; };
NpmPackagesGrid.propTypes = { NpmPackagesGrid.propTypes = {
@ -43,6 +46,7 @@ NpmPackagesGrid.propTypes = {
useYarn: PropTypes.bool.isRequired, useYarn: PropTypes.bool.isRequired,
isInDevelopmentMode: PropTypes.bool.isRequired, isInDevelopmentMode: PropTypes.bool.isRequired,
npmPackageType: PropTypes.string.isRequired, npmPackageType: PropTypes.string.isRequired,
strapiAppVersion: PropTypes.string,
}; };
export default NpmPackagesGrid; export default NpmPackagesGrid;

View File

@ -50,7 +50,7 @@ const MarketPlacePage = () => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [npmPackageType, setNpmPackageType] = useState('plugin'); const [npmPackageType, setNpmPackageType] = useState('plugin');
const { autoReload: isInDevelopmentMode, dependencies, useYarn } = useAppInfos(); const { autoReload: isInDevelopmentMode, dependencies, useYarn, strapiVersion } = useAppInfos();
const isOnline = useNavigatorOnLine(); const isOnline = useNavigatorOnLine();
useFocusWhenNavigate(); useFocusWhenNavigate();
@ -247,6 +247,7 @@ const MarketPlacePage = () => {
useYarn={useYarn} useYarn={useYarn}
isInDevelopmentMode={isInDevelopmentMode} isInDevelopmentMode={isInDevelopmentMode}
npmPackageType="plugin" npmPackageType="plugin"
strapiAppVersion={strapiVersion}
/> />
)} )}
</TabPanel> </TabPanel>

View File

@ -1223,6 +1223,12 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
</svg> </svg>
</div> </div>
</a> </a>
<span>
<div
aria-describedby="tooltip-3"
class=""
tabindex="0"
>
<button <button
aria-disabled="false" aria-disabled="false"
class="c51 c52" class="c51 c52"
@ -1256,6 +1262,8 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
</span> </span>
</button> </button>
</div> </div>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -1340,9 +1348,16 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
</svg> </svg>
</div> </div>
</a> </a>
<span>
<div
aria-describedby="tooltip-5"
class=""
tabindex="0"
>
<button <button
aria-disabled="false" aria-disabled="true"
class="c51 c52" class="c51 c52"
disabled=""
type="button" type="button"
> >
<div <div
@ -1373,6 +1388,8 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
</span> </span>
</button> </button>
</div> </div>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -1410,7 +1427,7 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
Documentation Documentation
<span> <span>
<div <div
aria-describedby="tooltip-3" aria-describedby="tooltip-7"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -1579,9 +1596,16 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
</svg> </svg>
</div> </div>
</a> </a>
<span>
<div
aria-describedby="tooltip-9"
class=""
tabindex="0"
>
<button <button
aria-disabled="false" aria-disabled="true"
class="c51 c52" class="c51 c52"
disabled=""
type="button" type="button"
> >
<div <div
@ -1612,6 +1636,8 @@ exports[`Marketplace page renders and matches the plugin tab snapshot 1`] = `
</span> </span>
</button> </button>
</div> </div>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -2724,7 +2750,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Amazon Ses Amazon Ses
<span> <span>
<div <div
aria-describedby="tooltip-9" aria-describedby="tooltip-21"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -2856,7 +2882,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
AWS S3 AWS S3
<span> <span>
<div <div
aria-describedby="tooltip-11" aria-describedby="tooltip-23"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -2988,7 +3014,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Cloudinary Cloudinary
<span> <span>
<div <div
aria-describedby="tooltip-13" aria-describedby="tooltip-25"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -3110,7 +3136,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Local Upload Local Upload
<span> <span>
<div <div
aria-describedby="tooltip-15" aria-describedby="tooltip-27"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -3242,7 +3268,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Mailgun Mailgun
<span> <span>
<div <div
aria-describedby="tooltip-17" aria-describedby="tooltip-29"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -3374,7 +3400,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Nodemailer Nodemailer
<span> <span>
<div <div
aria-describedby="tooltip-19" aria-describedby="tooltip-31"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -3506,7 +3532,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Rackspace Rackspace
<span> <span>
<div <div
aria-describedby="tooltip-21" aria-describedby="tooltip-33"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -3638,7 +3664,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
SendGrid SendGrid
<span> <span>
<div <div
aria-describedby="tooltip-23" aria-describedby="tooltip-35"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >
@ -3770,7 +3796,7 @@ exports[`Marketplace page renders and matches the provider tab snapshot 1`] = `
Sendmail Sendmail
<span> <span>
<div <div
aria-describedby="tooltip-25" aria-describedby="tooltip-37"
class="c43" class="c43"
tabindex="0" tabindex="0"
> >

View File

@ -8,6 +8,7 @@ import {
screen, screen,
getByText, getByText,
queryByText, queryByText,
getByRole,
} from '@testing-library/react'; } from '@testing-library/react';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query'; import { QueryClient, QueryClientProvider } from 'react-query';
@ -35,6 +36,7 @@ jest.mock('@strapi/helper-plugin', () => ({
'@strapi/plugin-documentation': '4.2.0', '@strapi/plugin-documentation': '4.2.0',
'@strapi/provider-upload-cloudinary': '4.2.0', '@strapi/provider-upload-cloudinary': '4.2.0',
}, },
strapiVersion: '4.1.0',
useYarn: true, useYarn: true,
})), })),
})); }));
@ -215,7 +217,7 @@ describe('Marketplace page', () => {
expect(pluginCardText).toEqual(null); expect(pluginCardText).toEqual(null);
}); });
it('shows the installed text for installed plugins', async () => { it('shows the installed text for installed plugins', () => {
render(App); render(App);
const pluginsTab = screen.getByRole('tab', { name: /plugins/i }); const pluginsTab = screen.getByRole('tab', { name: /plugins/i });
fireEvent.click(pluginsTab); fireEvent.click(pluginsTab);
@ -235,7 +237,7 @@ describe('Marketplace page', () => {
expect(notInstalledText).toBeVisible(); expect(notInstalledText).toBeVisible();
}); });
it('shows the installed text for installed providers', async () => { it('shows the installed text for installed providers', () => {
// Open providers tab // Open providers tab
render(App); render(App);
const providersTab = screen.getByRole('tab', { name: /providers/i }); const providersTab = screen.getByRole('tab', { name: /providers/i });
@ -255,4 +257,38 @@ describe('Marketplace page', () => {
const notInstalledText = queryByText(notInstalledCard, /copy install command/i); const notInstalledText = queryByText(notInstalledCard, /copy install command/i);
expect(notInstalledText).toBeVisible(); expect(notInstalledText).toBeVisible();
}); });
it('disables the button and shows compatibility tooltip message when version provided', async () => {
const { getByTestId } = render(App);
const alreadyInstalledCard = screen
.getAllByTestId('npm-package-card')
.find((div) => div.innerHTML.includes('Transformer'));
const button = getByRole(alreadyInstalledCard, 'button', { name: /copy install command/i });
const tooltip = getByTestId(`tooltip-Transformer`);
fireEvent.mouseOver(button);
await waitFor(() => {
expect(tooltip).toBeVisible();
});
expect(button).toBeDisabled();
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent('Update your Strapi version: "4.1.0" to: "4.0.7"');
});
it('shows compatibility tooltip message when no version provided', async () => {
const { getByTestId } = render(App);
const alreadyInstalledCard = screen
.getAllByTestId('npm-package-card')
.find((div) => div.innerHTML.includes('Config Sync'));
const button = getByRole(alreadyInstalledCard, 'button', { name: /copy install command/i });
const tooltip = getByTestId(`tooltip-Config Sync`);
fireEvent.mouseOver(button);
await waitFor(() => {
expect(tooltip).toBeVisible();
});
expect(button).not.toBeDisabled();
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent(
'Unable to verify compatibility with your Strapi version: "4.1.0"'
);
});
}); });

View File

@ -48,6 +48,7 @@ const handlers = [
validated: false, validated: false,
madeByStrapi: false, madeByStrapi: false,
strapiCompatibility: 'v3', strapiCompatibility: 'v3',
strapiVersion: '^4.0.0',
}, },
}, },
{ {
@ -221,6 +222,7 @@ const handlers = [
validated: false, validated: false,
madeByStrapi: false, madeByStrapi: false,
strapiCompatibility: 'v4', strapiCompatibility: 'v4',
strapiVersion: '4.x.x',
}, },
}, },
{ {
@ -291,6 +293,7 @@ const handlers = [
validated: true, validated: true,
madeByStrapi: false, madeByStrapi: false,
strapiCompatibility: 'v4', strapiCompatibility: 'v4',
strapiVersion: 'Contact developer',
}, },
}, },
{ {
@ -362,6 +365,7 @@ const handlers = [
validated: false, validated: false,
madeByStrapi: false, madeByStrapi: false,
strapiCompatibility: 'v4', strapiCompatibility: 'v4',
strapiVersion: '^3.4.2',
}, },
}, },
{ {
@ -404,6 +408,7 @@ const handlers = [
validated: true, validated: true,
madeByStrapi: true, madeByStrapi: true,
strapiCompatibility: 'v4', strapiCompatibility: 'v4',
strapiVersion: '^4.0.7',
}, },
}, },
{ {
@ -446,6 +451,7 @@ const handlers = [
validated: true, validated: true,
madeByStrapi: false, madeByStrapi: false,
strapiCompatibility: 'v3', strapiCompatibility: 'v3',
strapiVersion: '^4.3.0',
}, },
}, },
{ {
@ -488,6 +494,7 @@ const handlers = [
validated: false, validated: false,
madeByStrapi: false, madeByStrapi: false,
strapiCompatibility: 'v4', strapiCompatibility: 'v4',
strapiVersion: '4.0.7',
}, },
}, },
], ],

View File

@ -270,6 +270,8 @@
"admin.pages.MarketPlacePage.plugin.installed": "Installed", "admin.pages.MarketPlacePage.plugin.installed": "Installed",
"admin.pages.MarketPlacePage.plugin.tooltip.madeByStrapi": "Made by Strapi", "admin.pages.MarketPlacePage.plugin.tooltip.madeByStrapi": "Made by Strapi",
"admin.pages.MarketPlacePage.plugin.tooltip.verified": "Plugin verified by Strapi", "admin.pages.MarketPlacePage.plugin.tooltip.verified": "Plugin verified by Strapi",
"admin.pages.MarketPlacePage.plugin.version": "Update your Strapi version: \"{strapiAppVersion}\" to: \"{versionRange}\"",
"admin.pages.MarketPlacePage.plugin.version.null": "Unable to verify compatibility with your Strapi version: \"{strapiAppVersion}\"",
"admin.pages.MarketPlacePage.providers": "Providers", "admin.pages.MarketPlacePage.providers": "Providers",
"admin.pages.MarketPlacePage.search.clear": "Clear the search", "admin.pages.MarketPlacePage.search.clear": "Clear the search",
"admin.pages.MarketPlacePage.search.empty": "No result for \"{target}\"", "admin.pages.MarketPlacePage.search.empty": "No result for \"{target}\"",

View File

@ -110,5 +110,6 @@ coverage
license.txt license.txt
exports exports
*.cache *.cache
dist
build build
.strapi-updater.json .strapi-updater.json