@weaver-wow/cli 1.0.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 +28 -0
- package/package.json +29 -0
- package/src/cli/builder.ts +280 -0
- package/src/cli/commands/build.ts +34 -0
- package/src/cli/commands/dev.ts +92 -0
- package/src/cli/commands/init.ts +228 -0
- package/src/cli/commands/link.ts +126 -0
- package/src/cli/index.ts +31 -0
- package/src/cli/utils/config.ts +16 -0
- package/src/cli/utils/wow-path.ts +44 -0
- package/src/schema.ts +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Weaver CLI
|
|
2
|
+
|
|
3
|
+
The developer toolchain for Weaver addons.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @weaver/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
Initialize a new Weaver project:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
wvr init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
- `wvr init`: Scaffolds a new Weaver project in the current directory.
|
|
22
|
+
- `wvr build`: Compiles your TypeScript/JSX into a valid WoW addon.
|
|
23
|
+
- `wvr dev`: Watches for changes and rebuilds automatically.
|
|
24
|
+
- `wvr link`: Links your project to the WoW AddOns directory.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weaver-wow/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command line toolchain for building Weaver addons",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wvr": "src/cli/index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Mike Eling <mike.eling97@gmail.com>",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@clack/prompts": "^1.0.0",
|
|
17
|
+
"commander": "^14.0.3",
|
|
18
|
+
"picocolors": "^1.1.1",
|
|
19
|
+
"zod": "^4.3.6",
|
|
20
|
+
"@weaver-wow/core": "^1.0.0",
|
|
21
|
+
"typescript-to-lua": "^1.33.2"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "echo 'Building weaver CLI...'"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { WeaverConfig } from "../schema";
|
|
5
|
+
import picocolors from "picocolors";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
const GENERATED_TSTL_CONFIG = ".tstl.build.json";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the addon project.
|
|
14
|
+
*
|
|
15
|
+
* The builder:
|
|
16
|
+
* 1. Reads the base tsconfig.tstl.json template
|
|
17
|
+
* 2. Generates a temporary build config with the correct entry point and include
|
|
18
|
+
* 3. Runs TSTL to compile TypeScript → Lua
|
|
19
|
+
* 4. Post-processes the Lua output (namespace wrapping)
|
|
20
|
+
* 5. Generates the .toc manifest
|
|
21
|
+
* 6. Scans for assets
|
|
22
|
+
*/
|
|
23
|
+
export async function buildProject(config: WeaverConfig | null, silent = false) {
|
|
24
|
+
if (!silent) console.log("Building...");
|
|
25
|
+
|
|
26
|
+
let pkg: any = {};
|
|
27
|
+
if (existsSync("package.json")) {
|
|
28
|
+
try {
|
|
29
|
+
pkg = JSON.parse(readFileSync("package.json", "utf-8"));
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const addonName = config?.addonName || (pkg.name ? pkg.name.charAt(0).toUpperCase() + pkg.name.slice(1) : "MyAddon");
|
|
34
|
+
const interfaceVersion = config?.interface || "110000";
|
|
35
|
+
const author = pkg.author || "Unknown";
|
|
36
|
+
const version = pkg.version || "1.0.0";
|
|
37
|
+
const description = pkg.description || "Generated by Weaver";
|
|
38
|
+
// Pre-flight checks
|
|
39
|
+
if (!existsSync("tsconfig.json") && !existsSync("tsconfig.tstl.json")) {
|
|
40
|
+
throw new Error(`Missing ${picocolors.bold("tsconfig.json")}. \n Run ${picocolors.cyan("wvr init")} to set up your project structure.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Discovery of entry point if not specified
|
|
44
|
+
let entryPoint = config?.entry;
|
|
45
|
+
if (!entryPoint) {
|
|
46
|
+
const potentialEntries = ["src/App.tsx", "src/index.tsx", "src/App.ts", "src/index.ts"];
|
|
47
|
+
for (const pe of potentialEntries) {
|
|
48
|
+
if (existsSync(pe)) {
|
|
49
|
+
entryPoint = pe;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback to default if still not found
|
|
56
|
+
const finalEntryPoint = entryPoint || "src/App.tsx";
|
|
57
|
+
|
|
58
|
+
if (!existsSync(finalEntryPoint)) {
|
|
59
|
+
const others = ["src/App.tsx", "src/index.tsx", "src/App.ts", "src/index.ts"].filter(pe => pe !== finalEntryPoint && existsSync(pe));
|
|
60
|
+
let suggestion = "";
|
|
61
|
+
if (others.length > 0) {
|
|
62
|
+
suggestion = `\n ${picocolors.yellow("Suggestion:")} Found ${picocolors.cyan(others[0])}. Did you mean to use that?`;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Entry point not found: ${picocolors.red(finalEntryPoint)}${suggestion}\n Check your ${picocolors.bold("weaver.config.ts")} or ensure the source file exists.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!existsSync("dist")) mkdirSync("dist");
|
|
68
|
+
|
|
69
|
+
// 1. Generate build-time TSTL config
|
|
70
|
+
const buildConfig = generateBuildConfig(finalEntryPoint);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// 2. Run TSTL with the generated config
|
|
74
|
+
if (silent) {
|
|
75
|
+
await $`bun x tstl -p ${GENERATED_TSTL_CONFIG}`.quiet();
|
|
76
|
+
} else {
|
|
77
|
+
await $`bun x tstl -p ${GENERATED_TSTL_CONFIG}`;
|
|
78
|
+
}
|
|
79
|
+
} catch (e: any) {
|
|
80
|
+
let rawError = (e.stdout?.toString() || "") + (e.stderr?.toString() || "") + (e.message || "");
|
|
81
|
+
|
|
82
|
+
// Parse TSTL/TS errors for pretty printing
|
|
83
|
+
const lines = rawError.split('\n');
|
|
84
|
+
const formattedErrors: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
// Match pattern: path/to/file.tsx(line,col): error TSXXXX: message
|
|
88
|
+
const match = line.match(/^(.+)\((\d+),(\d+)\): error (TS\d+): (.+)$/);
|
|
89
|
+
if (match) {
|
|
90
|
+
const [_, file, lineNum, colNum, code, msg] = match;
|
|
91
|
+
let processedMsg = msg;
|
|
92
|
+
|
|
93
|
+
if (msg.includes("Cannot find module '@weaver-wow/core'")) {
|
|
94
|
+
processedMsg += `\n ${picocolors.yellow("Tip:")} Run ${picocolors.bold("bun install")} to link the Weaver runtime.`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
formattedErrors.push(
|
|
98
|
+
` ${picocolors.dim(file)}:${picocolors.cyan(lineNum)}:${picocolors.cyan(colNum)}\n` +
|
|
99
|
+
` ${picocolors.red("✖")} ${picocolors.white(processedMsg)} ${picocolors.dim(`[${code}]`)}`
|
|
100
|
+
);
|
|
101
|
+
} else if (line.includes("error TSTL")) {
|
|
102
|
+
let msg = line.trim();
|
|
103
|
+
let tip = "";
|
|
104
|
+
if (msg.includes("'@weaver-wow/core'")) {
|
|
105
|
+
tip = `\n ${picocolors.yellow("Tip:")} Run ${picocolors.bold("bun install")} or ensure Weaver is in your node_modules.`;
|
|
106
|
+
}
|
|
107
|
+
formattedErrors.push(` ${picocolors.red("✖")} ${picocolors.white(msg)}${tip}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (formattedErrors.length > 0) {
|
|
112
|
+
throw new Error(`Compilation failed:\n\n${formattedErrors.join('\n\n')}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(`Compilation failed with unknown error:\n${rawError}`);
|
|
116
|
+
} finally {
|
|
117
|
+
// Always clean up the temporary config
|
|
118
|
+
// cleanupBuildConfig();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Post-Process — wrap in addon namespace
|
|
122
|
+
const bundlePath = "./dist/App.lua";
|
|
123
|
+
let luaContent = "";
|
|
124
|
+
|
|
125
|
+
if (existsSync(bundlePath)) {
|
|
126
|
+
luaContent = readFileSync(bundlePath, "utf-8");
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error(`Could not find generated Lua file (${bundlePath})`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const wrappedLua = `
|
|
132
|
+
local addonName, ns = ...
|
|
133
|
+
-- [WEAVER RUNTIME STUB]
|
|
134
|
+
${luaContent}
|
|
135
|
+
`.trim();
|
|
136
|
+
|
|
137
|
+
writeFileSync("./dist/Core.lua", wrappedLua);
|
|
138
|
+
|
|
139
|
+
// Remove intermediate bundle (Core.lua is the final artifact)
|
|
140
|
+
try { unlinkSync(bundlePath); } catch {}
|
|
141
|
+
|
|
142
|
+
// 4. Generate TOC
|
|
143
|
+
const tocContent = `## Interface: ${interfaceVersion}
|
|
144
|
+
## Title: ${addonName}
|
|
145
|
+
## Notes: ${description}
|
|
146
|
+
## Author: ${author}
|
|
147
|
+
## Version: ${version}
|
|
148
|
+
|
|
149
|
+
Core.lua
|
|
150
|
+
`;
|
|
151
|
+
writeFileSync(`./dist/${addonName}.toc`, tocContent);
|
|
152
|
+
|
|
153
|
+
// 5. Assets
|
|
154
|
+
let assetCount = 0;
|
|
155
|
+
if (existsSync("assets")) {
|
|
156
|
+
const files = readdirSync("assets");
|
|
157
|
+
for (const file of files) {
|
|
158
|
+
if (file.endsWith(".png")) {
|
|
159
|
+
assetCount++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { addonName, assetCount };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generate a temporary TSTL config that extends the base template
|
|
169
|
+
* with the correct entry point and include paths.
|
|
170
|
+
*/
|
|
171
|
+
function generateBuildConfig(entryPoint: string): string {
|
|
172
|
+
console.log("BUILDER UPDATED V2");
|
|
173
|
+
// Read the base template
|
|
174
|
+
const baseConfigPath = existsSync("tsconfig.tstl.json") ? "tsconfig.tstl.json" : "tsconfig.json";
|
|
175
|
+
const baseConfig = JSON.parse(readFileSync(baseConfigPath, "utf-8"));
|
|
176
|
+
|
|
177
|
+
// Determine the entry's directory for include patterns
|
|
178
|
+
const entryDir = dirname(entryPoint);
|
|
179
|
+
|
|
180
|
+
// Extend include with the entry point
|
|
181
|
+
const include = [
|
|
182
|
+
...(baseConfig.include || []),
|
|
183
|
+
entryPoint,
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// Build the final config
|
|
187
|
+
// Heuristic:
|
|
188
|
+
// 1. If CWD has package.json with "weaver" dependency, use node_modules (Consumer Project)
|
|
189
|
+
// 2. Else if we are in the monorepo structure, use relative source paths (Monorepo specific)
|
|
190
|
+
|
|
191
|
+
let isConsumerProject = false;
|
|
192
|
+
console.log("CWD:", process.cwd());
|
|
193
|
+
if (existsSync("package.json")) {
|
|
194
|
+
try {
|
|
195
|
+
const pkgC = readFileSync("package.json", "utf-8");
|
|
196
|
+
const pkg = JSON.parse(pkgC);
|
|
197
|
+
console.log("Found package.json with deps:", Object.keys(pkg.dependencies || {}));
|
|
198
|
+
if ((pkg.dependencies && pkg.dependencies["@weaver-wow/core"]) || (pkg.devDependencies && pkg.devDependencies["@weaver-wow/core"])) {
|
|
199
|
+
isConsumerProject = true;
|
|
200
|
+
}
|
|
201
|
+
} catch (e) { console.log("Error reading pkg", e); }
|
|
202
|
+
}
|
|
203
|
+
console.log("isConsumerProject:", isConsumerProject);
|
|
204
|
+
|
|
205
|
+
let weaverPath: string;
|
|
206
|
+
let jsxRuntimePath: string;
|
|
207
|
+
let includePaths: string[] = [];
|
|
208
|
+
|
|
209
|
+
// Check if we are running from source (monorepo) or installed package
|
|
210
|
+
const absoluteRootDir = join(dirname(import.meta.url.replace("file:///", "")), "..", "..", "..", "..");
|
|
211
|
+
|
|
212
|
+
if (isConsumerProject) {
|
|
213
|
+
// We are in a consumer project, resolve from node_modules
|
|
214
|
+
// The project's node_modules should have weaver installed
|
|
215
|
+
weaverPath = "node_modules/@weaver-wow/core/src/index.ts";
|
|
216
|
+
jsxRuntimePath = "node_modules/@weaver-wow/core/src/types/react-jsx-runtime.d.ts";
|
|
217
|
+
includePaths = [
|
|
218
|
+
"node_modules/@weaver-wow/core/src/**/*.ts",
|
|
219
|
+
"node_modules/@weaver-wow/core/src/**/*.tsx"
|
|
220
|
+
];
|
|
221
|
+
} else if (existsSync(join(absoluteRootDir, "packages", "weaver"))) {
|
|
222
|
+
// We are in the monorepo (e.g. running sandbox tests)
|
|
223
|
+
const rootDir = relative(process.cwd(), absoluteRootDir) || ".";
|
|
224
|
+
weaverPath = join(rootDir, "packages/weaver/src/index.ts");
|
|
225
|
+
jsxRuntimePath = join(rootDir, "packages/weaver/src/types/react-jsx-runtime.d.ts");
|
|
226
|
+
includePaths = [
|
|
227
|
+
join(rootDir, "packages/weaver/src/**/*.ts"),
|
|
228
|
+
join(rootDir, "packages/weaver/src/**/*.tsx"),
|
|
229
|
+
];
|
|
230
|
+
} else {
|
|
231
|
+
// Fallback: linked but not detecting monorepo (should be covered by isConsumerProject if linked correctly)
|
|
232
|
+
// or strange install location. Default to node_modules
|
|
233
|
+
weaverPath = "node_modules/@weaver-wow/core/src/index.ts";
|
|
234
|
+
jsxRuntimePath = "node_modules/@weaver-wow/core/src/types/react-jsx-runtime.d.ts";
|
|
235
|
+
includePaths = [
|
|
236
|
+
"node_modules/@weaver-wow/core/src/**/*.ts",
|
|
237
|
+
"node_modules/@weaver-wow/core/src/**/*.tsx"
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const buildConfig = {
|
|
242
|
+
...baseConfig,
|
|
243
|
+
compilerOptions: {
|
|
244
|
+
...baseConfig.compilerOptions,
|
|
245
|
+
preserveSymlinks: true,
|
|
246
|
+
paths: {
|
|
247
|
+
...(baseConfig.compilerOptions?.paths || {}),
|
|
248
|
+
"@weaver-wow/core": [weaverPath],
|
|
249
|
+
"react/jsx-runtime": [jsxRuntimePath]
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
tstl: {
|
|
253
|
+
...baseConfig.tstl,
|
|
254
|
+
luaBundleEntry: entryPoint,
|
|
255
|
+
},
|
|
256
|
+
include: [
|
|
257
|
+
...include,
|
|
258
|
+
...includePaths
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Write the temporary config
|
|
263
|
+
if (process.env.DEBUG) console.log("Generated config:", JSON.stringify(buildConfig, null, 2));
|
|
264
|
+
writeFileSync(GENERATED_TSTL_CONFIG, JSON.stringify(buildConfig, null, 2));
|
|
265
|
+
|
|
266
|
+
return GENERATED_TSTL_CONFIG;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clean up the temporary build config file.
|
|
271
|
+
*/
|
|
272
|
+
function cleanupBuildConfig(): void {
|
|
273
|
+
try {
|
|
274
|
+
if (existsSync(GENERATED_TSTL_CONFIG)) {
|
|
275
|
+
unlinkSync(GENERATED_TSTL_CONFIG);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// Non-critical — don't block build on cleanup failure
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
import { intro, outro, spinner } from "@clack/prompts";
|
|
3
|
+
import picocolors from "picocolors";
|
|
4
|
+
import { loadConfig } from "../utils/config";
|
|
5
|
+
import { buildProject } from "../builder";
|
|
6
|
+
|
|
7
|
+
export async function buildCommand() {
|
|
8
|
+
intro(picocolors.bgBlue(" 🧵 Weaver Build "));
|
|
9
|
+
|
|
10
|
+
const s = spinner();
|
|
11
|
+
s.start("Reading configuration...");
|
|
12
|
+
|
|
13
|
+
const config = await loadConfig();
|
|
14
|
+
|
|
15
|
+
s.message(`Building configured project...`);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const result = await buildProject(config, true);
|
|
19
|
+
s.stop(picocolors.green("Build complete!"));
|
|
20
|
+
outro(`Built ${picocolors.cyan(result.addonName)} successfully to ${picocolors.bold("dist/")}`);
|
|
21
|
+
} catch (e: any) {
|
|
22
|
+
s.stop(picocolors.red("Build failed"));
|
|
23
|
+
|
|
24
|
+
console.log("");
|
|
25
|
+
const message = e.message || String(e);
|
|
26
|
+
message.split('\n').forEach(line => {
|
|
27
|
+
console.log(` ${line}`);
|
|
28
|
+
});
|
|
29
|
+
console.log("");
|
|
30
|
+
|
|
31
|
+
outro(picocolors.red("Build failed. Fix the issues above and try again."));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
|
|
2
|
+
import { watch } from "node:fs";
|
|
3
|
+
import { intro, outro, spinner } from "@clack/prompts";
|
|
4
|
+
import picocolors from "picocolors";
|
|
5
|
+
import { loadConfig } from "../utils/config";
|
|
6
|
+
import { buildProject } from "../builder";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export async function devCommand() {
|
|
10
|
+
intro(picocolors.bgBlue(" 🧵 Weaver Dev "));
|
|
11
|
+
|
|
12
|
+
const s = spinner();
|
|
13
|
+
s.start("Starting development server...");
|
|
14
|
+
|
|
15
|
+
const config = await loadConfig();
|
|
16
|
+
|
|
17
|
+
// Initial build
|
|
18
|
+
try {
|
|
19
|
+
await buildProject(config, true);
|
|
20
|
+
s.stop(picocolors.green("Initial build complete!"));
|
|
21
|
+
} catch (e) {
|
|
22
|
+
s.stop(picocolors.red("Initial build failed"));
|
|
23
|
+
console.error(e);
|
|
24
|
+
// Continue watching even if build fails? Yes, to fix errors.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(picocolors.cyan(`Watching for changes in ${picocolors.bold("src/")}...`));
|
|
28
|
+
|
|
29
|
+
let isBuilding = false;
|
|
30
|
+
|
|
31
|
+
// Recursive watch on src/
|
|
32
|
+
// Debounce simply by ignoring triggers while building?
|
|
33
|
+
watch("src", { recursive: true }, async (event, filename) => {
|
|
34
|
+
if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx"))) {
|
|
35
|
+
if (isBuilding) return;
|
|
36
|
+
isBuilding = true;
|
|
37
|
+
|
|
38
|
+
console.log(picocolors.dim(`File changed: ${filename}`));
|
|
39
|
+
const rebuildSpinner = spinner();
|
|
40
|
+
rebuildSpinner.start("Rebuilding...");
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Determine if we need full build or partial?
|
|
44
|
+
// For MVP, full build via buildProject.
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
await buildProject(config, true);
|
|
47
|
+
const end = performance.now();
|
|
48
|
+
rebuildSpinner.stop(picocolors.green(`Rebuilt in ${(end - start).toFixed(0)}ms`));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
rebuildSpinner.stop(picocolors.red("Rebuild failed"));
|
|
51
|
+
if (e instanceof Error) console.error(e.message);
|
|
52
|
+
} finally {
|
|
53
|
+
isBuilding = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Also watch sandbox if present?
|
|
59
|
+
// Since we moved sandbox to root, we should watch it too if configured.
|
|
60
|
+
// However, sandbox usually lives outside src in this setup.
|
|
61
|
+
// Let's watch 'sandbox' too if it exists.
|
|
62
|
+
// Or watch root/sandbox.
|
|
63
|
+
|
|
64
|
+
// Using fs.watch implies we need a process keep-alive?
|
|
65
|
+
// Node.js keeps process alive as long as watcher is active.
|
|
66
|
+
|
|
67
|
+
// Watch sandbox
|
|
68
|
+
try {
|
|
69
|
+
watch("sandbox", { recursive: true }, async (event, filename) => {
|
|
70
|
+
if (filename && (filename.endsWith(".ts") || filename.endsWith(".tsx"))) {
|
|
71
|
+
if (isBuilding) return;
|
|
72
|
+
isBuilding = true;
|
|
73
|
+
console.log(picocolors.dim(`Sandbox file changed: ${filename}`));
|
|
74
|
+
const rb = spinner();
|
|
75
|
+
rb.start("Rebuilding...");
|
|
76
|
+
try {
|
|
77
|
+
const start = performance.now();
|
|
78
|
+
await buildProject(config, true);
|
|
79
|
+
const end = performance.now();
|
|
80
|
+
rb.stop(picocolors.green(`Rebuilt (sandbox) in ${(end - start).toFixed(0)}ms`));
|
|
81
|
+
} catch (e) {
|
|
82
|
+
rb.stop(picocolors.red("Rebuild failed"));
|
|
83
|
+
if (e instanceof Error) console.error(e.message);
|
|
84
|
+
} finally {
|
|
85
|
+
isBuilding = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// sandbox might not exist or verify failed
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
|
|
2
|
+
import { intro, outro, text, select, confirm, spinner } from "@clack/prompts";
|
|
3
|
+
import { findWoWPath } from "../utils/wow-path";
|
|
4
|
+
import { writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import picocolors from "picocolors";
|
|
6
|
+
|
|
7
|
+
export async function initCommand(options: any) {
|
|
8
|
+
intro(picocolors.bgBlue(" 🧵 Weaver Init "));
|
|
9
|
+
|
|
10
|
+
// 1. Metadata Discovery
|
|
11
|
+
let defaultName = "MyAddon";
|
|
12
|
+
let defaultAuthor = "Me";
|
|
13
|
+
|
|
14
|
+
if (existsSync("package.json")) {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(await Bun.file("package.json").text());
|
|
17
|
+
if (pkg.name) defaultName = pkg.name;
|
|
18
|
+
if (pkg.author) defaultAuthor = pkg.author;
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let wowPath = "";
|
|
23
|
+
let addonName = defaultName;
|
|
24
|
+
let flavor = "_retail_";
|
|
25
|
+
|
|
26
|
+
if (options.yes) {
|
|
27
|
+
// NON-INTERACTIVE MODE
|
|
28
|
+
wowPath = (await findWoWPath()) || "C:\\Program Files (x86)\\World of Warcraft";
|
|
29
|
+
} else {
|
|
30
|
+
// INTERACTIVE MODE
|
|
31
|
+
// 2. WoW Path Discovery
|
|
32
|
+
const path = await findWoWPath();
|
|
33
|
+
wowPath = path || "";
|
|
34
|
+
|
|
35
|
+
if (path) {
|
|
36
|
+
const confirmPath = await confirm({
|
|
37
|
+
message: `Found WoW at ${picocolors.cyan(path)}. Use this?`,
|
|
38
|
+
initialValue: true
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!confirmPath) {
|
|
42
|
+
const manualPath = await text({
|
|
43
|
+
message: "Enter the path to your World of Warcraft installation:",
|
|
44
|
+
placeholder: "C:\\Program Files (x86)\\World of Warcraft",
|
|
45
|
+
validate(value) {
|
|
46
|
+
if (value.length === 0) return "Path is required";
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (typeof manualPath === 'symbol') { process.exit(0); }
|
|
50
|
+
wowPath = manualPath;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
const manualPath = await text({
|
|
54
|
+
message: "Could not auto-detect WoW. Enter the path manually:",
|
|
55
|
+
placeholder: "C:\\Program Files (x86)\\World of Warcraft",
|
|
56
|
+
validate(value) {
|
|
57
|
+
if (value.length === 0) return "Path is required";
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (typeof manualPath === 'symbol') { process.exit(0); }
|
|
61
|
+
wowPath = manualPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Project Configuration
|
|
65
|
+
const addonNameInput = await text({
|
|
66
|
+
message: "Addon Name (used for folder name and TOC):",
|
|
67
|
+
placeholder: defaultName,
|
|
68
|
+
validate(value) {
|
|
69
|
+
if (value && !/^[a-zA-Z0-9]+$/.test(value)) return "Only alphanumeric characters allowed";
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (typeof addonNameInput === 'symbol') { process.exit(0); }
|
|
73
|
+
addonName = (addonNameInput || defaultName) as string;
|
|
74
|
+
|
|
75
|
+
const flavorInput = await select({
|
|
76
|
+
message: "Target Game Version:",
|
|
77
|
+
options: [
|
|
78
|
+
{ value: "_retail_", label: "Retail (The War Within)" },
|
|
79
|
+
{ value: "_classic_", label: "Cataclysm Classic" },
|
|
80
|
+
{ value: "_classic_era_", label: "Classic Era / Hardcore" },
|
|
81
|
+
],
|
|
82
|
+
}) as string;
|
|
83
|
+
if (typeof flavorInput === 'symbol') { process.exit(0); }
|
|
84
|
+
flavor = flavorInput;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Map flavor to default interface version
|
|
88
|
+
const interfaceMap: Record<string, string> = {
|
|
89
|
+
"_retail_": "110007",
|
|
90
|
+
"_classic_": "40400",
|
|
91
|
+
"_classic_era_": "11503",
|
|
92
|
+
};
|
|
93
|
+
const interfaceVersion = interfaceMap[flavor] || "110000";
|
|
94
|
+
|
|
95
|
+
const s = spinner();
|
|
96
|
+
s.start("Scaffolding your Weaver project...");
|
|
97
|
+
|
|
98
|
+
// 4. File Generation
|
|
99
|
+
|
|
100
|
+
// weaver.config.ts
|
|
101
|
+
const configContent = `
|
|
102
|
+
import type { WeaverConfig } from "@weaver-wow/core";
|
|
103
|
+
|
|
104
|
+
const config: WeaverConfig = {
|
|
105
|
+
addonName: "${addonName}",
|
|
106
|
+
flavor: "${flavor}",
|
|
107
|
+
interface: "${interfaceVersion}",
|
|
108
|
+
wowPath: String.raw\`${wowPath}\`,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default config;
|
|
112
|
+
`.trim();
|
|
113
|
+
writeFileSync("weaver.config.ts", configContent);
|
|
114
|
+
|
|
115
|
+
// tsconfig.json (Standard TSTL + Weaver setup)
|
|
116
|
+
const tsconfigContent = `
|
|
117
|
+
{
|
|
118
|
+
"compilerOptions": {
|
|
119
|
+
"target": "ESNext",
|
|
120
|
+
"lib": ["ESNext"],
|
|
121
|
+
"moduleResolution": "Node",
|
|
122
|
+
"types": [],
|
|
123
|
+
"jsx": "react",
|
|
124
|
+
"jsxFactory": "createElement",
|
|
125
|
+
"strict": false,
|
|
126
|
+
"skipLibCheck": true,
|
|
127
|
+
"sourceMap": false,
|
|
128
|
+
"baseUrl": ".",
|
|
129
|
+
"paths": {
|
|
130
|
+
"react/jsx-runtime": ["node_modules/@weaver-wow/core/src/types/react-jsx-runtime.d.ts"]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"tstl": {
|
|
134
|
+
"luaTarget": "5.1",
|
|
135
|
+
"luaBundle": "dist/App.lua",
|
|
136
|
+
"noHeader": true
|
|
137
|
+
},
|
|
138
|
+
"include": ["src/**/*.tsx", "src/**/*.ts", "src/**/*.d.ts"]
|
|
139
|
+
}
|
|
140
|
+
`.trim();
|
|
141
|
+
if (!existsSync("tsconfig.json")) {
|
|
142
|
+
writeFileSync("tsconfig.json", tsconfigContent);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/App.tsx (Starter code)
|
|
146
|
+
if (!existsSync("src")) {
|
|
147
|
+
mkdirSync("src");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!existsSync("src/App.tsx") && !existsSync("src/index.tsx")) {
|
|
151
|
+
const appContent = `
|
|
152
|
+
import {
|
|
153
|
+
useState,
|
|
154
|
+
useEffect,
|
|
155
|
+
Frame,
|
|
156
|
+
Texture,
|
|
157
|
+
Button,
|
|
158
|
+
mount,
|
|
159
|
+
createElement
|
|
160
|
+
} from '@weaver-wow/core';
|
|
161
|
+
|
|
162
|
+
const ${addonName} = () => {
|
|
163
|
+
const [clicks, setClicks] = useState("clickCount", 0);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
print("${addonName} initialized!");
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Frame name="${addonName}Frame" size={[220, 100]} setPoint={["CENTER", 0, 0]}>
|
|
171
|
+
<Texture setAllPoints={true} color={[0, 0, 0, 0.6]} />
|
|
172
|
+
<Button
|
|
173
|
+
text={\`Clicks: \${clicks}\`}
|
|
174
|
+
onClick={() => setClicks(clicks + 1)}
|
|
175
|
+
/>
|
|
176
|
+
</Frame>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
mount("${addonName}", () => ${addonName}());
|
|
181
|
+
`.trim();
|
|
182
|
+
writeFileSync("src/App.tsx", appContent);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// package.json (if missing)
|
|
186
|
+
if (!existsSync("package.json")) {
|
|
187
|
+
const packageContent = {
|
|
188
|
+
name: addonName.toLowerCase(),
|
|
189
|
+
version: "0.1.0",
|
|
190
|
+
private: true,
|
|
191
|
+
type: "module",
|
|
192
|
+
scripts: {
|
|
193
|
+
"build": "bun x wvr build",
|
|
194
|
+
"dev": "bun x wvr dev",
|
|
195
|
+
"link": "bun x wvr link"
|
|
196
|
+
},
|
|
197
|
+
dependencies: {
|
|
198
|
+
"@weaver-wow/core": "^1.0.0"
|
|
199
|
+
},
|
|
200
|
+
devDependencies: {
|
|
201
|
+
"typescript-to-lua": "latest"
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
writeFileSync("package.json", JSON.stringify(packageContent, null, 2));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// .gitignore
|
|
208
|
+
const gitignoreContent = `
|
|
209
|
+
node_modules/
|
|
210
|
+
dist/
|
|
211
|
+
.tstl.build.json
|
|
212
|
+
*.log
|
|
213
|
+
`.trim();
|
|
214
|
+
if (!existsSync(".gitignore")) {
|
|
215
|
+
writeFileSync(".gitignore", gitignoreContent);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
s.stop("Project initialized successfully!");
|
|
219
|
+
|
|
220
|
+
outro(`
|
|
221
|
+
${picocolors.green("Project layout created:")}
|
|
222
|
+
- ${picocolors.cyan("src/App.tsx")} (Addon source)
|
|
223
|
+
- ${picocolors.cyan("weaver.config.ts")} (Addon metadata)
|
|
224
|
+
- ${picocolors.cyan("tsconfig.json")} (Compiler settings)
|
|
225
|
+
|
|
226
|
+
Run ${picocolors.bold("bun install")} and then ${picocolors.bold("bun run dev")} to start coding!
|
|
227
|
+
`);
|
|
228
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
|
|
2
|
+
import { symlinkSync, existsSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { intro, outro, spinner, confirm, text } from "@clack/prompts";
|
|
5
|
+
import picocolors from "picocolors";
|
|
6
|
+
import { loadConfig } from "../utils/config";
|
|
7
|
+
import { findWoWPath } from "../utils/wow-path";
|
|
8
|
+
|
|
9
|
+
export async function linkCommand() {
|
|
10
|
+
intro(picocolors.bgBlue(" 🧵 Weaver Link "));
|
|
11
|
+
|
|
12
|
+
const s = spinner();
|
|
13
|
+
s.start("Reading configuration...");
|
|
14
|
+
|
|
15
|
+
let config = await loadConfig();
|
|
16
|
+
|
|
17
|
+
// Determine Addon Name
|
|
18
|
+
let addonName = config?.addonName;
|
|
19
|
+
if (!addonName && existsSync("package.json")) {
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(await Bun.file("package.json").text());
|
|
22
|
+
addonName = pkg.name ? pkg.name.charAt(0).toUpperCase() + pkg.name.slice(1) : "MyAddon";
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
addonName = addonName || "MyAddon";
|
|
26
|
+
|
|
27
|
+
// Determine WoW Path
|
|
28
|
+
let wowPath = config?.wowPath;
|
|
29
|
+
if (!wowPath) {
|
|
30
|
+
s.stop("Searching for WoW installation...");
|
|
31
|
+
wowPath = await findWoWPath();
|
|
32
|
+
|
|
33
|
+
if (!wowPath) {
|
|
34
|
+
const manualPath = await text({
|
|
35
|
+
message: "Could not auto-detect WoW. Enter path manually:",
|
|
36
|
+
placeholder: "C:\\Program Files (x86)\\World of Warcraft"
|
|
37
|
+
});
|
|
38
|
+
if (typeof manualPath === 'symbol') process.exit(0);
|
|
39
|
+
wowPath = manualPath as string;
|
|
40
|
+
} else {
|
|
41
|
+
const useFound = await confirm({
|
|
42
|
+
message: `Found WoW at ${picocolors.cyan(wowPath)}. Use this?`,
|
|
43
|
+
initialValue: true
|
|
44
|
+
});
|
|
45
|
+
if (!useFound) {
|
|
46
|
+
const manualPath = await text({
|
|
47
|
+
message: "Enter path manually:",
|
|
48
|
+
placeholder: "C:\\Program Files (x86)\\World of Warcraft"
|
|
49
|
+
});
|
|
50
|
+
if (typeof manualPath === 'symbol') process.exit(0);
|
|
51
|
+
wowPath = manualPath as string;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!wowPath) {
|
|
57
|
+
console.error(picocolors.red("WoW path is required."));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Determine target directory (Interface/AddOns)
|
|
62
|
+
// Default to retail for now, or use config.flavor
|
|
63
|
+
const flavor = config?.flavor || "_retail_";
|
|
64
|
+
const addonsDir = join(wowPath, flavor, "Interface", "AddOns");
|
|
65
|
+
|
|
66
|
+
if (!existsSync(addonsDir)) {
|
|
67
|
+
console.error(picocolors.red(`Could not find AddOns directory at: ${addonsDir}`));
|
|
68
|
+
console.log(picocolors.yellow(`(Make sure you have run the game at least once to create folders)`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const targetLinkPath = join(addonsDir, addonName);
|
|
73
|
+
const sourceDistPath = resolve("dist"); // Absolute path to dist
|
|
74
|
+
|
|
75
|
+
if (!existsSync(sourceDistPath)) {
|
|
76
|
+
console.error(picocolors.red("dist/ folder does not exist. Run 'bun wvr build' first."));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
s.start(`Linking ${picocolors.cyan(addonName)} to WoW...`);
|
|
81
|
+
|
|
82
|
+
// Check if link already exists
|
|
83
|
+
if (existsSync(targetLinkPath)) {
|
|
84
|
+
const stats = statSync(targetLinkPath);
|
|
85
|
+
if (stats.isSymbolicLink()) {
|
|
86
|
+
s.stop("Link already exists.");
|
|
87
|
+
// Maybe check where it points?
|
|
88
|
+
outro("Addon is already linked.");
|
|
89
|
+
return;
|
|
90
|
+
} else {
|
|
91
|
+
// It's a real folder?
|
|
92
|
+
s.stop("Target exists as a real folder.");
|
|
93
|
+
const overwrite = await confirm({
|
|
94
|
+
message: `Folder ${targetLinkPath} already exists (not a link). Delete and link?`,
|
|
95
|
+
initialValue: false
|
|
96
|
+
});
|
|
97
|
+
if (!overwrite) {
|
|
98
|
+
outro("Aborted.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
s.start("Removing existing folder...");
|
|
102
|
+
try {
|
|
103
|
+
rmSync(targetLinkPath, { recursive: true, force: true }); // Dangerous?
|
|
104
|
+
} catch (e) {
|
|
105
|
+
s.stop(picocolors.red("Failed to remove existing folder."));
|
|
106
|
+
console.error(e);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create Symlink
|
|
113
|
+
try {
|
|
114
|
+
// Use "junction" for Windows directory links without admin
|
|
115
|
+
const type = process.platform === "win32" ? "junction" : "dir";
|
|
116
|
+
symlinkSync(sourceDistPath, targetLinkPath, type);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
s.stop(picocolors.red("Failed to create symlink"));
|
|
119
|
+
console.error("Error details:", e);
|
|
120
|
+
console.log(picocolors.yellow("Try running as Administrator if on Windows."));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
s.stop(picocolors.green("Linked successfully!"));
|
|
125
|
+
outro(`Dev changes in ${picocolors.bold("dist/")} will now reflect in WoW immediately.`);
|
|
126
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { initCommand } from "./commands/init";
|
|
3
|
+
import { buildCommand } from "./commands/build";
|
|
4
|
+
import { devCommand } from "./commands/dev";
|
|
5
|
+
import { linkCommand } from "./commands/link";
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name("wvr")
|
|
11
|
+
.description("Weaver Addon Toolchain")
|
|
12
|
+
.version("1.0.0");
|
|
13
|
+
|
|
14
|
+
program.command("init")
|
|
15
|
+
.description("Initialize a new Weaver project")
|
|
16
|
+
.option("-y, --yes", "Skip prompts and use defaults")
|
|
17
|
+
.action((options) => initCommand(options));
|
|
18
|
+
|
|
19
|
+
program.command("build")
|
|
20
|
+
.description("Build the addon for production")
|
|
21
|
+
.action(() => buildCommand());
|
|
22
|
+
|
|
23
|
+
program.command("dev")
|
|
24
|
+
.description("Start development server (watch mode)")
|
|
25
|
+
.action(() => devCommand());
|
|
26
|
+
|
|
27
|
+
program.command("link")
|
|
28
|
+
.description("Symlink dist folder to WoW AddOns")
|
|
29
|
+
.action(() => linkCommand());
|
|
30
|
+
|
|
31
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { WeaverConfig } from "../../schema";
|
|
4
|
+
|
|
5
|
+
export async function loadConfig(): Promise<WeaverConfig | null> {
|
|
6
|
+
if (existsSync("weaver.config.ts")) {
|
|
7
|
+
try {
|
|
8
|
+
const configPath = Bun.pathToFileURL("weaver.config.ts").href;
|
|
9
|
+
const configModule = await import(configPath);
|
|
10
|
+
return configModule.default;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
console.warn("Failed to load weaver.config.ts:", e);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export async function findWoWPath(): Promise<string | null> {
|
|
7
|
+
// 1. Try Registry (Windows only)
|
|
8
|
+
if (process.platform === "win32") {
|
|
9
|
+
try {
|
|
10
|
+
const result = await $`reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Blizzard Entertainment\\World of Warcraft" /v InstallPath`.text();
|
|
11
|
+
// Output looks like: " InstallPath REG_SZ C:\Program Files (x86)\World of Warcraft\"
|
|
12
|
+
const match = result.match(/REG_SZ\s+(.*)/);
|
|
13
|
+
if (match && match[1]) {
|
|
14
|
+
const path = match[1].trim();
|
|
15
|
+
if (existsSync(path)) return path;
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// Registry key not found or error accessing reg command
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Common Path Scan
|
|
23
|
+
const commonPaths = [
|
|
24
|
+
"C:\\Games\\World of Warcraft",
|
|
25
|
+
"D:\\Games\\World of Warcraft",
|
|
26
|
+
"E:\\Games\\World of Warcraft",
|
|
27
|
+
"C:\\Program Files (x86)\\World of Warcraft",
|
|
28
|
+
"C:\\Program Files\\World of Warcraft"
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
for (const path of commonPaths) {
|
|
32
|
+
if (existsSync(path)) return path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateWoWPath(path: string): boolean {
|
|
39
|
+
const retail = join(path, "_retail_");
|
|
40
|
+
const classic = join(path, "_classic_");
|
|
41
|
+
const classicEra = join(path, "_classic_era_");
|
|
42
|
+
|
|
43
|
+
return existsSync(retail) || existsSync(classic) || existsSync(classicEra);
|
|
44
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { WeaverConfig as IWeaverConfig } from "../../weaver/src/types/config";
|
|
4
|
+
|
|
5
|
+
export const WeaverConfigSchema = z.object({
|
|
6
|
+
addonName: z.string(),
|
|
7
|
+
flavor: z.enum(["_retail_", "_classic_", "_classic_era_"]),
|
|
8
|
+
interface: z.string(),
|
|
9
|
+
outDir: z.string().optional().default("Interface/AddOns"),
|
|
10
|
+
entry: z.string().optional().default("src/App.tsx"),
|
|
11
|
+
wowPath: z.string().optional().nullable(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type WeaverConfig = z.infer<typeof WeaverConfigSchema>;
|
|
15
|
+
|
|
16
|
+
// Verify it matches the interface
|
|
17
|
+
type Verification = WeaverConfig extends IWeaverConfig ? (IWeaverConfig extends WeaverConfig ? true : false) : false;
|
|
18
|
+
const _v: Verification = true;
|