@hoardodile/create-plugin 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/template/.github/workflows/release.yml +5 -2
- package/dist/template/README.md +28 -7
- package/dist/template/package.json +1 -0
- package/dist/template/scripts/check-intro.mjs +135 -0
- package/package.json +2 -2
- /package/dist/template/{intro.en.md → intro/intro.en.md} +0 -0
- /package/dist/template/{intro.zh.md → intro/intro.zh.md} +0 -0
|
@@ -43,12 +43,15 @@ jobs:
|
|
|
43
43
|
- name: Package the release artifacts
|
|
44
44
|
run: pnpm exec hoardodile plugin package --skip-build
|
|
45
45
|
|
|
46
|
+
- name: Gate the introduction (intro/ must be flat, images by bare filename)
|
|
47
|
+
run: pnpm intro:check
|
|
48
|
+
|
|
46
49
|
- name: Publish the GitHub release
|
|
47
50
|
env:
|
|
48
51
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
49
52
|
run: |
|
|
50
53
|
files=$(ls release/*)
|
|
51
|
-
if
|
|
52
|
-
files="$files intro
|
|
54
|
+
if [ -d intro ]; then
|
|
55
|
+
files="$files intro/*"
|
|
53
56
|
fi
|
|
54
57
|
gh release create "$GITHUB_REF_NAME" $files --generate-notes
|
package/dist/template/README.md
CHANGED
|
@@ -100,13 +100,34 @@ private packages.
|
|
|
100
100
|
## Publishing an introduction
|
|
101
101
|
|
|
102
102
|
The marketplace detail view shows a per-release **Intro** tab. Ship one
|
|
103
|
-
markdown file per supported language
|
|
104
|
-
`intro.<locale>.md` (e.g. `intro.en.md`, `intro.zh.md`) —
|
|
105
|
-
uploads
|
|
106
|
-
introduction** and every version shows independent notes.
|
|
107
|
-
supported language codes as file names (`en`, `zh`, `ja`,
|
|
108
|
-
a region-coded name like `intro.zh-CN.md` only matches a UI
|
|
109
|
-
resolved to that exact code, so `intro.zh.md` is what Chinese
|
|
103
|
+
markdown file per supported language inside the **`intro/` folder**, named
|
|
104
|
+
`intro.<locale>.md` (e.g. `intro/intro.en.md`, `intro/intro.zh.md`) —
|
|
105
|
+
`release.yml` uploads the whole folder alongside the zip, so **each release
|
|
106
|
+
carries its own introduction** and every version shows independent notes.
|
|
107
|
+
Use the app's supported language codes as file names (`en`, `zh`, `ja`,
|
|
108
|
+
`de`, `es`) — a region-coded name like `intro.zh-CN.md` only matches a UI
|
|
109
|
+
language resolved to that exact code, so `intro.zh.md` is what Chinese
|
|
110
|
+
users see.
|
|
111
|
+
|
|
112
|
+
### Adding images
|
|
113
|
+
|
|
114
|
+
An introduction may reference images. Place the image in `intro/` and
|
|
115
|
+
reference it by its **bare filename**:
|
|
116
|
+
|
|
117
|
+
```md
|
|
118
|
+

|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Every file in `intro/` is published as a release asset on each release, and
|
|
122
|
+
the app resolves a relative image reference against that release's download
|
|
123
|
+
URL. Because a GitHub release is a flat list of assets, the `intro/` folder
|
|
124
|
+
must stay **flat** and references must be bare filenames — a nested path
|
|
125
|
+
like `` resolves to a URL the release does not serve and
|
|
126
|
+
the image breaks. Absolute `http(s)://` and `data:` image URIs are allowed.
|
|
127
|
+
|
|
128
|
+
`pnpm intro:check` (run by `release.yml` before publishing) gates this: it
|
|
129
|
+
fails the release if `intro/` is absent-and-required, is not flat, ships no
|
|
130
|
+
`intro.<locale>.md`, or references an image by a nested/missing path.
|
|
110
131
|
|
|
111
132
|
The app resolves the intro for the user's UI language (exact locale → base
|
|
112
133
|
language → `en` → the only shipped language); the release body always shows
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Release gate for the plugin marketplace introduction images.
|
|
4
|
+
*
|
|
5
|
+
* The marketplace reads each release's `intro.<locale>.md` asset and the
|
|
6
|
+
* app resolves any image referenced inside it against the release's
|
|
7
|
+
* download URL. Because a GitHub release is a flat list of assets, every
|
|
8
|
+
* referenced image must be:
|
|
9
|
+
*
|
|
10
|
+
* 1. Shipped inside the `intro/` folder (the only folder the release
|
|
11
|
+
* workflow uploads), and
|
|
12
|
+
* 2. Referenced by its bare filename (``), never a
|
|
13
|
+
* nested path (`img/shot.png`) — a nested path resolves to a URL the
|
|
14
|
+
* release does not actually serve, so the image is silently broken.
|
|
15
|
+
*
|
|
16
|
+
* This gate fails a build/release when the `intro/` folder is absent, is
|
|
17
|
+
* not flat, ships no `intro.<locale>.md`, or references an image by a
|
|
18
|
+
* nested/missing path. External `http(s)://` and `data:` image URIs are
|
|
19
|
+
* allowed (they are not release assets).
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node scripts/check-intro.mjs # checks ./intro
|
|
23
|
+
* node scripts/check-intro.mjs <dir> # checks <dir>/intro
|
|
24
|
+
*
|
|
25
|
+
* Dependency-free on purpose — it ships inside every scaffolded plugin.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
|
|
29
|
+
import { join, resolve } from "node:path"
|
|
30
|
+
|
|
31
|
+
const ROOT = resolve(process.argv[2] ?? process.cwd())
|
|
32
|
+
const INTRO_DIR = join(ROOT, "intro")
|
|
33
|
+
|
|
34
|
+
const RULE_SUMMARY =
|
|
35
|
+
"intro/ must be flat; each intro.<locale>.md image is referenced by a " +
|
|
36
|
+
"bare filename that exists in intro/ (absolute http(s)/data URIs are ok)"
|
|
37
|
+
|
|
38
|
+
// ``, ``, `<img src="x">` (`src` images only).
|
|
39
|
+
const MARKDOWN_IMG_RE = /!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
|
|
40
|
+
const HTML_IMG_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])([^"']+)\1/gi
|
|
41
|
+
const EXT_URL_RE = /^(?:https?:\/\/|data:)/i
|
|
42
|
+
|
|
43
|
+
function isRelative(src) {
|
|
44
|
+
return !EXT_URL_RE.test(src)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A relative reference is valid only as a flat bare filename that exists
|
|
49
|
+
* inside `intro/` — not a nested path and not a missing/unshipsed file.
|
|
50
|
+
*/
|
|
51
|
+
function resolveRelativeRef(src, file, issues) {
|
|
52
|
+
const trimmed = src.trim().replace(/^\.\//, "")
|
|
53
|
+
if (trimmed.length === 0) {
|
|
54
|
+
issues.push(`${file}: empty image reference`)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
trimmed.includes("/") ||
|
|
59
|
+
trimmed.includes("\\") ||
|
|
60
|
+
trimmed.includes("..")
|
|
61
|
+
) {
|
|
62
|
+
issues.push(
|
|
63
|
+
`${file}: image "${src}" is not flat — use a bare filename like "shot.png" (release assets are flat)`,
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
const target = join(INTRO_DIR, trimmed)
|
|
68
|
+
if (!existsSync(target) || !statSync(target).isFile()) {
|
|
69
|
+
issues.push(
|
|
70
|
+
`${file}: image "${src}" is not in intro/ — every referenced image must be committed there (it is published with the release)`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectImageRefs(text) {
|
|
76
|
+
const refs = []
|
|
77
|
+
for (const match of text.matchAll(MARKDOWN_IMG_RE)) refs.push(match[1])
|
|
78
|
+
for (const match of text.matchAll(HTML_IMG_RE)) refs.push(match[2])
|
|
79
|
+
return refs
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function main() {
|
|
83
|
+
if (!existsSync(INTRO_DIR)) {
|
|
84
|
+
console.log(
|
|
85
|
+
"[check-intro] no intro/ folder — nothing to gate (a release without an introduction is valid).",
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const entries = readdirSync(INTRO_DIR, { withFileTypes: true })
|
|
91
|
+
const issues = []
|
|
92
|
+
|
|
93
|
+
// 1. Flat-only: any subdirectory makes the folder publish incorrectly.
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
issues.push(
|
|
97
|
+
`intro/${entry.name}/ is a subdirectory — the intro folder must be flat (release assets are a flat list)`,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const mdFiles = entries
|
|
103
|
+
.filter(
|
|
104
|
+
(entry) =>
|
|
105
|
+
entry.isFile() && /^intro\.[A-Za-z0-9-]+\.md$/.test(entry.name),
|
|
106
|
+
)
|
|
107
|
+
.map((entry) => entry.name)
|
|
108
|
+
|
|
109
|
+
if (mdFiles.length === 0) {
|
|
110
|
+
issues.push(
|
|
111
|
+
"intro/ has no intro.<locale>.md — the release would ship images but the marketplace could not display an introduction",
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 2. Flat, present image references inside each intro markdown.
|
|
116
|
+
for (const name of mdFiles) {
|
|
117
|
+
const text = readFileSync(join(INTRO_DIR, name), "utf-8")
|
|
118
|
+
for (const src of collectImageRefs(text)) {
|
|
119
|
+
if (isRelative(src)) resolveRelativeRef(src, `intro/${name}`, issues)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (issues.length > 0) {
|
|
124
|
+
console.error("[check-intro] gate failed:")
|
|
125
|
+
for (const issue of issues) console.error(` - ${issue}`)
|
|
126
|
+
console.error(`\n${RULE_SUMMARY}`)
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
`[check-intro] intro/ ok — ${mdFiles.length} introduction file(s), flat references only.`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoardodile/create-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffold a hoardodile content plugin.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "^1.7.0",
|
|
39
39
|
"zod": "^4.4.3",
|
|
40
|
-
"@hoardodile/sdk-types": "0.1.
|
|
40
|
+
"@hoardodile/sdk-types": "0.1.5"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
|
File without changes
|
|
File without changes
|