Skip to content

Commit 773d073

Browse files
authored
feat(templates): support nested templates to create nested routes (#123)
Fixes #122
1 parent 91c5768 commit 773d073

File tree

4 files changed

+218
-45
lines changed

4 files changed

+218
-45
lines changed

__tests__/templating/filesystem.test.ts

+132-8
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,19 @@ test('installation with basic functions', async () => {
8585
name: 'hello.js',
8686
type: 'functions',
8787
content: 'https://example.com/hello.js',
88+
directory: '',
89+
},
90+
{
91+
name: '.env',
92+
type: '.env',
93+
content: 'https://example.com/.env',
94+
directory: '',
8895
},
89-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
9096
{
9197
name: 'README.md',
9298
type: 'README.md',
9399
content: 'https://example.com/README.md',
100+
directory: '',
94101
},
95102
],
96103
'./testing/',
@@ -135,13 +142,20 @@ test('installation with functions and assets', async () => {
135142
name: 'hello.js',
136143
type: 'functions',
137144
content: 'https://example.com/hello.js',
145+
directory: '',
138146
},
139147
{
140148
name: 'hello.wav',
141149
type: 'assets',
142150
content: 'https://example.com/hello.wav',
151+
directory: '',
152+
},
153+
{
154+
name: '.env',
155+
type: '.env',
156+
content: 'https://example.com/.env',
157+
directory: '',
143158
},
144-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
145159
],
146160
'./testing/',
147161
'example',
@@ -185,18 +199,26 @@ test('installation with functions and assets and blank namespace', async () => {
185199
name: 'hello.js',
186200
type: 'functions',
187201
content: 'https://example.com/hello.js',
202+
directory: '',
188203
},
189204
{
190205
name: 'hello.wav',
191206
type: 'assets',
192207
content: 'https://example.com/hello.wav',
208+
directory: '',
193209
},
194210
{
195211
name: 'README.md',
196212
type: 'README.md',
197213
content: 'https://example.com/README.md',
214+
directory: '',
215+
},
216+
{
217+
name: '.env',
218+
type: '.env',
219+
content: 'https://example.com/.env',
220+
directory: '',
198221
},
199-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
200222
],
201223
'./testing/',
202224
'',
@@ -269,8 +291,14 @@ test('installation with an empty dependency file', async () => {
269291
name: 'package.json',
270292
type: 'package.json',
271293
content: 'https://example.com/package.json',
294+
directory: '',
295+
},
296+
{
297+
name: '.env',
298+
type: '.env',
299+
content: 'https://example.com/.env',
300+
directory: '',
272301
},
273-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
274302
],
275303
'./testing/',
276304
'example',
@@ -314,8 +342,14 @@ test('installation with a dependency file', async () => {
314342
name: 'package.json',
315343
type: 'package.json',
316344
content: 'https://example.com/package.json',
345+
directory: '',
346+
},
347+
{
348+
name: '.env',
349+
type: '.env',
350+
content: 'https://example.com/.env',
351+
directory: '',
317352
},
318-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
319353
],
320354
'./testing/',
321355
'example',
@@ -359,7 +393,14 @@ test('installation with an existing dot-env file', async () => {
359393
);
360394

361395
await writeFiles(
362-
[{ name: '.env', type: '.env', content: 'https://example.com/.env' }],
396+
[
397+
{
398+
name: '.env',
399+
type: '.env',
400+
content: 'https://example.com/.env',
401+
directory: '',
402+
},
403+
],
363404
'./testing/',
364405
'example',
365406
'hello'
@@ -398,8 +439,14 @@ test('installation with overlapping function files throws errors before writing'
398439
name: 'hello.js',
399440
type: 'functions',
400441
content: 'https://example.com/hello.js',
442+
directory: '',
443+
},
444+
{
445+
name: '.env',
446+
type: '.env',
447+
content: 'https://example.com/.env',
448+
directory: '',
401449
},
402-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
403450
],
404451
'./',
405452
'example',
@@ -431,13 +478,20 @@ test('installation with overlapping asset files throws errors before writing', a
431478
name: 'hello.js',
432479
type: 'functions',
433480
content: 'https://example.com/hello.js',
481+
directory: '',
434482
},
435483
{
436484
name: 'hello.wav',
437485
type: 'assets',
438486
content: 'https://example.com/hello.wav',
487+
directory: '',
488+
},
489+
{
490+
name: '.env',
491+
type: '.env',
492+
content: 'https://example.com/.env',
493+
directory: '',
439494
},
440-
{ name: '.env', type: '.env', content: 'https://example.com/.env' },
441495
],
442496
'./',
443497
'example',
@@ -450,3 +504,73 @@ test('installation with overlapping asset files throws errors before writing', a
450504
expect(downloadFile).toHaveBeenCalledTimes(0);
451505
expect(writeFile).toHaveBeenCalledTimes(0);
452506
});
507+
508+
test('installation with functions and assets in nested directories', async () => {
509+
// For this test, getFirstMatchingDirectory never errors.
510+
mocked(
511+
fsHelpers.getFirstMatchingDirectory
512+
).mockImplementation((basePath: string, directories: Array<string>): string =>
513+
path.join(basePath, directories[0])
514+
);
515+
516+
await writeFiles(
517+
[
518+
{
519+
name: 'hello.js',
520+
type: 'functions',
521+
content: 'https://example.com/hello.js',
522+
directory: 'admin',
523+
},
524+
{
525+
name: 'woohoo.jpg',
526+
type: 'assets',
527+
content: 'https://example.com/woohoo.jpg',
528+
directory: 'success',
529+
},
530+
{
531+
name: '.env',
532+
type: '.env',
533+
content: 'https://example.com/.env',
534+
directory: '',
535+
},
536+
{
537+
name: 'README.md',
538+
type: 'README.md',
539+
content: 'https://example.com/README.md',
540+
directory: '',
541+
},
542+
],
543+
'./testing/',
544+
'example',
545+
'hello'
546+
);
547+
548+
expect(downloadFile).toHaveBeenCalledTimes(4);
549+
expect(downloadFile).toHaveBeenCalledWith(
550+
'https://example.com/.env',
551+
'testing/.env'
552+
);
553+
expect(downloadFile).toHaveBeenCalledWith(
554+
'https://example.com/hello.js',
555+
'testing/functions/example/admin/hello.js'
556+
);
557+
expect(downloadFile).toHaveBeenCalledWith(
558+
'https://example.com/README.md',
559+
'testing/readmes/example/hello.md'
560+
);
561+
expect(downloadFile).toHaveBeenCalledWith(
562+
'https://example.com/woohoo.jpg',
563+
'testing/assets/example/success/woohoo.jpg'
564+
);
565+
566+
expect(mkdir).toHaveBeenCalledTimes(3);
567+
expect(mkdir).toHaveBeenCalledWith('testing/functions/example', {
568+
recursive: true,
569+
});
570+
expect(mkdir).toHaveBeenCalledWith('testing/readmes/example', {
571+
recursive: true,
572+
});
573+
expect(mkdir).toHaveBeenCalledWith('testing/assets/example', {
574+
recursive: true,
575+
});
576+
});

src/templating/data.ts

+58-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from 'path';
12
import { stripIndent } from 'common-tags';
23
import got from 'got';
34
import { OutgoingHttpHeaders } from 'http';
@@ -31,6 +32,7 @@ export async function fetchListOfTemplates(): Promise<Template[]> {
3132

3233
export type TemplateFileInfo = {
3334
name: string;
35+
directory: string;
3436
type: string;
3537
content: string;
3638
};
@@ -57,27 +59,67 @@ type GitHubError = {
5759
documentation_url?: string;
5860
};
5961

62+
function getFromUrl(url: string) {
63+
const headers = buildHeader();
64+
return got(url, {
65+
json: true,
66+
headers,
67+
});
68+
}
69+
70+
async function getNestedRepoContents(
71+
url: string,
72+
dirName: string,
73+
directory: string
74+
): Promise<TemplateFileInfo[]> {
75+
const response = await getFromUrl(url);
76+
const repoContents = response.body as RawContentsPayload;
77+
return (
78+
await Promise.all(
79+
repoContents.map(async file => {
80+
if (file.type === 'dir') {
81+
return await getNestedRepoContents(
82+
file.url,
83+
path.join(dirName, file.name),
84+
directory
85+
);
86+
} else {
87+
return {
88+
name: file.name,
89+
directory: dirName,
90+
type: directory,
91+
content: file.download_url,
92+
};
93+
}
94+
})
95+
)
96+
).flat();
97+
}
98+
6099
async function getFiles(
61100
templateId: string,
62101
directory: string
63102
): Promise<TemplateFileInfo[]> {
64-
const headers = buildHeader();
65-
const response = await got(
66-
CONTENT_BASE_URL +
67-
`/${templateId}/${directory}?ref=${TEMPLATE_BASE_BRANCH}`,
68-
{
69-
json: true,
70-
headers,
71-
}
103+
const response = await getFromUrl(
104+
CONTENT_BASE_URL + `/${templateId}/${directory}?ref=${TEMPLATE_BASE_BRANCH}`
72105
);
73106
const repoContents = response.body as RawContentsPayload;
74-
return repoContents.map(file => {
75-
return {
76-
name: file.name,
77-
type: directory,
78-
content: file.download_url,
79-
};
80-
});
107+
return (
108+
await Promise.all(
109+
repoContents.map(async file => {
110+
if (file.type === 'dir') {
111+
return await getNestedRepoContents(file.url, file.name, directory);
112+
} else {
113+
return {
114+
name: file.name,
115+
type: directory,
116+
content: file.download_url,
117+
directory: '',
118+
};
119+
}
120+
})
121+
)
122+
).flat();
81123
}
82124

83125
export async function getTemplateFiles(
@@ -114,6 +156,7 @@ export async function getTemplateFiles(
114156
name: file.name,
115157
type: file.name,
116158
content: file.download_url,
159+
directory: '',
117160
};
118161
});
119162
const files = otherFiles.concat(

src/templating/filesystem.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,15 @@ export async function writeFiles(
116116

117117
for (let file of files) {
118118
if (file.type === 'functions') {
119-
let filepath = path.join(functionsTargetDir, file.name);
119+
let filepath = path.join(functionsTargetDir, file.directory, file.name);
120120

121121
if (await fileExists(filepath)) {
122122
throw new Error(
123123
`Template with name "${namespace}" has duplicate file "${file.name}" in "${functionsDir}"`
124124
);
125125
}
126126
} else if (file.type === 'assets') {
127-
let filepath = path.join(assetsTargetDir, file.name);
127+
let filepath = path.join(assetsTargetDir, file.directory, file.name);
128128

129129
if (await fileExists(filepath)) {
130130
throw new Error(
@@ -138,18 +138,21 @@ export async function writeFiles(
138138
.map(file => {
139139
if (file.type === 'functions') {
140140
return {
141-
title: `Creating function: ${file.name}`,
141+
title: `Creating function: ${path.join(file.directory, file.name)}`,
142142
task: () =>
143143
downloadFile(
144144
file.content,
145-
path.join(functionsTargetDir, file.name)
145+
path.join(functionsTargetDir, file.directory, file.name)
146146
),
147147
};
148148
} else if (file.type === 'assets') {
149149
return {
150150
title: `Creating asset: ${file.name}`,
151151
task: () =>
152-
downloadFile(file.content, path.join(assetsTargetDir, file.name)),
152+
downloadFile(
153+
file.content,
154+
path.join(assetsTargetDir, file.directory, file.name)
155+
),
153156
};
154157
} else if (file.type === '.env') {
155158
return {

0 commit comments

Comments
 (0)