@noita-ts/create-mod 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/dist/index.js +140 -0
- package/package.json +23 -0
- package/template/readme.md +48 -0
- package/template/src/init.ts +5 -0
- package/template/src/settings.ts +28 -0
- package/template/tsconfig.json +3 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
function bail(msg) {
|
|
7
|
+
p.cancel(msg);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
function cancellable(value) {
|
|
11
|
+
return p.isCancel(value) ? bail("Cancelled.") : value;
|
|
12
|
+
}
|
|
13
|
+
async function main() {
|
|
14
|
+
p.intro("@noita-ts/create-mod");
|
|
15
|
+
const dirArg = process.argv[2];
|
|
16
|
+
const location = dirArg ??
|
|
17
|
+
cancellable(await p.text({
|
|
18
|
+
message: "Select a directory:",
|
|
19
|
+
validate(v) {
|
|
20
|
+
if (!v) {
|
|
21
|
+
return "A dir name is required";
|
|
22
|
+
}
|
|
23
|
+
if (fs.existsSync(v)) {
|
|
24
|
+
return `Location "${v}" already exists.`;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
const modId = cancellable(await p.text({
|
|
29
|
+
message: "Mod ID:",
|
|
30
|
+
placeholder: "my-ts-mod",
|
|
31
|
+
initialValue: path
|
|
32
|
+
.basename(location)
|
|
33
|
+
.replaceAll(/\s+/g, "-")
|
|
34
|
+
.toLowerCase(),
|
|
35
|
+
validate(v) {
|
|
36
|
+
if (!v) {
|
|
37
|
+
return "Name is required";
|
|
38
|
+
}
|
|
39
|
+
if (!/^[a-z0-9-_]{1,32}$/.test(v)) {
|
|
40
|
+
return "Use up to 32 lowercase alphanumeric characters, hyphens, and underscores only";
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
const name = cancellable(await p.text({
|
|
45
|
+
message: "Name:",
|
|
46
|
+
placeholder: "Example TS Mod",
|
|
47
|
+
}));
|
|
48
|
+
const description = cancellable(await p.text({
|
|
49
|
+
message: "Description:",
|
|
50
|
+
placeholder: "A mod written in typescript via noita-ts",
|
|
51
|
+
}));
|
|
52
|
+
const unsafe = cancellable(await p.confirm({
|
|
53
|
+
message: "Unsafe? (aka 'no API restrictions', prevents publishing to Steam Workshop)",
|
|
54
|
+
initialValue: false,
|
|
55
|
+
}));
|
|
56
|
+
const author = cancellable(await p.text({
|
|
57
|
+
message: "Author (leave empty to skip):",
|
|
58
|
+
placeholder: "",
|
|
59
|
+
}));
|
|
60
|
+
const extraBaseDeps = cancellable(await p.multiselect({
|
|
61
|
+
message: "Extra base dependencies (space to select, enter to confirm/skip):",
|
|
62
|
+
required: false,
|
|
63
|
+
options: [
|
|
64
|
+
{
|
|
65
|
+
value: "@noita-ts/nxml",
|
|
66
|
+
hint: "a library for convenient Noita XML file editing, wrapper around Nathans NXML fork",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
value: "@noita-ts/pollnet",
|
|
70
|
+
hint: "a networking library, wrapper around pollnet",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: "@noita-ts/ffi",
|
|
74
|
+
hint: "a set of Noita engine modding utitiles",
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
}));
|
|
78
|
+
const workshop = !unsafe &&
|
|
79
|
+
cancellable(await p.confirm({
|
|
80
|
+
message: "Setup (some of) Steam Workshop metadata?",
|
|
81
|
+
initialValue: true,
|
|
82
|
+
}));
|
|
83
|
+
let workshopName;
|
|
84
|
+
let workshopDescription;
|
|
85
|
+
if (workshop) {
|
|
86
|
+
workshopName = cancellable(await p.text({
|
|
87
|
+
message: "Workshop Name:",
|
|
88
|
+
placeholder: "My TS Mod",
|
|
89
|
+
initialValue: name,
|
|
90
|
+
}));
|
|
91
|
+
workshopDescription = cancellable(await p.text({
|
|
92
|
+
message: "Workshop Description:",
|
|
93
|
+
placeholder: "A description for the Steam Workshop",
|
|
94
|
+
initialValue: description,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
copyDir(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "template"), location);
|
|
98
|
+
const pkg = {
|
|
99
|
+
name: modId,
|
|
100
|
+
description,
|
|
101
|
+
...(author ? { author } : {}),
|
|
102
|
+
version: "0.1.0",
|
|
103
|
+
private: true,
|
|
104
|
+
["noita.name"]: name,
|
|
105
|
+
["noita.compat"]: 12,
|
|
106
|
+
...(unsafe ? { ["noita.unsafe"]: true } : {}),
|
|
107
|
+
...(workshopName ? { ["noita.workshop.name"]: workshopName } : {}),
|
|
108
|
+
...(workshopDescription
|
|
109
|
+
? { ["noita.workshop.description"]: workshopDescription }
|
|
110
|
+
: {}),
|
|
111
|
+
scripts: {
|
|
112
|
+
start: "nts run",
|
|
113
|
+
build: "nts build",
|
|
114
|
+
},
|
|
115
|
+
dependencies: {
|
|
116
|
+
"@noita-ts/base": "latest",
|
|
117
|
+
...Object.fromEntries(extraBaseDeps.map((dep) => [dep, "latest"])),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
fs.writeFileSync(path.join(location, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
121
|
+
p.note([`cd ${location}`, `npm install`, `npm start`, `npx nts publish`].join("\n"), "Next steps");
|
|
122
|
+
p.outro("Happy modding!");
|
|
123
|
+
}
|
|
124
|
+
function copyDir(src, dest) {
|
|
125
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
126
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
127
|
+
const srcPath = path.join(src, entry.name);
|
|
128
|
+
const destPath = path.join(dest, entry.name);
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
copyDir(srcPath, destPath);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
fs.copyFileSync(srcPath, destPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
main().catch((err) => {
|
|
138
|
+
console.error(err);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noita-ts/create-mod",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new noita-ts mod",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "./dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"template"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepublishOnly": "rm -rf dist && npm run build",
|
|
13
|
+
"build": "tsc"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^25.3.2",
|
|
18
|
+
"typescript": "^5.9.3"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@clack/prompts": "^1.0.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
## A noita-ts mod template
|
|
2
|
+
|
|
3
|
+
Includes a basic `init.ts` and `settings.ts` file setup, as well as generates
|
|
4
|
+
a sensible `package.json` that is ready to run.
|
|
5
|
+
|
|
6
|
+
A `workshop-preview.png` file in the root package directory will be used for
|
|
7
|
+
the workshop preview if present.
|
|
8
|
+
|
|
9
|
+
The keys from the `package.json` file used by `nts` to produce a mod:
|
|
10
|
+
|
|
11
|
+
- `"name": "example-mod"`\
|
|
12
|
+
Name of the package also doubles as mod ID, although you can override it in `noita.id`.
|
|
13
|
+
- `"description": "An example mod written in TypeScript using noita-ts"`\
|
|
14
|
+
Description of the npm package, also doubles as in-game mod description unless overridden by `noita.description`.
|
|
15
|
+
- `"author": "necauqua"`\
|
|
16
|
+
Author of the package, not actually used in mod creation (yet).
|
|
17
|
+
- `"noita.id": "example-mod"`\
|
|
18
|
+
Can be used to set the mod ID and free up the npm package name.
|
|
19
|
+
- `"noita.name": "Example TypeScript Mod"`\
|
|
20
|
+
Name of the mod shown in-game (defaults to `noita.id`/`name`)
|
|
21
|
+
- `"noita.description": "An example mod written in TypeScript using noita-ts"`\
|
|
22
|
+
Description of the mod shown in-game, defaults to package description.
|
|
23
|
+
- `"noita.unsafe": false`\
|
|
24
|
+
Sets the `"request_no_api_restrictions"="1"` key in mod.xml - unsafe mods cannot be published to the workshop and must be installed manually, but are not sandboxed - meaning you can do network/file access, ffi and literally anything at all, including being a crypto miner.
|
|
25
|
+
- `"noita.workshop.id": 0`\
|
|
26
|
+
This will be set by the publish command automatically. If you want to publish the mod as a separate workshop item (e.g. after deleting the original), remove this, otherwise don't touch it
|
|
27
|
+
- `"noita.workshop.name": "Example TypeScript Mod"`\
|
|
28
|
+
Name of the mod on the steam workshop page, defaults to value of noita.name
|
|
29
|
+
- `"noita.workshop.description": "Uploaded to the workshop!"`\
|
|
30
|
+
The description of the mod on the steam workshop page, defaults to value of `noita.description`.
|
|
31
|
+
- `"noita.workshop.tags": []`\
|
|
32
|
+
Tags for the mod on the steam workshop page, just sets the corresponding key in `mod.xml`.
|
|
33
|
+
- `"noita.workshop.skip-files": []`\
|
|
34
|
+
Files to skip when uploading to the workshop, just sets the corresponding key in `mod.xml`.
|
|
35
|
+
- `"noita.workshop.skip-folders": []`\
|
|
36
|
+
Folders to skip when uploading to the workshop, just sets the corresponding key in `mod.xml`.
|
|
37
|
+
- `"noita.compat": 12`\
|
|
38
|
+
API compatibility version, should Noita release a big update that changes this number, an icon "this mod has not been tested with latest mod api" will be shown in the modlist.
|
|
39
|
+
- `"noita.ui-newgame-name": null`\
|
|
40
|
+
This and everything below is just setting other mod.xml properties:
|
|
41
|
+
- `"noita.ui-newgame-description": null`
|
|
42
|
+
- `"noita.ui-newgame-gfx-banner-bg": null`
|
|
43
|
+
- `"noita.ui-newgame-gfx-banner-fg": null`
|
|
44
|
+
- `"noita.is-game-mode": false`
|
|
45
|
+
- `"noita.game-mode-supports-save-slots": false`
|
|
46
|
+
- `"noita.is-translation": false`
|
|
47
|
+
- `"noita.translation-xml-path": null`
|
|
48
|
+
- `"noita.translation-csv-path": null`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// By doing settings in such a way, we can have auto-completable
|
|
2
|
+
//`mod.settings.checkbox` anywhere, and it would also have a correct `boolean`
|
|
3
|
+
// type.
|
|
4
|
+
//
|
|
5
|
+
// Additionally, you also can declare something like this anywhere:
|
|
6
|
+
// ```typesctipt
|
|
7
|
+
// declare global {
|
|
8
|
+
// interface SettingsShape {
|
|
9
|
+
// a_setting_we_just_use_to_persist_stuff: string
|
|
10
|
+
// }
|
|
11
|
+
// }
|
|
12
|
+
// And just get/set/delete it via `mod.settings.<name> = <value>` for convenience.
|
|
13
|
+
// It will use `mod_id.setting_name` key for the actual stored setting of course.
|
|
14
|
+
declare global {
|
|
15
|
+
interface SettingsShape extends ExtractSettings<
|
|
16
|
+
typeof import("./settings")
|
|
17
|
+
> {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default [
|
|
21
|
+
{
|
|
22
|
+
id: "checkbox",
|
|
23
|
+
ui_name: "checkbox",
|
|
24
|
+
ui_description: "This is a test setting",
|
|
25
|
+
value_default: false,
|
|
26
|
+
scope: ModSettingScope.Runtime,
|
|
27
|
+
},
|
|
28
|
+
] as const satisfies ModSetting[];
|