@hoardodile/create-plugin 0.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/LICENSE +18 -0
- package/README.md +34 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/dist/template/LICENSE +18 -0
- package/dist/template/README.md +52 -0
- package/dist/template/index.html +2 -0
- package/dist/template/manifest.json +22 -0
- package/dist/template/package.json +34 -0
- package/dist/template/src/TemplateView.tsx +29 -0
- package/dist/template/src/__tests__/main.test.ts +64 -0
- package/dist/template/src/hooks.ts +5 -0
- package/dist/template/src/index.css +9 -0
- package/dist/template/src/main.ts +37 -0
- package/dist/template/src/render.tsx +7 -0
- package/dist/template/src/shared.ts +26 -0
- package/dist/template/testdata/README.md +8 -0
- package/dist/template/testdata/sample.hdtpl +1 -0
- package/dist/template/tsconfig.json +28 -0
- package/dist/template/vitest.config.ts +13 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wooloo <ayan0312000@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @hoardodile/create-plugin
|
|
2
|
+
|
|
3
|
+
Interactive scaffolder for hoardodile content plugins. Copies the
|
|
4
|
+
embedded template (kept in sync with `plugins/template` — CI checks for
|
|
5
|
+
drift), rewrites the manifest with a fresh UUID, rewires dependency
|
|
6
|
+
specs, installs, validates the result, and prints the dev-loop entry
|
|
7
|
+
points.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# installs the published @hoardodile/* SDK packages from npm
|
|
13
|
+
create-hoardodile-plugin my-plugin
|
|
14
|
+
|
|
15
|
+
# pre-release / offline: consume the packed tarballs instead
|
|
16
|
+
create-hoardodile-plugin my-plugin --tarballs ../hoardodile/tmp/sdks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What you get
|
|
20
|
+
|
|
21
|
+
A complete plugin directory: `manifest.json` (fresh UUID — never ship
|
|
22
|
+
the template's id), `src/main.ts` (`definePlugin` with `detect` +
|
|
23
|
+
`sourceMeta`), `src/render.tsx` (`createPluginRoot` + typed API pair),
|
|
24
|
+
`src/shared.ts` (the `PluginSchema` declared once), `testdata/` and
|
|
25
|
+
Vitest unit tests against `createResourceAPIFixture`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd my-plugin
|
|
29
|
+
pnpm dev # watch-build + workbench at http://127.0.0.1:5199
|
|
30
|
+
pnpm test # unit tests against the fixture API
|
|
31
|
+
pnpm build # dist/ — zip its contents (manifest at the zip root) and upload
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
MIT License.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, mkdirSync, cpSync, writeFileSync } from 'fs';
|
|
4
|
+
import { resolve, dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { log, intro, text, isCancel, cancel, confirm, outro } from '@clack/prompts';
|
|
7
|
+
import { pluginManifest } from '@hoardodile/sdk-types/schema';
|
|
8
|
+
|
|
9
|
+
// src/sdk-deps.gen.ts
|
|
10
|
+
var SDK_DEP_NAMES = [
|
|
11
|
+
"@hoardodile/cli",
|
|
12
|
+
"@hoardodile/host",
|
|
13
|
+
"@hoardodile/host-web",
|
|
14
|
+
"@hoardodile/i18n",
|
|
15
|
+
"@hoardodile/sdk-react",
|
|
16
|
+
"@hoardodile/sdk-server",
|
|
17
|
+
"@hoardodile/sdk-types",
|
|
18
|
+
"@hoardodile/sdk-web",
|
|
19
|
+
"@hoardodile/ui",
|
|
20
|
+
"@hoardodile/workbench"
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
// src/rewrite.ts
|
|
24
|
+
var THIRD_PARTY_VERSIONS = {
|
|
25
|
+
"@types/node": "^26.2.0",
|
|
26
|
+
"@types/react": "^19.2.18",
|
|
27
|
+
"@types/react-dom": "^19.2.4",
|
|
28
|
+
jsdom: "^30.0.1",
|
|
29
|
+
react: "^19.2.8",
|
|
30
|
+
"react-dom": "^19.2.8",
|
|
31
|
+
typescript: "^7.0.2",
|
|
32
|
+
vite: "^8.2.2",
|
|
33
|
+
vitest: "^4.1.11"
|
|
34
|
+
};
|
|
35
|
+
function rewriteManifest(manifest, pluginName) {
|
|
36
|
+
manifest.id = crypto.randomUUID();
|
|
37
|
+
manifest.name = pluginName;
|
|
38
|
+
manifest.description = `Content plugin: ${pluginName}`;
|
|
39
|
+
if (manifest.i18n?.name !== void 0) {
|
|
40
|
+
manifest.i18n.name.en = pluginName;
|
|
41
|
+
}
|
|
42
|
+
if (manifest.i18n?.description !== void 0) {
|
|
43
|
+
manifest.i18n.description.en = `Content plugin: ${pluginName}`;
|
|
44
|
+
}
|
|
45
|
+
manifest.version = "0.0.1";
|
|
46
|
+
return manifest;
|
|
47
|
+
}
|
|
48
|
+
function tarballSpec(tarballsDir, dep, version) {
|
|
49
|
+
const fileName = dep.replace("@", "").replace("/", "-");
|
|
50
|
+
return `file:${join(tarballsDir, `${fileName}-${version}.tgz`).replaceAll("\\", "/")}`;
|
|
51
|
+
}
|
|
52
|
+
function rewriteDepBlock(deps, tarballsDir, sdkVersion) {
|
|
53
|
+
const out = { ...deps };
|
|
54
|
+
for (const [dep, spec] of Object.entries(out)) {
|
|
55
|
+
if (SDK_DEP_NAMES.includes(dep)) {
|
|
56
|
+
out[dep] = tarballsDir !== void 0 ? tarballSpec(tarballsDir, dep, sdkVersion) : (
|
|
57
|
+
// `^0.2.0-alpha.3` would never match the 0.2.0 stable
|
|
58
|
+
// release (semver keeps prereleases out of plain ranges),
|
|
59
|
+
// so npm specs drop the prerelease suffix.
|
|
60
|
+
`^${sdkVersion.split("-")[0]}`
|
|
61
|
+
);
|
|
62
|
+
} else if (spec === "catalog:") {
|
|
63
|
+
const version = THIRD_PARTY_VERSIONS[dep];
|
|
64
|
+
if (version === void 0) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`template dependency "${dep}" has no concrete version mapping`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
out[dep] = version;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function rewritePackageJson(pkg, pluginName, opts) {
|
|
75
|
+
pkg.name = pluginName;
|
|
76
|
+
delete pkg.version;
|
|
77
|
+
pkg.version = "0.0.1";
|
|
78
|
+
if (pkg.dependencies !== void 0) {
|
|
79
|
+
pkg.dependencies = rewriteDepBlock(
|
|
80
|
+
pkg.dependencies,
|
|
81
|
+
opts.tarballsDir,
|
|
82
|
+
opts.sdkVersion
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (pkg.devDependencies !== void 0) {
|
|
86
|
+
pkg.devDependencies = rewriteDepBlock(
|
|
87
|
+
pkg.devDependencies,
|
|
88
|
+
opts.tarballsDir,
|
|
89
|
+
opts.sdkVersion
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return pkg;
|
|
93
|
+
}
|
|
94
|
+
var ALLOWED_BUILDS = [
|
|
95
|
+
"@derhuerst/ffprobe-static",
|
|
96
|
+
"@hoardodile/7z-bin",
|
|
97
|
+
"ffmpeg-static"
|
|
98
|
+
];
|
|
99
|
+
function tarballOverridesYaml(tarballsDir, sdkVersion) {
|
|
100
|
+
const lines = SDK_DEP_NAMES.map(
|
|
101
|
+
(dep) => ` '${dep}': '${tarballSpec(tarballsDir, dep, sdkVersion)}'`
|
|
102
|
+
).join("\n");
|
|
103
|
+
const builds = ALLOWED_BUILDS.map((dep) => ` '${dep}': true`).join("\n");
|
|
104
|
+
return [
|
|
105
|
+
"# Redirect the cross-SDK 0.0.0 specs packed into the tarballs to the",
|
|
106
|
+
"# sibling tarballs in this workspace; approve the install scripts of",
|
|
107
|
+
"# host's optional binaries (ffmpeg/ffprobe/7-Zip) so pnpm runs them.",
|
|
108
|
+
"overrides:",
|
|
109
|
+
lines,
|
|
110
|
+
"allowBuilds:",
|
|
111
|
+
builds,
|
|
112
|
+
""
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/index.ts
|
|
117
|
+
var ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
118
|
+
var TEMPLATE_DIR = resolve(
|
|
119
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
120
|
+
"template"
|
|
121
|
+
);
|
|
122
|
+
var SELF_VERSION = JSON.parse(
|
|
123
|
+
readFileSync(join(ROOT, "package.json"), "utf8")
|
|
124
|
+
).version;
|
|
125
|
+
function writeJson(path, value) {
|
|
126
|
+
writeFileSync(path, `${JSON.stringify(value, null, " ")}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
function writeTarballOverrides(targetDir, tarballsDir) {
|
|
130
|
+
writeFileSync(
|
|
131
|
+
join(targetDir, "pnpm-workspace.yaml"),
|
|
132
|
+
tarballOverridesYaml(tarballsDir, SELF_VERSION)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
async function runInstall(targetDir) {
|
|
136
|
+
log.step("Installing dependencies (pnpm install)");
|
|
137
|
+
return new Promise((resolveDone, reject) => {
|
|
138
|
+
const child = spawn("pnpm", ["install"], {
|
|
139
|
+
cwd: targetDir,
|
|
140
|
+
stdio: "inherit",
|
|
141
|
+
shell: process.platform === "win32"
|
|
142
|
+
});
|
|
143
|
+
child.on("error", reject);
|
|
144
|
+
child.on(
|
|
145
|
+
"exit",
|
|
146
|
+
(code) => code === 0 ? resolveDone() : reject(new Error(`pnpm install exited ${code}`))
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function main() {
|
|
151
|
+
const positional = process.argv.slice(2).filter((a) => !a.startsWith("--"));
|
|
152
|
+
const tarballsFlag = process.argv.indexOf("--tarballs");
|
|
153
|
+
const tarballsDir = tarballsFlag !== -1 ? resolve(process.argv[tarballsFlag + 1] ?? "") : void 0;
|
|
154
|
+
intro("hoardodile plugin scaffold");
|
|
155
|
+
const providedName = positional[0];
|
|
156
|
+
const pluginName = providedName ?? await text({
|
|
157
|
+
message: "Plugin name (also the directory name, npm-style):",
|
|
158
|
+
validate: (v) => /^[a-z0-9][a-z0-9-]*$/.test(v ?? "") ? void 0 : "lowercase letters, digits and dashes only"
|
|
159
|
+
});
|
|
160
|
+
if (isCancel(pluginName)) {
|
|
161
|
+
cancel("scaffold cancelled");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
const targetDir = resolve(pluginName);
|
|
165
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
|
|
166
|
+
const proceed = await confirm({
|
|
167
|
+
message: `${targetDir} is not empty \u2014 scaffold into it anyway?`,
|
|
168
|
+
initialValue: false
|
|
169
|
+
});
|
|
170
|
+
if (isCancel(proceed) || !proceed) {
|
|
171
|
+
cancel("scaffold cancelled");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
log.step(`Copying template into ${targetDir}`);
|
|
176
|
+
mkdirSync(targetDir, { recursive: true });
|
|
177
|
+
cpSync(TEMPLATE_DIR, targetDir, { recursive: true });
|
|
178
|
+
log.step("Rewriting manifest and package.json");
|
|
179
|
+
const manifestPath = join(targetDir, "manifest.json");
|
|
180
|
+
const pkgPath = join(targetDir, "package.json");
|
|
181
|
+
writeJson(
|
|
182
|
+
manifestPath,
|
|
183
|
+
rewriteManifest(JSON.parse(readFileSync(manifestPath, "utf8")), pluginName)
|
|
184
|
+
);
|
|
185
|
+
writeJson(
|
|
186
|
+
pkgPath,
|
|
187
|
+
rewritePackageJson(JSON.parse(readFileSync(pkgPath, "utf8")), pluginName, {
|
|
188
|
+
tarballsDir,
|
|
189
|
+
sdkVersion: SELF_VERSION
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
const parsed = pluginManifest.safeParse(
|
|
193
|
+
JSON.parse(readFileSync(manifestPath, "utf8"))
|
|
194
|
+
);
|
|
195
|
+
if (!parsed.success) {
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
log.error(`generated manifest is invalid: ${parsed.error.message}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (tarballsDir !== void 0) {
|
|
201
|
+
log.step(`Rewiring SDK deps to tarballs in ${tarballsDir}`);
|
|
202
|
+
writeTarballOverrides(targetDir, tarballsDir);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await runInstall(targetDir);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
log.error(
|
|
209
|
+
`install failed: ${err instanceof Error ? err.message : String(err)}`
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
outro("Plugin scaffolded. Next steps:");
|
|
214
|
+
console.log(` cd ${targetDir}`);
|
|
215
|
+
console.log(
|
|
216
|
+
" pnpm build # build dist/ (client + server bundle + manifest)"
|
|
217
|
+
);
|
|
218
|
+
console.log(
|
|
219
|
+
" pnpm dev # watch-build + serve the workbench (http://127.0.0.1:5199)"
|
|
220
|
+
);
|
|
221
|
+
console.log(" pnpm test # unit tests against the fixture API");
|
|
222
|
+
console.log(" hoardodile plugin run detect testdata --plugin-dir dist");
|
|
223
|
+
console.log(
|
|
224
|
+
" # Replace the manifest id? No \u2014 it was generated fresh. Zip dist/ contents and upload in Settings \u2192 Plugins."
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (process.argv[1] !== void 0 && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
228
|
+
main().catch((err) => {
|
|
229
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { main };
|
|
235
|
+
//# sourceMappingURL=index.js.map
|
|
236
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sdk-deps.gen.ts","../src/rewrite.ts","../src/index.ts"],"names":["join"],"mappings":"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","file":"index.js","sourcesContent":["// GENERATED by scripts/gen-sdk-deps.mjs — never hand-edit.\n// The published SDK closure a scaffolded plugin can depend on; kept in\n// lockstep with scripts/lib/sdk-closure.mjs (see sdks:pack).\nexport const SDK_DEP_NAMES: readonly string[] = [\n\t\"@hoardodile/cli\",\n\t\"@hoardodile/host\",\n\t\"@hoardodile/host-web\",\n\t\"@hoardodile/i18n\",\n\t\"@hoardodile/sdk-react\",\n\t\"@hoardodile/sdk-server\",\n\t\"@hoardodile/sdk-types\",\n\t\"@hoardodile/sdk-web\",\n\t\"@hoardodile/ui\",\n\t\"@hoardodile/workbench\",\n]\n","/**\n * Scaffold-time rewrites applied to the embedded template copy: fresh\n * manifest identity and concrete dependency specs (the template uses\n * `workspace:*` / `catalog:` which only resolve inside the monorepo).\n */\nimport { join } from \"node:path\"\nimport { SDK_DEP_NAMES } from \"./sdk-deps.gen.ts\"\n\n/** Concrete versions for the catalog: specs the template uses. */\nexport const THIRD_PARTY_VERSIONS: Readonly<Record<string, string>> = {\n\t\"@types/node\": \"^26.2.0\",\n\t\"@types/react\": \"^19.2.18\",\n\t\"@types/react-dom\": \"^19.2.4\",\n\tjsdom: \"^30.0.1\",\n\treact: \"^19.2.8\",\n\t\"react-dom\": \"^19.2.8\",\n\ttypescript: \"^7.0.2\",\n\tvite: \"^8.2.2\",\n\tvitest: \"^4.1.11\",\n}\n\nexport type ManifestShape = {\n\tid: string\n\tname: string\n\tdescription: string\n\tversion: string\n\ti18n?: {\n\t\tname?: Record<string, string>\n\t\tdescription?: Record<string, string>\n\t}\n}\n\nexport function rewriteManifest(\n\tmanifest: ManifestShape,\n\tpluginName: string,\n): ManifestShape {\n\tmanifest.id = crypto.randomUUID()\n\tmanifest.name = pluginName\n\tmanifest.description = `Content plugin: ${pluginName}`\n\tif (manifest.i18n?.name !== undefined) {\n\t\tmanifest.i18n.name.en = pluginName\n\t}\n\tif (manifest.i18n?.description !== undefined) {\n\t\tmanifest.i18n.description.en = `Content plugin: ${pluginName}`\n\t}\n\tmanifest.version = \"0.0.1\"\n\treturn manifest\n}\n\nexport type PackageJson = {\n\tname: string\n\tversion?: string\n\tdependencies?: Record<string, string>\n\tdevDependencies?: Record<string, string>\n}\n\nexport function tarballSpec(\n\ttarballsDir: string,\n\tdep: string,\n\tversion: string,\n): string {\n\tconst fileName = dep.replace(\"@\", \"\").replace(\"/\", \"-\")\n\treturn `file:${join(tarballsDir, `${fileName}-${version}.tgz`).replaceAll(\"\\\\\", \"/\")}`\n}\n\nfunction rewriteDepBlock(\n\tdeps: Record<string, string> | undefined,\n\ttarballsDir: string | undefined,\n\tsdkVersion: string,\n): Record<string, string> {\n\tconst out = { ...deps }\n\tfor (const [dep, spec] of Object.entries(out)) {\n\t\tif (SDK_DEP_NAMES.includes(dep)) {\n\t\t\tout[dep] =\n\t\t\t\ttarballsDir !== undefined\n\t\t\t\t\t? tarballSpec(tarballsDir, dep, sdkVersion)\n\t\t\t\t\t: // `^0.2.0-alpha.3` would never match the 0.2.0 stable\n\t\t\t\t\t\t// release (semver keeps prereleases out of plain ranges),\n\t\t\t\t\t\t// so npm specs drop the prerelease suffix.\n\t\t\t\t\t\t`^${sdkVersion.split(\"-\")[0]}`\n\t\t} else if (spec === \"catalog:\") {\n\t\t\tconst version = THIRD_PARTY_VERSIONS[dep]\n\t\t\tif (version === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`template dependency \"${dep}\" has no concrete version mapping`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tout[dep] = version\n\t\t}\n\t}\n\treturn out\n}\n\nexport function rewritePackageJson(\n\tpkg: PackageJson,\n\tpluginName: string,\n\topts: { readonly tarballsDir?: string; readonly sdkVersion: string },\n): PackageJson {\n\tpkg.name = pluginName\n\tdelete pkg.version\n\tpkg.version = \"0.0.1\"\n\tif (pkg.dependencies !== undefined) {\n\t\tpkg.dependencies = rewriteDepBlock(\n\t\t\tpkg.dependencies,\n\t\t\topts.tarballsDir,\n\t\t\topts.sdkVersion,\n\t\t)\n\t}\n\tif (pkg.devDependencies !== undefined) {\n\t\tpkg.devDependencies = rewriteDepBlock(\n\t\t\tpkg.devDependencies,\n\t\t\topts.tarballsDir,\n\t\t\topts.sdkVersion,\n\t\t)\n\t}\n\treturn pkg\n}\n\n/**\n * Install-script dependencies that land in a scaffolded plugin through\n * host's optionalDependencies (the same names as the root workspace's\n * allowBuilds). pnpm refuses to run their build scripts without an\n * explicit approval, so the generated workspace approves them up front.\n */\nconst ALLOWED_BUILDS = [\n\t\"@derhuerst/ffprobe-static\",\n\t\"@hoardodile/7z-bin\",\n\t\"ffmpeg-static\",\n]\n\nexport function tarballOverridesYaml(\n\ttarballsDir: string,\n\tsdkVersion: string,\n): string {\n\tconst lines = SDK_DEP_NAMES.map(\n\t\t(dep) => ` '${dep}': '${tarballSpec(tarballsDir, dep, sdkVersion)}'`,\n\t).join(\"\\n\")\n\tconst builds = ALLOWED_BUILDS.map((dep) => ` '${dep}': true`).join(\"\\n\")\n\treturn [\n\t\t\"# Redirect the cross-SDK 0.0.0 specs packed into the tarballs to the\",\n\t\t\"# sibling tarballs in this workspace; approve the install scripts of\",\n\t\t\"# host's optional binaries (ffmpeg/ffprobe/7-Zip) so pnpm runs them.\",\n\t\t\"overrides:\",\n\t\tlines,\n\t\t\"allowBuilds:\",\n\t\tbuilds,\n\t\t\"\",\n\t].join(\"\\n\")\n}\n","#!/usr/bin/env node\n/**\n * `create-hoardodile-plugin` — scaffold a new content plugin from the\n * embedded template copy (`src/template/`, kept in sync with\n * `plugins/template` by scripts/sync-template.mjs).\n *\n * Flow: interactive prompts (name, id auto-generated) → copy template →\n * rewrite manifest + package.json → install dependencies → validate the\n * manifest → print the dev loop entry points.\n *\n * create-hoardodile-plugin my-plugin\n * create-hoardodile-plugin my-plugin --tarballs ../hoardodile/tmp/sdks\n *\n * `--tarballs <dir>` rewires the SDK deps to packed tarballs plus a\n * pnpm-workspace.yaml with the cross-package overrides — the supported\n * way to consume the SDK before (and instead of) npm publication.\n */\nimport { spawn } from \"node:child_process\"\nimport {\n\tcpSync,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\twriteFileSync,\n} from \"node:fs\"\nimport { dirname, join, resolve } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport {\n\tcancel,\n\tconfirm,\n\tintro,\n\tisCancel,\n\tlog,\n\toutro,\n\ttext,\n} from \"@clack/prompts\"\nimport { pluginManifest } from \"@hoardodile/sdk-types/schema\"\nimport {\n\trewriteManifest,\n\trewritePackageJson,\n\ttarballOverridesYaml,\n} from \"./rewrite.ts\"\n\nconst ROOT = resolve(dirname(fileURLToPath(import.meta.url)), \"..\")\n// The template ships inside dist (dist/template) — the bin always runs\n// from the built artifact, so resolve relative to this module.\nconst TEMPLATE_DIR = resolve(\n\tdirname(fileURLToPath(import.meta.url)),\n\t\"template\",\n)\nconst SELF_VERSION = JSON.parse(\n\treadFileSync(join(ROOT, \"package.json\"), \"utf8\"),\n).version\n\nfunction writeJson(path: string, value: unknown): void {\n\twriteFileSync(path, `${JSON.stringify(value, null, \"\\t\")}\\n`)\n}\n\nfunction writeTarballOverrides(targetDir: string, tarballsDir: string): void {\n\twriteFileSync(\n\t\tjoin(targetDir, \"pnpm-workspace.yaml\"),\n\t\ttarballOverridesYaml(tarballsDir, SELF_VERSION),\n\t)\n}\n\nasync function runInstall(targetDir: string): Promise<void> {\n\tlog.step(\"Installing dependencies (pnpm install)\")\n\treturn new Promise<void>((resolveDone, reject) => {\n\t\tconst child = spawn(\"pnpm\", [\"install\"], {\n\t\t\tcwd: targetDir,\n\t\t\tstdio: \"inherit\",\n\t\t\tshell: process.platform === \"win32\",\n\t\t})\n\t\tchild.on(\"error\", reject)\n\t\tchild.on(\"exit\", (code) =>\n\t\t\tcode === 0\n\t\t\t\t? resolveDone()\n\t\t\t\t: reject(new Error(`pnpm install exited ${code}`)),\n\t\t)\n\t})\n}\n\nexport async function main() {\n\tconst positional = process.argv.slice(2).filter((a) => !a.startsWith(\"--\"))\n\tconst tarballsFlag = process.argv.indexOf(\"--tarballs\")\n\tconst tarballsDir =\n\t\ttarballsFlag !== -1\n\t\t\t? resolve(process.argv[tarballsFlag + 1] ?? \"\")\n\t\t\t: undefined\n\n\tintro(\"hoardodile plugin scaffold\")\n\n\tconst providedName = positional[0]\n\tconst pluginName =\n\t\tprovidedName ??\n\t\t(await text({\n\t\t\tmessage: \"Plugin name (also the directory name, npm-style):\",\n\t\t\tvalidate: (v) =>\n\t\t\t\t/^[a-z0-9][a-z0-9-]*$/.test(v ?? \"\")\n\t\t\t\t\t? undefined\n\t\t\t\t\t: \"lowercase letters, digits and dashes only\",\n\t\t}))\n\tif (isCancel(pluginName)) {\n\t\tcancel(\"scaffold cancelled\")\n\t\tprocess.exit(1)\n\t}\n\n\tconst targetDir = resolve(pluginName)\n\tif (existsSync(targetDir) && readdirSync(targetDir).length > 0) {\n\t\tconst proceed = await confirm({\n\t\t\tmessage: `${targetDir} is not empty — scaffold into it anyway?`,\n\t\t\tinitialValue: false,\n\t\t})\n\t\tif (isCancel(proceed) || !proceed) {\n\t\t\tcancel(\"scaffold cancelled\")\n\t\t\tprocess.exit(1)\n\t\t}\n\t}\n\n\tlog.step(`Copying template into ${targetDir}`)\n\tmkdirSync(targetDir, { recursive: true })\n\t// dist/template is a build artifact (already filtered when copied in);\n\t// copy it wholesale so template source dirs inside it survive.\n\tcpSync(TEMPLATE_DIR, targetDir, { recursive: true })\n\n\tlog.step(\"Rewriting manifest and package.json\")\n\tconst manifestPath = join(targetDir, \"manifest.json\")\n\tconst pkgPath = join(targetDir, \"package.json\")\n\twriteJson(\n\t\tmanifestPath,\n\t\trewriteManifest(JSON.parse(readFileSync(manifestPath, \"utf8\")), pluginName),\n\t)\n\twriteJson(\n\t\tpkgPath,\n\t\trewritePackageJson(JSON.parse(readFileSync(pkgPath, \"utf8\")), pluginName, {\n\t\t\ttarballsDir,\n\t\t\tsdkVersion: SELF_VERSION,\n\t\t}),\n\t)\n\n\tconst parsed = pluginManifest.safeParse(\n\t\tJSON.parse(readFileSync(manifestPath, \"utf8\")),\n\t)\n\tif (!parsed.success) {\n\t\tprocess.exitCode = 1\n\t\tlog.error(`generated manifest is invalid: ${parsed.error.message}`)\n\t\treturn\n\t}\n\n\tif (tarballsDir !== undefined) {\n\t\tlog.step(`Rewiring SDK deps to tarballs in ${tarballsDir}`)\n\t\twriteTarballOverrides(targetDir, tarballsDir)\n\t}\n\n\ttry {\n\t\tawait runInstall(targetDir)\n\t} catch (err) {\n\t\tprocess.exitCode = 1\n\t\tlog.error(\n\t\t\t`install failed: ${err instanceof Error ? err.message : String(err)}`,\n\t\t)\n\t\treturn\n\t}\n\n\toutro(\"Plugin scaffolded. Next steps:\")\n\tconsole.log(` cd ${targetDir}`)\n\tconsole.log(\n\t\t\" pnpm build # build dist/ (client + server bundle + manifest)\",\n\t)\n\tconsole.log(\n\t\t\" pnpm dev # watch-build + serve the workbench (http://127.0.0.1:5199)\",\n\t)\n\tconsole.log(\" pnpm test # unit tests against the fixture API\")\n\tconsole.log(\" hoardodile plugin run detect testdata --plugin-dir dist\")\n\tconsole.log(\n\t\t\" # Replace the manifest id? No — it was generated fresh. Zip dist/ contents and upload in Settings → Plugins.\",\n\t)\n}\n\nif (\n\tprocess.argv[1] !== undefined &&\n\tresolve(process.argv[1]) === fileURLToPath(import.meta.url)\n) {\n\tmain().catch((err) => {\n\t\tlog.error(err instanceof Error ? err.message : String(err))\n\t\tprocess.exit(1)\n\t})\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wooloo <ayan0312000@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# hoardodile plugin template
|
|
2
|
+
|
|
3
|
+
Minimal end-to-end content plugin: `detect` → `sourceMeta` → iframe render.
|
|
4
|
+
Copy this directory to start your own plugin.
|
|
5
|
+
|
|
6
|
+
## Quick start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# 1. Copy the template (never edit it in place — CI compiles this copy)
|
|
10
|
+
cp -r plugins/template my-plugin
|
|
11
|
+
|
|
12
|
+
# 2. Generate a fresh manifest UUID — never ship the template's id
|
|
13
|
+
node -e "console.log(crypto.randomUUID())"
|
|
14
|
+
|
|
15
|
+
# 3. Build and validate
|
|
16
|
+
pnpm install
|
|
17
|
+
pnpm build
|
|
18
|
+
|
|
19
|
+
# 4. Open the workbench (watch-build + serve at http://127.0.0.1:5199,
|
|
20
|
+
# data from testdata/, plus one sandboxed detect on startup)
|
|
21
|
+
pnpm dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What's inside
|
|
25
|
+
|
|
26
|
+
- `src/main.ts` — the server-side plugin definition (`definePlugin`):
|
|
27
|
+
`detect` claims resources containing `.hdtpl` files, `sourceMeta`
|
|
28
|
+
lists them. Composable detectors and probe helpers live in
|
|
29
|
+
`@hoardodile/sdk-server`.
|
|
30
|
+
- `src/render.tsx` — the iframe client via `@hoardodile/sdk-react`
|
|
31
|
+
(`createPluginRoot`), reading files through the typed API pair in
|
|
32
|
+
`src/hooks.ts`.
|
|
33
|
+
- `src/shared.ts` — the plugin schema (`PluginSchema`) typed once and
|
|
34
|
+
shared between the server and client sides.
|
|
35
|
+
- `testdata/` — sample data for `pnpm dev`.
|
|
36
|
+
- `__tests__/` — Vitest unit tests against `createResourceAPIFixture`.
|
|
37
|
+
|
|
38
|
+
## Testing
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm test # unit tests (in-memory fixture API)
|
|
42
|
+
pnpm run detect:smoke # sandboxed detect against testdata (needs a build first)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`hoardodile plugin run` runs hooks through the same worker sandbox the
|
|
46
|
+
server uses — the exact production execution path.
|
|
47
|
+
|
|
48
|
+
## Deploying
|
|
49
|
+
|
|
50
|
+
Zip the contents of `dist/` (with `manifest.json` at the zip root) and
|
|
51
|
+
upload in **Settings → Plugins**. The app validates the manifest,
|
|
52
|
+
installs it, and rescans.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "6bc0c75e-5fca-4525-84f1-cfdd98661630",
|
|
3
|
+
"name": "Template",
|
|
4
|
+
"description": "Template plugin — copy this directory to start your own plugin",
|
|
5
|
+
"version": "0.0.0",
|
|
6
|
+
"permissions": {
|
|
7
|
+
"sourceMeta": true,
|
|
8
|
+
"searchMeta": false,
|
|
9
|
+
"danmaku": false,
|
|
10
|
+
"message": false
|
|
11
|
+
},
|
|
12
|
+
"i18n": {
|
|
13
|
+
"name": {
|
|
14
|
+
"en": "Template",
|
|
15
|
+
"zh": "模板"
|
|
16
|
+
},
|
|
17
|
+
"description": {
|
|
18
|
+
"en": "Template plugin — copy this directory to start your own plugin",
|
|
19
|
+
"zh": "模板插件——复制此目录作为你自己插件的起点"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hoardodile/plugin-template",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "hoardodile plugin dev",
|
|
9
|
+
"build": "hoardodile plugin build",
|
|
10
|
+
"watch": "hoardodile plugin build --watch",
|
|
11
|
+
"detect:smoke": "hoardodile plugin run detect testdata --plugin-dir dist",
|
|
12
|
+
"lint": "tsc --noEmit",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@hoardodile/sdk-react": "workspace:*",
|
|
17
|
+
"@hoardodile/sdk-server": "workspace:*",
|
|
18
|
+
"@hoardodile/sdk-types": "workspace:*",
|
|
19
|
+
"react": "catalog:",
|
|
20
|
+
"react-dom": "catalog:"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@hoardodile/cli": "workspace:*",
|
|
24
|
+
"@hoardodile/host": "workspace:*",
|
|
25
|
+
"@hoardodile/workbench": "workspace:*",
|
|
26
|
+
"@types/node": "catalog:",
|
|
27
|
+
"@types/react": "catalog:",
|
|
28
|
+
"@types/react-dom": "catalog:",
|
|
29
|
+
"jsdom": "catalog:",
|
|
30
|
+
"typescript": "catalog:",
|
|
31
|
+
"vite": "catalog:",
|
|
32
|
+
"vitest": "catalog:"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { usePluginAPI } from "./hooks"
|
|
2
|
+
|
|
3
|
+
export function TemplateView() {
|
|
4
|
+
const api = usePluginAPI()
|
|
5
|
+
const files = api.resource.sourceMeta?.files ?? []
|
|
6
|
+
const hdtplCount = api.resource.sourceMeta?.hdtplCount
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex h-full flex-col gap-3 overflow-auto p-4 font-sans text-sm text-gray-800 dark:text-gray-100">
|
|
10
|
+
<header>
|
|
11
|
+
<h1 className="text-lg font-semibold">{api.resource.name}</h1>
|
|
12
|
+
<p className="text-gray-500 dark:text-gray-400">
|
|
13
|
+
Rendered by the template plugin. Edit src/TemplateView.tsx to make it
|
|
14
|
+
your own.
|
|
15
|
+
</p>
|
|
16
|
+
</header>
|
|
17
|
+
<p>
|
|
18
|
+
{hdtplCount !== undefined
|
|
19
|
+
? `${hdtplCount} .hdtpl file(s) — classified once by detect.`
|
|
20
|
+
: "No sourceMeta yet."}
|
|
21
|
+
</p>
|
|
22
|
+
<ul className="list-inside list-disc">
|
|
23
|
+
{files.map((file) => (
|
|
24
|
+
<li key={file}>{file}</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { createDirectoryResourceAPI } from "@hoardodile/host"
|
|
5
|
+
import { createResourceAPIFixture } from "@hoardodile/sdk-server"
|
|
6
|
+
import plugin from "../main"
|
|
7
|
+
import type { TemplateSchema } from "../shared"
|
|
8
|
+
|
|
9
|
+
describe("template plugin", () => {
|
|
10
|
+
it("detects resources containing a .hdtpl file, carrying the classification", async () => {
|
|
11
|
+
const { api } = createResourceAPIFixture<TemplateSchema>({
|
|
12
|
+
files: ["notes.hdtpl"],
|
|
13
|
+
})
|
|
14
|
+
const result = await plugin.detect(api)
|
|
15
|
+
expect(result).toEqual({ ok: true, hdtplCount: 1 })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("misses resources without .hdtpl files", async () => {
|
|
19
|
+
const { api } = createResourceAPIFixture<TemplateSchema>({
|
|
20
|
+
files: ["photo.jpg"],
|
|
21
|
+
})
|
|
22
|
+
const result = await plugin.detect(api)
|
|
23
|
+
expect(result.ok).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("lists only .hdtpl files in sourceMeta", async () => {
|
|
27
|
+
const { api } = createResourceAPIFixture<TemplateSchema>({
|
|
28
|
+
files: ["a.hdtpl", "photo.jpg", "b.hdtpl"],
|
|
29
|
+
})
|
|
30
|
+
const meta = await plugin.sourceMeta?.(api)
|
|
31
|
+
expect(meta).toEqual({ files: ["a.hdtpl", "b.hdtpl"], hdtplCount: 2 })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("sourceMeta reuses the detect payload via api.context", async () => {
|
|
35
|
+
const { api } = createResourceAPIFixture<TemplateSchema>({
|
|
36
|
+
files: ["a.hdtpl", "b.hdtpl"],
|
|
37
|
+
context: { detect: { hdtplCount: 2 } },
|
|
38
|
+
})
|
|
39
|
+
const meta = await plugin.sourceMeta?.(api)
|
|
40
|
+
expect(meta).toEqual({ files: ["a.hdtpl", "b.hdtpl"], hdtplCount: 2 })
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Layer 2 (see "Testing" in the plugin development docs): run the hooks
|
|
45
|
+
// against real files on disk. `createDirectoryResourceAPI` has import-time
|
|
46
|
+
// semantics — probes return undefined, so probe-dependent hooks need layer 3.
|
|
47
|
+
describe("against real files", () => {
|
|
48
|
+
let dir: string
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
rmSync(dir, { recursive: true, force: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("detects a directory containing a .hdtpl file", async () => {
|
|
55
|
+
dir = mkdtempSync(join(tmpdir(), "hdtpl-test-"))
|
|
56
|
+
writeFileSync(join(dir, "notes.hdtpl"), "template contents")
|
|
57
|
+
writeFileSync(join(dir, "photo.jpg"), "not a template")
|
|
58
|
+
const api = createDirectoryResourceAPI<TemplateSchema>(dir)
|
|
59
|
+
const result = await plugin.detect(api)
|
|
60
|
+
expect(result).toEqual({ ok: true, hdtplCount: 1 })
|
|
61
|
+
const meta = await plugin.sourceMeta?.(api)
|
|
62
|
+
expect(meta).toEqual({ files: ["notes.hdtpl"], hdtplCount: 1 })
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ResourceAPI } from "@hoardodile/sdk-server"
|
|
2
|
+
import { definePlugin } from "@hoardodile/sdk-server"
|
|
3
|
+
import { extname } from "@hoardodile/sdk-server/helpers"
|
|
4
|
+
import type { TemplateSchema, TemplateSourceMeta } from "./shared"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The template claims every resource that contains at least one `.hdtpl`
|
|
8
|
+
* file. Replace this with your own detection logic — see the composable
|
|
9
|
+
* detectors (`hasExt`, `hasName`, `minFiles`, `all`, `any`, `not`).
|
|
10
|
+
*/
|
|
11
|
+
const TEMPLATE_EXTS = new Set([".hdtpl"])
|
|
12
|
+
|
|
13
|
+
export default definePlugin<TemplateSchema>({
|
|
14
|
+
detect: async (api) => {
|
|
15
|
+
const files = await api.listFileNames()
|
|
16
|
+
const hdtpl = files.filter((name) => TEMPLATE_EXTS.has(extname(name)))
|
|
17
|
+
return hdtpl.length === 0
|
|
18
|
+
? { ok: false, reasons: ["no .hdtpl file"] }
|
|
19
|
+
: // The classification rides on the match: the host keeps it
|
|
20
|
+
// and exposes it to the other hooks as `api.context.detect`,
|
|
21
|
+
// so sourceMeta never rescans.
|
|
22
|
+
{ ok: true, hdtplCount: hdtpl.length }
|
|
23
|
+
},
|
|
24
|
+
sourceMeta: buildSourceMeta,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
async function buildSourceMeta(
|
|
28
|
+
api: ResourceAPI<TemplateSchema>,
|
|
29
|
+
): Promise<TemplateSourceMeta> {
|
|
30
|
+
const files = (await api.listFileNames()).filter((name) =>
|
|
31
|
+
TEMPLATE_EXTS.has(extname(name)),
|
|
32
|
+
)
|
|
33
|
+
// `api.context.detect` is typed via the schema's `detect` slot, but
|
|
34
|
+
// may be undefined on a fresh worker — fall back to re-deriving.
|
|
35
|
+
const hdtplCount = api.context.detect?.hdtplCount ?? files.length
|
|
36
|
+
return { files, hdtplCount }
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PluginSchema } from "@hoardodile/sdk-types"
|
|
2
|
+
|
|
3
|
+
export type TemplateFile = {
|
|
4
|
+
readonly filename: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type TemplateSourceMeta = {
|
|
8
|
+
readonly files: readonly string[]
|
|
9
|
+
/** Number of `.hdtpl` files — classified once by `detect`, reused by
|
|
10
|
+
* `sourceMeta` via `api.context.detect`. */
|
|
11
|
+
readonly hdtplCount: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Declared once and shared between the server definition (`definePlugin`)
|
|
16
|
+
* and the web API (`definePluginAPI`) so both sides stay in sync.
|
|
17
|
+
*/
|
|
18
|
+
export interface TemplateSchema extends PluginSchema {
|
|
19
|
+
readonly file: TemplateFile
|
|
20
|
+
readonly sourceMeta: TemplateSourceMeta
|
|
21
|
+
/**
|
|
22
|
+
* The classification `detect` spreads onto its match. Declaring the
|
|
23
|
+
* slot types `api.context.detect` for the other hooks.
|
|
24
|
+
*/
|
|
25
|
+
readonly detect: { readonly hdtplCount: number }
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Test data
|
|
2
|
+
|
|
3
|
+
This directory is the default data root for `hoardodile plugin dev` (the
|
|
4
|
+
dev CLI mounts it at `/data` and runs a sandboxed `detect` against it on
|
|
5
|
+
startup).
|
|
6
|
+
|
|
7
|
+
`sample.hdtpl` matches the template's detector (`.hdtpl` files) — drop
|
|
8
|
+
your own fixture files here as your detection logic evolves.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Sample content for the template plugin's detection logic.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Standalone on purpose: this file works both inside the hoardodile
|
|
3
|
+
// workspace and copied into an external plugin repository.
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
6
|
+
"target": "ESNext",
|
|
7
|
+
"module": "Preserve",
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"esModuleInterop": true,
|
|
21
|
+
"resolveJsonModule": true,
|
|
22
|
+
"isolatedModules": true,
|
|
23
|
+
"jsx": "react-jsx",
|
|
24
|
+
"types": ["vite/client", "vitest/globals", "node"]
|
|
25
|
+
},
|
|
26
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
27
|
+
"exclude": ["node_modules"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config"
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: "jsdom",
|
|
6
|
+
globals: true,
|
|
7
|
+
css: false,
|
|
8
|
+
include: ["src/**/*.test.{ts,tsx}"],
|
|
9
|
+
// Default is cores-1 workers; turbo runs several packages' tests in
|
|
10
|
+
// parallel, so cap each vitest run to keep the machine responsive.
|
|
11
|
+
maxWorkers: 2,
|
|
12
|
+
},
|
|
13
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hoardodile/create-plugin",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Scaffold a hoardodile content plugin.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"hoardodile",
|
|
8
|
+
"plugin",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/hoardodile/hoardodile.git",
|
|
15
|
+
"directory": "plugins/create-plugin"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"bin": {
|
|
19
|
+
"create-hoardodile-plugin": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=24"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@clack/prompts": "^1.7.0",
|
|
39
|
+
"zod": "^4.4.3",
|
|
40
|
+
"@hoardodile/sdk-types": "0.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^26.2.0",
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"vitest": "^4.1.11"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "node scripts/sync-template.mjs && tsup && node scripts/copy-template.mjs",
|
|
50
|
+
"lint": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"template:check": "node scripts/sync-template.mjs --check"
|
|
53
|
+
}
|
|
54
|
+
}
|