@iruka-edu/create-iruka-game 1.0.2 → 1.0.4
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.cjs +45 -21
- package/package.json +1 -1
- package/templates/basic-phaser/_gitignore +29 -0
- package/templates/basic-phaser/index.html +1 -1
- package/templates/basic-phaser/src/core/GameAdapter.ts +61 -0
- package/templates/basic-phaser/src/core/mockData.ts +9 -0
- package/templates/basic-phaser/src/main.ts +40 -23
- package/templates/basic-phaser/src/scenes/BootScene.ts +16 -0
- package/templates/basic-phaser/src/scenes/GameScene.ts +112 -0
- package/templates/basic-phaser/src/scenes/PreloadScene.ts +56 -0
- package/templates/basic-phaser/src/shared/constants.ts +9 -0
- package/templates/basic-phaser/src/style.css +21 -0
- package/templates/react-phaser/_gitignore +29 -0
package/dist/index.cjs
CHANGED
|
@@ -65,9 +65,17 @@ async function init() {
|
|
|
65
65
|
name: "token",
|
|
66
66
|
message: "Nh\u1EADp GitHub Token (\u0111\u1EC3 c\xE0i \u0111\u1EB7t SDK):",
|
|
67
67
|
validate: (value) => value.length < 10 ? "Token kh\xF4ng h\u1EE3p l\u1EC7" : true
|
|
68
|
+
},
|
|
69
|
+
// [MỚI] Câu hỏi Install
|
|
70
|
+
{
|
|
71
|
+
type: "confirm",
|
|
72
|
+
name: "shouldInstall",
|
|
73
|
+
message: "T\u1EF1 \u0111\u1ED9ng t\u1EA3i th\u01B0 vi\u1EC7n ngay b\xE2y gi\u1EDD? (M\u1EA5t v\xE0i ph\xFAt)",
|
|
74
|
+
initial: false
|
|
75
|
+
// Mặc định là KHÔNG
|
|
68
76
|
}
|
|
69
77
|
]);
|
|
70
|
-
const { projectName, templateType, token } = response;
|
|
78
|
+
const { projectName, templateType, token, shouldInstall } = response;
|
|
71
79
|
if (!projectName || !templateType || !token) process.exit(1);
|
|
72
80
|
const root = import_node_path.default.join(process.cwd(), projectName);
|
|
73
81
|
const templateDir = import_node_path.default.resolve(__dirname, `../templates/${templateType}`);
|
|
@@ -94,29 +102,45 @@ async function init() {
|
|
|
94
102
|
//npm.pkg.github.com/:_authToken=${token}
|
|
95
103
|
`;
|
|
96
104
|
import_fs_extra.default.writeFileSync(import_node_path.default.join(root, ".npmrc"), npmrcContent.trim());
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
.
|
|
105
|
+
const gitignoreSrc = import_node_path.default.join(root, "_gitignore");
|
|
106
|
+
const gitignoreDest = import_node_path.default.join(root, ".gitignore");
|
|
107
|
+
if (import_fs_extra.default.existsSync(gitignoreSrc)) {
|
|
108
|
+
import_fs_extra.default.renameSync(gitignoreSrc, gitignoreDest);
|
|
109
|
+
} else {
|
|
110
|
+
const basicGitignore = `node_modules
|
|
101
111
|
dist
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
.npmrc
|
|
113
|
+
.env`;
|
|
114
|
+
import_fs_extra.default.writeFileSync(gitignoreDest, basicGitignore);
|
|
115
|
+
}
|
|
116
|
+
if (shouldInstall) {
|
|
117
|
+
console.log(import_picocolors.default.blue("\u{1F4E6} \u0110ang c\xE0i \u0111\u1EB7t th\u01B0 vi\u1EC7n..."));
|
|
118
|
+
try {
|
|
119
|
+
const userAgent2 = process.env.npm_config_user_agent || "";
|
|
120
|
+
const pkgManager2 = userAgent2.startsWith("pnpm") ? "pnpm" : "npm";
|
|
121
|
+
(0, import_node_child_process.execSync)(`${pkgManager2} install`, { cwd: root, stdio: "inherit" });
|
|
122
|
+
console.log(import_picocolors.default.green(`
|
|
123
|
+
\u2705 C\xE0i \u0111\u1EB7t ho\xE0n t\u1EA5t!`));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(import_picocolors.default.red("\u274C L\u1ED7i khi c\xE0i \u0111\u1EB7t. Vui l\xF2ng th\u1EED ch\u1EA1y th\u1EE7 c\xF4ng."));
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
console.log(import_picocolors.default.yellow(`
|
|
129
|
+
\u26A0\uFE0F \u0110\xE3 b\u1ECF qua b\u01B0\u1EDBc c\xE0i \u0111\u1EB7t.`));
|
|
130
|
+
}
|
|
106
131
|
try {
|
|
107
|
-
const userAgent = process.env.npm_config_user_agent || "";
|
|
108
|
-
const pkgManager = userAgent.startsWith("pnpm") ? "pnpm" : "npm";
|
|
109
|
-
(0, import_node_child_process.execSync)(`${pkgManager} install`, { cwd: root, stdio: "inherit" });
|
|
110
132
|
(0, import_node_child_process.execSync)("git init", { cwd: root, stdio: "ignore" });
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
\u{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
console.log(`
|
|
136
|
+
\u{1F389} Project \u0111\xE3 s\u1EB5n s\xE0ng t\u1EA1i: ${import_picocolors.default.cyan(root)}`);
|
|
137
|
+
console.log(`\u{1F449} C\xE1c b\u01B0\u1EDBc ti\u1EBFp theo:`);
|
|
138
|
+
console.log(` cd ${projectName}`);
|
|
139
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
140
|
+
const pkgManager = userAgent.startsWith("pnpm") ? "pnpm" : "npm";
|
|
141
|
+
if (!shouldInstall) {
|
|
142
|
+
console.log(` ${pkgManager} install`);
|
|
120
143
|
}
|
|
144
|
+
console.log(` ${pkgManager} run dev`);
|
|
121
145
|
}
|
|
122
146
|
init().catch((e) => console.error(e));
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
# Dependencies
|
|
11
|
+
node_modules
|
|
12
|
+
dist
|
|
13
|
+
dist-ssr
|
|
14
|
+
*.local
|
|
15
|
+
|
|
16
|
+
# Editor directories and files
|
|
17
|
+
.vscode/*
|
|
18
|
+
!.vscode/extensions.json
|
|
19
|
+
.idea
|
|
20
|
+
.DS_Store
|
|
21
|
+
*.suo
|
|
22
|
+
*.ntvs
|
|
23
|
+
*.njsproj
|
|
24
|
+
*.sln
|
|
25
|
+
*.sw?
|
|
26
|
+
|
|
27
|
+
# Security [QUAN TRỌNG]
|
|
28
|
+
.env
|
|
29
|
+
.npmrc
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
connectToHub,
|
|
3
|
+
type GameBridge,
|
|
4
|
+
type LaunchContext,
|
|
5
|
+
} from "@iruka-edu/game-core";
|
|
6
|
+
|
|
7
|
+
class GameAdapter {
|
|
8
|
+
private static instance: GameAdapter;
|
|
9
|
+
private bridge: GameBridge | null = null;
|
|
10
|
+
|
|
11
|
+
// Singleton pattern để truy cập mọi nơi
|
|
12
|
+
public static getInstance(): GameAdapter {
|
|
13
|
+
if (!GameAdapter.instance) {
|
|
14
|
+
GameAdapter.instance = new GameAdapter();
|
|
15
|
+
}
|
|
16
|
+
return GameAdapter.instance;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 1. Kết nối (Gọi 1 lần ở main.ts)
|
|
20
|
+
public async init(): Promise<LaunchContext> {
|
|
21
|
+
try {
|
|
22
|
+
this.bridge = await connectToHub();
|
|
23
|
+
console.log("🔌 Connected to Hub. Context:", this.bridge.context);
|
|
24
|
+
return this.bridge.context;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error("Failed to connect", err);
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Helper lấy context hiện tại (difficulty, locale...)
|
|
32
|
+
public getContext(): LaunchContext | undefined {
|
|
33
|
+
return this.bridge?.context;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Báo cáo Game Ready (khi tải xong assets)
|
|
37
|
+
public notifyReady() {
|
|
38
|
+
this.bridge?.send("EVT:READY");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Gửi điểm số / Kết thúc game
|
|
42
|
+
public submitResult(score: number, maxScore: number, data?: any) {
|
|
43
|
+
// Tự động tính % hoàn thành
|
|
44
|
+
const accuracy = maxScore > 0 ? score / maxScore : 0;
|
|
45
|
+
|
|
46
|
+
this.bridge?.send("EVT:COMPLETE", {
|
|
47
|
+
score,
|
|
48
|
+
accuracy,
|
|
49
|
+
completion: 1.0, // Đã chơi xong
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
detail: data, // Metadata thêm (số câu đúng, sai...)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 4. Lưu game (Throttle save sẽ được handle ở Core hoặc ở đây)
|
|
56
|
+
public saveState(state: any) {
|
|
57
|
+
this.bridge?.send("REQUEST_SAVE", state);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const adapter = GameAdapter.getInstance();
|
|
@@ -1,30 +1,47 @@
|
|
|
1
1
|
import Phaser from "phaser";
|
|
2
|
+
import "./style.css";
|
|
3
|
+
import { adapter } from "./core/GameAdapter";
|
|
4
|
+
import { BootScene } from "./scenes/BootScene";
|
|
5
|
+
import { PreloadScene } from "./scenes/PreloadScene";
|
|
6
|
+
import { GameScene } from "./scenes/GameScene";
|
|
7
|
+
import { GAME_WIDTH, GAME_HEIGHT } from "./shared/constants";
|
|
2
8
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
// Config Phaser
|
|
10
|
+
const config: Phaser.Types.Core.GameConfig = {
|
|
11
|
+
type: Phaser.AUTO,
|
|
12
|
+
width: GAME_WIDTH,
|
|
13
|
+
height: GAME_HEIGHT,
|
|
14
|
+
parent: "app",
|
|
15
|
+
scale: {
|
|
16
|
+
mode: Phaser.Scale.FIT,
|
|
17
|
+
autoCenter: Phaser.Scale.CENTER_BOTH,
|
|
18
|
+
},
|
|
19
|
+
scene: [BootScene, PreloadScene, GameScene],
|
|
20
|
+
};
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
// Hàm khởi tạo chính
|
|
23
|
+
async function bootstrap() {
|
|
24
|
+
try {
|
|
25
|
+
console.log("🎮 Initializing Game...");
|
|
26
|
+
|
|
27
|
+
// 1. Đợi kết nối SDK (hoặc Mock nếu chạy local)
|
|
28
|
+
// SDK game-core sẽ tự động handle Mock Mode nếu không phát hiện được Hub
|
|
29
|
+
const context = await adapter.init();
|
|
11
30
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
// 2. Sau khi có context, mới start game
|
|
32
|
+
const game = new Phaser.Game(config);
|
|
33
|
+
|
|
34
|
+
// 3. Truyền context vào Global Registry của Phaser để các Scene dùng
|
|
35
|
+
game.registry.set("context", context);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Critical Error:", error);
|
|
38
|
+
document.body.innerHTML = `
|
|
39
|
+
<div style="color: white; text-align: center;">
|
|
40
|
+
<h1>Không thể tải game :(</h1>
|
|
41
|
+
<p>${error instanceof Error ? error.message : "Lỗi kết nối SDK"}</p>
|
|
42
|
+
</div>
|
|
43
|
+
`;
|
|
19
44
|
}
|
|
20
45
|
}
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
type: Phaser.AUTO,
|
|
24
|
-
width: 800,
|
|
25
|
-
height: 600,
|
|
26
|
-
scene: DemoScene,
|
|
27
|
-
parent: "game-container",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
new Phaser.Game(config);
|
|
47
|
+
bootstrap();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Scene } from "phaser";
|
|
2
|
+
import { SCENES } from "../shared/constants";
|
|
3
|
+
|
|
4
|
+
export class BootScene extends Scene {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(SCENES.BOOT);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
preload() {
|
|
10
|
+
// Load minimal assets for loading screen if needed
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
create() {
|
|
14
|
+
this.scene.start(SCENES.PRELOAD);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Scene } from "phaser";
|
|
2
|
+
import { adapter } from "../core/GameAdapter";
|
|
3
|
+
import type { LaunchContext } from "@iruka-edu/game-core";
|
|
4
|
+
import { SCENES } from "../shared/constants";
|
|
5
|
+
|
|
6
|
+
export class GameScene extends Scene {
|
|
7
|
+
private context!: LaunchContext;
|
|
8
|
+
private score: number = 0;
|
|
9
|
+
private scoreText!: Phaser.GameObjects.Text;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super(SCENES.GAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
init() {
|
|
16
|
+
// Lấy context đã lưu ở bước bootstrap trong main.ts
|
|
17
|
+
this.context = this.registry.get("context");
|
|
18
|
+
console.log("Game Mode:", this.context.difficulty);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
create() {
|
|
22
|
+
const { width, height } = this.scale;
|
|
23
|
+
|
|
24
|
+
// Background color based on difficulty
|
|
25
|
+
const bgColors: Record<string, number> = {
|
|
26
|
+
easy: 0x27ae60,
|
|
27
|
+
medium: 0x2980b9,
|
|
28
|
+
hard: 0xc0392b,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this.add
|
|
32
|
+
.rectangle(
|
|
33
|
+
0,
|
|
34
|
+
0,
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
bgColors[this.context.difficulty || "medium"]
|
|
38
|
+
)
|
|
39
|
+
.setOrigin(0);
|
|
40
|
+
|
|
41
|
+
// Welcome text
|
|
42
|
+
this.add
|
|
43
|
+
.text(width / 2, 100, "Iruka Mini Game", {
|
|
44
|
+
fontSize: "48px",
|
|
45
|
+
color: "#ffffff",
|
|
46
|
+
})
|
|
47
|
+
.setOrigin(0.5);
|
|
48
|
+
|
|
49
|
+
this.add
|
|
50
|
+
.text(width / 2, 160, `Mode: ${this.context.difficulty?.toUpperCase()}`, {
|
|
51
|
+
fontSize: "24px",
|
|
52
|
+
color: "#ffffff",
|
|
53
|
+
})
|
|
54
|
+
.setOrigin(0.5);
|
|
55
|
+
|
|
56
|
+
// Add Logo
|
|
57
|
+
const logo = this.add.image(width / 2, height / 2, "logo");
|
|
58
|
+
logo.setInteractive();
|
|
59
|
+
|
|
60
|
+
// Score Text
|
|
61
|
+
this.scoreText = this.add
|
|
62
|
+
.text(width / 2, height - 100, `Score: ${this.score}`, {
|
|
63
|
+
fontSize: "32px",
|
|
64
|
+
color: "#ffffff",
|
|
65
|
+
})
|
|
66
|
+
.setOrigin(0.5);
|
|
67
|
+
|
|
68
|
+
// Instruction
|
|
69
|
+
this.add
|
|
70
|
+
.text(width / 2, height - 50, "Click logo to score & finish", {
|
|
71
|
+
fontSize: "18px",
|
|
72
|
+
color: "#ecf0f1",
|
|
73
|
+
})
|
|
74
|
+
.setOrigin(0.5);
|
|
75
|
+
|
|
76
|
+
// Interaction
|
|
77
|
+
logo.on("pointerdown", () => {
|
|
78
|
+
this.handleWin();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Animation loop
|
|
82
|
+
this.tweens.add({
|
|
83
|
+
targets: logo,
|
|
84
|
+
y: height / 2 - 20,
|
|
85
|
+
duration: 1000,
|
|
86
|
+
ease: "Sine.easeInOut",
|
|
87
|
+
yoyo: true,
|
|
88
|
+
loop: -1,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private handleWin() {
|
|
93
|
+
this.score += 100;
|
|
94
|
+
this.scoreText.setText(`Score: ${this.score}`);
|
|
95
|
+
|
|
96
|
+
// Gửi kết quả về Hub
|
|
97
|
+
adapter.submitResult(this.score, 100, { stars: 3 });
|
|
98
|
+
|
|
99
|
+
// Feedback
|
|
100
|
+
this.add
|
|
101
|
+
.text(
|
|
102
|
+
this.scale.width / 2,
|
|
103
|
+
this.scale.height / 2 + 100,
|
|
104
|
+
"RESULT SUBMITTED!",
|
|
105
|
+
{
|
|
106
|
+
fontSize: "40px",
|
|
107
|
+
color: "#f1c40f",
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
.setOrigin(0.5);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Scene } from "phaser";
|
|
2
|
+
import { SCENES } from "../shared/constants";
|
|
3
|
+
import { adapter } from "../core/GameAdapter";
|
|
4
|
+
|
|
5
|
+
export class PreloadScene extends Scene {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(SCENES.PRELOAD);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
preload() {
|
|
11
|
+
// Create loading bar
|
|
12
|
+
const width = this.cameras.main.width;
|
|
13
|
+
const height = this.cameras.main.height;
|
|
14
|
+
|
|
15
|
+
const progressBar = this.add.graphics();
|
|
16
|
+
const progressBox = this.add.graphics();
|
|
17
|
+
progressBox.fillStyle(0x222222, 0.8);
|
|
18
|
+
progressBox.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
|
|
19
|
+
|
|
20
|
+
const loadingText = this.make.text({
|
|
21
|
+
x: width / 2,
|
|
22
|
+
y: height / 2 - 50,
|
|
23
|
+
text: "Loading...",
|
|
24
|
+
style: {
|
|
25
|
+
font: "20px monospace",
|
|
26
|
+
color: "#ffffff",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
loadingText.setOrigin(0.5, 0.5);
|
|
30
|
+
|
|
31
|
+
this.load.on("progress", (value: number) => {
|
|
32
|
+
progressBar.clear();
|
|
33
|
+
progressBar.fillStyle(0xffffff, 1);
|
|
34
|
+
progressBar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.load.on("complete", () => {
|
|
38
|
+
progressBar.destroy();
|
|
39
|
+
progressBox.destroy();
|
|
40
|
+
loadingText.destroy();
|
|
41
|
+
|
|
42
|
+
// Báo cáo SDK Ready
|
|
43
|
+
adapter.notifyReady();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Tải assets mẫu
|
|
47
|
+
this.load.image(
|
|
48
|
+
"logo",
|
|
49
|
+
"https://raw.githubusercontent.com/photonstorm/phaser3-examples/master/public/assets/sprites/phaser3-logo.png"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
create() {
|
|
54
|
+
this.scene.start(SCENES.GAME);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
background-color: #000;
|
|
10
|
+
display: flex;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
align-items: center;
|
|
13
|
+
height: 100vh;
|
|
14
|
+
width: 100vw;
|
|
15
|
+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#app {
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
# Dependencies
|
|
11
|
+
node_modules
|
|
12
|
+
dist
|
|
13
|
+
dist-ssr
|
|
14
|
+
*.local
|
|
15
|
+
|
|
16
|
+
# Editor directories and files
|
|
17
|
+
.vscode/*
|
|
18
|
+
!.vscode/extensions.json
|
|
19
|
+
.idea
|
|
20
|
+
.DS_Store
|
|
21
|
+
*.suo
|
|
22
|
+
*.ntvs
|
|
23
|
+
*.njsproj
|
|
24
|
+
*.sln
|
|
25
|
+
*.sw?
|
|
26
|
+
|
|
27
|
+
# Security [QUAN TRỌNG]
|
|
28
|
+
.env
|
|
29
|
+
.npmrc
|