Skip to content

Commit 9f09f3f

Browse files
fix: Simplified input and correct navigation in new structure [EXT-6255] (#2368)
* fix: Simplified input and correct navigation in new structure * fix: testing and linting * fix: fix test * fix: verbiage * chore: Changed 'source' to 'example' * chore: fixed tests
1 parent c5dc99f commit 9f09f3f

File tree

7 files changed

+82
-178
lines changed

7 files changed

+82
-178
lines changed

packages/contentful--app-scripts/src/bin.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,9 @@ async function runCommand(command: Command, options?: any) {
121121
program
122122
.command('generate-function')
123123
.description('Generate a new Contentful Function')
124-
.option('--name <name>', 'Name of the function')
125-
.option('--template <language>', 'Select a template and language for the function')
126-
.option('--example <example_name>', 'Select an example function to generate')
127-
.option('--language <language>', 'Select a language for the function')
124+
.option('-n, --name <name>', 'Name of the function')
125+
.option('-e, --example <example>', 'Name of the reference example')
126+
.option('-l, --language <language>', 'Select a language for the function')
128127
.action(async (options) => {
129128
await runCommand(generateFunction, options);
130129
});
Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,21 @@
1-
import { GenerateFunctionOptions, GenerateFunctionSettings } from "../types";
1+
import { GenerateFunctionSettings } from "../types";
22
import assert from 'node:assert'
3-
import { buildGenerateFunctionSettingsFromOptions } from './build-generate-function-settings';
3+
import { buildGenerateFunctionSettingsCLI } from './build-generate-function-settings';
44

5-
describe('buildGenerateFunctionSettingsFromOptions', () => {
5+
describe('buildGenerateFunctionSettingsCLI', () => {
66
it('should return GenerateFunctionSettings - using minimum template', async () => {
77
const options = {
88
name: 'test',
9-
template: 'typescript',
10-
} as GenerateFunctionOptions
9+
example: 'APPEVENT-HANDLER',
10+
language: 'typescript'
11+
} as GenerateFunctionSettings
1112
const expected = {
1213
name: 'test',
13-
sourceType: 'template',
14-
sourceName: 'typescript',
14+
example: 'appevent-handler',
1515
language: 'typescript',
1616
} as GenerateFunctionSettings
1717

18-
const settings = await buildGenerateFunctionSettingsFromOptions(options);
19-
assert.deepEqual(expected, settings);
20-
});
21-
22-
it('should return GenerateFunctionSettings - using minimum example', async () => {
23-
const options = {
24-
name: 'test',
25-
example: 'appevent-handler',
26-
language: 'typescript'
27-
} as GenerateFunctionOptions
28-
const expected = {
29-
name: 'test',
30-
sourceType: 'example',
31-
sourceName: 'appevent-handler',
32-
language: 'typescript',
33-
}
34-
const settings = await buildGenerateFunctionSettingsFromOptions(options);
18+
const settings = await buildGenerateFunctionSettingsCLI(options);
3519
assert.deepEqual(expected, settings);
3620
});
3721
})

packages/contentful--app-scripts/src/generate-function/build-generate-function-settings.ts

Lines changed: 56 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,91 @@
11
import inquirer from 'inquirer';
22
import path from 'path';
33
import { getGithubFolderNames } from './get-github-folder-names';
4-
import { ACCEPTED_EXAMPLE_FOLDERS, ACCEPTED_LANGUAGES, BANNED_FUNCTION_NAMES } from './constants';
5-
import { GenerateFunctionSettings, AcceptedFunctionExamples, SourceName, GenerateFunctionOptions, Language } from '../types';
4+
import { ACCEPTED_LANGUAGES, BANNED_FUNCTION_NAMES } from './constants';
5+
import { GenerateFunctionSettings, Language } from '../types';
66
import ora from 'ora';
77
import chalk from 'chalk';
88
import { warn } from './logger';
99

10-
export async function buildGenerateFunctionSettings() : Promise<GenerateFunctionSettings> {
10+
export async function buildGenerateFunctionSettingsInteractive() : Promise<GenerateFunctionSettings> {
1111
const baseSettings = await inquirer.prompt<GenerateFunctionSettings>([
1212
{
1313
name: 'name',
1414
message: `Function name (${path.basename(process.cwd())}):`,
1515
},
16-
{
17-
name: 'sourceType',
18-
message: 'Do you want to start with a blank template or use one of our examples?',
19-
type: 'list',
20-
choices: [
21-
{ name: 'Template', value: 'template' },
22-
{ name: 'Example', value: 'example' },
23-
],
24-
default: 'template',
25-
}
2616
]);
2717
if (BANNED_FUNCTION_NAMES.includes(baseSettings.name)) {
2818
throw new Error(`Invalid function name: ${baseSettings.name}`);
2919
}
30-
let sourceSpecificSettings : GenerateFunctionSettings;
31-
if (baseSettings.sourceType === 'template') {
32-
sourceSpecificSettings = await inquirer.prompt<GenerateFunctionSettings>([
33-
{
34-
name: 'language',
35-
message: 'Pick a template',
36-
type: 'list',
37-
choices: [
38-
{ name: 'TypeScript', value: 'typescript' },
39-
{ name: 'JavaScript', value: 'javascript' },
40-
],
41-
default: 'typescript',
42-
}
43-
])
44-
sourceSpecificSettings.sourceName = sourceSpecificSettings.language.toLowerCase() as SourceName
45-
} else {
46-
const availableExamples = await getGithubFolderNames();
47-
const filteredExamples = availableExamples.filter(
48-
(template) =>
49-
ACCEPTED_EXAMPLE_FOLDERS.includes(template as (typeof ACCEPTED_EXAMPLE_FOLDERS)[number])
50-
);
20+
const filteredSources = await getGithubFolderNames();
5121

52-
sourceSpecificSettings = await inquirer.prompt<GenerateFunctionSettings>([
53-
{
54-
name: 'sourceName',
55-
message: 'Select an example:',
56-
type: 'list',
57-
choices: filteredExamples,
58-
},
59-
{
60-
name: 'language',
61-
message: 'Pick a template',
62-
type: 'list',
63-
choices: [
64-
{ name: 'TypeScript', value: 'typescript' },
65-
{ name: 'JavaScript', value: 'javascript' },
66-
],
67-
default: 'typescript',
68-
}
69-
])
70-
}
71-
baseSettings.sourceName = sourceSpecificSettings.sourceName
72-
baseSettings.language = sourceSpecificSettings.language
73-
return baseSettings
22+
const sourceSpecificSettings = await inquirer.prompt<GenerateFunctionSettings>([
23+
{
24+
name: 'example',
25+
message: 'Select an example:',
26+
type: 'list',
27+
choices: filteredSources,
28+
},
29+
{
30+
name: 'language',
31+
message: 'Select a language',
32+
type: 'list',
33+
choices: [
34+
{ name: 'TypeScript', value: 'typescript' },
35+
{ name: 'JavaScript', value: 'javascript' },
36+
],
37+
default: 'typescript',
38+
}
39+
])
40+
baseSettings.example = sourceSpecificSettings.example
41+
baseSettings.language = sourceSpecificSettings.language
42+
return baseSettings
7443
}
7544

76-
function validateArguments(options: GenerateFunctionOptions) {
77-
const templateRequired = ['name', 'template'];
78-
const exampleRequired = ['name', 'example', 'language'];
79-
if (BANNED_FUNCTION_NAMES.includes(options.name)) {
45+
function validateArguments(options: GenerateFunctionSettings) {
46+
const requiredParams = ['name', 'example', 'language'];
47+
if (!requiredParams.every((key) => key in options)) {
48+
throw new Error('You must specify a function name, an example, and a language');
49+
}
50+
if (BANNED_FUNCTION_NAMES.includes(options.name)) {
8051
throw new Error(`Invalid function name: ${options.name}`);
8152
}
82-
if ('template' in options) {
83-
if (!templateRequired.every((key) => key in options)) {
84-
throw new Error('You must specify a function name and a template');
53+
// Convert options to lowercase and trim whitespace
54+
for (const key in options) {
55+
const optionKey = key as keyof GenerateFunctionSettings;
56+
const value = options[optionKey].toLowerCase().trim();
57+
58+
if (optionKey === 'language') {
59+
// Assert that the value is of type Language
60+
options[optionKey] = value as Language;
61+
} else {
62+
options[optionKey] = value;
8563
}
86-
} else if ('example' in options) {
87-
if (!exampleRequired.every((key) => key in options)) {
88-
throw new Error('You must specify a function name, an example, and a language');
89-
}
90-
} else {
91-
throw new Error('You must specify either --template or --example');
9264
}
9365
}
9466

95-
export async function buildGenerateFunctionSettingsFromOptions(options: GenerateFunctionOptions) : Promise<GenerateFunctionSettings> {
67+
export async function buildGenerateFunctionSettingsCLI(options: GenerateFunctionSettings) : Promise<GenerateFunctionSettings> {
9668
const validateSpinner = ora('Validating your input\n').start();
9769
const settings: GenerateFunctionSettings = {} as GenerateFunctionSettings;
9870
try {
9971
validateArguments(options);
100-
for (const key in options) { // convert all options to lowercase and trim
101-
const optionKey = key as keyof GenerateFunctionOptions;
102-
options[optionKey] = options[optionKey].toLowerCase().trim();
103-
}
104-
105-
if ('example' in options) {
106-
if ('template' in options) {
107-
throw new Error('Cannot specify both --template and --example');
108-
}
109-
110-
if (!ACCEPTED_EXAMPLE_FOLDERS.includes(options.example as AcceptedFunctionExamples)) {
111-
throw new Error(`Invalid example name: ${options.example}`);
112-
}
11372

114-
if (!ACCEPTED_LANGUAGES.includes(options.language)) {
115-
warn(`Invalid language: ${options.language}. Defaulting to TypeScript.`);
116-
settings.language = 'typescript';
117-
} else {
118-
settings.language = options.language;
119-
}
120-
settings.sourceType = 'example';
121-
settings.sourceName = options.example;
122-
settings.name = options.name;
123-
124-
} else if ('template' in options) {
125-
if ('language' in options && options.language && options.language != options.template) {
126-
console.warn(`Ignoring language option: ${options.language}. Defaulting to ${options.template}.`);
127-
}
128-
if (!ACCEPTED_LANGUAGES.includes(options.template as Language)) {
129-
console.warn(`Invalid language: ${options.template}. Defaulting to TypeScript.`);
130-
settings.language = 'typescript';
131-
settings.sourceName = 'typescript';
132-
} else {
133-
settings.language = options.template as Language;
134-
settings.sourceName = options.template;
135-
}
136-
settings.sourceType = 'template';
137-
settings.name = options.name;
73+
// Check if the source exists
74+
const filteredSources = await getGithubFolderNames();
75+
if (!filteredSources.includes(options.example)) {
76+
throw new Error(`Invalid example name: ${options.example}. Please choose from: ${filteredSources.join(', ')}`);
77+
}
78+
79+
// Check if the language is valid
80+
if (!ACCEPTED_LANGUAGES.includes(options.language)) {
81+
warn(`Invalid language: ${options.language}. Defaulting to TypeScript.`);
82+
settings.language = 'typescript';
83+
} else {
84+
settings.language = options.language;
13885
}
13986

87+
settings.example = options.example;
88+
settings.name = options.name;
14089
return settings;
14190
} catch (err: any) {
14291
console.log(`

packages/contentful--app-scripts/src/generate-function/clone.test.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,17 @@ import {
1818
import { REPO_URL, CONTENTFUL_APP_MANIFEST, APP_MANIFEST } from './constants';
1919
import { GenerateFunctionSettings, Language } from '../types';
2020

21-
const dummySettings = {
21+
let settings = {
2222
name: 'myFunction',
23-
sourceName: 'typescript',
24-
sourceType: 'template',
23+
example: 'typescript',
2524
language: 'typescript'
2625
} as GenerateFunctionSettings;
2726

2827
describe('Helper functions tests', () => {
2928

3029
describe('getCloneURL', () => {
31-
it('should return default clone URL for non-example sourceType', () => {
32-
const settings = { ...dummySettings, sourceType: 'template' } as GenerateFunctionSettings;
33-
const expected = `${REPO_URL}/${settings.sourceName}`;
34-
const url = getCloneURL(settings);
35-
assert.strictEqual(url, expected);
36-
});
37-
38-
it('should return example clone URL when sourceType is example', () => {
39-
const settings = { ...dummySettings, sourceType: 'example' } as GenerateFunctionSettings;
40-
const expected = `${REPO_URL}/${settings.sourceName}/${settings.language}`;
30+
it('should return example clone URL when exampleType is example', () => {
31+
const expected = `${REPO_URL}/${settings.example}/${settings.language}`;
4132
const url = getCloneURL(settings);
4233
assert.strictEqual(url, expected);
4334
});
@@ -59,12 +50,12 @@ describe('Helper functions tests', () => {
5950

6051
it('should update the manifest with new function id, path and entryFile', async () => {
6152
const renameFile = 'myFunction.ts';
62-
await touchupAppManifest(tempDir, dummySettings, renameFile);
53+
await touchupAppManifest(tempDir, settings, renameFile);
6354
const updatedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
6455
const entry = updatedManifest.functions[updatedManifest.functions.length - 1];
6556

6657
// The id should be the settings.name
67-
assert.strictEqual(entry.id, dummySettings.name);
58+
assert.strictEqual(entry.id, settings.name);
6859
// The path is updated to always have a .js extension
6960
assert.strictEqual(entry.path, `functions/${renameFile.replace('.ts', '.js')}`);
7061
// entryFile remains unchanged (uses the original renameFile)
@@ -111,7 +102,6 @@ describe('Helper functions tests', () => {
111102
const originalFilePath = path.join(tmpDir, originalName);
112103
fs.writeFileSync(originalFilePath, 'console.log("hello")', 'utf-8');
113104

114-
const settings = { ...dummySettings, language: 'typescript' } as GenerateFunctionSettings;
115105
const newName = renameClonedFiles(tmpDir, settings);
116106
const expectedName = `${settings.name}.ts`;
117107
assert.strictEqual(newName, expectedName);
@@ -121,7 +111,7 @@ describe('Helper functions tests', () => {
121111
});
122112

123113
it('should throw an error if no function file is found', () => {
124-
const settings = { ...dummySettings, language: 'javascript' } as GenerateFunctionSettings;
114+
settings = { ...settings, language: 'javascript' } as GenerateFunctionSettings;
125115
assert.throws(() => renameClonedFiles(tmpDir, settings), /No function file found/);
126116
});
127117
});

packages/contentful--app-scripts/src/generate-function/clone.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,7 @@ export async function cloneFunction(
4141
}
4242

4343
export function getCloneURL(settings: GenerateFunctionSettings) {
44-
let cloneURL = `${REPO_URL}/${settings.sourceName}`; // this is the default for template
45-
if (settings.sourceType === 'example') {
46-
cloneURL = `${REPO_URL}/${settings.sourceName}/${settings.language}`;
47-
}
48-
return cloneURL;
44+
return `${REPO_URL}/${settings.example}/${settings.language}`; // this is the default for template
4945
}
5046

5147
export async function touchupAppManifest(localPath: string, settings: GenerateFunctionSettings, renameFunctionFile: string) {

packages/contentful--app-scripts/src/generate-function/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { GenerateFunctionOptions } from "../types";
2-
import { buildGenerateFunctionSettings, buildGenerateFunctionSettingsFromOptions } from "./build-generate-function-settings";
1+
import { GenerateFunctionSettings } from "../types";
2+
import { buildGenerateFunctionSettingsInteractive, buildGenerateFunctionSettingsCLI } from "./build-generate-function-settings";
33
import { create } from "./create-function";
44

55
const interactive = async () => {
6-
const generateFunctionSettings = await buildGenerateFunctionSettings();
6+
const generateFunctionSettings = await buildGenerateFunctionSettingsInteractive();
77

88
return create(generateFunctionSettings);
99
};
1010

11-
const nonInteractive = async (options: GenerateFunctionOptions) => {
12-
const generateFunctionSettings = await buildGenerateFunctionSettingsFromOptions(options);
11+
const nonInteractive = async (options: GenerateFunctionSettings) => {
12+
const generateFunctionSettings = await buildGenerateFunctionSettingsCLI(options);
1313
return create(generateFunctionSettings);
1414
};
1515

packages/contentful--app-scripts/src/types.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,9 @@ export interface BuildFunctionsOptions {
8282
watch?: boolean;
8383
}
8484

85-
export type SourceType = 'template' | 'example';
8685
export type Language = 'javascript' | 'typescript';
87-
export type AcceptedFunctionExamples = 'appevent-handler'; // Union type of each accepted example folder name in apps/function-examples repo
88-
export type SourceName = Language | AcceptedFunctionExamples;
89-
9086
export interface GenerateFunctionSettings {
9187
name: string;
92-
sourceType: SourceType;
93-
sourceName: SourceName;
88+
example: string;
9489
language: Language;
95-
}
96-
97-
export type GenerateFunctionOptions = {
98-
name: string;
99-
} & ({
100-
example: AcceptedFunctionExamples;
101-
language: Language
102-
} | {
103-
template: Language;
104-
})
90+
}

0 commit comments

Comments
 (0)