UNPKG

xchange-create

Version:
314 lines (274 loc) 10.3 kB
import { execa } from "execa"; import { Extension, Options, TemplateDescriptor, isDefined } from "../types"; import { baseDir } from "../utils/consts"; import { extensionDict } from "../utils/extensions-tree"; import { findFilesRecursiveSync } from "../utils/find-files-recursively"; import { mergePackageJson } from "../utils/merge-package-json"; import fs from "fs"; import url from 'url' import ncp from "ncp"; import path from "path"; import { promisify } from "util"; import link from "../utils/link"; const copy = promisify(ncp); let copyOrLink = copy const expandExtensions = (options: Options): Extension[] => { const expandedExtensions = options.extensions .map((extension) => extensionDict[extension]) .map((extDescriptor) => [extDescriptor.extends, extDescriptor.value].filter(isDefined) ) .flat() // this reduce just removes duplications .reduce( (exts, ext) => (exts.includes(ext) ? exts : [...exts, ext]), [] as Extension[] ); return expandedExtensions; }; const isTemplateRegex = /([^\/\\]*?)\.template\./; const isPackageJsonRegex = /package\.json/; const isPnpmLockRegex = /pnpm-lock\.yaml/; const isNextGeneratedRegex = /packages\/nextjs\/generated/ const isConfigRegex = /([^\/\\]*?)\\config\.json/; const isArgsRegex = /([^\/\\]*?)\.args\./; const isExtensionFolderRegex = /extensions$/; const isPackagesFolderRegex = /packages$/; const copyBaseFiles = async ( { dev: isDev }: Options, basePath: string, targetDir: string ) => { await copyOrLink(basePath, targetDir, { clobber: false, filter: (fileName) => { // NOTE: filter IN const isTemplate = isTemplateRegex.test(fileName) const isPackageJson = isPackageJsonRegex.test(fileName) const isPnpmLock = isPnpmLockRegex.test(fileName) const isNextGenerated = isNextGeneratedRegex.test(fileName) const skipAlways = isTemplate || isPackageJson const skipDevOnly = isPnpmLock || isNextGenerated const shouldSkip = skipAlways || (isDev && skipDevOnly) return !shouldSkip }, }); const basePackageJsonPaths = findFilesRecursiveSync(basePath, path => isPackageJsonRegex.test(path)) basePackageJsonPaths.forEach(packageJsonPath => { const partialPath = packageJsonPath.split(basePath)[1] mergePackageJson( path.join(targetDir, partialPath), path.join(basePath, partialPath), isDev ); }) if (isDev) { const basePnpmLockPaths = findFilesRecursiveSync(basePath, path => isPnpmLockRegex.test(path)) basePnpmLockPaths.forEach(pnpmLockPath => { const partialPath = pnpmLockPath.split(basePath)[1] copy( path.join(basePath, partialPath), path.join(targetDir, partialPath) ) }) const nextGeneratedPaths = findFilesRecursiveSync(basePath, path => isNextGeneratedRegex.test(path)) nextGeneratedPaths.forEach(nextGeneratedPath => { const partialPath = nextGeneratedPath.split(basePath)[1] copy( path.join(basePath, partialPath), path.join(targetDir, partialPath) ) }) } } const copyExtensionsFiles = async ( { extensions, dev: isDev }: Options, targetDir: string ) => { await Promise.all(extensions.map(async (extension) => { const extensionPath = extensionDict[extension].path; // copy (or link if dev) root files await copyOrLink(extensionPath, path.join(targetDir), { clobber: false, filter: (path) => { const isConfig = isConfigRegex.test(path); const isArgs = isArgsRegex.test(path); const isExtensionFolder = isExtensionFolderRegex.test(path) && fs.lstatSync(path).isDirectory(); const isPackagesFolder = isPackagesFolderRegex.test(path) && fs.lstatSync(path).isDirectory(); const isTemplate = isTemplateRegex.test(path); // PR NOTE: this wasn't needed before because ncp had the clobber: false const isPackageJson = isPackageJsonRegex.test(path); const shouldSkip = isConfig || isArgs || isTemplate || isPackageJson || isExtensionFolder || isPackagesFolder; return !shouldSkip; }, }); // merge root package.json mergePackageJson( path.join(targetDir, "package.json"), path.join(extensionPath, "package.json"), isDev ); const extensionPackagesPath = path.join(extensionPath, "packages"); const hasPackages = fs.existsSync(extensionPackagesPath); if (hasPackages) { // copy extension packages files await copyOrLink(extensionPackagesPath, path.join(targetDir, "packages"), { clobber: false, filter: (path) => { const isArgs = isArgsRegex.test(path); const isTemplate = isTemplateRegex.test(path); const isPackageJson = isPackageJsonRegex.test(path); const shouldSkip = isArgs || isTemplate || isPackageJson; return !shouldSkip; }, }); // copy each package's package.json const extensionPackages = fs.readdirSync(extensionPackagesPath); extensionPackages.forEach((packageName) => { mergePackageJson( path.join(targetDir, "packages", packageName, "package.json"), path.join(extensionPath, "packages", packageName, "package.json"), isDev ); }); } })); }; const processTemplatedFiles = async ( { extensions, dev: isDev }: Options, basePath: string, targetDir: string ) => { const baseTemplatedFileDescriptors: TemplateDescriptor[] = findFilesRecursiveSync(basePath, (path) => isTemplateRegex.test(path)).map( (baseTemplatePath) => ({ path: baseTemplatePath, fileUrl: url.pathToFileURL(baseTemplatePath).href, relativePath: baseTemplatePath.split(basePath)[1], source: "base", }) ); const extensionsTemplatedFileDescriptors: TemplateDescriptor[] = extensions .map((ext) => findFilesRecursiveSync(extensionDict[ext].path, (filePath) => isTemplateRegex.test(filePath) ).map((extensionTemplatePath) => ({ path: extensionTemplatePath, fileUrl: url.pathToFileURL(extensionTemplatePath).href, relativePath: extensionTemplatePath.split(extensionDict[ext].path)[1], source: `extension ${extensionDict[ext].name}`, })) ) .flat(); await Promise.all( [ ...baseTemplatedFileDescriptors, ...extensionsTemplatedFileDescriptors, ].map(async (templateFileDescriptor) => { const templateTargetName = templateFileDescriptor.path.match(isTemplateRegex)?.[1]!; const argsPath = templateFileDescriptor.relativePath.replace( isTemplateRegex, `${templateTargetName}.args.` ); const argsFileUrls = extensions .map((extension) => { const argsFilePath = path.join( extensionDict[extension].path, argsPath ); const fileExists = fs.existsSync(argsFilePath); if (!fileExists) { return []; } return url.pathToFileURL(argsFilePath).href; }) .flat(); const args = await Promise.all( argsFileUrls.map(async (argsFileUrl) => await import(argsFileUrl)) ); const template = (await import(templateFileDescriptor.fileUrl)).default; if (!template) { throw new Error( `Template ${templateTargetName} from ${templateFileDescriptor.source} doesn't have a default export` ); } if (typeof template !== "function") { throw new Error( `Template ${templateTargetName} from ${templateFileDescriptor.source} is not exporting a function by default` ); } const freshArgs: { [key: string]: string[] } = Object.fromEntries( Object.keys(args[0] ?? {}).map((key) => [ key, // INFO: key for the freshArgs object [], // INFO: initial value for the freshArgs object ]) ); const combinedArgs: { [key: string]: string[] } = args.reduce( (accumulated, arg) => { Object.entries(arg).map(([key, value]) => { accumulated[key].push(value); }); return accumulated; }, freshArgs ); // TODO test: if first arg file found only uses 1 name, I think the rest are not used? const output = template(combinedArgs); const targetPath = path.join( targetDir, templateFileDescriptor.relativePath.split(templateTargetName)[0], templateTargetName ); fs.writeFileSync(targetPath, output); if (isDev) { const hasCombinedArgs = Object.keys(combinedArgs).length > 0 const hasArgsPaths = argsFileUrls.length > 0 const devOutput = `--- TEMPLATE FILE templates/${templateFileDescriptor.source}${templateFileDescriptor.relativePath} --- ARGS FILES ${hasArgsPaths ? argsFileUrls.map(url => `\t- ${path.join('templates', url.split('templates')[1])}`).join('\n') : '(no args files writing to the template)' } --- RESULTING ARGS ${hasCombinedArgs ? Object.entries(combinedArgs) .map(([argName, argValue]) => `\t- ${argName}:\t[${argValue.join(',')}]`) // TODO improvement: figure out how to add the values added by each args file .join('\n') : '(no args sent for the template)' } ` fs.writeFileSync(`${targetPath}.dev`, devOutput); } }) ); }; export async function copyTemplateFiles( options: Options, templateDir: string, targetDir: string ) { copyOrLink = options.dev ? link : copy const basePath = path.join(templateDir, baseDir); // 1. Copy base template to target directory await copyBaseFiles(options, basePath, targetDir) // 2. Add "parent" extensions (set via config.json#extend field) const expandedExtension = expandExtensions(options); options.extensions = expandedExtension; // 3. Copy extensions folders await copyExtensionsFiles(options, targetDir); // 4. Process templated files and generate output await processTemplatedFiles(options, basePath, targetDir); // 5. Initialize git repo to avoid husky error await execa("git", ["init"], { cwd: targetDir }); await execa("git", ["checkout", "-b", "main"], { cwd: targetDir }); }