diff --git a/__tests__/templating/filesystem.test.ts b/__tests__/templating/filesystem.test.ts index 177835e0..8ea139c6 100644 --- a/__tests__/templating/filesystem.test.ts +++ b/__tests__/templating/filesystem.test.ts @@ -85,12 +85,19 @@ test('installation with basic functions', async () => { name: 'hello.js', type: 'functions', content: 'https://example.com/hello.js', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, { name: 'README.md', type: 'README.md', content: 'https://example.com/README.md', + directory: '', }, ], './testing/', @@ -135,13 +142,20 @@ test('installation with functions and assets', async () => { name: 'hello.js', type: 'functions', content: 'https://example.com/hello.js', + directory: '', }, { name: 'hello.wav', type: 'assets', content: 'https://example.com/hello.wav', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, ], './testing/', 'example', @@ -185,18 +199,26 @@ test('installation with functions and assets and blank namespace', async () => { name: 'hello.js', type: 'functions', content: 'https://example.com/hello.js', + directory: '', }, { name: 'hello.wav', type: 'assets', content: 'https://example.com/hello.wav', + directory: '', }, { name: 'README.md', type: 'README.md', content: 'https://example.com/README.md', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, ], './testing/', '', @@ -269,8 +291,14 @@ test('installation with an empty dependency file', async () => { name: 'package.json', type: 'package.json', content: 'https://example.com/package.json', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, ], './testing/', 'example', @@ -314,8 +342,14 @@ test('installation with a dependency file', async () => { name: 'package.json', type: 'package.json', content: 'https://example.com/package.json', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, ], './testing/', 'example', @@ -359,7 +393,14 @@ test('installation with an existing dot-env file', async () => { ); await writeFiles( - [{ name: '.env', type: '.env', content: 'https://example.com/.env' }], + [ + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', + }, + ], './testing/', 'example', 'hello' @@ -398,8 +439,14 @@ test('installation with overlapping function files throws errors before writing' name: 'hello.js', type: 'functions', content: 'https://example.com/hello.js', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, ], './', 'example', @@ -431,13 +478,20 @@ test('installation with overlapping asset files throws errors before writing', a name: 'hello.js', type: 'functions', content: 'https://example.com/hello.js', + directory: '', }, { name: 'hello.wav', type: 'assets', content: 'https://example.com/hello.wav', + directory: '', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', }, - { name: '.env', type: '.env', content: 'https://example.com/.env' }, ], './', 'example', @@ -450,3 +504,73 @@ test('installation with overlapping asset files throws errors before writing', a expect(downloadFile).toHaveBeenCalledTimes(0); expect(writeFile).toHaveBeenCalledTimes(0); }); + +test('installation with functions and assets in nested directories', async () => { + // For this test, getFirstMatchingDirectory never errors. + mocked( + fsHelpers.getFirstMatchingDirectory + ).mockImplementation((basePath: string, directories: Array): string => + path.join(basePath, directories[0]) + ); + + await writeFiles( + [ + { + name: 'hello.js', + type: 'functions', + content: 'https://example.com/hello.js', + directory: 'admin', + }, + { + name: 'woohoo.jpg', + type: 'assets', + content: 'https://example.com/woohoo.jpg', + directory: 'success', + }, + { + name: '.env', + type: '.env', + content: 'https://example.com/.env', + directory: '', + }, + { + name: 'README.md', + type: 'README.md', + content: 'https://example.com/README.md', + directory: '', + }, + ], + './testing/', + 'example', + 'hello' + ); + + expect(downloadFile).toHaveBeenCalledTimes(4); + expect(downloadFile).toHaveBeenCalledWith( + 'https://example.com/.env', + 'testing/.env' + ); + expect(downloadFile).toHaveBeenCalledWith( + 'https://example.com/hello.js', + 'testing/functions/example/admin/hello.js' + ); + expect(downloadFile).toHaveBeenCalledWith( + 'https://example.com/README.md', + 'testing/readmes/example/hello.md' + ); + expect(downloadFile).toHaveBeenCalledWith( + 'https://example.com/woohoo.jpg', + 'testing/assets/example/success/woohoo.jpg' + ); + + expect(mkdir).toHaveBeenCalledTimes(3); + expect(mkdir).toHaveBeenCalledWith('testing/functions/example', { + recursive: true, + }); + expect(mkdir).toHaveBeenCalledWith('testing/readmes/example', { + recursive: true, + }); + expect(mkdir).toHaveBeenCalledWith('testing/assets/example', { + recursive: true, + }); +}); diff --git a/src/templating/data.ts b/src/templating/data.ts index a1a6edc7..760ae6c1 100644 --- a/src/templating/data.ts +++ b/src/templating/data.ts @@ -1,3 +1,4 @@ +import path from 'path'; import { stripIndent } from 'common-tags'; import got from 'got'; import { OutgoingHttpHeaders } from 'http'; @@ -31,6 +32,7 @@ export async function fetchListOfTemplates(): Promise { export type TemplateFileInfo = { name: string; + directory: string; type: string; content: string; }; @@ -57,27 +59,67 @@ type GitHubError = { documentation_url?: string; }; +function getFromUrl(url: string) { + const headers = buildHeader(); + return got(url, { + json: true, + headers, + }); +} + +async function getNestedRepoContents( + url: string, + dirName: string, + directory: string +): Promise { + const response = await getFromUrl(url); + const repoContents = response.body as RawContentsPayload; + return ( + await Promise.all( + repoContents.map(async file => { + if (file.type === 'dir') { + return await getNestedRepoContents( + file.url, + path.join(dirName, file.name), + directory + ); + } else { + return { + name: file.name, + directory: dirName, + type: directory, + content: file.download_url, + }; + } + }) + ) + ).flat(); +} + async function getFiles( templateId: string, directory: string ): Promise { - const headers = buildHeader(); - const response = await got( - CONTENT_BASE_URL + - `/${templateId}/${directory}?ref=${TEMPLATE_BASE_BRANCH}`, - { - json: true, - headers, - } + const response = await getFromUrl( + CONTENT_BASE_URL + `/${templateId}/${directory}?ref=${TEMPLATE_BASE_BRANCH}` ); const repoContents = response.body as RawContentsPayload; - return repoContents.map(file => { - return { - name: file.name, - type: directory, - content: file.download_url, - }; - }); + return ( + await Promise.all( + repoContents.map(async file => { + if (file.type === 'dir') { + return await getNestedRepoContents(file.url, file.name, directory); + } else { + return { + name: file.name, + type: directory, + content: file.download_url, + directory: '', + }; + } + }) + ) + ).flat(); } export async function getTemplateFiles( @@ -114,6 +156,7 @@ export async function getTemplateFiles( name: file.name, type: file.name, content: file.download_url, + directory: '', }; }); const files = otherFiles.concat( diff --git a/src/templating/filesystem.ts b/src/templating/filesystem.ts index 6dde6684..6fd3f15a 100644 --- a/src/templating/filesystem.ts +++ b/src/templating/filesystem.ts @@ -116,7 +116,7 @@ export async function writeFiles( for (let file of files) { if (file.type === 'functions') { - let filepath = path.join(functionsTargetDir, file.name); + let filepath = path.join(functionsTargetDir, file.directory, file.name); if (await fileExists(filepath)) { throw new Error( @@ -124,7 +124,7 @@ export async function writeFiles( ); } } else if (file.type === 'assets') { - let filepath = path.join(assetsTargetDir, file.name); + let filepath = path.join(assetsTargetDir, file.directory, file.name); if (await fileExists(filepath)) { throw new Error( @@ -138,18 +138,21 @@ export async function writeFiles( .map(file => { if (file.type === 'functions') { return { - title: `Creating function: ${file.name}`, + title: `Creating function: ${path.join(file.directory, file.name)}`, task: () => downloadFile( file.content, - path.join(functionsTargetDir, file.name) + path.join(functionsTargetDir, file.directory, file.name) ), }; } else if (file.type === 'assets') { return { title: `Creating asset: ${file.name}`, task: () => - downloadFile(file.content, path.join(assetsTargetDir, file.name)), + downloadFile( + file.content, + path.join(assetsTargetDir, file.directory, file.name) + ), }; } else if (file.type === '.env') { return { diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 3595697f..c4a77c83 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -25,7 +25,8 @@ export function downloadFile( targetPath: string ): Promise { return new Promise((resolve, reject) => { - return open(targetPath, 'wx') + return mkdir(path.dirname(targetPath), { recursive: true }) + .then(() => open(targetPath, 'wx')) .then(fd => { const writeStream = fs.createWriteStream('', { fd }); got @@ -43,22 +44,24 @@ export async function getDirContent( ext: string ): Promise { const rawFiles = await readdir(dir); - return (await Promise.all( - rawFiles.map>(async (file: string) => { - const filePath = path.join(dir, file); - const entry = await stat(filePath); - if (!entry.isFile()) { - return undefined; - } + return ( + await Promise.all( + rawFiles.map>(async (file: string) => { + const filePath = path.join(dir, file); + const entry = await stat(filePath); + if (!entry.isFile()) { + return undefined; + } - if (ext && path.extname(file) !== ext) { - return undefined; - } + if (ext && path.extname(file) !== ext) { + return undefined; + } - return { - name: file, - path: filePath, - }; - }) - )).filter(Boolean) as FileInfo[]; + return { + name: file, + path: filePath, + }; + }) + ) + ).filter(Boolean) as FileInfo[]; }