create-web-0to1 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/internal/engine/create-feature-crud-script.ts +134 -0
- package/internal/engine/create-feature-crud.template.mjs +601 -0
- package/internal/engine/create-feature-script.ts +142 -0
- package/internal/engine/generator-engine.ts +546 -0
- package/internal/engine/standalone-feature-preset.ts +34 -0
- package/internal/meta/preset-plan.ts +41 -0
- package/internal/meta/runtime-copy-plan.ts +220 -0
- package/internal/meta/runtime-layout.ts +262 -0
- package/internal/meta/scaffold-manifest.ts +169 -0
- package/internal/meta/standalone-dependency-manifest.ts +75 -0
- package/package.json +45 -0
- package/scripts/create-app.mjs +1612 -0
- package/source/core/auth/auth-events.ts +13 -0
- package/source/core/error/app-error.ts +85 -0
- package/source/core/error/handle-app-error.client.ts +35 -0
- package/source/core/lib/dayjs.ts +25 -0
- package/source/core/query/query-client.ts +126 -0
- package/source/core/request/request-core.ts +210 -0
- package/source/core/routes/route-paths.ts +4 -0
- package/source/core/ui/button.tsx +24 -0
- package/source/core/ui/modal-store.ts +32 -0
- package/source/core/ui/text-input-field.tsx +36 -0
- package/source/core/utils/build-query-string.ts +30 -0
- package/source/core/utils/format/date.ts +41 -0
- package/source/core/utils/format/index.ts +3 -0
- package/source/core/utils/format/number.ts +13 -0
- package/source/core/utils/format/text.ts +15 -0
- package/source/core/utils/schema-utils.ts +27 -0
- package/source/wrappers/monorepo/core/internal.ts +21 -0
- package/source/wrappers/monorepo/core/src/index.ts +4 -0
- package/source/wrappers/monorepo/core-next/src/auth.client.ts +1 -0
- package/source/wrappers/monorepo/core-next/src/auth.server.ts +94 -0
- package/source/wrappers/monorepo/core-next/src/bootstrap.client.tsx +21 -0
- package/source/wrappers/monorepo/core-next/src/bootstrap.tsx +18 -0
- package/source/wrappers/monorepo/core-next/src/index.ts +1 -0
- package/source/wrappers/monorepo/core-react/src/app-providers.tsx +36 -0
- package/source/wrappers/monorepo/core-react/src/auth.ts +42 -0
- package/source/wrappers/monorepo/core-react/src/hydration.tsx +21 -0
- package/source/wrappers/monorepo/core-react/src/index.ts +7 -0
- package/source/wrappers/monorepo/core-react/src/provider.tsx +49 -0
- package/source/wrappers/monorepo/core-react/src/query-client.ts +48 -0
- package/source/wrappers/monorepo/core-react/src/query-error-handler.ts +62 -0
- package/source/wrappers/monorepo/core-react/src/query-keys.ts +22 -0
- package/source/wrappers/monorepo/request/core-fetch.ts +27 -0
- package/source/wrappers/monorepo/request/core-request.ts +93 -0
- package/source/wrappers/next/auth/auth-error-listener.tsx +34 -0
- package/source/wrappers/next/error/handle-app-error.server.ts +41 -0
- package/source/wrappers/next/query/hydration.tsx +20 -0
- package/source/wrappers/next/query/providers.tsx +35 -0
- package/source/wrappers/next/request/request.client.ts +24 -0
- package/source/wrappers/next/request/request.server.ts +64 -0
- package/source/wrappers/next/request/request.ts +52 -0
- package/source/wrappers/next/ui/global-modal.tsx +29 -0
- package/source/wrappers/react/auth/auth-error-listener.tsx +34 -0
- package/source/wrappers/react/query/providers.tsx +31 -0
- package/source/wrappers/react/request/request.client.ts +24 -0
- package/source/wrappers/react/request/request.ts +51 -0
- package/source/wrappers/react/ui/global-modal.tsx +27 -0
- package/templates/monorepo/.dockerignore +38 -0
- package/templates/monorepo/README.md +292 -0
- package/templates/monorepo/_gitignore +38 -0
- package/templates/monorepo/_npmrc +1 -0
- package/templates/monorepo/apps/project/Dockerfile +32 -0
- package/templates/monorepo/apps/project/eslint.config.mjs +4 -0
- package/templates/monorepo/apps/project/index.html +14 -0
- package/templates/monorepo/apps/project/index.ts +15 -0
- package/templates/monorepo/apps/project/package.json +21 -0
- package/templates/monorepo/apps/project/tsconfig.json +9 -0
- package/templates/monorepo/apps/project/vite.config.ts +6 -0
- package/templates/monorepo/apps/web/Dockerfile +43 -0
- package/templates/monorepo/apps/web/README.md +111 -0
- package/templates/monorepo/apps/web/_gitignore +36 -0
- package/templates/monorepo/apps/web/app/favicon.ico +0 -0
- package/templates/monorepo/apps/web/app/global-error.tsx +12 -0
- package/templates/monorepo/apps/web/app/globals.css +0 -0
- package/templates/monorepo/apps/web/app/layout.tsx +28 -0
- package/templates/monorepo/apps/web/app/page.tsx +7 -0
- package/templates/monorepo/apps/web/app/providers.tsx +25 -0
- package/templates/monorepo/apps/web/eslint.config.js +4 -0
- package/templates/monorepo/apps/web/next-env.d.ts +6 -0
- package/templates/monorepo/apps/web/next.config.js +4 -0
- package/templates/monorepo/apps/web/package.json +31 -0
- package/templates/monorepo/apps/web/public/file-text.svg +3 -0
- package/templates/monorepo/apps/web/public/globe.svg +10 -0
- package/templates/monorepo/apps/web/public/next.svg +1 -0
- package/templates/monorepo/apps/web/public/turborepo-dark.svg +19 -0
- package/templates/monorepo/apps/web/public/turborepo-light.svg +19 -0
- package/templates/monorepo/apps/web/public/vercel.svg +10 -0
- package/templates/monorepo/apps/web/public/window.svg +3 -0
- package/templates/monorepo/apps/web/tsconfig.json +20 -0
- package/templates/monorepo/package.json +24 -0
- package/templates/monorepo/packages/core/eslint.config.mjs +4 -0
- package/templates/monorepo/packages/core/package.json +32 -0
- package/templates/monorepo/packages/core/tsconfig.json +8 -0
- package/templates/monorepo/packages/core-next/eslint.config.mjs +13 -0
- package/templates/monorepo/packages/core-next/package.json +43 -0
- package/templates/monorepo/packages/core-next/tsconfig.json +8 -0
- package/templates/monorepo/packages/core-react/eslint.config.mjs +4 -0
- package/templates/monorepo/packages/core-react/package.json +34 -0
- package/templates/monorepo/packages/core-react/tsconfig.json +8 -0
- package/templates/monorepo/packages/eslint-config/README.md +3 -0
- package/templates/monorepo/packages/eslint-config/base.js +57 -0
- package/templates/monorepo/packages/eslint-config/next.js +22 -0
- package/templates/monorepo/packages/eslint-config/package.json +25 -0
- package/templates/monorepo/packages/eslint-config/react-internal.js +33 -0
- package/templates/monorepo/packages/typescript-config/base.json +19 -0
- package/templates/monorepo/packages/typescript-config/nextjs.json +12 -0
- package/templates/monorepo/packages/typescript-config/package.json +9 -0
- package/templates/monorepo/packages/typescript-config/react-library.json +7 -0
- package/templates/monorepo/packages/ui/eslint.config.mjs +4 -0
- package/templates/monorepo/packages/ui/package.json +26 -0
- package/templates/monorepo/packages/ui/src/button.tsx +20 -0
- package/templates/monorepo/packages/ui/src/card.tsx +27 -0
- package/templates/monorepo/packages/ui/src/code.tsx +11 -0
- package/templates/monorepo/packages/ui/tsconfig.json +8 -0
- package/templates/monorepo/pnpm-workspace.yaml +9 -0
- package/templates/monorepo/turbo/generators/config.js +1336 -0
- package/templates/monorepo/turbo/generators/templates/next-app/Dockerfile.tpl +30 -0
- package/templates/monorepo/turbo/generators/templates/next-app/README.md.tpl +118 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/global-error.tsx.tpl +12 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/globals.css.tpl +1 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/layout.tsx.tpl +29 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/page.tsx.tpl +7 -0
- package/templates/monorepo/turbo/generators/templates/next-app/app/providers.tsx.tpl +25 -0
- package/templates/monorepo/turbo/generators/templates/next-app/eslint.config.js.tpl +4 -0
- package/templates/monorepo/turbo/generators/templates/next-app/next.config.js.tpl +6 -0
- package/templates/monorepo/turbo/generators/templates/next-app/tsconfig.json.tpl +18 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/Dockerfile.tpl +22 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/README.plain.md.tpl +90 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/README.react.md.tpl +107 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/eslint.config.mjs.tpl +4 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/index.html.tpl +12 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/index.ts.tpl +22 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/tsconfig.json.tpl +9 -0
- package/templates/monorepo/turbo/generators/templates/vite-app/vite.config.ts.tpl +6 -0
- package/templates/monorepo/turbo.json +28 -0
- package/templates/next/.env.example +2 -0
- package/templates/next/.prettierignore +9 -0
- package/templates/next/.prettierrc.json +9 -0
- package/templates/next/README.md +246 -0
- package/templates/next/_gitignore +44 -0
- package/templates/next/eslint.config.mjs +51 -0
- package/templates/next/next.config.ts +7 -0
- package/templates/next/package.json +24 -0
- package/templates/next/postcss.config.mjs +7 -0
- package/templates/next/scripts/create-feature-crud.mjs +5 -0
- package/templates/next/scripts/create-feature.mjs +5 -0
- package/templates/next/src/app/error.tsx +33 -0
- package/templates/next/src/app/globals.css +35 -0
- package/templates/next/src/app/layout.tsx +39 -0
- package/templates/next/src/app/login/page.tsx +17 -0
- package/templates/next/src/app/page.tsx +32 -0
- package/templates/next/src/app/providers.tsx +20 -0
- package/templates/next/tsconfig.json +34 -0
- package/templates/react/.env.example +1 -0
- package/templates/react/.prettierignore +10 -0
- package/templates/react/.prettierrc.json +9 -0
- package/templates/react/README.md +250 -0
- package/templates/react/_gitignore +31 -0
- package/templates/react/eslint.config.mjs +64 -0
- package/templates/react/package.json +19 -0
- package/templates/react/scripts/create-feature-crud.mjs +5 -0
- package/templates/react/scripts/create-feature.mjs +5 -0
- package/templates/react/src/app/app.tsx +15 -0
- package/templates/react/src/app/error-boundary.tsx +59 -0
- package/templates/react/src/app/frame.tsx +32 -0
- package/templates/react/src/app/globals.css +43 -0
- package/templates/react/src/app/not-found-page.tsx +23 -0
- package/templates/react/src/app/providers.tsx +16 -0
- package/templates/react/src/app/router.tsx +62 -0
- package/templates/react/src/main.tsx +12 -0
- package/templates/react/src/pages/index/page.tsx +36 -0
- package/templates/react/src/pages/login/page.tsx +18 -0
- package/templates/react/tsconfig.app.json +30 -0
- package/templates/react/tsconfig.json +4 -0
- package/templates/react/tsconfig.node.json +24 -0
- package/templates/react/vite.config.ts +14 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/00-/354/240/204/353/260/230/354/240/201/354/235/270-/355/217/264/353/215/224/352/265/254/354/241/260.md +150 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/01-/352/265/254/354/241/260/354/231/200-/353/235/274/354/232/260/355/214/205.md +186 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/02-/354/204/234/353/262/204/354/231/200-/355/201/264/353/235/274/354/235/264/354/226/270/355/212/270.md +86 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/03-/354/203/201/355/203/234/352/264/200/353/246/254.md +84 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/04-API/354/231/200-/353/215/260/354/235/264/355/204/260.md +199 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/05-/354/227/220/353/237/254/354/231/200-UI-/354/203/201/355/203/234.md +159 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/06-/355/217/274.md +116 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/07-/354/212/244/355/203/200/354/235/274/353/247/201/352/263/274-/354/240/221/352/267/274/354/204/261.md +73 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/08-/353/204/244/354/235/264/353/260/215-/354/204/244/354/240/225-/355/217/254/353/247/267/355/214/205.md +98 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/09-/353/235/274/354/232/260/355/212/270-/354/240/225/354/235/230.md +169 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/10-/354/273/244/353/260/213-/354/273/250/353/262/244/354/205/230.md +64 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/11-/352/270/260/355/203/200-/354/233/220/354/271/231.md +187 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/12-/354/213/244/353/254/264-/353/215/260/354/235/264/355/204/260-/355/214/250/355/204/264.md +302 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/13-/354/204/261/353/212/245-/354/233/220/354/271/231.md +175 -0
- package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/README.md +39 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
standaloneFeaturePresetConfigs,
|
|
3
|
+
type StandaloneFeaturePreset,
|
|
4
|
+
} from './standalone-feature-preset.ts';
|
|
5
|
+
|
|
6
|
+
const createFeatureScriptTemplate = `#!/usr/bin/env node
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
const rootDir = process.cwd();
|
|
11
|
+
const srcDir = path.join(rootDir, 'src');
|
|
12
|
+
const __ROUTE_ROOT_CONST__ = path.join(srcDir, '__ROUTE_ROOT_NAME__');
|
|
13
|
+
const featuresDir = path.join(srcDir, 'features');
|
|
14
|
+
|
|
15
|
+
const rawName = process.argv[2];
|
|
16
|
+
|
|
17
|
+
if (!rawName) {
|
|
18
|
+
console.error('사용법: npm run create:feature -- <feature-name>');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!/^[a-z][a-z0-9-]*$/.test(rawName)) {
|
|
23
|
+
console.error('feature 이름은 영문 소문자 kebab-case 형식이어야 하며 문자로 시작해야 합니다.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toPascalCase(value) {
|
|
28
|
+
return value
|
|
29
|
+
.split('-')
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureDir(dirPath) {
|
|
36
|
+
if (fs.existsSync(dirPath)) {
|
|
37
|
+
console.log(\`skip: \${path.relative(rootDir, dirPath)}\`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
42
|
+
console.log(\`create: \${path.relative(rootDir, dirPath)}\`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureFile(filePath, content) {
|
|
46
|
+
if (fs.existsSync(filePath)) {
|
|
47
|
+
console.log(\`skip: \${path.relative(rootDir, filePath)}\`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
52
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
53
|
+
console.log(\`create: \${path.relative(rootDir, filePath)}\`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getStyleMode() {
|
|
57
|
+
const packageJsonPath = path.join(rootDir, 'package.json');
|
|
58
|
+
|
|
59
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
60
|
+
return 'tailwind';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
65
|
+
return packageJson['__PACKAGE_JSON_KEY__']?.style === 'none' ? 'none' : 'tailwind';
|
|
66
|
+
} catch {
|
|
67
|
+
return 'tailwind';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getRoutePageContent(featureName, featureNamePascal, useStyles) {
|
|
72
|
+
if (!useStyles) {
|
|
73
|
+
return \`export default function \${featureNamePascal}Page() {
|
|
74
|
+
return (
|
|
75
|
+
<main>
|
|
76
|
+
<div>
|
|
77
|
+
<p>\${featureNamePascal}</p>
|
|
78
|
+
<h1>\${featureNamePascal}</h1>
|
|
79
|
+
<p>이 페이지를 실제 \${featureName} 도메인 화면으로 교체하세요.</p>
|
|
80
|
+
</div>
|
|
81
|
+
</main>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
\`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return \`export default function \${featureNamePascal}Page() {
|
|
88
|
+
return (
|
|
89
|
+
<main className="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-6 px-6 py-16">
|
|
90
|
+
<div>
|
|
91
|
+
<p className="text-sm font-medium uppercase tracking-[0.2em] text-zinc-500">\${featureNamePascal}</p>
|
|
92
|
+
<h1 className="mt-3 text-3xl font-semibold tracking-tight text-zinc-950">
|
|
93
|
+
\${featureNamePascal}
|
|
94
|
+
</h1>
|
|
95
|
+
<p className="mt-3 text-sm leading-6 text-zinc-600">
|
|
96
|
+
이 페이지를 실제 \${featureName} 도메인 화면으로 교체하세요.
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
</main>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
\`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const featureName = rawName;
|
|
106
|
+
const featureNamePascal = toPascalCase(featureName);
|
|
107
|
+
const featureDir = path.join(featuresDir, featureName);
|
|
108
|
+
const routeDir = path.join(__ROUTE_ROOT_CONST__, featureName);
|
|
109
|
+
const featurePath = \`/\${featureName}\`;
|
|
110
|
+
const useStyles = getStyleMode() !== 'none';
|
|
111
|
+
|
|
112
|
+
ensureDir(path.join(featureDir, 'api'));
|
|
113
|
+
ensureDir(path.join(featureDir, 'components'));
|
|
114
|
+
ensureDir(path.join(featureDir, 'hooks'));
|
|
115
|
+
ensureDir(path.join(featureDir, 'lib'));
|
|
116
|
+
|
|
117
|
+
ensureFile(
|
|
118
|
+
path.join(routeDir, 'page.tsx'),
|
|
119
|
+
getRoutePageContent(featureName, featureNamePascal, useStyles),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(\`"\${featureName}" feature 최소 스캐폴드 생성이 완료되었습니다.\`);
|
|
124
|
+
console.log('생성 대상: api/, components/, hooks/, lib/, __ROUTE_OUTPUT_LABEL__');
|
|
125
|
+
console.log('CRUD 예제가 필요하면 npm run create:feature:crud -- <feature-name> 을 사용하세요.');
|
|
126
|
+
__OPTIONAL_ROUTE_HINT__console.log(\`중앙 route 상수가 필요하면 src/shared/routes/route-paths.ts에 직접 추가하세요:\`);
|
|
127
|
+
console.log(\` \${featureName}: '\${featurePath}',\`);
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
export function buildStandaloneCreateFeatureScript(
|
|
131
|
+
preset: StandaloneFeaturePreset,
|
|
132
|
+
) {
|
|
133
|
+
const config = standaloneFeaturePresetConfigs[preset];
|
|
134
|
+
|
|
135
|
+
// 실제 출력 스크립트는 preset별로 완결된 한 파일이 되도록 여기서 치환합니다.
|
|
136
|
+
return createFeatureScriptTemplate
|
|
137
|
+
.replaceAll('__ROUTE_ROOT_CONST__', config.routeRootConst)
|
|
138
|
+
.replaceAll('__ROUTE_ROOT_NAME__', config.routeRootName)
|
|
139
|
+
.replaceAll('__PACKAGE_JSON_KEY__', config.packageJsonKey)
|
|
140
|
+
.replaceAll('__ROUTE_OUTPUT_LABEL__', config.routeOutputLabel)
|
|
141
|
+
.replaceAll('__OPTIONAL_ROUTE_HINT__', config.optionalRouteHint);
|
|
142
|
+
}
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as posixPath from 'node:path/posix';
|
|
4
|
+
|
|
5
|
+
import { buildStandaloneCreateFeatureCrudScript } from './create-feature-crud-script.ts';
|
|
6
|
+
import { buildStandaloneCreateFeatureScript } from './create-feature-script.ts';
|
|
7
|
+
import { getPresetPlan, type PresetPlan } from '../meta/preset-plan.ts';
|
|
8
|
+
import {
|
|
9
|
+
createRuntimeCopyPlan,
|
|
10
|
+
rewriteRuntimeImports,
|
|
11
|
+
type RuntimeCopyEntry,
|
|
12
|
+
} from '../meta/runtime-copy-plan.ts';
|
|
13
|
+
import type { PresetId } from '../meta/scaffold-manifest.ts';
|
|
14
|
+
|
|
15
|
+
export type ProjectFileCopyKind = 'template' | 'runtime';
|
|
16
|
+
|
|
17
|
+
export type ProjectFileCopy = {
|
|
18
|
+
kind: ProjectFileCopyKind;
|
|
19
|
+
sourceRelativePath: string;
|
|
20
|
+
sourceAbsolutePath: string;
|
|
21
|
+
targetRelativePath: string;
|
|
22
|
+
targetAbsolutePath: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ProjectGenerationPlan = {
|
|
26
|
+
preset: PresetId;
|
|
27
|
+
targetDir: string;
|
|
28
|
+
templateDir: string;
|
|
29
|
+
removePaths: string[];
|
|
30
|
+
templateFiles: ProjectFileCopy[];
|
|
31
|
+
runtimeFiles: Array<ProjectFileCopy & { runtimeEntry: RuntimeCopyEntry }>;
|
|
32
|
+
notes: string[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ExecuteProjectGenerationPlanOptions = {
|
|
36
|
+
plan: ProjectGenerationPlan;
|
|
37
|
+
dryRun?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ExecuteProjectGenerationPlanResult = {
|
|
41
|
+
removedPaths: string[];
|
|
42
|
+
copiedTemplateFiles: string[];
|
|
43
|
+
copiedRuntimeFiles: string[];
|
|
44
|
+
dryRun: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type BuildProjectGenerationPlanOptions = {
|
|
48
|
+
workspaceRoot: string;
|
|
49
|
+
targetDir: string;
|
|
50
|
+
preset: PresetId;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function toPosixPath(value: string) {
|
|
54
|
+
return value.replaceAll(path.sep, '/');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toSystemPath(rootDir: string, relativePosixPath: string) {
|
|
58
|
+
return path.join(rootDir, ...relativePosixPath.split('/'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveTemplateTargetRelativePath(relativeFilePath: string) {
|
|
62
|
+
const dirName = posixPath.dirname(relativeFilePath);
|
|
63
|
+
const baseName = posixPath.basename(relativeFilePath);
|
|
64
|
+
const mappedBaseName =
|
|
65
|
+
baseName === '_gitignore'
|
|
66
|
+
? '.gitignore'
|
|
67
|
+
: baseName === '_npmrc'
|
|
68
|
+
? '.npmrc'
|
|
69
|
+
: baseName;
|
|
70
|
+
|
|
71
|
+
if (dirName === '.') {
|
|
72
|
+
return mappedBaseName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return posixPath.join(dirName, mappedBaseName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function listRelativeFiles(rootDir: string) {
|
|
79
|
+
const collected: string[] = [];
|
|
80
|
+
|
|
81
|
+
async function walk(currentDir: string) {
|
|
82
|
+
const entries = await fs.readdir(currentDir, {
|
|
83
|
+
withFileTypes: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
88
|
+
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
await walk(absolutePath);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!entry.isFile()) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
collected.push(toPosixPath(path.relative(rootDir, absolutePath)));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await walk(rootDir);
|
|
103
|
+
|
|
104
|
+
return collected.sort((left, right) => left.localeCompare(right));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findDuplicateTargetPaths(fileCopies: ProjectFileCopy[]) {
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
const duplicates = new Set<string>();
|
|
110
|
+
|
|
111
|
+
for (const fileCopy of fileCopies) {
|
|
112
|
+
if (seen.has(fileCopy.targetRelativePath)) {
|
|
113
|
+
duplicates.add(fileCopy.targetRelativePath);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
seen.add(fileCopy.targetRelativePath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [...duplicates].sort((left, right) => left.localeCompare(right));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function buildTemplateFileCopies(
|
|
124
|
+
templateDir: string,
|
|
125
|
+
targetDir: string,
|
|
126
|
+
) {
|
|
127
|
+
const relativeFiles = await listRelativeFiles(templateDir);
|
|
128
|
+
|
|
129
|
+
return relativeFiles.map((relativeFilePath) => ({
|
|
130
|
+
kind: 'template' as const,
|
|
131
|
+
sourceRelativePath: relativeFilePath,
|
|
132
|
+
sourceAbsolutePath: toSystemPath(templateDir, relativeFilePath),
|
|
133
|
+
targetRelativePath: resolveTemplateTargetRelativePath(relativeFilePath),
|
|
134
|
+
targetAbsolutePath: toSystemPath(
|
|
135
|
+
targetDir,
|
|
136
|
+
resolveTemplateTargetRelativePath(relativeFilePath),
|
|
137
|
+
),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function buildRuntimeFileCopies(
|
|
142
|
+
workspaceRoot: string,
|
|
143
|
+
presetPlan: PresetPlan,
|
|
144
|
+
targetDir: string,
|
|
145
|
+
) {
|
|
146
|
+
const sourceRoot = path.join(workspaceRoot, 'source');
|
|
147
|
+
const runtimeSourceFiles = new Set<string>();
|
|
148
|
+
|
|
149
|
+
for (const mapping of presetPlan.runtime.mappings) {
|
|
150
|
+
const sourceDir = toSystemPath(sourceRoot, mapping.sourceDir);
|
|
151
|
+
const relativeFiles = await listRelativeFiles(sourceDir);
|
|
152
|
+
|
|
153
|
+
for (const relativeFilePath of relativeFiles) {
|
|
154
|
+
runtimeSourceFiles.add(posixPath.join(mapping.sourceDir, relativeFilePath));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const runtimeCopyEntries = createRuntimeCopyPlan(
|
|
159
|
+
presetPlan.preset,
|
|
160
|
+
[...runtimeSourceFiles],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return runtimeCopyEntries.map((runtimeEntry) => ({
|
|
164
|
+
kind: 'runtime' as const,
|
|
165
|
+
sourceRelativePath: runtimeEntry.sourcePath,
|
|
166
|
+
sourceAbsolutePath: toSystemPath(sourceRoot, runtimeEntry.sourcePath),
|
|
167
|
+
targetRelativePath: runtimeEntry.outputPath,
|
|
168
|
+
targetAbsolutePath: toSystemPath(targetDir, runtimeEntry.outputPath),
|
|
169
|
+
runtimeEntry,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function buildProjectGenerationPlan({
|
|
174
|
+
workspaceRoot,
|
|
175
|
+
targetDir,
|
|
176
|
+
preset,
|
|
177
|
+
}: BuildProjectGenerationPlanOptions): Promise<ProjectGenerationPlan> {
|
|
178
|
+
const presetPlan = getPresetPlan(preset);
|
|
179
|
+
const templateDir = path.join(workspaceRoot, presetPlan.templateDir);
|
|
180
|
+
const templateFiles = await buildTemplateFileCopies(templateDir, targetDir);
|
|
181
|
+
const runtimeFiles = await buildRuntimeFileCopies(
|
|
182
|
+
workspaceRoot,
|
|
183
|
+
presetPlan,
|
|
184
|
+
targetDir,
|
|
185
|
+
);
|
|
186
|
+
const duplicateTargetPaths = findDuplicateTargetPaths([
|
|
187
|
+
...templateFiles,
|
|
188
|
+
...runtimeFiles,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
if (duplicateTargetPaths.length > 0) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Duplicate generation targets detected for preset "${preset}": ${duplicateTargetPaths.join(
|
|
194
|
+
', ',
|
|
195
|
+
)}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
preset,
|
|
201
|
+
targetDir,
|
|
202
|
+
templateDir,
|
|
203
|
+
removePaths: presetPlan.scaffold.removeAfterScaffold,
|
|
204
|
+
templateFiles,
|
|
205
|
+
runtimeFiles,
|
|
206
|
+
notes: [
|
|
207
|
+
...presetPlan.notes,
|
|
208
|
+
'templateFiles는 정적 override 파일입니다.',
|
|
209
|
+
'runtimeFiles는 source/ 기반 runtime source of truth입니다.',
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function ensureParentDir(filePath: string) {
|
|
215
|
+
await fs.mkdir(path.dirname(filePath), {
|
|
216
|
+
recursive: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function removeTargetPaths(
|
|
221
|
+
targetDir: string,
|
|
222
|
+
relativePaths: string[],
|
|
223
|
+
dryRun: boolean,
|
|
224
|
+
) {
|
|
225
|
+
const removedPaths: string[] = [];
|
|
226
|
+
|
|
227
|
+
for (const relativePath of relativePaths) {
|
|
228
|
+
const absolutePath = toSystemPath(targetDir, relativePath);
|
|
229
|
+
removedPaths.push(relativePath);
|
|
230
|
+
|
|
231
|
+
if (dryRun) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await fs.rm(absolutePath, {
|
|
236
|
+
recursive: true,
|
|
237
|
+
force: true,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return removedPaths;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getFallbackProjectPackageName(targetAbsolutePath: string) {
|
|
245
|
+
const projectDirName = path.basename(path.dirname(targetAbsolutePath));
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
projectDirName
|
|
249
|
+
.trim()
|
|
250
|
+
.toLowerCase()
|
|
251
|
+
.replaceAll(/[^a-z0-9._-]+/g, '-')
|
|
252
|
+
.replaceAll(/^[._-]+|[._-]+$/g, '') || 'app'
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function readJsonIfExists(filePath: string) {
|
|
257
|
+
try {
|
|
258
|
+
const text = await fs.readFile(filePath, 'utf8');
|
|
259
|
+
|
|
260
|
+
return JSON.parse(text) as Record<string, unknown>;
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function buildTemplateFileContents(
|
|
267
|
+
workspaceRoot: string,
|
|
268
|
+
preset: PresetId,
|
|
269
|
+
fileCopy: ProjectFileCopy,
|
|
270
|
+
) {
|
|
271
|
+
if (
|
|
272
|
+
(preset === 'react' || preset === 'next') &&
|
|
273
|
+
fileCopy.targetRelativePath === 'scripts/create-feature-crud.mjs'
|
|
274
|
+
) {
|
|
275
|
+
return buildStandaloneCreateFeatureCrudScript(preset, workspaceRoot);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
(preset === 'react' || preset === 'next') &&
|
|
280
|
+
fileCopy.targetRelativePath === 'scripts/create-feature.mjs'
|
|
281
|
+
) {
|
|
282
|
+
return buildStandaloneCreateFeatureScript(preset);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
(preset === 'react' || preset === 'next') &&
|
|
287
|
+
fileCopy.targetRelativePath === 'package.json'
|
|
288
|
+
) {
|
|
289
|
+
const templatePackageJson = (await readJsonIfExists(
|
|
290
|
+
fileCopy.sourceAbsolutePath,
|
|
291
|
+
)) ?? {};
|
|
292
|
+
const existingPackageJson = await readJsonIfExists(fileCopy.targetAbsolutePath);
|
|
293
|
+
const mergedPackageJson = {
|
|
294
|
+
...(existingPackageJson ?? {}),
|
|
295
|
+
...templatePackageJson,
|
|
296
|
+
} as Record<string, unknown>;
|
|
297
|
+
|
|
298
|
+
// Standalone preset은 upstream scaffold가 먼저 package.json을 만들기 때문에
|
|
299
|
+
// 앱 이름과 기본 dependency는 upstream 값을 유지하고,
|
|
300
|
+
// 템플릿은 scripts/metadata만 얹는 쪽으로 merge합니다.
|
|
301
|
+
mergedPackageJson.name =
|
|
302
|
+
typeof existingPackageJson?.name === 'string'
|
|
303
|
+
? existingPackageJson.name
|
|
304
|
+
: getFallbackProjectPackageName(fileCopy.targetAbsolutePath);
|
|
305
|
+
mergedPackageJson.version =
|
|
306
|
+
typeof existingPackageJson?.version === 'string'
|
|
307
|
+
? existingPackageJson.version
|
|
308
|
+
: templatePackageJson.version ?? '0.1.0';
|
|
309
|
+
mergedPackageJson.private = true;
|
|
310
|
+
|
|
311
|
+
if (typeof existingPackageJson?.packageManager === 'string') {
|
|
312
|
+
mergedPackageJson.packageManager = existingPackageJson.packageManager;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
delete mergedPackageJson.bin;
|
|
316
|
+
delete mergedPackageJson.files;
|
|
317
|
+
|
|
318
|
+
return `${JSON.stringify(mergedPackageJson, null, 2)}\n`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function copyTemplateFiles(
|
|
325
|
+
workspaceRoot: string,
|
|
326
|
+
preset: PresetId,
|
|
327
|
+
fileCopies: ProjectFileCopy[],
|
|
328
|
+
dryRun: boolean,
|
|
329
|
+
) {
|
|
330
|
+
const copiedFiles: string[] = [];
|
|
331
|
+
|
|
332
|
+
for (const fileCopy of fileCopies) {
|
|
333
|
+
copiedFiles.push(fileCopy.targetRelativePath);
|
|
334
|
+
|
|
335
|
+
if (dryRun) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await ensureParentDir(fileCopy.targetAbsolutePath);
|
|
340
|
+
const fileContents = await buildTemplateFileContents(
|
|
341
|
+
workspaceRoot,
|
|
342
|
+
preset,
|
|
343
|
+
fileCopy,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (fileContents === null) {
|
|
347
|
+
await fs.copyFile(fileCopy.sourceAbsolutePath, fileCopy.targetAbsolutePath);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await fs.writeFile(fileCopy.targetAbsolutePath, fileContents, 'utf8');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return copiedFiles;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function escapeRegExp(value: string) {
|
|
358
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function stripImportFromModule(sourceText: string, moduleSpecifier: string) {
|
|
362
|
+
const escapedSpecifier = escapeRegExp(moduleSpecifier);
|
|
363
|
+
|
|
364
|
+
return sourceText.replace(
|
|
365
|
+
new RegExp(
|
|
366
|
+
`import\\s*\\{[\\s\\S]*?\\}\\s*from\\s*['"]${escapedSpecifier}['"];\\n?`,
|
|
367
|
+
'm',
|
|
368
|
+
),
|
|
369
|
+
'',
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function stripTypeReExportBlock(sourceText: string) {
|
|
374
|
+
return sourceText.replace(/export type\s*\{[\s\S]*?\};\n\n?/m, '');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function splitLeadingImports(sourceText: string) {
|
|
378
|
+
const match = sourceText.match(/^(?:import[\s\S]*?;\n\s*)+/);
|
|
379
|
+
|
|
380
|
+
if (!match) {
|
|
381
|
+
return {
|
|
382
|
+
imports: '',
|
|
383
|
+
body: sourceText.trim(),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
imports: match[0].trim(),
|
|
389
|
+
body: sourceText.slice(match[0].length).trim(),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function renameStandaloneRequestCoreEntryPoint(sourceText: string) {
|
|
394
|
+
return sourceText.replace(
|
|
395
|
+
/export async function executeRequest\s*</,
|
|
396
|
+
'async function executeSharedRequest<',
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function buildStandaloneMergedRequestText(coreText: string, wrapperText: string) {
|
|
401
|
+
const mergedCoreText = renameStandaloneRequestCoreEntryPoint(coreText).trim();
|
|
402
|
+
const mergedWrapperText = stripTypeReExportBlock(
|
|
403
|
+
stripImportFromModule(wrapperText, './request-core'),
|
|
404
|
+
).trim();
|
|
405
|
+
|
|
406
|
+
return `${mergedCoreText}\n\n${mergedWrapperText}\n`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function renameMonorepoRequestCoreTypes(sourceText: string) {
|
|
410
|
+
return sourceText.replaceAll(/\bRequestOptions\b/g, 'BaseRequestOptions');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function buildMonorepoMergedRequestText(coreText: string, wrapperText: string) {
|
|
414
|
+
const mergedCoreText = renameMonorepoRequestCoreTypes(
|
|
415
|
+
renameStandaloneRequestCoreEntryPoint(coreText),
|
|
416
|
+
).trim();
|
|
417
|
+
const normalizedWrapperText = stripTypeReExportBlock(
|
|
418
|
+
stripImportFromModule(wrapperText, './request-core'),
|
|
419
|
+
)
|
|
420
|
+
.replaceAll('SharedRequestOptions', 'BaseRequestOptions')
|
|
421
|
+
.trim();
|
|
422
|
+
const { imports, body } = splitLeadingImports(normalizedWrapperText);
|
|
423
|
+
|
|
424
|
+
return `${imports}\n\n${mergedCoreText}\n\n${body}\n`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function applyRuntimeOutputCompositions(
|
|
428
|
+
preset: PresetId,
|
|
429
|
+
sourceTextByTargetPath: Map<string, string>,
|
|
430
|
+
) {
|
|
431
|
+
const nextSourceTextByTargetPath = new Map(sourceTextByTargetPath);
|
|
432
|
+
const skippedTargetPaths = new Set<string>();
|
|
433
|
+
|
|
434
|
+
if (preset === 'react' || preset === 'next') {
|
|
435
|
+
const requestCoreTargetPath = 'src/shared/api/request-core.ts';
|
|
436
|
+
const requestTargetPath = 'src/shared/api/request.ts';
|
|
437
|
+
const requestCoreText = nextSourceTextByTargetPath.get(requestCoreTargetPath);
|
|
438
|
+
const requestText = nextSourceTextByTargetPath.get(requestTargetPath);
|
|
439
|
+
|
|
440
|
+
if (requestCoreText && requestText) {
|
|
441
|
+
nextSourceTextByTargetPath.set(
|
|
442
|
+
requestTargetPath,
|
|
443
|
+
buildStandaloneMergedRequestText(requestCoreText, requestText),
|
|
444
|
+
);
|
|
445
|
+
skippedTargetPaths.add(requestCoreTargetPath);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (preset === 'monorepo') {
|
|
450
|
+
const requestCoreTargetPath = 'packages/core/src/request/request-core.ts';
|
|
451
|
+
const requestTargetPath = 'packages/core/src/request/core-request.ts';
|
|
452
|
+
const requestCoreText = nextSourceTextByTargetPath.get(requestCoreTargetPath);
|
|
453
|
+
const requestText = nextSourceTextByTargetPath.get(requestTargetPath);
|
|
454
|
+
|
|
455
|
+
if (requestCoreText && requestText) {
|
|
456
|
+
nextSourceTextByTargetPath.set(
|
|
457
|
+
requestTargetPath,
|
|
458
|
+
buildMonorepoMergedRequestText(requestCoreText, requestText),
|
|
459
|
+
);
|
|
460
|
+
skippedTargetPaths.add(requestCoreTargetPath);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
sourceTextByTargetPath: nextSourceTextByTargetPath,
|
|
466
|
+
skippedTargetPaths,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function copyRuntimeFiles(
|
|
471
|
+
preset: PresetId,
|
|
472
|
+
runtimeFiles: Array<ProjectFileCopy & { runtimeEntry: RuntimeCopyEntry }>,
|
|
473
|
+
dryRun: boolean,
|
|
474
|
+
) {
|
|
475
|
+
const copiedFiles: string[] = [];
|
|
476
|
+
const availableSourceFiles = runtimeFiles.map((runtimeFile) => runtimeFile.sourceRelativePath);
|
|
477
|
+
const rewrittenSourceTextByTargetPath = new Map<string, string>();
|
|
478
|
+
|
|
479
|
+
for (const runtimeFile of runtimeFiles) {
|
|
480
|
+
const sourceText = await fs.readFile(runtimeFile.sourceAbsolutePath, 'utf8');
|
|
481
|
+
const rewrittenSourceText = rewriteRuntimeImports({
|
|
482
|
+
preset,
|
|
483
|
+
sourcePath: runtimeFile.sourceRelativePath,
|
|
484
|
+
sourceText,
|
|
485
|
+
availableSourceFiles,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
rewrittenSourceTextByTargetPath.set(
|
|
489
|
+
runtimeFile.targetRelativePath,
|
|
490
|
+
rewrittenSourceText,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const { sourceTextByTargetPath, skippedTargetPaths } = applyRuntimeOutputCompositions(
|
|
495
|
+
preset,
|
|
496
|
+
rewrittenSourceTextByTargetPath,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
for (const runtimeFile of runtimeFiles) {
|
|
500
|
+
if (skippedTargetPaths.has(runtimeFile.targetRelativePath)) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
copiedFiles.push(runtimeFile.targetRelativePath);
|
|
505
|
+
|
|
506
|
+
if (dryRun) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const rewrittenSourceText = sourceTextByTargetPath.get(
|
|
511
|
+
runtimeFile.targetRelativePath,
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (!rewrittenSourceText) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
`Missing rewritten runtime source for "${runtimeFile.targetRelativePath}"`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
await ensureParentDir(runtimeFile.targetAbsolutePath);
|
|
521
|
+
await fs.writeFile(runtimeFile.targetAbsolutePath, rewrittenSourceText, 'utf8');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return copiedFiles;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function executeProjectGenerationPlan({
|
|
528
|
+
plan,
|
|
529
|
+
dryRun = false,
|
|
530
|
+
}: ExecuteProjectGenerationPlanOptions): Promise<ExecuteProjectGenerationPlanResult> {
|
|
531
|
+
const removedPaths = await removeTargetPaths(plan.targetDir, plan.removePaths, dryRun);
|
|
532
|
+
const copiedTemplateFiles = await copyTemplateFiles(
|
|
533
|
+
path.dirname(path.dirname(plan.templateDir)),
|
|
534
|
+
plan.preset,
|
|
535
|
+
plan.templateFiles,
|
|
536
|
+
dryRun,
|
|
537
|
+
);
|
|
538
|
+
const copiedRuntimeFiles = await copyRuntimeFiles(plan.preset, plan.runtimeFiles, dryRun);
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
removedPaths,
|
|
542
|
+
copiedTemplateFiles,
|
|
543
|
+
copiedRuntimeFiles,
|
|
544
|
+
dryRun,
|
|
545
|
+
};
|
|
546
|
+
}
|