create-meith 0.21.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +532 -32
- package/package.json +2 -2
- package/src/bin.ts +1 -1
- package/src/cli.ts +46 -10
- package/src/extension-templates.ts +43 -0
- package/src/index.ts +12 -0
- package/src/scaffold-extension.ts +372 -0
- package/src/scaffold.ts +28 -21
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-meith",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a Meith board — npx create-meith <name> writes a workspace
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "Scaffold a Meith board, plugin or theme — npx create-meith <name> writes a deployable board workspace; --plugin and --theme write extension workspaces built on the published kits.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/bin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from './cli'
|
|
3
3
|
|
|
4
|
-
const result = await run(process.argv.slice(2), '0.
|
|
4
|
+
const result = await run(process.argv.slice(2), '0.23.0')
|
|
5
5
|
for (const line of result.lines) {
|
|
6
6
|
if (result.code === 0) console.log(line)
|
|
7
7
|
else console.error(line)
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
4
4
|
import { promisify } from 'node:util'
|
|
5
5
|
|
|
6
6
|
import { DEFAULT_REPOSITORY_URL, nextSteps, scaffold, validateName } from './scaffold'
|
|
7
|
+
import {
|
|
8
|
+
type ExtensionKind,
|
|
9
|
+
extensionNextSteps,
|
|
10
|
+
scaffoldExtension,
|
|
11
|
+
validateExtensionName,
|
|
12
|
+
} from './scaffold-extension'
|
|
7
13
|
|
|
8
14
|
const execFileAsync = promisify(execFile)
|
|
9
15
|
|
|
@@ -46,21 +52,37 @@ export async function run(argv: readonly string[], version: string): Promise<Cli
|
|
|
46
52
|
return {
|
|
47
53
|
code: 0,
|
|
48
54
|
lines: [
|
|
49
|
-
'create-meith — scaffold a forum project.',
|
|
55
|
+
'create-meith — scaffold a forum project, a plugin or a theme.',
|
|
50
56
|
'',
|
|
51
57
|
' npx create-meith <name> [--repo <url>] [--no-git]',
|
|
58
|
+
' npx create-meith --plugin <name> [--repo <url>] [--no-git]',
|
|
59
|
+
' npx create-meith --theme <name> [--repo <url>] [--no-git]',
|
|
52
60
|
'',
|
|
53
|
-
'
|
|
54
|
-
'
|
|
61
|
+
'The first form writes a deployable board into ./<name>, then tells you',
|
|
62
|
+
'what to run. --plugin and --theme write an extension workspace instead:',
|
|
63
|
+
'source and a passing test copied from the meith repository’s worked',
|
|
64
|
+
'examples, plus a README and a marketplace listing.json.',
|
|
55
65
|
'',
|
|
56
66
|
'--no-git skips initializing a git repository in the new directory.',
|
|
57
67
|
],
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
const
|
|
71
|
+
const wantsPlugin = argv.includes('--plugin')
|
|
72
|
+
const wantsTheme = argv.includes('--theme')
|
|
73
|
+
if (wantsPlugin && wantsTheme) {
|
|
74
|
+
return {
|
|
75
|
+
code: 1,
|
|
76
|
+
lines: ['create-meith: --plugin and --theme are two different scaffolds — pass one.'],
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const kind: ExtensionKind | null = wantsPlugin ? 'plugin' : wantsTheme ? 'theme' : null
|
|
80
|
+
const usage =
|
|
81
|
+
kind === null ? 'Usage: npx create-meith <name>' : `Usage: npx create-meith --${kind} <name>`
|
|
82
|
+
|
|
83
|
+
const invalid = kind === null ? validateName(name) : validateExtensionName(name)
|
|
62
84
|
if (invalid !== null) {
|
|
63
|
-
return { code: 1, lines: [`create-meith: ${invalid}`, '',
|
|
85
|
+
return { code: 1, lines: [`create-meith: ${invalid}`, '', usage] }
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
const repoIndex = argv.indexOf('--repo')
|
|
@@ -78,7 +100,14 @@ export async function run(argv: readonly string[], version: string): Promise<Cli
|
|
|
78
100
|
}
|
|
79
101
|
}
|
|
80
102
|
|
|
81
|
-
const files =
|
|
103
|
+
const files =
|
|
104
|
+
kind === null
|
|
105
|
+
? scaffold({ name, version, repositoryUrl })
|
|
106
|
+
: scaffoldExtension(kind, {
|
|
107
|
+
name,
|
|
108
|
+
version,
|
|
109
|
+
repositoryUrl: repoIndex === -1 ? undefined : repositoryUrl,
|
|
110
|
+
})
|
|
82
111
|
for (const [relative, contents] of files) {
|
|
83
112
|
const path = join(target, relative)
|
|
84
113
|
await mkdir(dirname(path), { recursive: true })
|
|
@@ -92,7 +121,7 @@ export async function run(argv: readonly string[], version: string): Promise<Cli
|
|
|
92
121
|
lines: [
|
|
93
122
|
`Created ${name} — ${files.size} files.`,
|
|
94
123
|
'',
|
|
95
|
-
...nextSteps(name).map((step) => ` ${step}`),
|
|
124
|
+
...(kind === null ? nextSteps(name) : extensionNextSteps(name)).map((step) => ` ${step}`),
|
|
96
125
|
'',
|
|
97
126
|
...(gitReady
|
|
98
127
|
? [
|
|
@@ -112,9 +141,16 @@ export async function run(argv: readonly string[], version: string): Promise<Cli
|
|
|
112
141
|
' git push -u origin main',
|
|
113
142
|
]),
|
|
114
143
|
'',
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
...(kind === null
|
|
145
|
+
? [
|
|
146
|
+
'Then set DATABASE_URL, AUTH_SECRET and TICK_SECRET and deploy.',
|
|
147
|
+
'Something must run the tick every minute — the worker process, or',
|
|
148
|
+
'`community task:run`. Without it nothing catches up, and nothing errors.',
|
|
149
|
+
]
|
|
150
|
+
: [
|
|
151
|
+
`Then follow README.md — it walks through running the ${kind} inside a`,
|
|
152
|
+
'scaffolded board and submitting it to the meith.dev marketplace.',
|
|
153
|
+
]),
|
|
118
154
|
],
|
|
119
155
|
}
|
|
120
156
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: template contents are emitted into scaffolded files verbatim, never interpolated here
|
|
2
|
+
import type { ExtensionTemplate } from './scaffold-extension'
|
|
3
|
+
|
|
4
|
+
export const PLUGIN_TEMPLATES: readonly ExtensionTemplate[] = [
|
|
5
|
+
{
|
|
6
|
+
path: 'src/plugin.tsx',
|
|
7
|
+
contents:
|
|
8
|
+
"import { definePlugin } from '@meith/plugin-kit'\n\nexport const __MEITH_EXTENSION_CAMEL__Plugin = definePlugin({\n key: '__MEITH_EXTENSION_KEY__',\n name: '__MEITH_EXTENSION_TITLE__',\n version: '0.1.0',\n description:\n 'A plugin scaffolded by create-meith: a footer link, a greeting ' +\n 'on the board index, one setting, one migration, one task and an admin page.',\n apiVersion: '0',\n\n settings: [\n {\n key: 'greeting',\n label: 'Greeting',\n default: 'A greeting from the __MEITH_EXTENSION_TITLE__ plugin!',\n description: 'Shown on the status page under Admin → Plugins → __MEITH_EXTENSION_TITLE__.',\n },\n ],\n\n migrations: [\n {\n id: '0001_create_wave_table',\n statements: [\n `create table if not exists plugin___MEITH_EXTENSION_SNAKE___wave (\n id integer generated by default as identity primary key,\n waved_at timestamptz not null default now()\n )`,\n ],\n },\n ],\n\n tasks: [\n {\n id: 'wave',\n intervalSeconds: 3600,\n run: (context) => {\n context.logger.info('__MEITH_EXTENSION_KEY__ plugin: waving', {\n greeting: context.settings.greeting,\n })\n },\n },\n ],\n\n adminPages: [\n {\n path: 'status',\n title: '__MEITH_EXTENSION_TITLE__ plugin',\n render: (context) => (\n <div className=\"flex flex-col gap-2 text-sm\">\n <p>{String(context.settings.greeting)}</p>\n <p className=\"text-muted-foreground\">\n This page is rendered by the plugin itself. Edit the greeting on the plugin’s\n settings screen and reload to see the resolved value arrive in{' '}\n <code className=\"text-xs\">context.settings</code>.\n </p>\n </div>\n ),\n },\n ],\n\n contributions: [\n {\n region: 'index.footer',\n render: (context) => (\n <p className=\"text-xs text-muted-foreground\" data-plugin=\"__MEITH_EXTENSION_KEY__\">\n {context.viewer.isGuest\n ? 'Greetings, guest — this line comes from the __MEITH_EXTENSION_TITLE__ plugin.'\n : 'Greetings, member — this line comes from the __MEITH_EXTENSION_TITLE__ plugin.'}\n </p>\n ),\n },\n ],\n\n hooks: {\n 'view.footer': (footer) => ({\n ...footer,\n links: [\n ...footer.links,\n {\n label: '__MEITH_EXTENSION_TITLE__ plugin',\n href: '__MEITH_EXTENSION_REPOSITORY__',\n },\n ],\n }),\n\n 'post.created': () => {},\n },\n})\n",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
path: 'src/plugin.test.ts',
|
|
12
|
+
contents:
|
|
13
|
+
"import { describe, expect, it } from 'vitest'\n\nimport { type FilterHandler, unavailableHookRuntime } from '@meith/plugin-kit'\n\nimport { __MEITH_EXTENSION_CAMEL__Plugin } from './plugin'\n\ndescribe('the __MEITH_EXTENSION_KEY__ plugin', () => {\n it('has a validated manifest (definePlugin threw at import time otherwise)', () => {\n expect(__MEITH_EXTENSION_CAMEL__Plugin.key).toBe('__MEITH_EXTENSION_KEY__')\n expect(__MEITH_EXTENSION_CAMEL__Plugin.version).toBe('0.1.0')\n })\n\n it('appends its footer link without disturbing the board’s own', () => {\n const filter = __MEITH_EXTENSION_CAMEL__Plugin.hooks?.['view.footer'] as FilterHandler<'view.footer'>\n const footer = {\n boardTitle: 'A board',\n links: [{ label: 'Contact', href: '/contact' }],\n timezoneLabel: 'Europe/Dublin',\n }\n\n const filtered = filter(\n footer,\n { userId: null, isGuest: true, requestId: null },\n unavailableHookRuntime('this test drives the filter directly'),\n )\n\n expect(filtered).toMatchObject({\n boardTitle: 'A board',\n links: [\n { label: 'Contact', href: '/contact' },\n { label: '__MEITH_EXTENSION_TITLE__ plugin', href: expect.stringContaining('__MEITH_EXTENSION_REPOSITORY__') },\n ],\n })\n expect(footer.links).toHaveLength(1)\n })\n})\n",
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const THEME_TEMPLATES: readonly ExtensionTemplate[] = [
|
|
18
|
+
{
|
|
19
|
+
path: 'src/index.ts',
|
|
20
|
+
contents:
|
|
21
|
+
"export { __MEITH_EXTENSION_CAMEL__Theme } from './theme'\nexport { BROWSER_THEME_COLOR, DARK_TOKENS, LIGHT_TOKENS } from './tokens'\n",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: 'src/theme.ts',
|
|
25
|
+
contents:
|
|
26
|
+
"import { defaultTheme } from '@meith/theme-default'\nimport { defineTheme } from '@meith/theme-kit'\n\nimport { Footer } from './slots/footer'\n\nexport const __MEITH_EXTENSION_CAMEL__Theme = defineTheme({\n key: '__MEITH_EXTENSION_KEY__',\n title: '__MEITH_EXTENSION_TITLE__',\n extends: defaultTheme,\n slots: { Footer },\n})\n",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: 'src/tokens.ts',
|
|
30
|
+
contents:
|
|
31
|
+
"import { DARK_TOKENS as DEFAULT_DARK, LIGHT_TOKENS as DEFAULT_LIGHT } from '@meith/theme-default'\n\nexport const LIGHT_TOKENS: Record<string, string> = {\n ...DEFAULT_LIGHT,\n primary: 'oklch(0.49 0.19 300)',\n 'primary-hover': 'oklch(0.42 0.17 300)',\n ring: 'oklch(0.49 0.19 300)',\n}\n\nexport const DARK_TOKENS: Record<string, string> = {\n ...DEFAULT_DARK,\n primary: 'oklch(0.78 0.12 300)',\n 'primary-foreground': 'oklch(0.18 0.03 300)',\n 'primary-hover': 'oklch(0.84 0.11 300)',\n ring: 'oklch(0.78 0.12 300)',\n}\n\nexport { BROWSER_THEME_COLOR } from '@meith/theme-default'\n",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
path: 'src/slots/footer.tsx',
|
|
35
|
+
contents:
|
|
36
|
+
'import type { FooterModel } from \'@meith/theme-kit\'\n\nexport function Footer({ boardTitle, links, timezoneLabel, poweredBy }: FooterModel) {\n return (\n <footer className="mt-auto border-t-2 border-primary bg-card">\n <div className="mx-auto flex w-full max-w-5xl flex-col items-center gap-2 px-4 py-6 text-center text-xs text-muted-foreground">\n <span className="font-semibold uppercase tracking-widest text-foreground">\n {boardTitle}\n </span>\n\n {links.length > 0 && (\n <nav aria-label="Footer" className="flex flex-wrap justify-center gap-x-4 gap-y-1">\n {links.map((link) => (\n <a\n key={link.href}\n href={link.href}\n className="underline decoration-primary underline-offset-2 hover:text-foreground"\n >\n {link.label}\n </a>\n ))}\n </nav>\n )}\n\n <span>\n Times are shown in {timezoneLabel}\n {poweredBy && (\n <>\n {\' — \'}\n <a\n href={poweredBy.href}\n className="underline decoration-primary underline-offset-2 hover:text-foreground"\n >\n {poweredBy.label}\n </a>\n </>\n )}\n </span>\n </div>\n </footer>\n )\n}\n',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: 'src/theme.test.ts',
|
|
40
|
+
contents:
|
|
41
|
+
"import { describe, expect, it } from 'vitest'\n\nimport { defaultTheme, TOKEN_NAMES } from '@meith/theme-default'\nimport { assertThemeContract, resolveTheme } from '@meith/theme-kit'\n\nimport { __MEITH_EXTENSION_CAMEL__Theme } from './theme'\nimport { DARK_TOKENS, LIGHT_TOKENS } from './tokens'\n\ndescribe('the __MEITH_EXTENSION_KEY__ theme', () => {\n it('satisfies the theme-kit contract', () => {\n expect(assertThemeContract(resolveTheme(__MEITH_EXTENSION_CAMEL__Theme)).missing).toEqual([])\n })\n\n it('inherits from the default theme rather than copying it', () => {\n expect(resolveTheme(__MEITH_EXTENSION_CAMEL__Theme).chain).toEqual(['__MEITH_EXTENSION_KEY__', 'default'])\n })\n\n it('overrides exactly one slot: the footer', () => {\n expect(Object.keys(__MEITH_EXTENSION_CAMEL__Theme.slots)).toEqual(['Footer'])\n expect(__MEITH_EXTENSION_CAMEL__Theme.slots.Footer).not.toBe(defaultTheme.slots.Footer)\n })\n\n it('declares a value for every token the theme layer names', () => {\n for (const name of TOKEN_NAMES) {\n expect(LIGHT_TOKENS[name], `light ${name}`).toBeDefined()\n expect(DARK_TOKENS[name], `dark ${name}`).toBeDefined()\n }\n })\n\n it('recolours the brand group and nothing greyscale', () => {\n expect(LIGHT_TOKENS.primary).not.toBe(DARK_TOKENS.primary)\n expect(LIGHT_TOKENS.background).toBe('oklch(0.968 0 0)')\n expect(DARK_TOKENS.background).toBe('oklch(0.15 0 0)')\n })\n})\n",
|
|
42
|
+
},
|
|
43
|
+
]
|
package/src/index.ts
CHANGED
|
@@ -14,3 +14,15 @@ export {
|
|
|
14
14
|
VERCEL_PROMPTED_ENV,
|
|
15
15
|
validateName,
|
|
16
16
|
} from './scaffold'
|
|
17
|
+
export {
|
|
18
|
+
EXTENSION_KEY_PATTERN,
|
|
19
|
+
type ExtensionKind,
|
|
20
|
+
type ExtensionScaffoldOptions,
|
|
21
|
+
type ExtensionTemplate,
|
|
22
|
+
extensionNextSteps,
|
|
23
|
+
meithRange,
|
|
24
|
+
scaffoldExtension,
|
|
25
|
+
scaffoldPlugin,
|
|
26
|
+
scaffoldTheme,
|
|
27
|
+
validateExtensionName,
|
|
28
|
+
} from './scaffold-extension'
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { PLUGIN_TEMPLATES, THEME_TEMPLATES } from './extension-templates'
|
|
2
|
+
|
|
3
|
+
export type ExtensionKind = 'plugin' | 'theme'
|
|
4
|
+
|
|
5
|
+
export interface ExtensionTemplate {
|
|
6
|
+
readonly path: string
|
|
7
|
+
readonly contents: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ExtensionScaffoldOptions {
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly version: string
|
|
13
|
+
readonly repositoryUrl?: string | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const EXTENSION_KEY_PATTERN = /^[a-z][a-z0-9-]{1,39}$/
|
|
17
|
+
|
|
18
|
+
export function validateExtensionName(name: string): string | null {
|
|
19
|
+
if (name === '') return 'An extension name is required.'
|
|
20
|
+
if (!EXTENSION_KEY_PATTERN.test(name)) {
|
|
21
|
+
return (
|
|
22
|
+
'Use lower-case letters, digits and hyphens, starting with a letter, 2 to 40 characters — ' +
|
|
23
|
+
'the name becomes the definePlugin/defineTheme key and the npm package name.'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function extensionTitle(name: string): string {
|
|
30
|
+
return name
|
|
31
|
+
.split('-')
|
|
32
|
+
.filter((word) => word !== '')
|
|
33
|
+
.map((word) => `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`)
|
|
34
|
+
.join(' ')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extensionCamel(name: string): string {
|
|
38
|
+
const words = name.split('-').filter((word) => word !== '')
|
|
39
|
+
const [head, ...rest] = words
|
|
40
|
+
return `${head ?? ''}${rest
|
|
41
|
+
.map((word) => `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`)
|
|
42
|
+
.join('')}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function extensionSnake(name: string): string {
|
|
46
|
+
return name.replace(/-/g, '_')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function defaultExtensionRepositoryUrl(name: string): string {
|
|
50
|
+
return `https://github.com/your-name/${name}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function meithRange(version: string): string {
|
|
54
|
+
const [major = '0', minor = '0'] = version.split('.')
|
|
55
|
+
return `>=${major}.${minor} <${Number(major) + 1}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function substitute(
|
|
59
|
+
contents: string,
|
|
60
|
+
values: { key: string; title: string; camel: string; snake: string; repositoryUrl: string },
|
|
61
|
+
): string {
|
|
62
|
+
return contents
|
|
63
|
+
.replaceAll('__MEITH_EXTENSION_CAMEL__', values.camel)
|
|
64
|
+
.replaceAll('__MEITH_EXTENSION_SNAKE__', values.snake)
|
|
65
|
+
.replaceAll('__MEITH_EXTENSION_REPOSITORY__', values.repositoryUrl)
|
|
66
|
+
.replaceAll('__MEITH_EXTENSION_KEY__', values.key)
|
|
67
|
+
.replaceAll('__MEITH_EXTENSION_TITLE__', values.title)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extensionManifest(
|
|
71
|
+
options: ExtensionScaffoldOptions,
|
|
72
|
+
repositoryUrl: string,
|
|
73
|
+
description: string,
|
|
74
|
+
dependencies: Record<string, string>,
|
|
75
|
+
): string {
|
|
76
|
+
return `${JSON.stringify(
|
|
77
|
+
{
|
|
78
|
+
name: options.name,
|
|
79
|
+
version: '0.1.0',
|
|
80
|
+
description,
|
|
81
|
+
license: 'MIT',
|
|
82
|
+
repository: { type: 'git', url: repositoryUrl },
|
|
83
|
+
type: 'module',
|
|
84
|
+
main: './src/index.ts',
|
|
85
|
+
types: './src/index.ts',
|
|
86
|
+
files: ['src', '!src/**/*.test.*'],
|
|
87
|
+
scripts: { test: 'vitest run', typecheck: 'tsc --noEmit' },
|
|
88
|
+
dependencies,
|
|
89
|
+
peerDependencies: { react: '^19.2.0' },
|
|
90
|
+
devDependencies: {
|
|
91
|
+
'@types/node': '^26.2.0',
|
|
92
|
+
'@types/react': '^19.2.18',
|
|
93
|
+
react: '^19.2.0',
|
|
94
|
+
typescript: '^7.0.2',
|
|
95
|
+
vitest: '^4.1.10',
|
|
96
|
+
},
|
|
97
|
+
publishConfig: { access: 'public' },
|
|
98
|
+
},
|
|
99
|
+
null,
|
|
100
|
+
2,
|
|
101
|
+
)}\n`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extensionTsconfig(): string {
|
|
105
|
+
return `${JSON.stringify(
|
|
106
|
+
{
|
|
107
|
+
compilerOptions: {
|
|
108
|
+
target: 'es2022',
|
|
109
|
+
lib: ['es2022', 'dom'],
|
|
110
|
+
module: 'esnext',
|
|
111
|
+
moduleResolution: 'bundler',
|
|
112
|
+
jsx: 'react-jsx',
|
|
113
|
+
strict: true,
|
|
114
|
+
resolveJsonModule: true,
|
|
115
|
+
skipLibCheck: true,
|
|
116
|
+
noEmit: true,
|
|
117
|
+
},
|
|
118
|
+
include: ['src'],
|
|
119
|
+
},
|
|
120
|
+
null,
|
|
121
|
+
2,
|
|
122
|
+
)}\n`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extensionVitestConfig(): string {
|
|
126
|
+
return `import { defineConfig } from 'vitest/config'
|
|
127
|
+
|
|
128
|
+
export default defineConfig({
|
|
129
|
+
test: { environment: 'node' },
|
|
130
|
+
})
|
|
131
|
+
`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extensionGitignore(): string {
|
|
135
|
+
return `node_modules
|
|
136
|
+
*.log
|
|
137
|
+
.DS_Store
|
|
138
|
+
`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function listing(
|
|
142
|
+
options: ExtensionScaffoldOptions,
|
|
143
|
+
kind: ExtensionKind,
|
|
144
|
+
repositoryUrl: string,
|
|
145
|
+
description: string,
|
|
146
|
+
): string {
|
|
147
|
+
return `${JSON.stringify(
|
|
148
|
+
{
|
|
149
|
+
key: options.name,
|
|
150
|
+
kind,
|
|
151
|
+
package: options.name,
|
|
152
|
+
name: extensionTitle(options.name),
|
|
153
|
+
description,
|
|
154
|
+
screenshots: [`${options.name}-light.png`],
|
|
155
|
+
version: '0.1.0',
|
|
156
|
+
apiVersion: 0,
|
|
157
|
+
meith: meithRange(options.version),
|
|
158
|
+
repository: repositoryUrl,
|
|
159
|
+
licence: 'MIT',
|
|
160
|
+
},
|
|
161
|
+
null,
|
|
162
|
+
2,
|
|
163
|
+
)}\n`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pluginReadme(options: ExtensionScaffoldOptions, camel: string, title: string): string {
|
|
167
|
+
const { name } = options
|
|
168
|
+
return `# ${title}
|
|
169
|
+
|
|
170
|
+
A Meith plugin, scaffolded by \`create-meith\` from the meith repository's
|
|
171
|
+
\`examples/hello-plugin\`: a footer link, a greeting on the board index, one
|
|
172
|
+
setting, one migration, one task and an admin page — every extension point
|
|
173
|
+
exercised once, each easy to delete.
|
|
174
|
+
|
|
175
|
+
## Develop
|
|
176
|
+
|
|
177
|
+
npm install
|
|
178
|
+
npm test
|
|
179
|
+
|
|
180
|
+
\`src/plugin.tsx\` is the plugin. What a plugin may and may not do is
|
|
181
|
+
documented in the meith repository under \`docs/customization/plugins.md\`;
|
|
182
|
+
every hook and payload is listed in \`docs/reference/plugin-hooks.md\`.
|
|
183
|
+
|
|
184
|
+
## Run it inside a board
|
|
185
|
+
|
|
186
|
+
Scaffold a board next to this directory if you do not have one
|
|
187
|
+
(\`npx create-meith my-board\`), then install this workspace into it by path:
|
|
188
|
+
|
|
189
|
+
cd ../my-board
|
|
190
|
+
npm install ../${name}
|
|
191
|
+
|
|
192
|
+
npm installs a local directory as a symlink, so edits here are picked up by
|
|
193
|
+
the board's next build without reinstalling.
|
|
194
|
+
|
|
195
|
+
Register the plugin in the board's \`community.plugins.ts\` — the comment at
|
|
196
|
+
the top of that file shows the shape:
|
|
197
|
+
|
|
198
|
+
import { messages as ${camel}Messages, plugin as ${camel}Plugin } from '${name}'
|
|
199
|
+
|
|
200
|
+
export const INSTALLED_PLUGINS: readonly InstalledPlugin[] = [
|
|
201
|
+
{ key: '${name}', enabled: true, plugin: ${camel}Plugin, messages: ${camel}Messages },
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
and add the matching entry to \`board.plugins.json\`:
|
|
205
|
+
|
|
206
|
+
{ "plugins": [{ "key": "${name}", "package": "${name}", "enabled": true }] }
|
|
207
|
+
|
|
208
|
+
Rebuild the board (\`npm run build\`) and, because this plugin ships a
|
|
209
|
+
migration, run \`npx community migrate\`. The plugin then appears under
|
|
210
|
+
**Admin → Plugins**.
|
|
211
|
+
|
|
212
|
+
## Publish and list it
|
|
213
|
+
|
|
214
|
+
\`npm publish\` ships \`src/\` as TypeScript source, the way every
|
|
215
|
+
\`@meith/*\` package ships. To offer the plugin on the meith.dev marketplace,
|
|
216
|
+
finish \`listing.json\` (its \`repository\` field starts as a placeholder),
|
|
217
|
+
add the screenshot it names, and open a pull request against the meith
|
|
218
|
+
repository — the submission process and the review bar are documented there
|
|
219
|
+
in \`docs/customization/marketplace.md\`.
|
|
220
|
+
`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function themeReadme(options: ExtensionScaffoldOptions, camel: string, title: string): string {
|
|
224
|
+
const { name } = options
|
|
225
|
+
return `# ${title}
|
|
226
|
+
|
|
227
|
+
A Meith theme, scaffolded by \`create-meith\` from the meith repository's
|
|
228
|
+
\`examples/iris-theme\`: the default theme recoloured (one brand group of
|
|
229
|
+
tokens) plus a single slot override, the footer.
|
|
230
|
+
|
|
231
|
+
## Develop
|
|
232
|
+
|
|
233
|
+
npm install
|
|
234
|
+
npm test
|
|
235
|
+
|
|
236
|
+
\`src/theme.ts\` declares the theme, \`src/tokens.ts\` carries the palette,
|
|
237
|
+
and slots live in \`src/slots/\`. What a theme may and may not do is
|
|
238
|
+
documented in the meith repository under \`docs/customization/themes.md\`;
|
|
239
|
+
every slot and view model is listed in \`docs/reference/theme-slots.md\`.
|
|
240
|
+
|
|
241
|
+
## Run it inside a board
|
|
242
|
+
|
|
243
|
+
Scaffold a board next to this directory if you do not have one
|
|
244
|
+
(\`npx create-meith my-board\`), then install this workspace into it by path:
|
|
245
|
+
|
|
246
|
+
cd ../my-board
|
|
247
|
+
npm install ../${name}
|
|
248
|
+
|
|
249
|
+
npm installs a local directory as a symlink, so edits here are picked up by
|
|
250
|
+
the board's next build without reinstalling.
|
|
251
|
+
|
|
252
|
+
Register the theme in the board's \`community.config.ts\`, beside the
|
|
253
|
+
default entry:
|
|
254
|
+
|
|
255
|
+
import { defaultMessages } from '@meith/theme-default'
|
|
256
|
+
import { BROWSER_THEME_COLOR, DARK_TOKENS, LIGHT_TOKENS, ${camel}Theme } from '${name}'
|
|
257
|
+
|
|
258
|
+
themes: {
|
|
259
|
+
'${name}': {
|
|
260
|
+
key: '${name}',
|
|
261
|
+
title: '${title}',
|
|
262
|
+
tokens: { light: LIGHT_TOKENS, dark: DARK_TOKENS },
|
|
263
|
+
browserThemeColor: BROWSER_THEME_COLOR,
|
|
264
|
+
theme: ${camel}Theme,
|
|
265
|
+
messages: defaultMessages,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
Rebuild the board (\`npm run build\`); the theme is then offered to members
|
|
270
|
+
on the appearance screen and to administrators under **Admin → Themes**.
|
|
271
|
+
|
|
272
|
+
## Publish and list it
|
|
273
|
+
|
|
274
|
+
\`npm publish\` ships \`src/\` as TypeScript source, the way every
|
|
275
|
+
\`@meith/*\` package ships. To offer the theme on the meith.dev marketplace,
|
|
276
|
+
finish \`listing.json\` (its \`repository\` field starts as a placeholder),
|
|
277
|
+
add the screenshot it names, and open a pull request against the meith
|
|
278
|
+
repository — the submission process and the review bar are documented there
|
|
279
|
+
in \`docs/customization/marketplace.md\`.
|
|
280
|
+
`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function scaffoldPlugin(options: ExtensionScaffoldOptions): ReadonlyMap<string, string> {
|
|
284
|
+
const key = options.name
|
|
285
|
+
const title = extensionTitle(key)
|
|
286
|
+
const camel = extensionCamel(key)
|
|
287
|
+
const snake = extensionSnake(key)
|
|
288
|
+
const repositoryUrl = options.repositoryUrl ?? defaultExtensionRepositoryUrl(key)
|
|
289
|
+
const description = `${title} — a Meith plugin.`
|
|
290
|
+
|
|
291
|
+
const files = new Map<string, string>()
|
|
292
|
+
|
|
293
|
+
files.set(
|
|
294
|
+
'package.json',
|
|
295
|
+
extensionManifest(options, repositoryUrl, description, {
|
|
296
|
+
'@meith/plugin-kit': options.version,
|
|
297
|
+
}),
|
|
298
|
+
)
|
|
299
|
+
files.set('tsconfig.json', extensionTsconfig())
|
|
300
|
+
files.set('vitest.config.ts', extensionVitestConfig())
|
|
301
|
+
files.set('.gitignore', extensionGitignore())
|
|
302
|
+
files.set('README.md', pluginReadme(options, camel, title))
|
|
303
|
+
files.set('listing.json', listing(options, 'plugin', repositoryUrl, description))
|
|
304
|
+
|
|
305
|
+
files.set(
|
|
306
|
+
'src/index.ts',
|
|
307
|
+
`export { ${camel}Plugin, ${camel}Plugin as plugin } from './plugin'
|
|
308
|
+
export { ${camel}Messages, ${camel}Messages as messages } from './messages'
|
|
309
|
+
`,
|
|
310
|
+
)
|
|
311
|
+
files.set(
|
|
312
|
+
'src/messages/index.ts',
|
|
313
|
+
`import en from './en.json'
|
|
314
|
+
|
|
315
|
+
export const ${camel}Messages = { en }
|
|
316
|
+
`,
|
|
317
|
+
)
|
|
318
|
+
files.set('src/messages/en.json', '{}\n')
|
|
319
|
+
|
|
320
|
+
for (const template of PLUGIN_TEMPLATES) {
|
|
321
|
+
files.set(
|
|
322
|
+
template.path,
|
|
323
|
+
substitute(template.contents, { key, title, camel, snake, repositoryUrl }),
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return files
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function scaffoldTheme(options: ExtensionScaffoldOptions): ReadonlyMap<string, string> {
|
|
331
|
+
const key = options.name
|
|
332
|
+
const title = extensionTitle(key)
|
|
333
|
+
const camel = extensionCamel(key)
|
|
334
|
+
const snake = extensionSnake(key)
|
|
335
|
+
const repositoryUrl = options.repositoryUrl ?? defaultExtensionRepositoryUrl(key)
|
|
336
|
+
const description = `${title} — a Meith theme.`
|
|
337
|
+
|
|
338
|
+
const files = new Map<string, string>()
|
|
339
|
+
|
|
340
|
+
files.set(
|
|
341
|
+
'package.json',
|
|
342
|
+
extensionManifest(options, repositoryUrl, description, {
|
|
343
|
+
'@meith/theme-default': options.version,
|
|
344
|
+
'@meith/theme-kit': options.version,
|
|
345
|
+
}),
|
|
346
|
+
)
|
|
347
|
+
files.set('tsconfig.json', extensionTsconfig())
|
|
348
|
+
files.set('vitest.config.ts', extensionVitestConfig())
|
|
349
|
+
files.set('.gitignore', extensionGitignore())
|
|
350
|
+
files.set('README.md', themeReadme(options, camel, title))
|
|
351
|
+
files.set('listing.json', listing(options, 'theme', repositoryUrl, description))
|
|
352
|
+
|
|
353
|
+
for (const template of THEME_TEMPLATES) {
|
|
354
|
+
files.set(
|
|
355
|
+
template.path,
|
|
356
|
+
substitute(template.contents, { key, title, camel, snake, repositoryUrl }),
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return files
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function scaffoldExtension(
|
|
364
|
+
kind: ExtensionKind,
|
|
365
|
+
options: ExtensionScaffoldOptions,
|
|
366
|
+
): ReadonlyMap<string, string> {
|
|
367
|
+
return kind === 'plugin' ? scaffoldPlugin(options) : scaffoldTheme(options)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function extensionNextSteps(name: string): readonly string[] {
|
|
371
|
+
return [`cd ${name}`, 'npm install', 'npm test']
|
|
372
|
+
}
|