@contentful/app-scripts 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -220,6 +220,32 @@ When passing the `--ci` argument adding all variables as arguments is required
220
220
 
221
221
  **Note:** You can also pass all arguments in interactive mode to skip being asked for it.
222
222
 
223
+ ### Install the AppDefinition into a specific space / environment
224
+
225
+ It opens a dialog to select the space and environment where the app associated with the given [AppDefinition](https://www.contentful.com/developers/docs/extensibility/app-framework/app-definition/) should be installed.
226
+
227
+ > **Example**
228
+ >
229
+ > ```shell
230
+ > $ npx --no-install @contentful/app-scripts install --definition-id some-definition-id
231
+ > ```
232
+
233
+ You can also execute this command without the argument if the environment variable (`CONTENTFUL_APP_DEF_ID`) has been set.
234
+
235
+ > **Example**
236
+ >
237
+ > ```shell
238
+ > $ CONTENTFUL_APP_DEF_ID=some-definition-id npx --no-install @contentful/app-scripts install
239
+ > ```
240
+
241
+ By default, the script will install the app into the default host URL: `app.contentful.com`. If you want to install the app into a different host URL, you can set the argument `--host` to the desired host URL.
242
+
243
+ > **Example**
244
+ >
245
+ > ```shell
246
+ > $ npx --no-install @contentful/app-scripts install --definition-id some-definition-id --host api.eu.contentful.com
247
+ > ```
248
+
223
249
  ### Tracking
224
250
 
225
251
  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:
package/lib/analytics.js CHANGED
@@ -26,7 +26,7 @@ function track({ command, ci }) {
26
26
  command,
27
27
  ci: String(ci),
28
28
  },
29
- anonymousId: Date.now().toString(),
29
+ anonymousId: Date.now().toString(), // generate a random id
30
30
  timestamp: new Date(),
31
31
  });
32
32
  // eslint-disable-next-line no-empty
package/lib/bin.js CHANGED
@@ -65,6 +65,14 @@ async function runCommand(command, options) {
65
65
  .action(async (options) => {
66
66
  await runCommand(feedback_1.feedback, options);
67
67
  });
68
+ commander_1.program
69
+ .command('install')
70
+ .description('Opens a picker to select the space and environment for installing the app associated with a given AppDefinition')
71
+ .option('--definition-id [defId]', 'The id of your apps definition')
72
+ .option('--host [host]', 'Contentful domain to use')
73
+ .action(async (options) => {
74
+ await runCommand(index_1.install, options);
75
+ });
68
76
  commander_1.program.hook('preAction', (thisCommand) => {
69
77
  (0, index_1.track)({ command: thisCommand.args[0], ci: thisCommand.opts().ci });
70
78
  });
@@ -6,3 +6,4 @@ export declare const DEFAULT_BUNDLES_TO_KEEP = 50;
6
6
  export declare const DEFAULT_BUNDLES_TO_FETCH = 1000;
7
7
  export declare const MAX_CONCURRENT_DELETION_CALLS = 5;
8
8
  export declare const DEFAULT_CONTENTFUL_API_HOST = "api.contentful.com";
9
+ export declare const DEFAULT_CONTENTFUL_APP_HOST = "app.contentful.com";
package/lib/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_CONTENTFUL_API_HOST = exports.MAX_CONCURRENT_DELETION_CALLS = exports.DEFAULT_BUNDLES_TO_FETCH = exports.DEFAULT_BUNDLES_TO_KEEP = exports.APP_DEF_ENV_KEY = exports.ORG_ID_ENV_KEY = exports.ACCESS_TOKEN_ENV_KEY = exports.DOTENV_FILE = void 0;
3
+ exports.DEFAULT_CONTENTFUL_APP_HOST = exports.DEFAULT_CONTENTFUL_API_HOST = exports.MAX_CONCURRENT_DELETION_CALLS = exports.DEFAULT_BUNDLES_TO_FETCH = exports.DEFAULT_BUNDLES_TO_KEEP = exports.APP_DEF_ENV_KEY = exports.ORG_ID_ENV_KEY = exports.ACCESS_TOKEN_ENV_KEY = exports.DOTENV_FILE = void 0;
4
4
  exports.DOTENV_FILE = '.env';
5
5
  exports.ACCESS_TOKEN_ENV_KEY = 'CONTENTFUL_ACCESS_TOKEN';
6
6
  exports.ORG_ID_ENV_KEY = 'CONTENTFUL_ORG_ID';
@@ -9,3 +9,4 @@ exports.DEFAULT_BUNDLES_TO_KEEP = 50;
9
9
  exports.DEFAULT_BUNDLES_TO_FETCH = 1000;
10
10
  exports.MAX_CONCURRENT_DELETION_CALLS = 5;
11
11
  exports.DEFAULT_CONTENTFUL_API_HOST = 'api.contentful.com';
12
+ exports.DEFAULT_CONTENTFUL_APP_HOST = 'app.contentful.com';
@@ -1,8 +1,13 @@
1
- import { AppLocation, FieldType } from 'contentful-management';
1
+ import { AppLocation, FieldType, ParameterDefinition, InstallationParameterType } from 'contentful-management';
2
2
  export interface AppDefinitionSettings {
3
3
  name: string;
4
4
  locations: AppLocation['location'][];
5
5
  fields?: FieldType[];
6
6
  host?: string;
7
+ buildAppParameters: boolean;
8
+ parameters?: {
9
+ instance: ParameterDefinition[];
10
+ installation: ParameterDefinition<InstallationParameterType>[];
11
+ };
7
12
  }
8
13
  export declare function buildAppDefinitionSettings(): Promise<AppDefinitionSettings>;
@@ -8,6 +8,7 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const inquirer_1 = __importDefault(require("inquirer"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const constants_1 = require("../constants");
11
+ const build_app_parameter_settings_1 = require("./build-app-parameter-settings");
11
12
  async function buildAppDefinitionSettings() {
12
13
  console.log(chalk_1.default.dim(`
13
14
  NOTE: This will create an app definition in your Contentful organization.
@@ -79,7 +80,16 @@ NOTE: This will create an app definition in your Contentful organization.
79
80
  message: `Contentful CMA endpoint URL:`,
80
81
  default: constants_1.DEFAULT_CONTENTFUL_API_HOST,
81
82
  },
83
+ {
84
+ name: 'buildAppParameters',
85
+ message: 'Would you like to specify App Parameter schemas? (see https://ctfl.io/app-parameters)',
86
+ type: 'confirm',
87
+ default: false,
88
+ },
82
89
  ]);
90
+ if (appDefinitionSettings.buildAppParameters) {
91
+ appDefinitionSettings.parameters = await (0, build_app_parameter_settings_1.buildAppParameterSettings)();
92
+ }
83
93
  appDefinitionSettings.locations = ['dialog', ...appDefinitionSettings.locations];
84
94
  return appDefinitionSettings;
85
95
  }
@@ -0,0 +1,5 @@
1
+ import { InstallationParameterType, ParameterDefinition } from 'contentful-management';
2
+ export declare function buildAppParameterSettings(): Promise<{
3
+ instance: ParameterDefinition[];
4
+ installation: ParameterDefinition<InstallationParameterType>[];
5
+ }>;
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildAppParameterSettings = void 0;
7
+ const inquirer_1 = __importDefault(require("inquirer"));
8
+ const lodash_1 = require("lodash");
9
+ const PARAMETER_ID_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
10
+ const validateDefault = (input, type, options) => {
11
+ if (input === '')
12
+ return true;
13
+ switch (type) {
14
+ case 'Symbol':
15
+ return (0, lodash_1.isString)(input) || 'Default value must be a string.';
16
+ case 'Enum':
17
+ if (!(0, lodash_1.isString)(input))
18
+ return 'Default value must be a string.';
19
+ else if (options && !options.includes(input))
20
+ return 'Default value must be one of the options.';
21
+ return true;
22
+ case 'Number':
23
+ return (0, lodash_1.isNumber)(Number(input)) || 'Default value must be a number.';
24
+ case 'Boolean':
25
+ return input === 'true' || input === 'false' || 'Default value must be a boolean.';
26
+ default:
27
+ return true;
28
+ }
29
+ };
30
+ const validateEnumOptions = (input, type) => {
31
+ if (type !== 'Enum')
32
+ return true;
33
+ const allString = input.every(lodash_1.isString);
34
+ const allLabelled = input.every(lodash_1.isPlainObject);
35
+ return allString || allLabelled || 'Options should be all strings or all label objects.';
36
+ };
37
+ async function promptForParameter() {
38
+ const parameter = await inquirer_1.default.prompt([
39
+ {
40
+ name: 'instanceOrInstallation',
41
+ message: 'Is this an Instance or an Installation parameter?',
42
+ type: 'list',
43
+ choices: ['Instance', 'Installation'],
44
+ },
45
+ {
46
+ name: 'name',
47
+ message: 'Parameter name:',
48
+ validate(input) {
49
+ return input ? true : 'Parameter name is required.';
50
+ },
51
+ },
52
+ {
53
+ name: 'id',
54
+ message: 'Parameter ID:',
55
+ validate(input) {
56
+ if (!input)
57
+ return 'Parameter ID is required.';
58
+ else if (!PARAMETER_ID_RE.test(input))
59
+ return 'Parameter ID must start with a letter and contain only letters, numbers, and underscores.';
60
+ return true;
61
+ },
62
+ },
63
+ {
64
+ name: 'description',
65
+ message: 'Parameter description (optional):',
66
+ },
67
+ {
68
+ name: 'type',
69
+ message: 'Parameter type:',
70
+ type: 'list',
71
+ choices(answers) {
72
+ const parameterTypes = ['Boolean', 'Symbol', 'Number', 'Enum'];
73
+ // TODO uncomment when secret app installation parameters are finalized in the API
74
+ // if (answers.instanceOrInstallation === 'Installation') {
75
+ // parameterTypes.push('Secret');
76
+ // }
77
+ return parameterTypes;
78
+ },
79
+ },
80
+ {
81
+ name: 'required',
82
+ message: 'Is this parameter required?',
83
+ type: 'confirm',
84
+ default: false,
85
+ },
86
+ {
87
+ name: 'options',
88
+ message: 'Parameter options (comma-separated) (optional):',
89
+ when(answers) {
90
+ return answers.type === 'Enum';
91
+ },
92
+ filter(input) {
93
+ return input ? input.split(',').map((opt) => opt.trim()) : [];
94
+ },
95
+ validate(input, answers) {
96
+ if (!input)
97
+ return 'Options are required for Enum parameters.';
98
+ return validateEnumOptions(input, answers.type);
99
+ },
100
+ },
101
+ {
102
+ name: 'default',
103
+ message: 'Default value (leave blank if none):',
104
+ // TODO uncomment when secret app installation parameters are finalized in the API
105
+ // when(answers) {
106
+ // return answers.type !== 'Secret';
107
+ // },
108
+ validate(input, answers) {
109
+ return validateDefault(input, answers.type, answers.options);
110
+ },
111
+ },
112
+ {
113
+ name: 'booleanLabels',
114
+ message: 'Parameter labels (true/false comma-separated) (optional):',
115
+ when(answers) {
116
+ return answers.type === 'Boolean';
117
+ },
118
+ filter(input) {
119
+ const labels = input ? input.split(',').map((label) => label.trim()) : [];
120
+ return JSON.stringify({
121
+ true: labels[0] || '',
122
+ false: labels[1] || '',
123
+ });
124
+ },
125
+ },
126
+ {
127
+ name: 'enumEmptyLabel',
128
+ message: 'Empty label (optional):',
129
+ filter(input) {
130
+ return JSON.stringify({
131
+ empty: input.trim(),
132
+ });
133
+ },
134
+ when(answers) {
135
+ return answers.type === 'Enum';
136
+ },
137
+ },
138
+ ]);
139
+ return parameter;
140
+ }
141
+ async function buildAppParameterSettings() {
142
+ const parameters = {
143
+ instance: [],
144
+ installation: [],
145
+ };
146
+ let addMore = true;
147
+ while (addMore) {
148
+ try {
149
+ const parameter = await promptForParameter();
150
+ const labels = parameter.booleanLabels || parameter.enumEmptyLabel;
151
+ parameters[parameter.instanceOrInstallation.toLowerCase()].push({
152
+ id: parameter.id,
153
+ name: parameter.name,
154
+ description: parameter.description,
155
+ type: parameter.type,
156
+ required: parameter.required,
157
+ default: parameter.default,
158
+ options: parameter.options,
159
+ labels: labels ? JSON.parse(labels) : undefined,
160
+ });
161
+ }
162
+ catch (e) {
163
+ console.error('Failed to build parameter', e);
164
+ }
165
+ const { addAnother } = await inquirer_1.default.prompt({
166
+ name: 'addAnother',
167
+ message: 'Do you want to add another parameter?',
168
+ type: 'confirm',
169
+ default: false,
170
+ });
171
+ addMore = addAnother;
172
+ }
173
+ if ((0, lodash_1.uniq)(parameters.instance.map((p) => p.id)).length !== parameters.instance.length) {
174
+ console.log('Instance parameter IDs must be unique.');
175
+ return buildAppParameterSettings();
176
+ }
177
+ if ((0, lodash_1.uniq)(parameters.installation.map((p) => p.id)).length !== parameters.installation.length) {
178
+ console.log('Installation parameter IDs must be unique.');
179
+ return buildAppParameterSettings();
180
+ }
181
+ return parameters;
182
+ }
183
+ exports.buildAppParameterSettings = buildAppParameterSettings;
@@ -33,7 +33,7 @@ function assertValidArguments(accessToken, appDefinitionSettings) {
33
33
  (0, utils_1.throwValidationException)('AccessToken', `Expected string got ${typeof accessToken}`);
34
34
  }
35
35
  if (!(0, lodash_1.isPlainObject)(appDefinitionSettings) || !(0, lodash_1.has)(appDefinitionSettings, 'locations')) {
36
- (0, utils_1.throwValidationException)('AppDefinitionSettings', `Expected plain object with 'location' property, got ${JSON.stringify(appDefinitionSettings, null, 2)}`, `Example: ${JSON.stringify({
36
+ (0, utils_1.throwValidationException)('AppDefinitionSettings', `Expected plain object with 'locations' property, got ${JSON.stringify(appDefinitionSettings, null, 2)}`, `Example: ${JSON.stringify({
37
37
  name: 'app-name',
38
38
  locations: ['entry-field'],
39
39
  fields: [{ type: 'Boolean' }],
@@ -61,6 +61,14 @@ async function createAppDefinition(accessToken, appDefinitionSettings) {
61
61
  location,
62
62
  };
63
63
  }),
64
+ parameters: {
65
+ ...(appDefinitionSettings.parameters?.instance && {
66
+ instance: appDefinitionSettings.parameters.instance,
67
+ }),
68
+ ...(appDefinitionSettings.parameters?.installation && {
69
+ installation: appDefinitionSettings.parameters.installation,
70
+ }),
71
+ },
64
72
  };
65
73
  try {
66
74
  const organization = await client.getOrganization(organizationId);
@@ -1,8 +1,8 @@
1
1
  export declare const getAppInfo: ({ organizationId, definitionId, token, host, }: {
2
- organizationId?: string | undefined;
3
- definitionId?: string | undefined;
4
- token?: string | undefined;
5
- host?: string | undefined;
2
+ organizationId?: string;
3
+ definitionId?: string;
4
+ token?: string;
5
+ host?: string;
6
6
  }) => Promise<{
7
7
  accessToken: string;
8
8
  organization: import("./organization-api").Organization;
package/lib/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { cleanup } from './clean-up';
5
5
  export { open } from './open';
6
6
  export { track } from './analytics';
7
7
  export { feedback } from './feedback';
8
+ export { install } from './install';
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.feedback = exports.track = exports.open = exports.cleanup = exports.activate = exports.upload = exports.createAppDefinition = void 0;
3
+ exports.install = exports.feedback = exports.track = exports.open = exports.cleanup = exports.activate = exports.upload = exports.createAppDefinition = void 0;
4
4
  var create_app_definition_1 = require("./create-app-definition");
5
5
  Object.defineProperty(exports, "createAppDefinition", { enumerable: true, get: function () { return create_app_definition_1.createAppDefinition; } });
6
6
  var upload_1 = require("./upload");
@@ -15,3 +15,5 @@ var analytics_1 = require("./analytics");
15
15
  Object.defineProperty(exports, "track", { enumerable: true, get: function () { return analytics_1.track; } });
16
16
  var feedback_1 = require("./feedback");
17
17
  Object.defineProperty(exports, "feedback", { enumerable: true, get: function () { return feedback_1.feedback; } });
18
+ var install_1 = require("./install");
19
+ Object.defineProperty(exports, "install", { enumerable: true, get: function () { return install_1.install; } });
@@ -0,0 +1,5 @@
1
+ import { InstallOptions } from '../types';
2
+ export declare const install: {
3
+ interactive: (options: InstallOptions) => Promise<void>;
4
+ nonInteractive: () => Promise<never>;
5
+ };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.install = void 0;
4
+ const install_1 = require("./install");
5
+ const interactive = async (options) => {
6
+ (0, install_1.installToEnvironment)(options);
7
+ };
8
+ const nonInteractive = async () => {
9
+ throw new Error(`"install" is not available in non-interactive mode`);
10
+ };
11
+ exports.install = {
12
+ interactive,
13
+ nonInteractive,
14
+ };
@@ -0,0 +1,2 @@
1
+ import { InstallOptions } from '../types';
2
+ export declare function installToEnvironment(options: InstallOptions): Promise<void>;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installToEnvironment = void 0;
7
+ const open_1 = __importDefault(require("open"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const constants_1 = require("../constants");
11
+ async function installToEnvironment(options) {
12
+ let definitionId;
13
+ if (options.definitionId) {
14
+ definitionId = options.definitionId;
15
+ }
16
+ else if (process.env[constants_1.APP_DEF_ENV_KEY]) {
17
+ definitionId = process.env[constants_1.APP_DEF_ENV_KEY];
18
+ }
19
+ else {
20
+ const prompts = await inquirer_1.default.prompt([
21
+ {
22
+ name: 'definitionId',
23
+ message: `The id of the app:`,
24
+ },
25
+ ]);
26
+ definitionId = prompts.definitionId;
27
+ }
28
+ if (!definitionId) {
29
+ console.log(`
30
+ ${chalk_1.default.red('Error:')} There was no app-definition defined.
31
+
32
+ Please add it with ${chalk_1.default.cyan('--definition-id=<app-definition-id>')}
33
+ or set the environment variable ${chalk_1.default.cyan(`${constants_1.APP_DEF_ENV_KEY} = <app-definition-id>`)}
34
+ `);
35
+ throw new Error('No app-definition-id');
36
+ }
37
+ const host = options.host || constants_1.DEFAULT_CONTENTFUL_APP_HOST;
38
+ const redirectUrl = `https://${host}/deeplink?link=apps`;
39
+ try {
40
+ (0, open_1.default)(`${redirectUrl}&id=${definitionId}`);
41
+ }
42
+ catch (err) {
43
+ console.log(`${chalk_1.default.red('Error:')} Failed to open browser`);
44
+ console.log(err.message);
45
+ throw err;
46
+ }
47
+ }
48
+ exports.installToEnvironment = installToEnvironment;
package/lib/types.d.ts CHANGED
@@ -50,6 +50,10 @@ export interface CleanupSettings {
50
50
  export interface OpenSettingsOptions {
51
51
  definitionId?: string;
52
52
  }
53
+ export interface InstallOptions {
54
+ definitionId?: string;
55
+ host?: string;
56
+ }
53
57
  export interface UploadOptions {
54
58
  organizationId?: string;
55
59
  definitionId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/app-scripts",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "A collection of scripts for building Contentful Apps",
5
5
  "author": "Contentful GmbH",
6
6
  "license": "MIT",
@@ -48,11 +48,11 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@segment/analytics-node": "^2.0.0",
51
- "adm-zip": "0.5.12",
51
+ "adm-zip": "0.5.14",
52
52
  "bottleneck": "2.19.5",
53
53
  "chalk": "4.1.2",
54
54
  "commander": "12.1.0",
55
- "contentful-management": "11.26.2",
55
+ "contentful-management": "11.27.0",
56
56
  "dotenv": "16.4.5",
57
57
  "ignore": "5.3.1",
58
58
  "inquirer": "8.2.6",
@@ -60,16 +60,19 @@
60
60
  "open": "8.4.2",
61
61
  "ora": "5.4.1"
62
62
  },
63
- "gitHead": "81cc3ea1a7090350e4791112816a82b243c3fc14",
63
+ "gitHead": "e6a2617d30e8d0cc1e78b7bf5edac5cd0543e532",
64
64
  "devDependencies": {
65
65
  "@tsconfig/node18": "18.2.4",
66
66
  "@types/adm-zip": "0.5.5",
67
67
  "@types/analytics-node": "3.1.14",
68
+ "@types/chai": "4.3.16",
68
69
  "@types/inquirer": "8.2.1",
69
- "@types/lodash": "4.17.4",
70
+ "@types/lodash": "4.17.5",
70
71
  "@types/mocha": "10.0.6",
71
72
  "@types/proxyquire": "1.3.31",
72
73
  "@types/sinon": "17.0.3",
74
+ "chai": "5.1.1",
75
+ "mocha": "10.4.0",
73
76
  "proxyquire": "2.1.3",
74
77
  "sinon": "18.0.0",
75
78
  "ts-mocha": "10.0.0",