Skip to content

Commit a9f3fd2

Browse files
philnashdkundel
authored andcommitted
feat(new): updates new to download multiple file templates (#39)
Uses the new layout of the functions-templates project to download multiple file templates into a twilio-run project. Templates will be downloaded into the functions/{bundleName} and assets/{bundleName} directories within the project. This also allows for passing an empty bundleName (or filename in the twilio-run CLI parlance right now). When empty, this adds the functions/assets directly into the functions/assets directories. This will be used for `create-twilio-function` template options. The twilio-run CLI still enforces a name. BREAKING CHANGE: This needs the functions-templates repo to be up to date. Currently it points to the next branch, which is up to date. re #20
1 parent b12ed02 commit a9f3fd2

File tree

5 files changed

+116
-43
lines changed

5 files changed

+116
-43
lines changed

src/commands/new.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import prompts from 'prompts';
55
import { Merge } from 'type-fest';
66
import { Arguments, Argv } from 'yargs';
77
import checkProjectStructure from '../checks/project-structure';
8-
import { fetchListOfTemplates, getTemplateFiles } from '../templating/data';
9-
import { writeFiles } from '../templating/filesystem';
108
import { CliInfo } from './types';
119
import { getFullCommand } from './utils';
10+
import { downloadTemplate, fetchListOfTemplates } from '../templating/actions';
1211

1312
export type NewCliFlags = Arguments<{
1413
filename?: string;
@@ -129,14 +128,9 @@ export async function handler(flagsInput: NewCliFlags): Promise<void> {
129128
return;
130129
}
131130

132-
const functionName = flags.filename.replace(/\.js$/, '');
133-
const files = await getTemplateFiles(flags.template, functionName);
134-
try {
135-
await writeFiles(files, targetDirectory, functionName);
136-
console.log(chalk`{green SUCCESS} Created new function ${functionName}`);
137-
} catch (err) {
138-
console.error(chalk`{red ERROR} ${err.message}`);
139-
}
131+
const bundleName = flags.filename.replace(/\.js$/, '');
132+
133+
downloadTemplate(flags.template, bundleName, targetDirectory);
140134
}
141135

142136
export const cliInfo: CliInfo = {

src/templating/actions.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getTemplateFiles } from './data';
2+
import { writeFiles } from './filesystem';
3+
import chalk from 'chalk';
4+
5+
export async function downloadTemplate(
6+
templateName: string,
7+
bundleName: string,
8+
targetDirectory: string
9+
): Promise<void> {
10+
const files = await getTemplateFiles(templateName);
11+
try {
12+
await writeFiles(files, targetDirectory, bundleName);
13+
console.log(chalk`{green SUCCESS} Created new bundle ${bundleName}`);
14+
} catch (err) {
15+
console.error(chalk`{red ERROR} ${err.message}`);
16+
}
17+
}
18+
19+
export { fetchListOfTemplates } from './data';

src/templating/data.ts

+40-13
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export async function fetchListOfTemplates(): Promise<Template[]> {
2525
}
2626

2727
export type TemplateFileInfo = {
28+
name: string;
2829
type: string;
29-
functionName: string;
3030
content: string;
3131
};
3232

@@ -47,30 +47,57 @@ type RawContentsPayload = {
4747
};
4848
}[];
4949

50-
export async function getTemplateFiles(
50+
async function getFiles(
5151
templateId: string,
52-
functionName: string
52+
directory: string
53+
): Promise<TemplateFileInfo[]> {
54+
const response = await got(
55+
CONTENT_BASE_URL + `/${templateId}/${directory}?ref=next`,
56+
{
57+
json: true,
58+
}
59+
);
60+
const repoContents = response.body as RawContentsPayload;
61+
return repoContents.map(file => {
62+
return {
63+
name: file.name,
64+
type: directory,
65+
content: file.download_url,
66+
};
67+
});
68+
}
69+
70+
export async function getTemplateFiles(
71+
templateId: string
5372
): Promise<TemplateFileInfo[]> {
5473
try {
55-
const response = await got(CONTENT_BASE_URL + `/${templateId}`, {
74+
const response = await got(CONTENT_BASE_URL + `/${templateId}?ref=next`, {
5675
json: true,
5776
});
58-
const output = (response.body as RawContentsPayload)
77+
const repoContents = response.body as RawContentsPayload;
78+
79+
const assets = repoContents.find(file => file.name === 'assets')
80+
? getFiles(templateId, 'assets')
81+
: [];
82+
const functions = repoContents.find(file => file.name === 'functions')
83+
? getFiles(templateId, 'functions')
84+
: [];
85+
86+
const otherFiles = repoContents
5987
.filter(file => {
60-
return (
61-
file.name === 'package.json' ||
62-
file.name === '.env' ||
63-
(file.name.endsWith('.js') && !file.name.endsWith('.test.js'))
64-
);
88+
return file.name === 'package.json' || file.name === '.env';
6589
})
6690
.map(file => {
6791
return {
68-
type: file.name.endsWith('.js') ? 'function' : file.name,
69-
functionName,
92+
name: file.name,
93+
type: file.name,
7094
content: file.download_url,
7195
};
7296
});
73-
return output;
97+
const files = otherFiles.concat(
98+
...(await Promise.all([assets, functions]))
99+
);
100+
return files;
74101
} catch (err) {
75102
log(err.message);
76103
console.error(err);

src/templating/filesystem.ts

+42-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import path from 'path';
77
import { install, InstallResult } from 'pkg-install';
88
import { downloadFile, fileExists, readFile, writeFile } from '../utils/fs';
99
import { TemplateFileInfo } from './data';
10+
import { mkdir as oldMkdir } from 'fs';
11+
import { promisify } from 'util';
12+
const mkdir = promisify(oldMkdir);
1013

1114
async function writeEnvFile(
1215
contentUrl: string,
@@ -67,36 +70,62 @@ async function installDependencies(
6770
export async function writeFiles(
6871
files: TemplateFileInfo[],
6972
targetDir: string,
70-
functionName: string
73+
bundleName: string
7174
): Promise<void> {
7275
const functionsDir = fsHelpers.getFirstMatchingDirectory(targetDir, [
7376
'functions',
7477
'src',
7578
]);
76-
const functionTargetPath = path.join(functionsDir, `/${functionName}.js`);
77-
if (await fileExists(functionTargetPath)) {
78-
throw new Error(
79-
`Function with name "${functionName} already exists in "${functionsDir}"`
80-
);
79+
const assetsDir = fsHelpers.getFirstMatchingDirectory(targetDir, [
80+
'assets',
81+
'static',
82+
]);
83+
const functionsTargetDir = path.join(functionsDir, bundleName);
84+
const assetsTargetDir = path.join(assetsDir, bundleName);
85+
if (functionsTargetDir !== functionsDir) {
86+
try {
87+
await mkdir(functionsTargetDir);
88+
} catch (err) {
89+
console.error(err);
90+
throw new Error(
91+
`Bundle with name "${bundleName}" already exists in "${functionsDir}"`
92+
);
93+
}
94+
try {
95+
await mkdir(assetsTargetDir);
96+
} catch (err) {
97+
console.error(err);
98+
throw new Error(
99+
`Bundle with name "${bundleName}" already exists in "${assetsDir}"`
100+
);
101+
}
81102
}
82103

83104
const tasks = files
84105
.map(file => {
85-
if (file.type === 'function') {
106+
if (file.type === 'functions') {
86107
return {
87-
title: 'Create Function',
88-
task: () => {
89-
return downloadFile(file.content, functionTargetPath);
90-
},
108+
title: `Creating function: ${file.name}`,
109+
task: () =>
110+
downloadFile(
111+
file.content,
112+
path.join(functionsTargetDir, file.name)
113+
),
114+
};
115+
} else if (file.type === 'assets') {
116+
return {
117+
title: `Creating asset: ${file.name}`,
118+
task: () =>
119+
downloadFile(file.content, path.join(assetsTargetDir, file.name)),
91120
};
92121
} else if (file.type === '.env') {
93122
return {
94-
title: 'Configure Environment Variables in .env',
123+
title: 'Configuring Environment Variables in .env',
95124
task: async (ctx: any) => {
96125
const output = await writeEnvFile(
97126
file.content,
98127
targetDir,
99-
file.functionName
128+
file.name
100129
);
101130
ctx.env = output;
102131
},

src/utils/fs.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import fs from 'fs';
33
import got from 'got';
44
import path from 'path';
55
import { promisify } from 'util';
6-
76
const access = promisify(fs.access);
87
export const readFile = promisify(fs.readFile);
98
export const writeFile = promisify(fs.writeFile);
109
export const readdir = promisify(fs.readdir);
1110
const stat = promisify(fs.stat);
11+
const open = promisify(fs.open);
1212

1313
export async function fileExists(filePath: string): Promise<boolean> {
1414
try {
@@ -24,12 +24,16 @@ export function downloadFile(
2424
targetPath: string
2525
): Promise<void> {
2626
return new Promise((resolve, reject) => {
27-
const writeStream = fs.createWriteStream(targetPath);
28-
got
29-
.stream(contentUrl)
30-
.on('response', resolve)
31-
.on('error', reject)
32-
.pipe(writeStream);
27+
return open(targetPath, 'wx')
28+
.then(fd => {
29+
const writeStream = fs.createWriteStream('', { fd });
30+
got
31+
.stream(contentUrl)
32+
.on('response', resolve)
33+
.on('error', reject)
34+
.pipe(writeStream);
35+
})
36+
.catch(err => reject(err));
3337
});
3438
}
3539

0 commit comments

Comments
 (0)