rs3-api-ts 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +27 -2
- package/data/modio-libraries.json +21 -0
- package/dist/build.js +130 -0
- package/dist/checks.js +335 -0
- package/dist/dev.js +81 -0
- package/dist/errors.js +13 -0
- package/dist/index.js +194 -0
- package/dist/init.js +427 -0
- package/dist/libraries.js +99 -0
- package/dist/permissions.js +59 -0
- package/dist/postprocess.js +36 -0
- package/dist/project.js +98 -0
- package/dist/prompt.js +160 -0
- package/dist/report.js +87 -0
- package/package.json +53 -4
- package/templates/_gitignore +2 -0
- package/templates/_tsconfig.json +8 -0
- package/templates/src/main.ts +9 -0
- package/templates/src/modules/xp.ts +16 -0
- package/tsconfig.base.json +18 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const build_1 = require("./build");
|
|
39
|
+
const dev_1 = require("./dev");
|
|
40
|
+
const init_1 = require("./init");
|
|
41
|
+
const report_1 = require("./report");
|
|
42
|
+
const permissions_1 = require("./permissions");
|
|
43
|
+
const project_1 = require("./project");
|
|
44
|
+
const BOOLEAN_FLAGS = new Set([
|
|
45
|
+
"dev", "help", "version", "quiet", "yes", "sync-version", "no-sync-version", "local", "registry",
|
|
46
|
+
"settings-icon", "no-settings-icon", "ribbon-icon", "no-ribbon-icon",
|
|
47
|
+
]);
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const flags = {};
|
|
50
|
+
const positional = [];
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (!arg.startsWith("--")) {
|
|
54
|
+
positional.push(arg);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const key = arg.slice(2);
|
|
58
|
+
const next = argv[i + 1];
|
|
59
|
+
if (BOOLEAN_FLAGS.has(key) || next === undefined || next.startsWith("--")) {
|
|
60
|
+
flags[key] = true;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
flags[key] = next;
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { flags, positional };
|
|
68
|
+
}
|
|
69
|
+
function usage() {
|
|
70
|
+
return [
|
|
71
|
+
`rs3-plugin ${(0, project_1.sdkVersion)()}`,
|
|
72
|
+
"",
|
|
73
|
+
"Usage:",
|
|
74
|
+
" rs3-plugin init [name] scaffold a new plugin in ./<name> (asks what it does not know)",
|
|
75
|
+
" rs3-plugin build [--out <dir>] compile src/ to readable Lua in dist/ and run checks",
|
|
76
|
+
" rs3-plugin build --dev single-file bundle with source-mapped tracebacks",
|
|
77
|
+
" rs3-plugin dev [--out <dir>] rebuild on change (dev mode)",
|
|
78
|
+
"",
|
|
79
|
+
"Options:",
|
|
80
|
+
" --out <dir> copy the built plugin into <dir>/<plugin name>/ (your client's plugin folder)",
|
|
81
|
+
" --cwd <dir> run as if started from <dir>",
|
|
82
|
+
" --quiet only print problems",
|
|
83
|
+
"",
|
|
84
|
+
"init answers (skip the prompts):",
|
|
85
|
+
" --yes accept defaults for anything not given",
|
|
86
|
+
" --permissions <a,b> required permissions (default none)",
|
|
87
|
+
" --optional-permissions <a,b> optional permissions (default none)",
|
|
88
|
+
" --urls <list> comma-separated, each `name=url` or a bare url (named api, api2, ...);",
|
|
89
|
+
" asked for when a chosen permission needs urls",
|
|
90
|
+
" --deps <list> required library plugins from mod.io: `prettyui` (known name) or `name=<mod.io id>`",
|
|
91
|
+
" --optional-deps <list> same, for optional dependencies",
|
|
92
|
+
" --settings-icon | --no-settings-icon",
|
|
93
|
+
" --ribbon-icon | --no-ribbon-icon",
|
|
94
|
+
" plugin.json hasSettingsIcon / hasRibbonIcon (default no)",
|
|
95
|
+
" --contact-email <address> plugin.json contactEmailAddress (default none)",
|
|
96
|
+
" --plugin-version <x.y.z> initial version (default 0.1.0)",
|
|
97
|
+
" --sync-version | --no-sync-version",
|
|
98
|
+
" keep plugin.json's version equal to package.json's (default yes)",
|
|
99
|
+
" --local | --registry depend on this SDK checkout (file:...) or on the npm package (^version);",
|
|
100
|
+
" default: local when run from a checkout, registry when installed",
|
|
101
|
+
"",
|
|
102
|
+
`known permissions: ${(0, permissions_1.permissionNames)().join(", ")}`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
function stringFlag(flags, name) {
|
|
106
|
+
const value = flags[name];
|
|
107
|
+
return typeof value === "string" ? value : undefined;
|
|
108
|
+
}
|
|
109
|
+
/** `--name` -> true, `--no-name` -> false, neither -> undefined (ask or default). */
|
|
110
|
+
function tristate(flags, name) {
|
|
111
|
+
if (flags[`no-${name}`] === true) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (flags[name] === true) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
/** `--flag a,b,c` -> ["a", "b", "c"]; a bare `--flag` means an empty list. */
|
|
120
|
+
function listFlag(flags, name) {
|
|
121
|
+
const value = flags[name];
|
|
122
|
+
if (value === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (typeof value !== "string") {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return value
|
|
129
|
+
.split(",")
|
|
130
|
+
.map((s) => s.trim())
|
|
131
|
+
.filter((s) => s !== "");
|
|
132
|
+
}
|
|
133
|
+
async function main() {
|
|
134
|
+
const { flags, positional } = parseArgs(process.argv.slice(2));
|
|
135
|
+
const command = positional[0];
|
|
136
|
+
const cwd = path.resolve(stringFlag(flags, "cwd") ?? process.cwd());
|
|
137
|
+
if (flags.version === true) {
|
|
138
|
+
console.log((0, project_1.sdkVersion)());
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (command === undefined || flags.help === true) {
|
|
142
|
+
console.log(usage());
|
|
143
|
+
process.exitCode = flags.help === true ? 0 : 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
switch (command) {
|
|
147
|
+
case "build": {
|
|
148
|
+
const result = (0, build_1.build)({
|
|
149
|
+
cwd,
|
|
150
|
+
dev: flags.dev === true,
|
|
151
|
+
deployTo: stringFlag(flags, "out"),
|
|
152
|
+
});
|
|
153
|
+
(0, report_1.printProblems)(result.problems);
|
|
154
|
+
if (flags.quiet !== true) {
|
|
155
|
+
(0, report_1.printSummary)(result);
|
|
156
|
+
}
|
|
157
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
case "dev": {
|
|
161
|
+
(0, dev_1.dev)({ cwd, deployTo: stringFlag(flags, "out") });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
case "init": {
|
|
165
|
+
await (0, init_1.init)({
|
|
166
|
+
cwd,
|
|
167
|
+
interactive: flags.yes !== true && process.stdin.isTTY === true,
|
|
168
|
+
answers: {
|
|
169
|
+
name: positional[1],
|
|
170
|
+
requiredPermissions: listFlag(flags, "permissions"),
|
|
171
|
+
optionalPermissions: listFlag(flags, "optional-permissions"),
|
|
172
|
+
urls: stringFlag(flags, "urls") ?? stringFlag(flags, "api-url"),
|
|
173
|
+
requiredDependencies: (0, init_1.dependenciesFromFlag)(stringFlag(flags, "deps"), "--deps"),
|
|
174
|
+
optionalDependencies: (0, init_1.dependenciesFromFlag)(stringFlag(flags, "optional-deps"), "--optional-deps"),
|
|
175
|
+
settingsIcon: tristate(flags, "settings-icon"),
|
|
176
|
+
ribbonIcon: tristate(flags, "ribbon-icon"),
|
|
177
|
+
contactEmail: stringFlag(flags, "contact-email"),
|
|
178
|
+
version: stringFlag(flags, "plugin-version"),
|
|
179
|
+
syncVersion: tristate(flags, "sync-version"),
|
|
180
|
+
sdkSource: flags.registry === true ? "registry" : flags.local === true ? "local" : undefined,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
console.error(`error: unknown command "${command}"\n`);
|
|
187
|
+
console.log(usage());
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
(0, report_1.printError)(error);
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
});
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.init = init;
|
|
37
|
+
exports.dependenciesFromFlag = dependenciesFromFlag;
|
|
38
|
+
exports.parseUrls = parseUrls;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const errors_1 = require("./errors");
|
|
42
|
+
const libraries_1 = require("./libraries");
|
|
43
|
+
const permissions_1 = require("./permissions");
|
|
44
|
+
const prompt_1 = require("./prompt");
|
|
45
|
+
const project_1 = require("./project");
|
|
46
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
47
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
48
|
+
const RENAMES = {
|
|
49
|
+
_gitignore: ".gitignore",
|
|
50
|
+
};
|
|
51
|
+
const DEFAULTS = {
|
|
52
|
+
version: "0.1.0",
|
|
53
|
+
syncVersion: true,
|
|
54
|
+
};
|
|
55
|
+
async function init(options) {
|
|
56
|
+
const answers = await collectAnswers(options.answers, options.interactive);
|
|
57
|
+
const target = path.join(options.cwd, answers.name);
|
|
58
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
|
|
59
|
+
throw new errors_1.CliError(`${answers.name} already exists here and is not empty`, [
|
|
60
|
+
`nothing was written to ${target}`,
|
|
61
|
+
"scaffold under a different name, or empty that folder first",
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
const written = [];
|
|
65
|
+
const write = (relative, content) => {
|
|
66
|
+
const to = path.join(target, relative);
|
|
67
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
68
|
+
fs.writeFileSync(to, content);
|
|
69
|
+
written.push(relative);
|
|
70
|
+
};
|
|
71
|
+
const dependencies = sdkDependencies(answers, target);
|
|
72
|
+
write("package.json", `${JSON.stringify(packageJson(answers, dependencies), null, 2)}\n`);
|
|
73
|
+
write("plugin.json", `${JSON.stringify(pluginJson(answers), null, 2)}\n`);
|
|
74
|
+
copyTemplates(path.join((0, project_1.sdkRoot)(), "templates"), answers, write);
|
|
75
|
+
console.log(`\ncreated ${answers.name}/ with ${written.length} files`);
|
|
76
|
+
if (answers.sdkSource === "local") {
|
|
77
|
+
console.log(`SDK packages are referenced from this checkout (${dependencies["rs3-api-ts"]}); pass --registry to depend on the published packages instead`);
|
|
78
|
+
}
|
|
79
|
+
if (answers.syncVersion) {
|
|
80
|
+
console.log("plugin.json follows package.json's version: bump with `npm version patch|minor|major`");
|
|
81
|
+
}
|
|
82
|
+
console.log("\nnext steps:");
|
|
83
|
+
console.log(` cd ${answers.name}`);
|
|
84
|
+
console.log(" npm install");
|
|
85
|
+
console.log(" npm run build # readable Lua in dist/");
|
|
86
|
+
console.log(" npm run dev -- --out <your client's plugins folder>");
|
|
87
|
+
}
|
|
88
|
+
async function collectAnswers(given, interactive) {
|
|
89
|
+
validatePermissionList(given.requiredPermissions, "--permissions");
|
|
90
|
+
validatePermissionList(given.optionalPermissions, "--optional-permissions");
|
|
91
|
+
const name = await resolveText(interactive, given.name, "Plugin name (folder, package and plugin id)", undefined, validateName, NAME_HINT);
|
|
92
|
+
let required = given.requiredPermissions;
|
|
93
|
+
let optional = given.optionalPermissions;
|
|
94
|
+
if (required === undefined && optional === undefined && interactive) {
|
|
95
|
+
required = await selectPermissions("Permissions the plugin needs", (0, permissions_1.permissionNames)(), []);
|
|
96
|
+
const remaining = (0, permissions_1.permissionNames)().filter((p) => !required.includes(p));
|
|
97
|
+
optional = remaining.length > 0 ? await selectPermissions("Optional permissions (the user can grant or refuse them)", remaining, []) : [];
|
|
98
|
+
}
|
|
99
|
+
required = required ?? [];
|
|
100
|
+
optional = (optional ?? []).filter((p) => !required.includes(p));
|
|
101
|
+
let urls = given.urls;
|
|
102
|
+
const needsUrls = [...required, ...optional].some((p) => (0, permissions_1.findPermission)(p)?.needsUrls === true);
|
|
103
|
+
if (urls !== undefined) {
|
|
104
|
+
const error = validateUrls(urls);
|
|
105
|
+
if (error !== undefined) {
|
|
106
|
+
throw new errors_1.CliError(`--urls: ${error}`, [
|
|
107
|
+
"comma separated, each entry either a url or name=url",
|
|
108
|
+
'for example: --urls "https://api.example.com/, wiki=https://runescape.wiki/"',
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (needsUrls) {
|
|
113
|
+
urls = await resolveText(interactive, undefined, "URLs the plugin may call, comma separated (name=url or just url)", "https://example.com/", validateUrls);
|
|
114
|
+
}
|
|
115
|
+
let requiredDeps = given.requiredDependencies;
|
|
116
|
+
let optionalDeps = given.optionalDependencies;
|
|
117
|
+
if (requiredDeps === undefined && optionalDeps === undefined && interactive) {
|
|
118
|
+
const catalog = (0, libraries_1.loadCatalog)();
|
|
119
|
+
if (catalog.libraries.length > 0) {
|
|
120
|
+
const stamp = catalog.generatedAt !== null ? `list from ${catalog.generatedAt.slice(0, 10)}` : "hand-seeded list";
|
|
121
|
+
requiredDeps = await selectLibraries(`Library plugins from mod.io the plugin requires (${stamp})`, catalog.libraries);
|
|
122
|
+
const chosen = new Set(requiredDeps.map((d) => d.name));
|
|
123
|
+
const remaining = catalog.libraries.filter((lib) => !chosen.has(lib.name_id));
|
|
124
|
+
optionalDeps = remaining.length > 0 ? await selectLibraries("Optional library plugins (used when installed)", remaining) : [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
requiredDeps = requiredDeps ?? [];
|
|
128
|
+
const requiredNames = new Set(requiredDeps.map((d) => d.name));
|
|
129
|
+
optionalDeps = (optionalDeps ?? []).filter((d) => !requiredNames.has(d.name));
|
|
130
|
+
let settingsIcon = given.settingsIcon;
|
|
131
|
+
let ribbonIcon = given.ribbonIcon;
|
|
132
|
+
if (settingsIcon === undefined && ribbonIcon === undefined && interactive) {
|
|
133
|
+
const picked = await (0, prompt_1.multiSelect)("Client integration", [
|
|
134
|
+
{ label: "settings icon", hint: "hasSettingsIcon: the plugin appears in the settings panel" },
|
|
135
|
+
{ label: "ribbon icon", hint: "hasRibbonIcon: the plugin gets a button on the ribbon" },
|
|
136
|
+
]);
|
|
137
|
+
settingsIcon = picked[0];
|
|
138
|
+
ribbonIcon = picked[1];
|
|
139
|
+
}
|
|
140
|
+
settingsIcon = settingsIcon ?? false;
|
|
141
|
+
ribbonIcon = ribbonIcon ?? false;
|
|
142
|
+
let contactEmail = given.contactEmail;
|
|
143
|
+
if (contactEmail !== undefined) {
|
|
144
|
+
const error = validateEmail(contactEmail);
|
|
145
|
+
if (error !== undefined) {
|
|
146
|
+
throw new errors_1.CliError(`--contact-email: ${error}`, ["for example: --contact-email you@example.com"]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (interactive) {
|
|
150
|
+
contactEmail = await (0, prompt_1.text)("Contact email for plugin.json (enter to skip)", "", validateEmail);
|
|
151
|
+
}
|
|
152
|
+
if (contactEmail === "") {
|
|
153
|
+
contactEmail = undefined;
|
|
154
|
+
}
|
|
155
|
+
const version = await resolveText(interactive, given.version, "Version", DEFAULTS.version, validateVersion, [
|
|
156
|
+
"for example: --plugin-version 1.0.0",
|
|
157
|
+
]);
|
|
158
|
+
const syncVersion = await resolveBoolean(interactive, given.syncVersion, "Should plugin.json's version follow package.json's version?", DEFAULTS.syncVersion);
|
|
159
|
+
const sdkSource = given.sdkSource ?? (isInstalledFromRegistry() ? "registry" : "local");
|
|
160
|
+
return {
|
|
161
|
+
name,
|
|
162
|
+
requiredPermissions: required,
|
|
163
|
+
optionalPermissions: optional,
|
|
164
|
+
urls,
|
|
165
|
+
requiredDependencies: requiredDeps,
|
|
166
|
+
optionalDependencies: optionalDeps,
|
|
167
|
+
settingsIcon,
|
|
168
|
+
ribbonIcon,
|
|
169
|
+
contactEmail,
|
|
170
|
+
version,
|
|
171
|
+
syncVersion,
|
|
172
|
+
sdkSource,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function validateEmail(value) {
|
|
176
|
+
if (value === "") {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
180
|
+
return `"${value}" does not look like an email address`;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
async function selectLibraries(question, libraries) {
|
|
185
|
+
const items = libraries.map((lib) => ({
|
|
186
|
+
label: lib.name_id,
|
|
187
|
+
hint: `${lib.name}${lib.summary !== "" ? ` - ${truncate(lib.summary, 70)}` : ""} (id ${lib.id})`,
|
|
188
|
+
}));
|
|
189
|
+
const picked = await (0, prompt_1.multiSelect)(question, items);
|
|
190
|
+
return libraries.filter((_, i) => picked[i]).map((lib) => ({ name: lib.name_id, id: lib.id }));
|
|
191
|
+
}
|
|
192
|
+
function truncate(text, max) {
|
|
193
|
+
return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
|
|
194
|
+
}
|
|
195
|
+
function dependenciesFromFlag(value, flag) {
|
|
196
|
+
if (value === undefined) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
return (0, libraries_1.parseDependencies)(value);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
const hint = error instanceof errors_1.CliError ? error.hint : undefined;
|
|
205
|
+
throw new errors_1.CliError(`${flag}: ${message}`, hint);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function isInstalledFromRegistry() {
|
|
209
|
+
return (0, project_1.sdkRoot)().split(path.sep).includes("node_modules");
|
|
210
|
+
}
|
|
211
|
+
const SDK_PACKAGES = ["rs3-api-core", "rs3-api-extended", "rs3-api-ts"];
|
|
212
|
+
/** Follows the workspace and npm link symlinks to the real folder. */
|
|
213
|
+
function packageRoot(name) {
|
|
214
|
+
if (name === "rs3-api-ts") {
|
|
215
|
+
return (0, project_1.sdkRoot)();
|
|
216
|
+
}
|
|
217
|
+
return fs.realpathSync(path.dirname(require.resolve(`${name}/package.json`)));
|
|
218
|
+
}
|
|
219
|
+
function sdkDependencies(answers, target) {
|
|
220
|
+
const out = {};
|
|
221
|
+
for (const name of SDK_PACKAGES) {
|
|
222
|
+
const root = packageRoot(name);
|
|
223
|
+
if (answers.sdkSource === "registry") {
|
|
224
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
225
|
+
out[name] = `^${pkg.version}`;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
out[name] = `file:${path.relative(target, root).split(path.sep).join("/")}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
/** `"https://a.example/, backend=https://b.example/"` -> `{ api: ..., backend: ... }` */
|
|
234
|
+
function parseUrls(input) {
|
|
235
|
+
const out = {};
|
|
236
|
+
let unnamed = 0;
|
|
237
|
+
for (const raw of input.split(",")) {
|
|
238
|
+
const entry = raw.trim();
|
|
239
|
+
if (entry === "") {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const eq = entry.indexOf("=");
|
|
243
|
+
let name;
|
|
244
|
+
let url;
|
|
245
|
+
if (eq > 0 && !/^[a-z][a-z0-9+.-]*:\/\//i.test(entry)) {
|
|
246
|
+
name = entry.slice(0, eq).trim();
|
|
247
|
+
url = entry.slice(eq + 1).trim();
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
unnamed++;
|
|
251
|
+
name = unnamed === 1 ? "api" : `api${unnamed}`;
|
|
252
|
+
url = entry;
|
|
253
|
+
}
|
|
254
|
+
out[name] = url;
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function validateUrls(input) {
|
|
259
|
+
const seen = new Set();
|
|
260
|
+
let unnamed = 0;
|
|
261
|
+
for (const raw of input.split(",")) {
|
|
262
|
+
const entry = raw.trim();
|
|
263
|
+
if (entry === "") {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const eq = entry.indexOf("=");
|
|
267
|
+
let name;
|
|
268
|
+
let url;
|
|
269
|
+
if (eq > 0 && !/^[a-z][a-z0-9+.-]*:\/\//i.test(entry)) {
|
|
270
|
+
name = entry.slice(0, eq).trim();
|
|
271
|
+
url = entry.slice(eq + 1).trim();
|
|
272
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) {
|
|
273
|
+
return `"${name}" is not a valid url name (letters, digits, _ and -)`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
unnamed++;
|
|
278
|
+
name = unnamed === 1 ? "api" : `api${unnamed}`;
|
|
279
|
+
url = entry;
|
|
280
|
+
}
|
|
281
|
+
if (seen.has(name)) {
|
|
282
|
+
return `url name "${name}" is used twice`;
|
|
283
|
+
}
|
|
284
|
+
seen.add(name);
|
|
285
|
+
const error = validateUrl(url);
|
|
286
|
+
if (error !== undefined) {
|
|
287
|
+
return error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (seen.size === 0) {
|
|
291
|
+
return "enter at least one URL";
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
async function selectPermissions(question, names, preselected) {
|
|
296
|
+
const items = names.map((name) => {
|
|
297
|
+
const info = (0, permissions_1.findPermission)(name);
|
|
298
|
+
return { label: name, hint: info?.description };
|
|
299
|
+
});
|
|
300
|
+
const picked = await (0, prompt_1.multiSelect)(question, items, names.map((n) => preselected.includes(n)));
|
|
301
|
+
return names.filter((_, i) => picked[i]);
|
|
302
|
+
}
|
|
303
|
+
function validatePermissionList(list, flag) {
|
|
304
|
+
if (list === undefined) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
for (const name of list) {
|
|
308
|
+
if ((0, permissions_1.findPermission)(name) === undefined) {
|
|
309
|
+
throw new errors_1.CliError(`${flag}: "${name}" is not a permission the client knows`, [
|
|
310
|
+
`valid permissions: ${(0, permissions_1.permissionNames)().join(", ")}`,
|
|
311
|
+
"the client refuses to load a plugin that asks for anything else",
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function resolveText(interactive, given, question, fallback, validate, hint) {
|
|
317
|
+
if (given !== undefined) {
|
|
318
|
+
const error = validate(given);
|
|
319
|
+
if (error !== undefined) {
|
|
320
|
+
throw new errors_1.CliError(error, hint);
|
|
321
|
+
}
|
|
322
|
+
return given;
|
|
323
|
+
}
|
|
324
|
+
if (!interactive) {
|
|
325
|
+
if (fallback === undefined) {
|
|
326
|
+
const label = question.replace(/\s*\(.*\)\s*$/, "").toLowerCase();
|
|
327
|
+
throw new errors_1.CliError(`${label} is required`, hint ?? ["pass it on the command line, or run without --yes to be asked"]);
|
|
328
|
+
}
|
|
329
|
+
return fallback;
|
|
330
|
+
}
|
|
331
|
+
return (0, prompt_1.text)(question, fallback, validate);
|
|
332
|
+
}
|
|
333
|
+
async function resolveBoolean(interactive, given, question, fallback) {
|
|
334
|
+
if (given !== undefined) {
|
|
335
|
+
return given;
|
|
336
|
+
}
|
|
337
|
+
if (!interactive) {
|
|
338
|
+
return fallback;
|
|
339
|
+
}
|
|
340
|
+
return (0, prompt_1.confirm)(question, fallback);
|
|
341
|
+
}
|
|
342
|
+
function validateName(value) {
|
|
343
|
+
if (!NAME_PATTERN.test(value)) {
|
|
344
|
+
return `"${value}" must be lowercase letters, digits and dashes, starting with a letter or digit`;
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
const NAME_HINT = [
|
|
349
|
+
"it becomes the folder, the npm package name and the plugin id, so: my-plugin",
|
|
350
|
+
"pass it as an argument: rs3-plugin init my-plugin",
|
|
351
|
+
];
|
|
352
|
+
function validateVersion(value) {
|
|
353
|
+
if (!VERSION_PATTERN.test(value)) {
|
|
354
|
+
return `"${value}" must be three numbers separated by dots, like 0.1.0`;
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
function validateUrl(value) {
|
|
359
|
+
try {
|
|
360
|
+
const url = new URL(value);
|
|
361
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
362
|
+
return "the URL must start with http:// or https://";
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return `"${value}" is not a valid URL`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function packageJson(answers, sdkDependencies) {
|
|
371
|
+
return {
|
|
372
|
+
name: answers.name,
|
|
373
|
+
version: answers.version,
|
|
374
|
+
private: true,
|
|
375
|
+
scripts: {
|
|
376
|
+
build: "rs3-plugin build",
|
|
377
|
+
dev: "rs3-plugin dev",
|
|
378
|
+
},
|
|
379
|
+
rs3: {
|
|
380
|
+
syncVersion: answers.syncVersion,
|
|
381
|
+
},
|
|
382
|
+
devDependencies: sdkDependencies,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function pluginJson(answers) {
|
|
386
|
+
const manifest = {
|
|
387
|
+
name: answers.name,
|
|
388
|
+
version: answers.version,
|
|
389
|
+
dependencies: {},
|
|
390
|
+
requiredDependencies: (0, libraries_1.toManifestDependencies)(answers.requiredDependencies),
|
|
391
|
+
optionalDependencies: (0, libraries_1.toManifestDependencies)(answers.optionalDependencies),
|
|
392
|
+
requiredPermissions: answers.requiredPermissions,
|
|
393
|
+
optionalPermissions: answers.optionalPermissions,
|
|
394
|
+
urls: answers.urls !== undefined ? parseUrls(answers.urls) : {},
|
|
395
|
+
resources: {},
|
|
396
|
+
hasSettingsIcon: answers.settingsIcon,
|
|
397
|
+
hasRibbonIcon: answers.ribbonIcon,
|
|
398
|
+
};
|
|
399
|
+
if (answers.contactEmail !== undefined) {
|
|
400
|
+
manifest.contactEmailAddress = answers.contactEmail;
|
|
401
|
+
}
|
|
402
|
+
return manifest;
|
|
403
|
+
}
|
|
404
|
+
function copyTemplates(templates, answers, write) {
|
|
405
|
+
const replacements = {
|
|
406
|
+
__NAME__: answers.name,
|
|
407
|
+
};
|
|
408
|
+
const walk = (dir) => {
|
|
409
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
410
|
+
const from = path.join(dir, entry.name);
|
|
411
|
+
if (entry.isDirectory()) {
|
|
412
|
+
walk(from);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
// Templates npm or git would act on inside this package are stored with a leading
|
|
416
|
+
// underscore: _tsconfig.json -> tsconfig.json, _gitignore -> .gitignore.
|
|
417
|
+
const relativeDir = path.dirname(path.relative(templates, from));
|
|
418
|
+
const destName = RENAMES[entry.name] ?? (entry.name.startsWith("_") ? entry.name.slice(1) : entry.name);
|
|
419
|
+
let content = fs.readFileSync(from, "utf8");
|
|
420
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
421
|
+
content = content.split(token).join(value);
|
|
422
|
+
}
|
|
423
|
+
write(path.join(relativeDir, destName), content);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
walk(templates);
|
|
427
|
+
}
|