213 lines
5.1 KiB
TypeScript
Raw Normal View History

import path from 'node:path';
import url from 'node:url';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import * as tar from 'tar';
import retry from 'async-retry';
import fse from 'fs-extra';
import type { Scope } from '../types';
const stripTrailingSlash = (str: string) => {
return str.endsWith('/') ? str.slice(0, -1) : str;
};
// Merge template with new project being created
export async function copyTemplate(scope: Scope, rootPath: string) {
const { template } = scope;
if (!template) {
throw new Error('Missing template or example app option');
}
if (await isOfficialTemplate(template, scope.templateBranch)) {
await retry(
() =>
downloadGithubRepo(rootPath, {
owner: 'strapi',
repo: 'strapi',
branch: scope.templateBranch,
subPath: `templates/${template}`,
}),
{
retries: 3,
onRetry(err, attempt) {
console.log(`Retrying to download the template. Attempt ${attempt}. Error: ${err}`);
},
}
);
return;
}
if (isLocalTemplate(template)) {
const filePath = template.startsWith('file://') ? url.fileURLToPath(template) : template;
await fse.copy(filePath, rootPath);
}
if (isGithubShorthand(template)) {
const [owner, repo, ...pathSegments] = template.split('/');
const subPath = pathSegments.length ? pathSegments.join('/') : scope.templatePath;
await retry(
() => downloadGithubRepo(rootPath, { owner, repo, branch: scope.templateBranch, subPath }),
{
retries: 3,
onRetry(err, attempt) {
console.log(`Retrying to download the template. Attempt ${attempt}. Error: ${err}`);
},
}
);
return;
}
if (isGithubRepo(template)) {
const url = new URL(template);
2024-08-06 14:29:45 +02:00
const [owner, repo, t, branch, ...pathSegments] = stripTrailingSlash(
url.pathname.slice(1)
).split('/');
if (t !== undefined && t !== 'tree') {
throw new Error(`Invalid GitHub template URL: ${template}`);
}
if (scope.templateBranch) {
await downloadGithubRepo(rootPath, {
owner,
repo,
branch: scope.templateBranch,
subPath: scope.templatePath,
});
}
await retry(
() =>
downloadGithubRepo(rootPath, {
owner,
repo,
branch: decodeURIComponent(branch) ?? scope.templateBranch,
2024-08-06 14:29:45 +02:00
subPath: pathSegments.length
? decodeURIComponent(pathSegments.join('/'))
: scope.templatePath,
}),
{
retries: 3,
onRetry(err, attempt) {
console.log(`Retrying to download the template. Attempt ${attempt}. Error: ${err}`);
},
}
);
throw new Error(`Invalid GitHub template URL: ${template}`);
}
}
type RepoInfo = {
owner: string;
repo: string;
branch?: string;
subPath?: string | null;
};
async function downloadGithubRepo(rootPath: string, { owner, repo, branch, subPath }: RepoInfo) {
const filePath = subPath ? subPath.split('/').join(path.posix.sep) : null;
let checkContentUrl = `https://api.github.com/repos/${owner}/${repo}/contents`;
if (filePath) {
checkContentUrl = `${checkContentUrl}/${filePath}`;
}
if (branch) {
checkContentUrl = `${checkContentUrl}?ref=${branch}`;
}
const checkRes = await fetch(checkContentUrl, {
method: 'HEAD',
});
if (checkRes.status !== 200) {
throw new Error(
`Could not find a template at https://github.com/${owner}/${repo}${branch ? ` on branch ${branch}` : ''}${filePath ? ` at path ${filePath}` : ''}`
);
}
let url = `https://api.github.com/repos/${owner}/${repo}/tarball`;
if (branch) {
url = `${url}/${branch}`;
}
const res = await fetch(url);
if (!res.body) {
throw new Error(`Failed to download ${url}`);
}
await pipeline(
// @ts-expect-error - Readable is not a valid source
Readable.fromWeb(res.body),
tar.x({
cwd: rootPath,
strip: filePath ? filePath.split('/').length + 1 : 1,
filter(path) {
if (filePath) {
return path.split('/').slice(1).join('/').startsWith(filePath);
}
return true;
},
})
);
}
function isLocalTemplate(template: string) {
return (
template.startsWith('file://') ||
fse.existsSync(path.isAbsolute(template) ? template : path.resolve(process.cwd(), template))
);
}
function isGithubShorthand(value: string) {
if (isValidUrl(value)) {
return false;
}
return /^[\w-]+\/[\w-.]+(\/[\w-.]+)*$/.test(value);
}
function isGithubRepo(value: string) {
try {
const url = new URL(value);
return url.origin === 'https://github.com';
} catch {
return false;
}
}
function isValidUrl(value: string) {
try {
// eslint-disable-next-line no-new
new URL(value);
return true;
} catch {
return false;
}
}
async function isOfficialTemplate(template: string, branch: string | undefined) {
if (isValidUrl(template)) {
return false;
}
const res = await fetch(
`https://api.github.com/repos/strapi/strapi/contents/templates/${template}?${branch ? `ref=${branch}` : ''}`,
{ method: 'HEAD' }
);
return res.status === 200;
}