From 9323ad9c9385f1d100749b59ae845ba3469d42af Mon Sep 17 00:00:00 2001 From: juntak45 Date: Fri, 22 May 2026 14:50:59 +0900 Subject: [PATCH] fix(create-granite-app): avoid concurrent tool template writes --- .../src/copyToolTemplate.spec.ts | 67 +++++++++++++++++++ .../src/copyToolTemplate.ts | 40 +++++++---- packages/create-granite-app/src/index.ts | 4 +- 3 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 packages/create-granite-app/src/copyToolTemplate.spec.ts diff --git a/packages/create-granite-app/src/copyToolTemplate.spec.ts b/packages/create-granite-app/src/copyToolTemplate.spec.ts new file mode 100644 index 000000000..33f250ee9 --- /dev/null +++ b/packages/create-granite-app/src/copyToolTemplate.spec.ts @@ -0,0 +1,67 @@ +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { copyToolTemplates } from './copyToolTemplate'; + +describe('copyToolTemplates', () => { + let tmpDir: string | null = null; + + afterEach(async () => { + if (tmpDir != null) { + await fs.rm(tmpDir, { force: true, recursive: true }); + tmpDir = null; + } + }); + + it('merges package.json changes from multiple tool templates', async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'create-granite-app-')); + + const appName = 'test-app'; + const appPath = path.join(tmpDir, appName); + await fs.mkdir(appPath, { recursive: true }); + await fs.writeFile( + path.join(appPath, 'package.json'), + JSON.stringify( + { + name: appName, + scripts: { + dev: 'granite dev', + }, + devDependencies: { + typescript: '^5.0.0', + }, + }, + null, + 2 + ) + ); + + const cwd = process.cwd(); + process.chdir(tmpDir); + + try { + await copyToolTemplates(['biome', 'eslint-prettier'], { appPath: appName }); + } finally { + process.chdir(cwd); + } + + const packageJson = JSON.parse(await fs.readFile(path.join(appPath, 'package.json'), 'utf-8')); + expect(packageJson.scripts).toMatchObject({ + dev: 'granite dev', + lint: 'eslint .', + }); + expect(packageJson.devDependencies).toMatchObject({ + typescript: '^5.0.0', + '@biomejs/biome': '^1.9.4', + '@eslint/js': '^9.17.0', + eslint: '^9.17.0', + 'eslint-plugin-react': '^7.37.2', + prettier: '3.4.2', + 'typescript-eslint': '^8.31.0', + }); + + await expect(fs.access(path.join(appPath, 'biome.json'))).resolves.toBeUndefined(); + await expect(fs.access(path.join(appPath, 'eslint.config.mjs'))).resolves.toBeUndefined(); + }); +}); diff --git a/packages/create-granite-app/src/copyToolTemplate.ts b/packages/create-granite-app/src/copyToolTemplate.ts index 0c614adba..4c3a0e44b 100644 --- a/packages/create-granite-app/src/copyToolTemplate.ts +++ b/packages/create-granite-app/src/copyToolTemplate.ts @@ -7,32 +7,44 @@ export const TOOL_TEMPLATE_LIST = ['biome', 'eslint-prettier'] as const; export type ToolTemplateName = (typeof TOOL_TEMPLATE_LIST)[number]; -export async function copyToolTemplate(toolTemplateName: ToolTemplateName, templateOptions: { appPath: string }) { +function getToolTemplatePath(toolTemplateName: ToolTemplateName) { if (!TOOL_TEMPLATE_LIST.includes(toolTemplateName)) { throw new Error(`Template ${toolTemplateName} not found`); } - const toolTemplatePath = path.resolve(__dirname, '..', 'tool-templates', toolTemplateName); - const _appPath = path.join(process.cwd(), templateOptions.appPath); - - // package.json 파일 경로 - const toolTemplatePackageJsonPath = path.join(toolTemplatePath, 'package.json'); - const appPackageJsonPath = path.join(_appPath, 'package.json'); - - const toolTemplatePackageJson = JSON.parse(await fs.readFile(toolTemplatePackageJsonPath, 'utf-8')); - const appPackageJson = JSON.parse(await fs.readFile(appPackageJsonPath, 'utf-8')); - - const mergedPackageJson = merge(appPackageJson, toolTemplatePackageJson); - await fs.writeFile(appPackageJsonPath, JSON.stringify(mergedPackageJson, null, 2)); + return path.resolve(__dirname, '..', 'tool-templates', toolTemplateName); +} +async function copyToolTemplateFiles(toolTemplatePath: string, appPath: string) { const files = await fs.readdir(toolTemplatePath); await Promise.all( files .filter((file) => file !== 'package.json') .map((file) => { const srcPath = path.join(toolTemplatePath, file); - const destPath = path.join(_appPath, file); + const destPath = path.join(appPath, file); return fs.cp(srcPath, destPath, { recursive: true }); }) ); } + +export async function copyToolTemplates(toolTemplateNames: ToolTemplateName[], templateOptions: { appPath: string }) { + const appPath = path.join(process.cwd(), templateOptions.appPath); + const appPackageJsonPath = path.join(appPath, 'package.json'); + + let mergedPackageJson = JSON.parse(await fs.readFile(appPackageJsonPath, 'utf-8')); + const toolTemplatePaths = toolTemplateNames.map(getToolTemplatePath); + + for (const toolTemplatePath of toolTemplatePaths) { + const toolTemplatePackageJsonPath = path.join(toolTemplatePath, 'package.json'); + const toolTemplatePackageJson = JSON.parse(await fs.readFile(toolTemplatePackageJsonPath, 'utf-8')); + mergedPackageJson = merge(mergedPackageJson, toolTemplatePackageJson); + } + + await fs.writeFile(appPackageJsonPath, JSON.stringify(mergedPackageJson, null, 2)); + await Promise.all(toolTemplatePaths.map((toolTemplatePath) => copyToolTemplateFiles(toolTemplatePath, appPath))); +} + +export async function copyToolTemplate(toolTemplateName: ToolTemplateName, templateOptions: { appPath: string }) { + await copyToolTemplates([toolTemplateName], templateOptions); +} diff --git a/packages/create-granite-app/src/index.ts b/packages/create-granite-app/src/index.ts index f807baf99..09b5c09ff 100644 --- a/packages/create-granite-app/src/index.ts +++ b/packages/create-granite-app/src/index.ts @@ -3,7 +3,7 @@ import { kebabCase } from 'es-toolkit/string'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { copyTemplate } from './copyTemplate'; -import { copyToolTemplate, TOOL_TEMPLATE_LIST } from './copyToolTemplate'; +import { copyToolTemplates, TOOL_TEMPLATE_LIST } from './copyToolTemplate'; import { getPackageManager } from './getPackageManager'; import { resolveFallback } from './resolveFallback'; @@ -98,7 +98,7 @@ async function run() { appName: getAppName(appPath), needYarnrc: Boolean(pkgInfo.packageManager === 'yarn' && pkgInfo.version && pkgInfo?.version >= '2.0.0'), }); - await Promise.all(toolTemplate.map((tool) => copyToolTemplate(tool, { appPath }))); + await copyToolTemplates(toolTemplate, { appPath }); } catch (e) { console.error(e); throw e;