@tavora/create-module 0.0.0-stage → 0.1.1
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +22 -3
- package/dist/create.d.ts +29 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +104 -0
- package/dist/create.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +136 -0
- package/dist/index.js.map +1 -0
- package/dist/versions.d.ts +6 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +9 -0
- package/dist/versions.js.map +1 -0
- package/package.json +45 -3
- package/templates/react/package.json +24 -0
- package/templates/react/src/index.html +12 -0
- package/templates/react/src/main.tsx +67 -0
- package/templates/react/tsconfig.json +14 -0
- package/templates/react/vite.config.ts +15 -0
- package/templates/shared/README.md +36 -0
- package/templates/shared/gitignore +4 -0
- package/templates/shared/src/style.css +17 -0
- package/templates/shared/tavora.module.json +13 -0
- package/templates/vanilla/package.json +19 -0
- package/templates/vanilla/src/index.html +16 -0
- package/templates/vanilla/src/main.ts +54 -0
- package/templates/vanilla/tsconfig.json +13 -0
- package/templates/vanilla/vite.config.ts +13 -0
- package/templates/vue/package.json +21 -0
- package/templates/vue/src/App.vue +60 -0
- package/templates/vue/src/env.d.ts +5 -0
- package/templates/vue/src/index.html +12 -0
- package/templates/vue/src/main.ts +4 -0
- package/templates/vue/tsconfig.json +14 -0
- package/templates/vue/vite.config.ts +15 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What changed in `@tavora/create-module`, newest first.
|
|
4
|
+
|
|
5
|
+
## 0.1.1
|
|
6
|
+
|
|
7
|
+
- Asks with arrow keys: where the project goes (the empty folder you are in, or a new folder in it) and which template.
|
|
8
|
+
- The next steps say to leave `npm run dev` running and which folder to add in Tavora under Settings > Development; a folder with several projects works too (Tavora 0.17.20).
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
- First release on npm: `npm create @tavora/module` makes a vanilla TypeScript, Vue or React module project.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wellbrained
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @tavora/create-module
|
|
2
|
+
|
|
3
|
+
Makes a new [Tavora](https://mytavora.app) module project.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm create @tavora/module
|
|
7
|
+
# or
|
|
8
|
+
pnpm create @tavora/module
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It asks where the project goes (the empty folder you are in, or a new folder in it), the module's name, its id (`publisher.module-name`) and a template: **vanilla** TypeScript, **vue** or **react**, picked with the arrow keys. Give them up front to skip the questions:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm create @tavora/module raid-timer -- --template vue --id example.raid-timer --name "Raid Timer"
|
|
15
|
+
pnpm create @tavora/module raid-timer --template vue --id example.raid-timer --name "Raid Timer"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`--yes` takes the suggestions for anything not given.
|
|
19
|
+
|
|
20
|
+
The project builds with Vite, has a `dev` script that rebuilds on every change (add the folder in Tavora under **Settings > Development > Add a development folder…**; a folder with several projects in it works too), and a `zip` script that packs and signs it with `@tavora/pack`. Its manifest sets `minTavora` to the oldest Tavora that has everything the SDK it starts with offers. The project's README says the rest.
|
|
21
|
+
|
|
22
|
+
More in the [developer docs](https://docs.mytavora.app/guide/getting-started).
|
package/dist/create.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const TEMPLATES: readonly ["vanilla", "vue", "react"];
|
|
2
|
+
export type Template = (typeof TEMPLATES)[number];
|
|
3
|
+
export interface ModuleOptions {
|
|
4
|
+
/** The project folder to make; it must not exist yet, or be empty. */
|
|
5
|
+
dir: string;
|
|
6
|
+
/** The module id, publisher.module-name. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** The name players see. */
|
|
9
|
+
name: string;
|
|
10
|
+
template: Template;
|
|
11
|
+
/** The package manager the commands in the README use. */
|
|
12
|
+
packageManager?: "npm" | "pnpm" | "yarn";
|
|
13
|
+
}
|
|
14
|
+
export declare class CreateError extends Error {
|
|
15
|
+
}
|
|
16
|
+
/** A folder name as a lowercase slug: letters, digits and dashes, starting with a letter. */
|
|
17
|
+
export declare function slugOf(folder: string): string;
|
|
18
|
+
/** What the starter offers when nothing was given: a name and an id from the folder's name. */
|
|
19
|
+
export declare function defaultsFor(folder: string): {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
/** Why an id cannot be used, or null. */
|
|
24
|
+
export declare function idProblem(id: string): string | null;
|
|
25
|
+
/** Why a name cannot be used, or null. */
|
|
26
|
+
export declare function nameProblem(name: string): string | null;
|
|
27
|
+
/** Makes the project and answers with the files it wrote, relative to `dir`. */
|
|
28
|
+
export declare function createModule(options: ModuleOptions): Promise<string[]>;
|
|
29
|
+
//# sourceMappingURL=create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,SAAS,sCAAuC,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAElD,MAAM,WAAW,aAAa;IAC5B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,EAAE,EAAE,MAAM,CAAC;IACX,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;CAC1C;AAED,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAKzC,6FAA6F;AAC7F,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAO7C;AAED,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQxE;AAED,yCAAyC;AACzC,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInD;AAED,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEvD;AAyBD,gFAAgF;AAChF,wBAAsB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAgC5E"}
|
package/dist/create.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isModuleId } from "@tavora/module-schema";
|
|
5
|
+
import { VERSIONS } from "./versions.js";
|
|
6
|
+
// Makes a new module project from a template: the shared files (manifest, README, styles,
|
|
7
|
+
// .gitignore) and the template's own (package.json, build config, page and code). Only the
|
|
8
|
+
// placeholders listed here are filled in; anything else in braces (such as Vue's own {{ }}) is
|
|
9
|
+
// left as it is.
|
|
10
|
+
export const TEMPLATES = ["vanilla", "vue", "react"];
|
|
11
|
+
export class CreateError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** The templates folder, next to dist/ (built) and src/ (in Tavora's repository). */
|
|
14
|
+
const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
|
|
15
|
+
/** A folder name as a lowercase slug: letters, digits and dashes, starting with a letter. */
|
|
16
|
+
export function slugOf(folder) {
|
|
17
|
+
const slug = path
|
|
18
|
+
.basename(path.resolve(folder))
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
21
|
+
.replace(/^-+|-+$/g, "");
|
|
22
|
+
return /^[a-z]/.test(slug) ? slug.slice(0, 60) : `module-${slug}`.replace(/-+$/, "").slice(0, 60);
|
|
23
|
+
}
|
|
24
|
+
/** What the starter offers when nothing was given: a name and an id from the folder's name. */
|
|
25
|
+
export function defaultsFor(folder) {
|
|
26
|
+
const slug = slugOf(folder);
|
|
27
|
+
const name = slug
|
|
28
|
+
.split("-")
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
31
|
+
.join(" ");
|
|
32
|
+
return { id: `my.${slug}`, name: name || "My Module" };
|
|
33
|
+
}
|
|
34
|
+
/** Why an id cannot be used, or null. */
|
|
35
|
+
export function idProblem(id) {
|
|
36
|
+
if (!isModuleId(id))
|
|
37
|
+
return "an id looks like publisher.module-name: 2 to 5 parts of lowercase letters, digits and dashes, separated by dots, each starting with a letter";
|
|
38
|
+
if (id.startsWith("tavora."))
|
|
39
|
+
return "ids starting with tavora. are Tavora's own";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** Why a name cannot be used, or null. */
|
|
43
|
+
export function nameProblem(name) {
|
|
44
|
+
return name.trim() === "" || name.length > 80 ? "a name is 1 to 80 characters" : null;
|
|
45
|
+
}
|
|
46
|
+
const run = (manager) => (manager === "pnpm" ? "pnpm" : manager === "yarn" ? "yarn" : "npm run");
|
|
47
|
+
const exec = (manager) => (manager === "pnpm" ? "pnpm exec" : manager === "yarn" ? "yarn" : "npx");
|
|
48
|
+
/** Fills the placeholders of one file, escaped for where they stand. */
|
|
49
|
+
function fill(text, file, values) {
|
|
50
|
+
const escape = file.endsWith(".json")
|
|
51
|
+
? (value) => JSON.stringify(value).slice(1, -1)
|
|
52
|
+
: file.endsWith(".html")
|
|
53
|
+
? (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """)
|
|
54
|
+
: (value) => value;
|
|
55
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, key) => (key in values ? escape(values[key]) : match));
|
|
56
|
+
}
|
|
57
|
+
async function files(root, prefix = "") {
|
|
58
|
+
const found = [];
|
|
59
|
+
for (const entry of await readdir(path.join(root, prefix), { withFileTypes: true })) {
|
|
60
|
+
const relative = path.join(prefix, entry.name);
|
|
61
|
+
if (entry.isDirectory())
|
|
62
|
+
found.push(...(await files(root, relative)));
|
|
63
|
+
else
|
|
64
|
+
found.push(relative);
|
|
65
|
+
}
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
/** Makes the project and answers with the files it wrote, relative to `dir`. */
|
|
69
|
+
export async function createModule(options) {
|
|
70
|
+
const problem = idProblem(options.id) ?? nameProblem(options.name);
|
|
71
|
+
if (problem)
|
|
72
|
+
throw new CreateError(problem);
|
|
73
|
+
if (!TEMPLATES.includes(options.template))
|
|
74
|
+
throw new CreateError(`the template is one of ${TEMPLATES.join(", ")}`);
|
|
75
|
+
const dir = path.resolve(options.dir);
|
|
76
|
+
const existing = await readdir(dir).catch(() => null);
|
|
77
|
+
if (existing && existing.length > 0)
|
|
78
|
+
throw new CreateError(`${dir} is not empty`);
|
|
79
|
+
const values = {
|
|
80
|
+
id: options.id,
|
|
81
|
+
name: options.name.trim(),
|
|
82
|
+
description: `${options.name.trim()}, a Tavora module.`,
|
|
83
|
+
package: slugOf(dir),
|
|
84
|
+
sdkVersion: VERSIONS.sdk,
|
|
85
|
+
packVersion: VERSIONS.pack,
|
|
86
|
+
minTavora: VERSIONS.minTavora,
|
|
87
|
+
run: run(options.packageManager),
|
|
88
|
+
exec: exec(options.packageManager),
|
|
89
|
+
};
|
|
90
|
+
const written = [];
|
|
91
|
+
for (const source of ["shared", options.template]) {
|
|
92
|
+
const root = path.join(TEMPLATES_DIR, source);
|
|
93
|
+
for (const file of await files(root)) {
|
|
94
|
+
// npm drops .gitignore from published packages, so it travels as gitignore.
|
|
95
|
+
const target = file === "gitignore" ? ".gitignore" : file;
|
|
96
|
+
const text = await readFile(path.join(root, file), "utf8");
|
|
97
|
+
await mkdir(path.dirname(path.join(dir, target)), { recursive: true });
|
|
98
|
+
await writeFile(path.join(dir, target), fill(text, target, values));
|
|
99
|
+
written.push(target.split(path.sep).join("/"));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return written.sort();
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.js","sourceRoot":"","sources":["../src/create.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm create @tavora/module [folder] -- [--template vanilla|vue|react] [--id publisher.name] [--name "Name"] [--yes]
|
|
3
|
+
// pnpm create @tavora/module [folder] [--template ...]
|
|
4
|
+
// Makes a new Tavora module project. Asks for what was not given, unless --yes (or no terminal):
|
|
5
|
+
// where (this empty folder or a new one in it) and the template are picked with the arrow keys.
|
|
6
|
+
import { readdir } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
import { cancel, intro, isCancel, note, outro, select, text } from "@clack/prompts";
|
|
10
|
+
import { createModule, CreateError, defaultsFor, idProblem, nameProblem, TEMPLATES } from "./create.js";
|
|
11
|
+
const USAGE = "usage: npm create @tavora/module [folder] -- [--template vanilla|vue|react] [--id publisher.module-name] [--name \"Module name\"] [--yes]";
|
|
12
|
+
const TEMPLATE_LABELS = { vanilla: "Vanilla TypeScript", vue: "Vue", react: "React" };
|
|
13
|
+
/** The package manager that ran the starter, so the printed commands fit. */
|
|
14
|
+
function packageManager() {
|
|
15
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
16
|
+
return agent.startsWith("pnpm") ? "pnpm" : agent.startsWith("yarn") ? "yarn" : "npm";
|
|
17
|
+
}
|
|
18
|
+
/** Thrown when the developer pressed Ctrl+C or Esc in a question. */
|
|
19
|
+
class Cancelled extends Error {
|
|
20
|
+
}
|
|
21
|
+
function answer(value) {
|
|
22
|
+
if (isCancel(value))
|
|
23
|
+
throw new Cancelled();
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
async function isEmptyFolder(dir) {
|
|
27
|
+
return ((await readdir(dir).catch(() => null))?.length ?? -1) === 0;
|
|
28
|
+
}
|
|
29
|
+
async function main() {
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = parseArgs({
|
|
33
|
+
allowPositionals: true,
|
|
34
|
+
options: { template: { type: "string", short: "t" }, id: { type: "string" }, name: { type: "string" }, yes: { type: "boolean", short: "y" }, help: { type: "boolean", short: "h" } },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error(`${error.message}\n${USAGE}`);
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
const { values, positionals } = parsed;
|
|
42
|
+
if (values.help || positionals.length > 1) {
|
|
43
|
+
console.log(USAGE);
|
|
44
|
+
return values.help ? 0 : 2;
|
|
45
|
+
}
|
|
46
|
+
if (values.template !== undefined && !TEMPLATES.includes(values.template)) {
|
|
47
|
+
console.error(`create-module: the template is one of ${TEMPLATES.join(", ")}`);
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
const interactive = !values.yes && process.stdin.isTTY === true;
|
|
51
|
+
const here = process.cwd();
|
|
52
|
+
const emptyHere = await isEmptyFolder(here);
|
|
53
|
+
try {
|
|
54
|
+
if (interactive)
|
|
55
|
+
intro("A new Tavora module");
|
|
56
|
+
// Where: the empty folder the developer is in, or a new folder in it.
|
|
57
|
+
let folder = positionals[0];
|
|
58
|
+
if (folder === undefined && interactive) {
|
|
59
|
+
const where = emptyHere
|
|
60
|
+
? answer(await select({
|
|
61
|
+
message: "Where should the project go?",
|
|
62
|
+
options: [
|
|
63
|
+
{ value: ".", label: `This folder (${path.basename(here)})`, hint: "it is empty" },
|
|
64
|
+
{ value: "new", label: "A new folder in it" },
|
|
65
|
+
],
|
|
66
|
+
}))
|
|
67
|
+
: "new";
|
|
68
|
+
folder =
|
|
69
|
+
where === "."
|
|
70
|
+
? "."
|
|
71
|
+
: answer(await text({
|
|
72
|
+
message: "Folder name for the project",
|
|
73
|
+
placeholder: "my-tavora-module",
|
|
74
|
+
defaultValue: "my-tavora-module",
|
|
75
|
+
validate: (value) => (value && /[\\/:*?"<>|]/.test(value) ? "a folder name, without \\ / : * ? \" < > |" : undefined),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
folder ??= emptyHere ? "." : "my-tavora-module";
|
|
79
|
+
const defaults = defaultsFor(folder);
|
|
80
|
+
const name = values.name ??
|
|
81
|
+
(interactive
|
|
82
|
+
? answer(await text({ message: "Module name", placeholder: defaults.name, defaultValue: defaults.name, validate: (value) => (value ? (nameProblem(value) ?? undefined) : undefined) }))
|
|
83
|
+
: defaults.name);
|
|
84
|
+
const id = values.id ??
|
|
85
|
+
(interactive
|
|
86
|
+
? answer(await text({
|
|
87
|
+
message: "Module id (publisher.module-name)",
|
|
88
|
+
placeholder: defaults.id,
|
|
89
|
+
defaultValue: defaults.id,
|
|
90
|
+
validate: (value) => (value ? (idProblem(value) ?? undefined) : undefined),
|
|
91
|
+
}))
|
|
92
|
+
: defaults.id);
|
|
93
|
+
const template = (values.template ??
|
|
94
|
+
(interactive
|
|
95
|
+
? answer(await select({ message: "Template", options: TEMPLATES.map((value) => ({ value, label: TEMPLATE_LABELS[value] })), initialValue: "vanilla" }))
|
|
96
|
+
: "vanilla"));
|
|
97
|
+
const manager = packageManager();
|
|
98
|
+
await createModule({ dir: folder, id, name, template, packageManager: manager });
|
|
99
|
+
const run = manager === "npm" ? "npm run" : manager;
|
|
100
|
+
const project = path.resolve(folder);
|
|
101
|
+
const steps = [
|
|
102
|
+
...(folder === "." ? [] : [`cd ${folder}`]),
|
|
103
|
+
`${manager} install`,
|
|
104
|
+
`${run} dev # builds, then rebuilds on every change: leave it running`,
|
|
105
|
+
"",
|
|
106
|
+
"Then in Tavora: Settings > Development > Add a development folder…",
|
|
107
|
+
`and pick ${project}`,
|
|
108
|
+
...(folder === "." ? [] : [`(or ${here}, to load every module project you keep in it)`]),
|
|
109
|
+
];
|
|
110
|
+
if (interactive) {
|
|
111
|
+
note(steps.join("\n"), "Next");
|
|
112
|
+
outro(`Made ${name} (${id}). The README says how to share it and show your name on it.`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(`\nMade ${name} (${id}) in ${project}.\n\nNext:\n${steps.map((line) => (line ? ` ${line}` : "")).join("\n")}\n`);
|
|
116
|
+
console.log("The README says how to share the module and show your name on it.");
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof Cancelled) {
|
|
122
|
+
cancel("Nothing was made.");
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
if (error instanceof CreateError) {
|
|
126
|
+
console.error(`create-module: ${error.message}`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
main().then((code) => (process.exitCode = code), (error) => {
|
|
133
|
+
console.error(error);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versions.d.ts","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;;;;CAIX,CAAC"}
|
package/dist/versions.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// What a new module starts with: the SDK and pack versions released together with this starter,
|
|
2
|
+
// and the oldest Tavora that has everything that SDK offers (its MIN_TAVORA). A test keeps these
|
|
3
|
+
// equal to the packages in Tavora's repository, so a release never ships an old starter.
|
|
4
|
+
export const VERSIONS = {
|
|
5
|
+
sdk: "0.2.5",
|
|
6
|
+
pack: "0.1.0",
|
|
7
|
+
minTavora: "0.17.19",
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versions.js","sourceRoot":"","sources":["../src/versions.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,iGAAiG;AACjG,yFAAyF;AACzF,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,GAAG,EAAE,OAAO;IACZ,IAAI,EAAE,OAAO;IACb,SAAS,EAAE,SAAS;CACZ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tavora/create-module",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Makes a new Tavora module project: npm create @tavora/module.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"tavora",
|
|
8
|
+
"module",
|
|
9
|
+
"create",
|
|
10
|
+
"starter",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/wellbrained/tavora.git",
|
|
16
|
+
"directory": "packages/create-module"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://docs.mytavora.app/guide/getting-started",
|
|
19
|
+
"bugs": "https://github.com/wellbrained/tavora/issues",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"create-module": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"templates",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.12"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@clack/prompts": "^1.8.1",
|
|
37
|
+
"@tavora/module-schema": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.20.5",
|
|
41
|
+
"@tavora/sdk": "0.2.5"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.build.json",
|
|
46
|
+
"typecheck": "tsc --noEmit -p ."
|
|
47
|
+
}
|
|
6
48
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{package}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite build --watch",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"zip": "tavora-pack ."
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@tavora/sdk": "^{{sdkVersion}}",
|
|
13
|
+
"react": "^19.3.0",
|
|
14
|
+
"react-dom": "^19.3.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@tavora/pack": "^{{packVersion}}",
|
|
18
|
+
"@types/react": "^19.3.0",
|
|
19
|
+
"@types/react-dom": "^19.3.0",
|
|
20
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"vite": "^8.3.4"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>{{name}}</title>
|
|
6
|
+
<link rel="stylesheet" href="./style.css" />
|
|
7
|
+
<script type="module" src="./main.tsx"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { connect, type TavoraSDK, type TavoraTheme } from "@tavora/sdk";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
|
|
5
|
+
// This page runs inside a Tavora panel; in a normal browser connect() fails. Build it and add this
|
|
6
|
+
// folder in Tavora (Settings > Development > Add a development folder) to see it.
|
|
7
|
+
|
|
8
|
+
/** Your texts in English and German; Tavora says which language the player uses. */
|
|
9
|
+
const TEXTS = {
|
|
10
|
+
en: { connecting: "Connecting to Tavora…", playing: "Playing {game}.", clicked: "Clicked {count} times in this game." },
|
|
11
|
+
de: { connecting: "Verbinde mit Tavora …", playing: "Du spielst {game}.", clicked: "{count}-mal in diesem Spiel geklickt." },
|
|
12
|
+
};
|
|
13
|
+
type Language = keyof typeof TEXTS;
|
|
14
|
+
const text = (language: Language, key: keyof (typeof TEXTS)["en"], params: Record<string, string | number> = {}) =>
|
|
15
|
+
TEXTS[language][key].replace(/\{(\w+)\}/g, (match, name: string) => String(params[name] ?? match));
|
|
16
|
+
|
|
17
|
+
/** Tavora's theme as CSS variables (--background, --foreground, --primary, ...), so the module looks like Tavora. */
|
|
18
|
+
function applyTheme(theme: TavoraTheme) {
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
for (const [name, value] of Object.entries(theme.tokens)) root.style.setProperty(`--${name}`, value);
|
|
21
|
+
root.style.setProperty("--radius", `${theme.radius}px`);
|
|
22
|
+
root.style.colorScheme = theme.mode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function App() {
|
|
26
|
+
const [tavora, setTavora] = useState<TavoraSDK | null>(null);
|
|
27
|
+
const [language, setLanguage] = useState<Language>("en");
|
|
28
|
+
const [game, setGame] = useState("");
|
|
29
|
+
const [count, setCount] = useState(0);
|
|
30
|
+
const [failed, setFailed] = useState<string | null>(null);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const pick = (code: string): Language => (code.startsWith("de") ? "de" : "en");
|
|
34
|
+
connect().then(
|
|
35
|
+
async (sdk) => {
|
|
36
|
+
applyTheme(await sdk.ui.getTheme());
|
|
37
|
+
sdk.ui.onThemeChange(applyTheme);
|
|
38
|
+
setLanguage(pick(await sdk.ui.getLanguage()));
|
|
39
|
+
sdk.ui.onLanguageChange((code) => setLanguage(pick(code)));
|
|
40
|
+
setGame((await sdk.game.getCurrent()).displayName);
|
|
41
|
+
// storage keeps small settings for this module in the current game.
|
|
42
|
+
setCount((await sdk.storage.get<number>("count")) ?? 0);
|
|
43
|
+
setTavora(sdk);
|
|
44
|
+
},
|
|
45
|
+
(error: unknown) => setFailed(`Not connected to Tavora: ${error instanceof Error ? error.message : String(error)}`),
|
|
46
|
+
);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const add = () => {
|
|
50
|
+
const next = count + 1;
|
|
51
|
+
setCount(next);
|
|
52
|
+
void tavora?.storage.set("count", next);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const status = failed ?? (tavora ? `${text(language, "playing", { game })} ${text(language, "clicked", { count })}` : text(language, "connecting"));
|
|
56
|
+
return (
|
|
57
|
+
<main>
|
|
58
|
+
<h1>{document.title}</h1>
|
|
59
|
+
<p id="status">{status}</p>
|
|
60
|
+
<button type="button" disabled={!tavora} onClick={add}>
|
|
61
|
+
+1
|
|
62
|
+
</button>
|
|
63
|
+
</main>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createRoot(document.getElementById("app")!).render(<App />);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "vite.config.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
// A module ships as one self-contained folder: relative paths, nothing loaded from the internet.
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: "src",
|
|
7
|
+
base: "./",
|
|
8
|
+
plugins: [react()],
|
|
9
|
+
build: {
|
|
10
|
+
outDir: "../dist",
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
target: "chrome130",
|
|
13
|
+
modulePreload: { polyfill: false },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
A [Tavora](https://mytavora.app) module, made with `@tavora/create-module`. Tavora shows it in a panel next to a game; it talks to Tavora through [`@tavora/sdk`](https://docs.mytavora.app/api/).
|
|
4
|
+
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
{{run}} install
|
|
9
|
+
{{run}} dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`dev` builds the module into `dist/` and builds it again on every change. Leave it running. In Tavora, open **Settings > Development > Add a development folder…** and choose this folder, or the folder you keep all your module projects in. Add the module to a game with **Add module**; its panels reload by themselves after every build.
|
|
13
|
+
|
|
14
|
+
- `tavora.module.json` is the manifest: its id, name, the panel's smallest size, and what it asks the player for (websites to reach, connectors). See the [manifest reference](https://docs.mytavora.app/reference/manifest).
|
|
15
|
+
- `minTavora` is the oldest Tavora the module works with. Raise it when you use something newer from the SDK (the [changelog](https://docs.mytavora.app/reference/changelog) says which Tavora added what).
|
|
16
|
+
|
|
17
|
+
## Share
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
{{run}} build
|
|
21
|
+
{{run}} zip
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`zip` packs the built module into `{{id}}-0.1.0.zip`, signed with your publisher key. The first run makes the key in `~/.tavora/publisher-key.pem`. **Back it up and never share it**: the data your module saves for players belongs to that key, so a new key means a new publisher. Players install the zip with **Settings > Modules > Install from zip**; a newer zip signed with the same key updates it.
|
|
25
|
+
|
|
26
|
+
## Show your name instead of a fingerprint
|
|
27
|
+
|
|
28
|
+
In Tavora, sign in under **Settings > Account**, choose a developer name under **Module developer** and press **Link a publisher key**. Then run the command it shows here, for example:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
{{exec}} tavora-pack link ABCD-EFGH
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Players then see your modules as "by" your name.
|
|
35
|
+
|
|
36
|
+
More in the [developer docs](https://docs.mytavora.app/).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* Tavora gives the page its fonts and the look of buttons and fields. The colours come from
|
|
2
|
+
Tavora's theme as CSS variables (main.ts sets them), so the module matches every theme. */
|
|
3
|
+
body {
|
|
4
|
+
margin: 0;
|
|
5
|
+
background: var(--background);
|
|
6
|
+
color: var(--foreground);
|
|
7
|
+
}
|
|
8
|
+
main {
|
|
9
|
+
padding: 16px;
|
|
10
|
+
}
|
|
11
|
+
h1 {
|
|
12
|
+
margin: 0 0 8px;
|
|
13
|
+
font-size: 16px;
|
|
14
|
+
}
|
|
15
|
+
#status {
|
|
16
|
+
color: var(--muted-foreground);
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.mytavora.app/schema/tavora.module.schema.json",
|
|
3
|
+
"id": "{{id}}",
|
|
4
|
+
"name": "{{name}}",
|
|
5
|
+
"description": "{{description}}",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"apiVersion": "1",
|
|
8
|
+
"minTavora": "{{minTavora}}",
|
|
9
|
+
"entry": "dist/index.html",
|
|
10
|
+
"games": [],
|
|
11
|
+
"viewport": { "minWidth": 240, "minHeight": 160 },
|
|
12
|
+
"permissions": { "network": [] }
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{package}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite build --watch",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"zip": "tavora-pack ."
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@tavora/sdk": "^{{sdkVersion}}"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@tavora/pack": "^{{packVersion}}",
|
|
16
|
+
"typescript": "^5.9.3",
|
|
17
|
+
"vite": "^8.3.4"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>{{name}}</title>
|
|
6
|
+
<link rel="stylesheet" href="./style.css" />
|
|
7
|
+
<script type="module" src="./main.ts"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>{{name}}</h1>
|
|
12
|
+
<p id="status">Connecting to Tavora…</p>
|
|
13
|
+
<button type="button" id="counter" disabled>+1</button>
|
|
14
|
+
</main>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { connect, type TavoraSDK, type TavoraTheme } from "@tavora/sdk";
|
|
2
|
+
|
|
3
|
+
// This page runs inside a Tavora panel; in a normal browser connect() fails. Build it and add this
|
|
4
|
+
// folder in Tavora (Settings > Development > Add a development folder) to see it.
|
|
5
|
+
|
|
6
|
+
const status = document.querySelector<HTMLParagraphElement>("#status")!;
|
|
7
|
+
const counter = document.querySelector<HTMLButtonElement>("#counter")!;
|
|
8
|
+
|
|
9
|
+
/** Your texts in English and German; Tavora says which language the player uses. */
|
|
10
|
+
const TEXTS = {
|
|
11
|
+
en: { playing: "Playing {game}.", clicked: "Clicked {count} times in this game." },
|
|
12
|
+
de: { playing: "Du spielst {game}.", clicked: "{count}-mal in diesem Spiel geklickt." },
|
|
13
|
+
};
|
|
14
|
+
let language: keyof typeof TEXTS = "en";
|
|
15
|
+
const text = (key: keyof (typeof TEXTS)["en"], params: Record<string, string | number>) =>
|
|
16
|
+
TEXTS[language][key].replace(/\{(\w+)\}/g, (match, name: string) => String(params[name] ?? match));
|
|
17
|
+
|
|
18
|
+
/** Tavora's theme as CSS variables (--background, --foreground, --primary, ...), so the module looks like Tavora. */
|
|
19
|
+
function applyTheme(theme: TavoraTheme) {
|
|
20
|
+
const root = document.documentElement;
|
|
21
|
+
for (const [name, value] of Object.entries(theme.tokens)) root.style.setProperty(`--${name}`, value);
|
|
22
|
+
root.style.setProperty("--radius", `${theme.radius}px`);
|
|
23
|
+
root.style.colorScheme = theme.mode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function start(tavora: TavoraSDK) {
|
|
27
|
+
applyTheme(await tavora.ui.getTheme());
|
|
28
|
+
tavora.ui.onThemeChange(applyTheme);
|
|
29
|
+
const setLanguage = (code: string) => (language = code.startsWith("de") ? "de" : "en");
|
|
30
|
+
setLanguage(await tavora.ui.getLanguage());
|
|
31
|
+
|
|
32
|
+
const game = await tavora.game.getCurrent();
|
|
33
|
+
// storage keeps small settings for this module in the current game.
|
|
34
|
+
let count = (await tavora.storage.get<number>("count")) ?? 0;
|
|
35
|
+
const render = () => {
|
|
36
|
+
status.textContent = `${text("playing", { game: game.displayName })} ${text("clicked", { count })}`;
|
|
37
|
+
};
|
|
38
|
+
tavora.ui.onLanguageChange((code) => {
|
|
39
|
+
setLanguage(code);
|
|
40
|
+
render();
|
|
41
|
+
});
|
|
42
|
+
render();
|
|
43
|
+
|
|
44
|
+
counter.disabled = false;
|
|
45
|
+
counter.addEventListener("click", async () => {
|
|
46
|
+
count += 1;
|
|
47
|
+
render();
|
|
48
|
+
await tavora.storage.set("count", count);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
connect().then(start, (error: unknown) => {
|
|
53
|
+
status.textContent = `Not connected to Tavora: ${error instanceof Error ? error.message : String(error)}`;
|
|
54
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"skipLibCheck": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src", "vite.config.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
// A module ships as one self-contained folder: relative paths, nothing loaded from the internet.
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
root: "src",
|
|
6
|
+
base: "./",
|
|
7
|
+
build: {
|
|
8
|
+
outDir: "../dist",
|
|
9
|
+
emptyOutDir: true,
|
|
10
|
+
target: "chrome130",
|
|
11
|
+
modulePreload: { polyfill: false },
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{package}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite build --watch",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"zip": "tavora-pack ."
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@tavora/sdk": "^{{sdkVersion}}",
|
|
13
|
+
"vue": "^3.5.43"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@tavora/pack": "^{{packVersion}}",
|
|
17
|
+
"@vitejs/plugin-vue": "^6.0.9",
|
|
18
|
+
"typescript": "^5.9.3",
|
|
19
|
+
"vite": "^8.3.4"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// This page runs inside a Tavora panel; in a normal browser connect() fails. Build it and add this
|
|
3
|
+
// folder in Tavora (Settings > Development > Add a development folder) to see it.
|
|
4
|
+
import { connect, type TavoraSDK, type TavoraTheme } from "@tavora/sdk";
|
|
5
|
+
import { computed, onMounted, ref, shallowRef } from "vue";
|
|
6
|
+
|
|
7
|
+
/** Your texts in English and German; Tavora says which language the player uses. */
|
|
8
|
+
const TEXTS = {
|
|
9
|
+
en: { connecting: "Connecting to Tavora…", playing: "Playing {game}.", clicked: "Clicked {count} times in this game." },
|
|
10
|
+
de: { connecting: "Verbinde mit Tavora …", playing: "Du spielst {game}.", clicked: "{count}-mal in diesem Spiel geklickt." },
|
|
11
|
+
};
|
|
12
|
+
const language = ref<keyof typeof TEXTS>("en");
|
|
13
|
+
const text = (key: keyof (typeof TEXTS)["en"], params: Record<string, string | number> = {}) =>
|
|
14
|
+
TEXTS[language.value][key].replace(/\{(\w+)\}/g, (match, name: string) => String(params[name] ?? match));
|
|
15
|
+
|
|
16
|
+
const title = document.title;
|
|
17
|
+
const tavora = shallowRef<TavoraSDK | null>(null);
|
|
18
|
+
const game = ref("");
|
|
19
|
+
const count = ref(0);
|
|
20
|
+
const failed = ref<string | null>(null);
|
|
21
|
+
const status = computed(() => (failed.value ? failed.value : tavora.value ? `${text("playing", { game: game.value })} ${text("clicked", { count: count.value })}` : text("connecting")));
|
|
22
|
+
|
|
23
|
+
/** Tavora's theme as CSS variables (--background, --foreground, --primary, ...), so the module looks like Tavora. */
|
|
24
|
+
function applyTheme(theme: TavoraTheme) {
|
|
25
|
+
const root = document.documentElement;
|
|
26
|
+
for (const [name, value] of Object.entries(theme.tokens)) root.style.setProperty(`--${name}`, value);
|
|
27
|
+
root.style.setProperty("--radius", `${theme.radius}px`);
|
|
28
|
+
root.style.colorScheme = theme.mode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMounted(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const sdk = await connect();
|
|
34
|
+
applyTheme(await sdk.ui.getTheme());
|
|
35
|
+
sdk.ui.onThemeChange(applyTheme);
|
|
36
|
+
const setLanguage = (code: string) => (language.value = code.startsWith("de") ? "de" : "en");
|
|
37
|
+
setLanguage(await sdk.ui.getLanguage());
|
|
38
|
+
sdk.ui.onLanguageChange(setLanguage);
|
|
39
|
+
game.value = (await sdk.game.getCurrent()).displayName;
|
|
40
|
+
// storage keeps small settings for this module in the current game.
|
|
41
|
+
count.value = (await sdk.storage.get<number>("count")) ?? 0;
|
|
42
|
+
tavora.value = sdk;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
failed.value = `Not connected to Tavora: ${error instanceof Error ? error.message : String(error)}`;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
async function add() {
|
|
49
|
+
count.value += 1;
|
|
50
|
+
await tavora.value?.storage.set("count", count.value);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<main>
|
|
56
|
+
<h1>{{ title }}</h1>
|
|
57
|
+
<p id="status">{{ status }}</p>
|
|
58
|
+
<button type="button" :disabled="!tavora" @click="add">+1</button>
|
|
59
|
+
</main>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>{{name}}</title>
|
|
6
|
+
<link rel="stylesheet" href="./style.css" />
|
|
7
|
+
<script type="module" src="./main.ts"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "vite.config.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import vue from "@vitejs/plugin-vue";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
// A module ships as one self-contained folder: relative paths, nothing loaded from the internet.
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: "src",
|
|
7
|
+
base: "./",
|
|
8
|
+
plugins: [vue()],
|
|
9
|
+
build: {
|
|
10
|
+
outDir: "../dist",
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
target: "chrome130",
|
|
13
|
+
modulePreload: { polyfill: false },
|
|
14
|
+
},
|
|
15
|
+
});
|