Skip to content

Commit

Permalink
fix: support host option for all commands [EXT-5971] (#2325)
Browse files Browse the repository at this point in the history
* fix: pass host value to getAppInfo for interactive modes

* fix: use correct hostname in success messages

* feat: update open settings and install to support host

* chore: update README

* chore: update host description to subdomain
  • Loading branch information
whitelisab authored Feb 7, 2025
1 parent b76050f commit a292360
Show file tree
Hide file tree
Showing 15 changed files with 231 additions and 87 deletions.
28 changes: 24 additions & 4 deletions packages/contentful--app-scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ You can also execute this command without the argument if the environment variab
> $ CONTENTFUL_APP_DEF_ID=some-definition-id npx --no-install @contentful/app-scripts open-settings
> ```
**Options:**
| Argument | Description | Default value |
| ----------------- | -------------------------------------------- | -------------------- |
| |
| `--definition-id` | The ID of the app to which to add the bundle |
| `--host` | (optional) Contentful CMA-endpoint to use | `api.contentful.com` |
**Note:** You can also pass all arguments in interactive mode to skip being asked for it.
### Clean up bundles
Allows you to clean the list of previous bundles. It fetches the list and deletes all bundles except the 50 newest ones.
Expand Down Expand Up @@ -246,6 +256,16 @@ By default, the script will install the app into the default host URL: `app.cont
> $ npx --no-install @contentful/app-scripts install --definition-id some-definition-id --host api.eu.contentful.com
> ```
**Options:**
| Argument | Description | Default value |
| ----------------- | -------------------------------------------- | -------------------- |
| |
| `--definition-id` | The ID of the app to which to add the bundle |
| `--host` | (optional) Contentful CMA-endpoint to use | `api.contentful.com` |
**Note:** You can also pass all arguments in interactive mode to skip being asked for it.
### Tracking
We gather depersonalized usage data of our CLI tools in order to improve experience. If you do not want your data to be gathered, you can opt out by providing an env variable `DISABLE_ANALYTICS` set to any value:
Expand Down Expand Up @@ -286,7 +306,7 @@ When passing the `--ci` argument adding all variables as arguments is required
**Options:**
Options:
-e, --esbuild-config <path> custom esbuild config file path
-m, --manifest-file <path> Contentful app manifest file path
-w, --watch watch for changes
-h, --help display help for command
-e, --esbuild-config <path> custom esbuild config file path
-m, --manifest-file <path> Contentful app manifest file path
-w, --watch watch for changes
-h, --help display help for command
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getAppInfo } from '../get-app-info';
import { ActivateOptions, ActivateSettings } from '../types';

export async function buildBundleActivateSettings(
options: ActivateOptions,
options: ActivateOptions
): Promise<ActivateSettings> {
const { bundleId, host } = options;
const prompts = [];
Expand All @@ -23,12 +23,13 @@ export async function buildBundleActivateSettings(
});
}

const appActivateSettings = await inquirer.prompt(prompts);
const appInfo = await getAppInfo(options);
const { host: interactiveHost, ...appActivateSettings } = await inquirer.prompt(prompts);
const hostValue = host || interactiveHost;
const appInfo = await getAppInfo({ ...options, host: hostValue });

return {
bundleId,
host,
host: hostValue,
...appActivateSettings,
...appInfo,
};
Expand Down
9 changes: 5 additions & 4 deletions packages/contentful--app-scripts/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async function runCommand(command: Command, options?: any) {
.option('--token [accessToken]', 'Your content management access token')
.option('--comment [comment]', 'Optional comment for the created bundle')
.option('--skip-activation', 'A Boolean flag to skip automatic activation')
.option('--host [host]', 'Contentful domain to use')
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
.action(async (options) => {
await runCommand(upload, options);
});
Expand All @@ -60,7 +60,7 @@ async function runCommand(command: Command, options?: any) {
.option('--organization-id [orgId]', 'The id of your organization')
.option('--definition-id [defId]', 'The id of your apps definition')
.option('--token [accessToken]', 'Your content management access token')
.option('--host [host]', 'Contentful domain to use')
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
.action(async (options) => {
await runCommand(activate, options);
});
Expand All @@ -69,6 +69,7 @@ async function runCommand(command: Command, options?: any) {
.command('open-settings')
.description('Opens the app editor for a given AppDefinition')
.option('--definition-id [defId]', 'The id of your apps definition')
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
.action(async (options) => {
await runCommand(open, options);
});
Expand All @@ -80,7 +81,7 @@ async function runCommand(command: Command, options?: any) {
.option('--definition-id [defId]', 'The id of your apps definition')
.option('--token [accessToken]', 'Your content management access token')
.option('--keep [keepAmount]', 'The amount of bundles that should remain')
.option('--host [host]', 'Contentful domain to use')
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
.action(async (options) => {
await runCommand(cleanup, options);
});
Expand All @@ -98,7 +99,7 @@ async function runCommand(command: Command, options?: any) {
'Opens a picker to select the space and environment for installing the app associated with a given AppDefinition'
)
.option('--definition-id [defId]', 'The id of your apps definition')
.option('--host [host]', 'Contentful domain to use')
.option('--host [host]', 'Contentful subdomain to use, e.g. "api.contentful.com"')
.action(async (options) => {
await runCommand(install, options);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export async function buildCleanUpSettings(options: CleanupOptions): Promise<Cle
});
}

const appCleanUpSettings = await prompt(prompts);
const appInfo = await getAppInfo(options);
const { host: interactiveHost, ...appCleanUpSettings } = await prompt(prompts);
const hostValue = host || interactiveHost;
const appInfo = await getAppInfo({ ...options, host: hostValue });

return {
keep: keep === undefined ? +appCleanUpSettings.keep : +keep,
host,
host: hostValue,
...appCleanUpSettings,
...appInfo,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ describe('createAppDefinition', () => {
selectFromListMock.returns({ name: 'name', value: organizationId });

await assert.rejects(() => subject(token, { locations: [] }));
assert((console.log as SinonStub).calledWith(match(/Something went wrong while creating the app definition/)));
assert(
(console.log as SinonStub).calledWith(
match(/Something went wrong while creating the app definition/)
)
);
});

it('logs success message', async () => {
it('logs success message with default host', async () => {
const appId = 'appId';
const orgSettingsLink = 'https://app.contentful.com/deeplink?link=org';
const orgSettingsLink = 'https://app.contentful.com/deeplink?link=app-definition-list';
const appLink = `https://app.contentful.com/deeplink?link=apps&id=${appId}`;
const tutorialLink = 'https://ctfl.io/app-tutorial';

Expand All @@ -93,6 +97,33 @@ describe('createAppDefinition', () => {
assert.deepStrictEqual(cachedEnvVarsMock.args[0][0], { [APP_DEF_ENV_KEY]: 'appId' });
});

it('logs success message with EU host option', async () => {
const appId = 'appId';
const orgSettingsLink = 'https://app.eu.contentful.com/deeplink?link=app-definition-list';
const appLink = `https://app.eu.contentful.com/deeplink?link=apps&id=${appId}`;
const tutorialLink = 'https://ctfl.io/app-tutorial';

clientMock.getOrganization = stub().resolves({
createAppDefinition: stub().resolves({ sys: { id: 'appId' } }),
});
clientMock.getOrganizations = stub().resolves({
items: [{ name: 'name', sys: { id: organizationId } }],
});
selectFromListMock.returns({ name: 'name', value: organizationId });

await assert.doesNotReject(() =>
subject(token, { locations: [], host: 'api.eu.contentful.com' })
);

const loggedMessage = (console.log as SinonStub).getCall(0).args[0];

assert(loggedMessage.includes('Success'));
assert(loggedMessage.includes(orgSettingsLink));
assert(loggedMessage.includes(appLink));
assert(loggedMessage.includes(tutorialLink));
assert.deepStrictEqual(cachedEnvVarsMock.args[0][0], { [APP_DEF_ENV_KEY]: 'appId' });
});

it('sets default src if any frontend location is specified', async () => {
const createAppDefinitionStub = stub().resolves({ sys: { id: 'testId' } });
clientMock.getOrganization = stub().resolves({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ClientAPI, createClient } from 'contentful-management';
import chalk from 'chalk';
import { isString, isPlainObject, has } from 'lodash';

import { throwValidationException, selectFromList } from '../utils';
import { throwValidationException, selectFromList, getWebAppHostname } from '../utils';
import { cacheEnvVars } from '../cache-credential';
import { ORG_ID_ENV_KEY, APP_DEF_ENV_KEY } from '../constants';
import { AppDefinitionSettings } from './build-app-definition-settings';
Expand Down Expand Up @@ -62,7 +62,8 @@ export async function createAppDefinition(
) {
assertValidArguments(accessToken, appDefinitionSettings);

const client = createClient({ accessToken, host: appDefinitionSettings.host });
const host = appDefinitionSettings.host;
const client = createClient({ accessToken, host });
const organizations = await fetchOrganizations(client);

const selectedOrg = await selectFromList(
Expand Down Expand Up @@ -100,7 +101,7 @@ export async function createAppDefinition(
return {
location,
};
})
});
const hasFrontendLocation = locations.some(({ location }) => location !== 'dialog');
const body = {
name: appName,
Expand All @@ -123,20 +124,20 @@ export async function createAppDefinition(
[APP_DEF_ENV_KEY]: createdAppDefinition.sys.id,
});

const webApp = getWebAppHostname(host);

console.log(`
${chalk.greenBright('Success!')} Created an app definition for ${chalk.bold(
appName
)} in ${chalk.bold(selectedOrg.name)}.
${chalk.dim(`NOTE: You can update this app definition in your organization settings:
${chalk.underline(`https://app.contentful.com/deeplink?link=org`)}`)}
${chalk.dim(`NOTE: You can update this app definition in your apps settings:
${chalk.underline(`https://${webApp}/deeplink?link=app-definition-list`)}`)}
${chalk.bold('Next steps:')}
1. Run your app with ${chalk.cyan('`npm start`')} inside of your app folder.
2. Install this app definition to one of your spaces by opening:
${chalk.underline(
`https://app.contentful.com/deeplink?link=apps&id=${createdAppDefinition.sys.id}`
)}
${chalk.underline(`https://${webApp}/deeplink?link=apps&id=${createdAppDefinition.sys.id}`)}
3. Learn how to build your first Contentful app:
${chalk.underline(`https://ctfl.io/app-tutorial`)}
`);
Expand Down
42 changes: 26 additions & 16 deletions packages/contentful--app-scripts/src/install/install.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { SinonStub, stub } from 'sinon';
import assert from 'assert';
import { APP_DEF_ENV_KEY } from '../constants';
import {
APP_DEF_ENV_KEY,
DEFAULT_CONTENTFUL_API_HOST,
DEFAULT_CONTENTFUL_APP_HOST,
} from '../constants';
import proxyquire from 'proxyquire';

const TEST_DEF_ID = 'test-def-id';
const TEST_HOST = 'test.host.com';

describe('install', () => {
let subject: typeof import('./install').installToEnvironment,
Expand All @@ -29,33 +32,40 @@ describe('install', () => {
}));
});

it('works with an app ID option passed', () => {
subject({ definitionId: TEST_DEF_ID });
it('works with both host and app ID options passed', async () => {
await subject({ host: DEFAULT_CONTENTFUL_API_HOST, definitionId: TEST_DEF_ID });
assert(
installMock.calledWith(`https://app.contentful.com/deeplink?link=apps&id=${TEST_DEF_ID}`)
installMock.calledWith(
`https://${DEFAULT_CONTENTFUL_APP_HOST}/deeplink?link=apps&id=${TEST_DEF_ID}`
)
);
});

it('works with both host and app ID options passed', () => {
subject({ host: TEST_HOST, definitionId: TEST_DEF_ID });
assert(installMock.calledWith(`https://${TEST_HOST}/deeplink?link=apps&id=${TEST_DEF_ID}`));
});

it('shows prompt when no app definition is provided', () => {
it('shows prompt when no options are provided', () => {
subject({});
assert.strictEqual(inquirerMock.called, true);
});

it('shows prompt when host is provided, but no app definition', () => {
subject({ host: TEST_HOST });
assert.strictEqual(inquirerMock.called, true);
it('throws an error when no app definition is defined', async () => {
try {
await subject({});
} catch (err) {
assert.strictEqual(err.message, 'No app-definition-id');
}
});

it('works with env variable set', () => {
it('works with env variable set', async () => {
process.env[APP_DEF_ENV_KEY] = TEST_DEF_ID;
subject({});
await subject({});
assert(
installMock.calledWith(`https://app.contentful.com/deeplink?link=apps&id=${TEST_DEF_ID}`)
);
});

it('works with EU host option passed', async () => {
await subject({ definitionId: TEST_DEF_ID, host: 'api.eu.contentful.com' });
assert(
installMock.calledWith(`https://app.eu.contentful.com/deeplink?link=apps&id=${TEST_DEF_ID}`)
);
});
});
34 changes: 23 additions & 11 deletions packages/contentful--app-scripts/src/install/install.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
import open from 'open';
import chalk from 'chalk';
import inquirer from 'inquirer';
import { APP_DEF_ENV_KEY, DEFAULT_CONTENTFUL_APP_HOST } from '../constants';
import { APP_DEF_ENV_KEY, DEFAULT_CONTENTFUL_API_HOST } from '../constants';
import { InstallOptions } from '../types';
import { getWebAppHostname } from '../utils';

export async function installToEnvironment(options: InstallOptions) {
let definitionId;
const prompts = [];

if (options.definitionId) {
definitionId = options.definitionId;
} else if (process.env[APP_DEF_ENV_KEY]) {
definitionId = process.env[APP_DEF_ENV_KEY];
} else {
const prompts = await inquirer.prompt([
{
name: 'definitionId',
message: `The id of the app:`,
},
]);
definitionId = prompts.definitionId;
prompts.push({
name: 'definitionId',
message: `The id of the app:`,
});
}

if (!options.host) {
prompts.push({
name: 'host',
message: `Contentful CMA endpoint URL:`,
default: DEFAULT_CONTENTFUL_API_HOST,
});
}

if (!definitionId) {
const openSettingsOptions = await inquirer.prompt(prompts);
const hostValue = options.host || openSettingsOptions?.host;
const appDefinitionIdValue = definitionId || openSettingsOptions?.definitionId;

if (!appDefinitionIdValue) {
console.log(`
${chalk.red('Error:')} There was no app-definition defined.
Expand All @@ -30,8 +42,8 @@ export async function installToEnvironment(options: InstallOptions) {
throw new Error('No app-definition-id');
}

const host = options.host || DEFAULT_CONTENTFUL_APP_HOST;
const redirectUrl = `https://${host}/deeplink?link=apps`;
const webApp = getWebAppHostname(hostValue);
const redirectUrl = `https://${webApp}/deeplink?link=apps`;

try {
open(`${redirectUrl}&id=${definitionId}`);
Expand Down
Loading

0 comments on commit a292360

Please sign in to comment.