@apps-in-toss/cli 3.0.1 → 3.0.3
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/dist/index.js +323 -36
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { Builtins, Cli, Command, Option } from "clipanion";
|
|
3
4
|
import * as p from "@clack/prompts";
|
|
4
5
|
import path from "path";
|
|
5
6
|
import { appendFile, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import path$1, { extname, join, resolve } from "node:path";
|
|
7
|
+
import path$1, { basename, dirname, extname, join, parse, resolve } from "node:path";
|
|
7
8
|
import { AppsInTossBundle, PlatformType } from "@apps-in-toss/ait-format";
|
|
8
9
|
import { loadConfig } from "c12";
|
|
9
10
|
import { uuidv7 } from "uuidv7";
|
|
10
|
-
import fs, { existsSync } from "node:fs";
|
|
11
|
+
import fs, { existsSync, readFileSync } from "node:fs";
|
|
11
12
|
import assert from "node:assert";
|
|
12
13
|
import os from "node:os";
|
|
13
14
|
import { setTimeout } from "node:timers/promises";
|
|
14
15
|
import jscodeshift from "jscodeshift";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
15
17
|
//#region src/utils/getPackageRoot.ts
|
|
16
18
|
function getPackageRoot(base) {
|
|
17
19
|
let dirPath = base;
|
|
@@ -72,8 +74,16 @@ async function buildArtifact() {
|
|
|
72
74
|
outPath
|
|
73
75
|
};
|
|
74
76
|
}
|
|
77
|
+
const SDK_PACKAGE_NAME = "@apps-in-toss/web-framework";
|
|
75
78
|
async function getSDKPackageJson() {
|
|
76
|
-
const
|
|
79
|
+
const projectRequire = createRequire(path$1.resolve(getPackageRoot(process.cwd()), "package.json"));
|
|
80
|
+
let packageJsonPath;
|
|
81
|
+
try {
|
|
82
|
+
packageJsonPath = projectRequire.resolve(`${SDK_PACKAGE_NAME}/package.json`);
|
|
83
|
+
} catch {
|
|
84
|
+
packageJsonPath = path$1.resolve(getPackageRoot(import.meta.dirname), "package.json");
|
|
85
|
+
}
|
|
86
|
+
const packageJsonString = (await readFile(packageJsonPath)).toString();
|
|
77
87
|
return JSON.parse(packageJsonString);
|
|
78
88
|
}
|
|
79
89
|
async function getAppsInTossConfig() {
|
|
@@ -430,12 +440,227 @@ var DeployCommand = class extends Command {
|
|
|
430
440
|
}
|
|
431
441
|
};
|
|
432
442
|
//#endregion
|
|
443
|
+
//#region src/utils/jscodeshift.ts
|
|
444
|
+
/**
|
|
445
|
+
* 설정 파일 확장자에 맞는 파서를 골라요.
|
|
446
|
+
* 기본 파서는 JavaScript 전용이라 .ts 설정 파일의 type 선언, satisfies 같은
|
|
447
|
+
* TypeScript 문법에서 파싱이 실패해요.
|
|
448
|
+
*/
|
|
449
|
+
function getParserForFile(filepath) {
|
|
450
|
+
switch (extname(filepath)) {
|
|
451
|
+
case ".ts":
|
|
452
|
+
case ".mts":
|
|
453
|
+
case ".cts": return jscodeshift.withParser("ts");
|
|
454
|
+
case ".tsx": return jscodeshift.withParser("tsx");
|
|
455
|
+
default: return jscodeshift.withParser("babel");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 이름이 있는 오브젝트 프로퍼티만 골라내요.
|
|
460
|
+
* 파서에 따라 Property(estree) 또는 ObjectProperty(babel) 노드로 파싱되므로 둘 다 확인해요.
|
|
461
|
+
*/
|
|
462
|
+
function asNamedProperty(prop) {
|
|
463
|
+
if ((jscodeshift.Property.check(prop) || jscodeshift.ObjectProperty.check(prop)) && jscodeshift.Identifier.check(prop.key)) return prop;
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/utils/devtools.ts
|
|
468
|
+
const DEVTOOLS_PACKAGE_NAME = "@apps-in-toss/devtools";
|
|
469
|
+
const DEVTOOLS_UNPLUGIN_SPECIFIER = "@apps-in-toss/devtools/unplugin";
|
|
470
|
+
const DEVTOOLS_IMPORT_NAME = "aitDevtools";
|
|
471
|
+
/**
|
|
472
|
+
* changesets fixed 그룹으로 CLI와 devtools가 항상 같은 버전으로 배포돼요.
|
|
473
|
+
* 그래서 CLI 자신의 버전이 곧 함께 설치할 devtools의 호환 버전이에요.
|
|
474
|
+
*/
|
|
475
|
+
function getDevtoolsVersionRange() {
|
|
476
|
+
const { version } = createRequire(import.meta.url)("@apps-in-toss/cli/package.json");
|
|
477
|
+
return `^${version}`;
|
|
478
|
+
}
|
|
479
|
+
const BUNDLER_CONFIG_BASES = [
|
|
480
|
+
["vite.config", "vite"],
|
|
481
|
+
["webpack.config", "webpack"],
|
|
482
|
+
["rspack.config", "rspack"],
|
|
483
|
+
["rollup.config", "rollup"]
|
|
484
|
+
];
|
|
485
|
+
const CONFIG_EXTENSIONS = [
|
|
486
|
+
".ts",
|
|
487
|
+
".mts",
|
|
488
|
+
".cts",
|
|
489
|
+
".js",
|
|
490
|
+
".mjs",
|
|
491
|
+
".cjs"
|
|
492
|
+
];
|
|
493
|
+
/**
|
|
494
|
+
* 프로젝트 루트에서 devtools unplugin을 꽂을 번들러 설정 파일을 찾아요.
|
|
495
|
+
*/
|
|
496
|
+
function findBundlerConfig(projectRoot) {
|
|
497
|
+
for (const [base, bundler] of BUNDLER_CONFIG_BASES) for (const extension of CONFIG_EXTENSIONS) {
|
|
498
|
+
const path = join(projectRoot, `${base}${extension}`);
|
|
499
|
+
if (existsSync(path)) return {
|
|
500
|
+
path,
|
|
501
|
+
bundler
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* 번들러 설정 소스의 plugins 배열 맨 앞에 devtools unplugin을 꽂아요.
|
|
508
|
+
* 맨 앞이어야 SDK → mock alias가 다른 플러그인의 resolve보다 먼저 걸려요.
|
|
509
|
+
*
|
|
510
|
+
* 파일에 쓰지 않고 변환된 소스만 돌려줘요. 변환할 수 없는 모양(CommonJS 설정,
|
|
511
|
+
* plugins 배열 없음)이면 needs-manual로 알려서 호출부가 수동 안내를 하게 해요.
|
|
512
|
+
*/
|
|
513
|
+
function injectDevtoolsPlugin(source, configPath, bundler) {
|
|
514
|
+
if (source.includes("@apps-in-toss/devtools/unplugin")) return { status: "already-installed" };
|
|
515
|
+
if (/\bmodule\.exports\b|\brequire\s*\(/.test(source)) return {
|
|
516
|
+
status: "needs-manual",
|
|
517
|
+
reason: "CommonJS 형식 설정 파일은 자동으로 수정하지 않아요."
|
|
518
|
+
};
|
|
519
|
+
const j = getParserForFile(configPath);
|
|
520
|
+
let root;
|
|
521
|
+
try {
|
|
522
|
+
root = j(source);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
return {
|
|
525
|
+
status: "needs-manual",
|
|
526
|
+
reason: `설정 파일을 파싱하지 못했어요: ${error instanceof Error ? error.message : String(error)}`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
let pluginsArray = null;
|
|
530
|
+
root.find(jscodeshift.ObjectExpression).forEach((path) => {
|
|
531
|
+
if (pluginsArray != null) return;
|
|
532
|
+
for (const rawProp of path.value.properties) {
|
|
533
|
+
const prop = asNamedProperty(rawProp);
|
|
534
|
+
if (prop?.key.name === "plugins" && jscodeshift.ArrayExpression.check(prop.value)) {
|
|
535
|
+
pluginsArray = prop.value;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
if (pluginsArray == null) return {
|
|
541
|
+
status: "needs-manual",
|
|
542
|
+
reason: "설정 파일에서 plugins 배열을 찾지 못했어요."
|
|
543
|
+
};
|
|
544
|
+
const pluginCall = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(DEVTOOLS_IMPORT_NAME), jscodeshift.identifier(bundler)), []);
|
|
545
|
+
pluginsArray.elements.unshift(pluginCall);
|
|
546
|
+
const importDecl = jscodeshift.importDeclaration([jscodeshift.importDefaultSpecifier(jscodeshift.identifier(DEVTOOLS_IMPORT_NAME))], jscodeshift.stringLiteral(DEVTOOLS_UNPLUGIN_SPECIFIER));
|
|
547
|
+
const imports = root.find(jscodeshift.ImportDeclaration);
|
|
548
|
+
if (imports.size() > 0) imports.at(imports.size() - 1).insertAfter(importDecl);
|
|
549
|
+
else root.get().node.program.body.unshift(importDecl);
|
|
550
|
+
return {
|
|
551
|
+
status: "transformed",
|
|
552
|
+
source: root.toSource()
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* 자동 주입에 실패했을 때 보여줄 수동 설치 안내예요.
|
|
557
|
+
*/
|
|
558
|
+
function devtoolsManualGuide(bundler = "vite") {
|
|
559
|
+
return [
|
|
560
|
+
`번들러 설정의 plugins 배열 맨 앞에 devtools를 직접 추가해주세요.`,
|
|
561
|
+
"",
|
|
562
|
+
` import ${DEVTOOLS_IMPORT_NAME} from "${DEVTOOLS_UNPLUGIN_SPECIFIER}";`,
|
|
563
|
+
"",
|
|
564
|
+
` plugins: [${DEVTOOLS_IMPORT_NAME}.${bundler}(), /* 기존 플러그인들 */]`
|
|
565
|
+
].join("\n");
|
|
566
|
+
}
|
|
567
|
+
//#endregion
|
|
433
568
|
//#region src/utils/ensureSelect.ts
|
|
434
569
|
async function ensureSelect({ value, prompt }) {
|
|
435
570
|
if (value) return value;
|
|
436
571
|
return await prompt();
|
|
437
572
|
}
|
|
438
573
|
//#endregion
|
|
574
|
+
//#region src/utils/installDependencies.ts
|
|
575
|
+
function asPackageManager(name) {
|
|
576
|
+
if (name === "npm" || name === "yarn" || name === "pnpm" || name === "bun") return name;
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* CLI를 실행한 패키지 매니저를 `npm_config_user_agent`에서 읽어요.
|
|
581
|
+
* `yarn ait ...`, `npx ait ...`처럼 매니저를 거쳐 실행되면 항상 세팅되는 값이라
|
|
582
|
+
* lockfile 추측보다 정확해요. 예: "yarn/4.12.0 npm/? node/v24.14.0 darwin arm64"
|
|
583
|
+
*/
|
|
584
|
+
function packageManagerFromUserAgent(userAgent) {
|
|
585
|
+
return asPackageManager(userAgent?.split("/")[0]);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* 프로젝트에서 위로 올라가며 package.json의 `packageManager` 필드를 찾아요.
|
|
589
|
+
* 모노레포처럼 필드가 상위 디렉토리에 있는 경우까지 커버해요.
|
|
590
|
+
*/
|
|
591
|
+
function packageManagerFromField(projectRoot) {
|
|
592
|
+
let dir = projectRoot;
|
|
593
|
+
const { root } = parse(dir);
|
|
594
|
+
while (true) {
|
|
595
|
+
const packageJsonPath = join(dir, "package.json");
|
|
596
|
+
if (existsSync(packageJsonPath)) try {
|
|
597
|
+
const { packageManager } = JSON.parse(readFileSync(packageJsonPath, { encoding: "utf-8" }));
|
|
598
|
+
const found = asPackageManager(packageManager?.split("@")[0]);
|
|
599
|
+
if (found != null) return found;
|
|
600
|
+
} catch {}
|
|
601
|
+
if (dir === root) return null;
|
|
602
|
+
dir = dirname(dir);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* 의존성을 설치할 패키지 매니저를 알아내요.
|
|
607
|
+
* CLI를 실행한 매니저(`npm_config_user_agent`)가 1순위, 직접 실행됐을 때는
|
|
608
|
+
* `packageManager` 필드로 폴백하고, 둘 다 없으면 npm을 써요.
|
|
609
|
+
*/
|
|
610
|
+
function detectPackageManager(projectRoot) {
|
|
611
|
+
return packageManagerFromUserAgent(process.env.npm_config_user_agent) ?? packageManagerFromField(projectRoot) ?? "npm";
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* 감지한 패키지 매니저로 의존성 설치를 한 번 실행해요.
|
|
615
|
+
* 실패해도 전체 흐름을 멈추지 않고, 직접 실행할 명령어를 안내해요.
|
|
616
|
+
*/
|
|
617
|
+
async function installDependencies(projectRoot) {
|
|
618
|
+
const packageManager = detectPackageManager(projectRoot);
|
|
619
|
+
const spinner = p.spinner();
|
|
620
|
+
spinner.start(`의존성을 설치하는 중... (${packageManager} install)`);
|
|
621
|
+
const { code, output } = await runInstall(packageManager, projectRoot);
|
|
622
|
+
if (code === 0) {
|
|
623
|
+
spinner.stop(`의존성을 설치했어요. (${packageManager} install)`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
spinner.stop(`${packageManager} install 실행에 실패했어요.`);
|
|
627
|
+
const outputTail = output.trim().split("\n").slice(-15).join("\n");
|
|
628
|
+
if (outputTail.length > 0) p.log.message(outputTail);
|
|
629
|
+
p.log.warn(`직접 실행해주세요: ${packageManager} install`);
|
|
630
|
+
}
|
|
631
|
+
function runInstall(packageManager, cwd) {
|
|
632
|
+
return new Promise((resolvePromise) => {
|
|
633
|
+
const child = spawn(packageManager, ["install"], {
|
|
634
|
+
cwd,
|
|
635
|
+
shell: process.platform === "win32",
|
|
636
|
+
stdio: [
|
|
637
|
+
"ignore",
|
|
638
|
+
"pipe",
|
|
639
|
+
"pipe"
|
|
640
|
+
]
|
|
641
|
+
});
|
|
642
|
+
let output = "";
|
|
643
|
+
child.stdout.on("data", (chunk) => {
|
|
644
|
+
output += chunk.toString();
|
|
645
|
+
});
|
|
646
|
+
child.stderr.on("data", (chunk) => {
|
|
647
|
+
output += chunk.toString();
|
|
648
|
+
});
|
|
649
|
+
child.on("error", (error) => {
|
|
650
|
+
resolvePromise({
|
|
651
|
+
code: null,
|
|
652
|
+
output: `${output}\n${error.message}`
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
child.on("close", (code) => {
|
|
656
|
+
resolvePromise({
|
|
657
|
+
code,
|
|
658
|
+
output
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
//#endregion
|
|
439
664
|
//#region src/utils/kebabCase.ts
|
|
440
665
|
/**
|
|
441
666
|
* 문자열을 케밥-케이스로 변환해요. (예: "My App" -> "my-app", "myApp" -> "my-app")
|
|
@@ -484,6 +709,11 @@ async function templateWebFramework({ appName, cwd, skipInput }) {
|
|
|
484
709
|
packageJson.scripts ??= {};
|
|
485
710
|
if (packageJson.scripts.build) packageJson.scripts.build = packageJson.scripts.build + " && ait build";
|
|
486
711
|
packageJson.scripts.deploy = "ait deploy";
|
|
712
|
+
p.log.step(`${DEVTOOLS_PACKAGE_NAME}를 devDependencies에 추가하는 중...`);
|
|
713
|
+
packageJson.devDependencies = {
|
|
714
|
+
...packageJson.devDependencies,
|
|
715
|
+
[DEVTOOLS_PACKAGE_NAME]: getDevtoolsVersionRange()
|
|
716
|
+
};
|
|
487
717
|
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), { encoding: "utf-8" });
|
|
488
718
|
p.log.step(".gitignore 파일을 업데이트하는 중...");
|
|
489
719
|
await appendFile(join(cwd, ".gitignore"), "\n*.ait\n");
|
|
@@ -502,6 +732,33 @@ async function templateWebFramework({ appName, cwd, skipInput }) {
|
|
|
502
732
|
webBundleDir
|
|
503
733
|
});
|
|
504
734
|
await writeFile(join(cwd, "apps-in-toss.config.ts"), config, { encoding: "utf-8" });
|
|
735
|
+
await setupDevtoolsPlugin(cwd);
|
|
736
|
+
await installDependencies(cwd);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* 번들러 설정의 plugins 맨 앞에 devtools unplugin을 자동으로 꽂아줘요.
|
|
740
|
+
* 자동 주입이 어려운 모양이면 파일을 건드리지 않고 수동 안내만 보여줘요.
|
|
741
|
+
*/
|
|
742
|
+
async function setupDevtoolsPlugin(cwd) {
|
|
743
|
+
const bundlerConfig = findBundlerConfig(cwd);
|
|
744
|
+
if (bundlerConfig == null) {
|
|
745
|
+
p.note(devtoolsManualGuide(), "devtools 수동 설치 안내");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const result = injectDevtoolsPlugin(await readFile(bundlerConfig.path, { encoding: "utf-8" }), bundlerConfig.path, bundlerConfig.bundler);
|
|
749
|
+
switch (result.status) {
|
|
750
|
+
case "transformed":
|
|
751
|
+
await writeFile(bundlerConfig.path, result.source, { encoding: "utf-8" });
|
|
752
|
+
p.log.step(`${basename(bundlerConfig.path)}에 devtools 플러그인을 추가했어요.`);
|
|
753
|
+
break;
|
|
754
|
+
case "already-installed":
|
|
755
|
+
p.log.info("devtools 플러그인이 이미 설정되어 있어요.");
|
|
756
|
+
break;
|
|
757
|
+
case "needs-manual":
|
|
758
|
+
p.log.warn(result.reason);
|
|
759
|
+
p.note(devtoolsManualGuide(bundlerConfig.bundler), "devtools 수동 설치 안내");
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
505
762
|
}
|
|
506
763
|
var InitCommand = class extends Command {
|
|
507
764
|
static paths = [["init"]];
|
|
@@ -560,14 +817,15 @@ async function migrateWebFrameworkV3() {
|
|
|
560
817
|
process.exit(1);
|
|
561
818
|
}
|
|
562
819
|
await runMigration(plan);
|
|
820
|
+
await installDependencies(projectRoot);
|
|
563
821
|
p.log.success("마이그레이션이 완료됐어요.");
|
|
564
|
-
printPostMigrationNotes(plan
|
|
822
|
+
printPostMigrationNotes(plan);
|
|
565
823
|
}
|
|
566
824
|
/**
|
|
567
825
|
* 마이그레이션 이후 개발자가 꼭 알아야 하는 유의사항을 안내해요.
|
|
568
826
|
*/
|
|
569
|
-
function printPostMigrationNotes(
|
|
570
|
-
const host = appName ?? "<appName>";
|
|
827
|
+
function printPostMigrationNotes(plan) {
|
|
828
|
+
const host = plan.appName ?? "<appName>";
|
|
571
829
|
p.note([
|
|
572
830
|
"1. SDK 3.x 출시 후 롤백이 안돼요",
|
|
573
831
|
" SDK 3.x 이상이 적용된 앱 번들을 출시하면 SDK 2.x 버전으로 롤백할 수 없어요.",
|
|
@@ -579,8 +837,13 @@ function printPostMigrationNotes(appName) {
|
|
|
579
837
|
" Origin 허용 목록에 다음 도메인을 등록하세요.",
|
|
580
838
|
"",
|
|
581
839
|
` - https://${host}.web.tossmini.com : 실제 서비스 환경`,
|
|
582
|
-
` - https://${host}.private-web.tossmini.com : 콘솔 QR 테스트
|
|
840
|
+
` - https://${host}.private-web.tossmini.com : 콘솔 QR 테스트 환경`,
|
|
841
|
+
"",
|
|
842
|
+
`3. 개발용 devtools가 함께 설치돼요`,
|
|
843
|
+
` ${DEVTOOLS_PACKAGE_NAME}가 devDependencies에 추가됐어요.`,
|
|
844
|
+
" 브라우저에서 mock SDK와 devtools 패널로 미니앱을 개발할 수 있어요."
|
|
583
845
|
].join("\n"), "유의사항");
|
|
846
|
+
if (plan.devtoolsManualBundler != null) p.note(devtoolsManualGuide(plan.devtoolsManualBundler), "devtools 수동 설치 안내");
|
|
584
847
|
}
|
|
585
848
|
/**
|
|
586
849
|
* 파일을 변경하기 전에 모든 조건을 검증하고, 실행할 변경 내용을 준비해요.
|
|
@@ -662,8 +925,13 @@ async function planMigration(projectRoot) {
|
|
|
662
925
|
...packageJson.parsed.scripts,
|
|
663
926
|
dev: devCommand,
|
|
664
927
|
build: `${buildCommand} && ait build`
|
|
928
|
+
},
|
|
929
|
+
devDependencies: {
|
|
930
|
+
...packageJson.parsed.devDependencies,
|
|
931
|
+
[DEVTOOLS_PACKAGE_NAME]: getDevtoolsVersionRange()
|
|
665
932
|
}
|
|
666
933
|
};
|
|
934
|
+
const { bundlerConfig, devtoolsManualBundler } = await planDevtoolsPluginInjection(projectRoot);
|
|
667
935
|
const appName = graniteConfig.config.appName;
|
|
668
936
|
return {
|
|
669
937
|
issues,
|
|
@@ -676,33 +944,74 @@ async function planMigration(projectRoot) {
|
|
|
676
944
|
hasExistingAppsInTossConfig,
|
|
677
945
|
packageJsonPath,
|
|
678
946
|
packageJsonSource: packageJson.source,
|
|
679
|
-
nextPackageJsonSource: JSON.stringify(nextPackageJson, null, 2) + (packageJson.source.endsWith("\n") ? "\n" : "")
|
|
947
|
+
nextPackageJsonSource: JSON.stringify(nextPackageJson, null, 2) + (packageJson.source.endsWith("\n") ? "\n" : ""),
|
|
948
|
+
bundlerConfig,
|
|
949
|
+
devtoolsManualBundler
|
|
680
950
|
}
|
|
681
951
|
};
|
|
682
952
|
}
|
|
683
953
|
/**
|
|
954
|
+
* devtools unplugin을 꽂을 번들러 설정 변경을 메모리에서만 준비해요.
|
|
955
|
+
* 자동으로 못 꽂는 모양이면 파일을 건드리지 않고 수동 안내 대상만 정해요.
|
|
956
|
+
*/
|
|
957
|
+
async function planDevtoolsPluginInjection(projectRoot) {
|
|
958
|
+
const found = findBundlerConfig(projectRoot);
|
|
959
|
+
if (found == null) return {
|
|
960
|
+
bundlerConfig: null,
|
|
961
|
+
devtoolsManualBundler: "vite"
|
|
962
|
+
};
|
|
963
|
+
const source = (await readFile(found.path)).toString();
|
|
964
|
+
const result = injectDevtoolsPlugin(source, found.path, found.bundler);
|
|
965
|
+
switch (result.status) {
|
|
966
|
+
case "transformed": return {
|
|
967
|
+
bundlerConfig: {
|
|
968
|
+
path: found.path,
|
|
969
|
+
source,
|
|
970
|
+
nextSource: result.source
|
|
971
|
+
},
|
|
972
|
+
devtoolsManualBundler: null
|
|
973
|
+
};
|
|
974
|
+
case "already-installed": return {
|
|
975
|
+
bundlerConfig: null,
|
|
976
|
+
devtoolsManualBundler: null
|
|
977
|
+
};
|
|
978
|
+
case "needs-manual": return {
|
|
979
|
+
bundlerConfig: null,
|
|
980
|
+
devtoolsManualBundler: found.bundler
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
684
985
|
* 준비된 변경 내용을 트랜잭션으로 실행해요.
|
|
685
986
|
* 스텝이 하나라도 실패하면 완료된 스텝을 역순으로 되돌리고 종료해요.
|
|
686
987
|
* 기존 apps-in-toss.config가 있으면 백업해뒀다가, 모두 성공하면 백업을 지워요.
|
|
687
988
|
*/
|
|
688
989
|
async function runMigration(plan) {
|
|
689
990
|
const backupPath = `${plan.appsInTossConfigPath}.bak`;
|
|
991
|
+
const backupSteps = plan.hasExistingAppsInTossConfig ? [{
|
|
992
|
+
description: "기존 apps-in-toss.config 파일을 백업해요.",
|
|
993
|
+
run: () => rename(plan.appsInTossConfigPath, backupPath),
|
|
994
|
+
undo: () => rename(backupPath, plan.appsInTossConfigPath)
|
|
995
|
+
}] : [];
|
|
996
|
+
const bundlerConfig = plan.bundlerConfig;
|
|
997
|
+
const bundlerConfigSteps = bundlerConfig == null ? [] : [{
|
|
998
|
+
description: "번들러 설정에 devtools 플러그인을 추가해요.",
|
|
999
|
+
run: () => atomicWriteFile(bundlerConfig.path, bundlerConfig.nextSource),
|
|
1000
|
+
undo: () => atomicWriteFile(bundlerConfig.path, bundlerConfig.source)
|
|
1001
|
+
}];
|
|
690
1002
|
await executeTransaction([
|
|
691
|
-
...
|
|
692
|
-
description: "기존 apps-in-toss.config 파일을 백업해요.",
|
|
693
|
-
run: () => rename(plan.appsInTossConfigPath, backupPath),
|
|
694
|
-
undo: () => rename(backupPath, plan.appsInTossConfigPath)
|
|
695
|
-
}] : [],
|
|
1003
|
+
...backupSteps,
|
|
696
1004
|
{
|
|
697
1005
|
description: "granite.config를 apps-in-toss.config로 마이그레이션해요.",
|
|
698
1006
|
run: () => atomicWriteFile(plan.appsInTossConfigPath, plan.appsInTossConfigSource),
|
|
699
1007
|
undo: () => rm(plan.appsInTossConfigPath, { force: true })
|
|
700
1008
|
},
|
|
701
1009
|
{
|
|
702
|
-
description: "package.json의 dev, build 스크립트를
|
|
1010
|
+
description: "package.json의 dev, build 스크립트를 수정하고 devtools를 추가해요.",
|
|
703
1011
|
run: () => atomicWriteFile(plan.packageJsonPath, plan.nextPackageJsonSource),
|
|
704
1012
|
undo: () => atomicWriteFile(plan.packageJsonPath, plan.packageJsonSource)
|
|
705
1013
|
},
|
|
1014
|
+
...bundlerConfigSteps,
|
|
706
1015
|
{
|
|
707
1016
|
description: "granite.config 파일을 삭제해요.",
|
|
708
1017
|
run: () => rm(plan.graniteConfigPath),
|
|
@@ -789,28 +1098,6 @@ function transformGraniteConfig(source, configPath) {
|
|
|
789
1098
|
});
|
|
790
1099
|
return root.toSource();
|
|
791
1100
|
}
|
|
792
|
-
/**
|
|
793
|
-
* granite.config 파일 확장자에 맞는 파서를 골라요.
|
|
794
|
-
* 기본 파서는 JavaScript 전용이라 .ts 설정 파일의 type 선언, satisfies 같은
|
|
795
|
-
* TypeScript 문법에서 파싱이 실패해요.
|
|
796
|
-
*/
|
|
797
|
-
function getParserForFile(filepath) {
|
|
798
|
-
switch (extname(filepath)) {
|
|
799
|
-
case ".ts":
|
|
800
|
-
case ".mts":
|
|
801
|
-
case ".cts": return jscodeshift.withParser("ts");
|
|
802
|
-
case ".tsx": return jscodeshift.withParser("tsx");
|
|
803
|
-
default: return jscodeshift.withParser("babel");
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* 이름이 있는 오브젝트 프로퍼티만 골라내요.
|
|
808
|
-
* 파서에 따라 Property(estree) 또는 ObjectProperty(babel) 노드로 파싱되므로 둘 다 확인해요.
|
|
809
|
-
*/
|
|
810
|
-
function asNamedProperty(prop) {
|
|
811
|
-
if ((jscodeshift.Property.check(prop) || jscodeshift.ObjectProperty.check(prop)) && jscodeshift.Identifier.check(prop.key)) return prop;
|
|
812
|
-
return null;
|
|
813
|
-
}
|
|
814
1101
|
function getErrorMessage(error) {
|
|
815
1102
|
return error instanceof Error ? error.message : String(error);
|
|
816
1103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apps-in-toss/cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ait": "dist/index.js"
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"typecheck": "tsc --noEmit"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@apps-in-toss/ait-format": "
|
|
21
|
+
"@apps-in-toss/ait-format": "1.0.0",
|
|
22
22
|
"@clack/prompts": "^1.3.0",
|
|
23
|
-
"c12": "
|
|
23
|
+
"c12": "3.3.4",
|
|
24
24
|
"clipanion": "^4.0.0-rc.4",
|
|
25
25
|
"jscodeshift": "^17.1.2",
|
|
26
26
|
"uuidv7": "^1.2.1"
|