toes new --<template>
This commit is contained in:
parent
f4e13dd932
commit
f12854fc04
5
TODO.txt
5
TODO.txt
|
|
@ -37,8 +37,9 @@
|
||||||
[x] `toes pull`
|
[x] `toes pull`
|
||||||
[x] `toes push`
|
[x] `toes push`
|
||||||
[x] `toes sync`
|
[x] `toes sync`
|
||||||
[ ] `toes new --spa`
|
[x] `toes new --spa`
|
||||||
[ ] `toes new --ssr`
|
[x] `toes new --ssr`
|
||||||
|
[x] `toes new --bare`
|
||||||
[ ] needs to either check toes.local or take something like TOES_URL
|
[ ] needs to either check toes.local or take something like TOES_URL
|
||||||
|
|
||||||
## webui
|
## webui
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { App } from '@types'
|
import type { App } from '@types'
|
||||||
import { generateTemplates } from '@templates'
|
import { generateTemplates, type TemplateType } from '@templates'
|
||||||
import color from 'kleur'
|
import color from 'kleur'
|
||||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
||||||
import { basename, join } from 'path'
|
import { basename, join } from 'path'
|
||||||
|
|
@ -55,10 +55,21 @@ export async function listApps() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function newApp(name?: string) {
|
interface NewAppOptions {
|
||||||
|
ssr?: boolean
|
||||||
|
bare?: boolean
|
||||||
|
spa?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function newApp(name: string | undefined, options: NewAppOptions) {
|
||||||
const appPath = name ? join(process.cwd(), name) : process.cwd()
|
const appPath = name ? join(process.cwd(), name) : process.cwd()
|
||||||
const appName = name ?? basename(process.cwd())
|
const appName = name ?? basename(process.cwd())
|
||||||
|
|
||||||
|
// Determine template type from flags
|
||||||
|
let template: TemplateType = 'ssr'
|
||||||
|
if (options.bare) template = 'bare'
|
||||||
|
else if (options.spa) template = 'spa'
|
||||||
|
|
||||||
if (name && existsSync(appPath)) {
|
if (name && existsSync(appPath)) {
|
||||||
console.error(`Directory already exists: ${name}`)
|
console.error(`Directory already exists: ${name}`)
|
||||||
return
|
return
|
||||||
|
|
@ -71,12 +82,18 @@ export async function newApp(name?: string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const ok = await confirm(`Create ${color.bold(appName)} in ${appPath}?`)
|
const templateLabel = template === 'ssr' ? '' : ` (${template})`
|
||||||
|
const ok = await confirm(`Create ${color.bold(appName)}${templateLabel} in ${appPath}?`)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|
||||||
mkdirSync(join(appPath, 'src', 'pages'), { recursive: true })
|
const templates = generateTemplates(appName, template)
|
||||||
|
|
||||||
|
// Create directories for all template files
|
||||||
|
for (const filename of Object.keys(templates)) {
|
||||||
|
const dir = join(appPath, filename, '..')
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
const templates = generateTemplates(appName)
|
|
||||||
for (const [filename, content] of Object.entries(templates)) {
|
for (const [filename, content] of Object.entries(templates)) {
|
||||||
writeFileSync(join(appPath, filename), content)
|
writeFileSync(join(appPath, filename), content)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
import color from 'kleur'
|
import color from 'kleur'
|
||||||
import {
|
import {
|
||||||
getApp,
|
getApp,
|
||||||
|
|
@ -19,7 +21,7 @@ import {
|
||||||
|
|
||||||
program
|
program
|
||||||
.name('toes')
|
.name('toes')
|
||||||
.version('0.0.1', '-v, --version')
|
.version('v0.0.3', '-v, --version')
|
||||||
.addHelpText('beforeAll', (ctx) => {
|
.addHelpText('beforeAll', (ctx) => {
|
||||||
if (ctx.command === program) {
|
if (ctx.command === program) {
|
||||||
return color.bold().cyan('\n🐾 Toes') + color.gray(' - personal web appliance\n')
|
return color.bold().cyan('\n🐾 Toes') + color.gray(' - personal web appliance\n')
|
||||||
|
|
@ -99,6 +101,9 @@ program
|
||||||
.command('new')
|
.command('new')
|
||||||
.description('Create a new toes app')
|
.description('Create a new toes app')
|
||||||
.argument('[name]', 'app name (uses current directory if omitted)')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
|
.option('--ssr', 'SSR template with pages directory (default)')
|
||||||
|
.option('--bare', 'minimal template with no pages')
|
||||||
|
.option('--spa', 'single-page app with client-side rendering')
|
||||||
.action(newApp)
|
.action(newApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,59 @@
|
||||||
|
import { readdirSync, readFileSync, statSync } from 'fs'
|
||||||
|
import { join, relative } from 'path'
|
||||||
import { DEFAULT_EMOJI } from './types'
|
import { DEFAULT_EMOJI } from './types'
|
||||||
|
|
||||||
export interface AppTemplates {
|
export type TemplateType = 'ssr' | 'bare' | 'spa'
|
||||||
'index.tsx': string
|
|
||||||
'package.json': string
|
export type AppTemplates = Record<string, string>
|
||||||
'tsconfig.json': string
|
|
||||||
'.npmrc': string
|
interface TemplateVars {
|
||||||
'src/pages/index.tsx': string
|
APP_NAME: string
|
||||||
|
APP_EMOJI: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const tsconfig = {
|
const TEMPLATES_DIR = join(import.meta.dirname, '../../templates')
|
||||||
compilerOptions: {
|
|
||||||
lib: ['ESNext'],
|
function readDir(dir: string): string[] {
|
||||||
target: 'ESNext',
|
const files: string[] = []
|
||||||
module: 'Preserve',
|
for (const entry of readdirSync(dir)) {
|
||||||
moduleDetection: 'force',
|
const path = join(dir, entry)
|
||||||
jsx: 'react-jsx',
|
if (statSync(path).isDirectory()) {
|
||||||
jsxImportSource: 'hono/jsx',
|
files.push(...readDir(path))
|
||||||
allowJs: true,
|
} else {
|
||||||
moduleResolution: 'bundler',
|
files.push(path)
|
||||||
allowImportingTsExtensions: true,
|
}
|
||||||
verbatimModuleSyntax: true,
|
}
|
||||||
noEmit: true,
|
return files
|
||||||
strict: true,
|
|
||||||
skipLibCheck: true,
|
|
||||||
noFallthroughCasesInSwitch: true,
|
|
||||||
noUncheckedIndexedAccess: true,
|
|
||||||
noImplicitOverride: true,
|
|
||||||
noUnusedLocals: false,
|
|
||||||
noUnusedParameters: false,
|
|
||||||
noPropertyAccessFromIndexSignature: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateTemplates(appName: string): AppTemplates {
|
function replaceVars(content: string, vars: TemplateVars): string {
|
||||||
const packageJson = {
|
return content
|
||||||
name: appName,
|
.replace(/\$\$APP_NAME\$\$/g, vars.APP_NAME)
|
||||||
module: 'index.tsx',
|
.replace(/\$\$APP_EMOJI\$\$/g, vars.APP_EMOJI)
|
||||||
type: 'module',
|
}
|
||||||
private: true,
|
|
||||||
scripts: {
|
export function generateTemplates(appName: string, template: TemplateType = 'ssr'): AppTemplates {
|
||||||
toes: 'bun run --watch index.tsx',
|
const vars: TemplateVars = {
|
||||||
start: 'bun toes',
|
APP_NAME: appName,
|
||||||
dev: 'bun run --hot index.tsx',
|
APP_EMOJI: DEFAULT_EMOJI,
|
||||||
},
|
|
||||||
toes: {
|
|
||||||
icon: DEFAULT_EMOJI,
|
|
||||||
},
|
|
||||||
devDependencies: {
|
|
||||||
'@types/bun': 'latest',
|
|
||||||
},
|
|
||||||
peerDependencies: {
|
|
||||||
typescript: '^5.9.2',
|
|
||||||
},
|
|
||||||
dependencies: {
|
|
||||||
'@because/hype': '*',
|
|
||||||
'@because/forge': '*',
|
|
||||||
'@because/howl': '*',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result: AppTemplates = {}
|
||||||
'.npmrc': 'registry=https://npm.nose.space',
|
|
||||||
'package.json': JSON.stringify(packageJson, null, 2) + '\n',
|
// Read shared files from templates/
|
||||||
'src/pages/index.tsx': `export default () => <h1>${appName}</h1>`,
|
for (const filename of ['.npmrc', 'package.json', 'tsconfig.json']) {
|
||||||
'index.tsx': `import { Hype } from '@because/hype'\nconst app = new Hype\nexport default app.defaults`,
|
const path = join(TEMPLATES_DIR, filename)
|
||||||
'tsconfig.json': JSON.stringify(tsconfig, null, 2) + '\n',
|
const content = readFileSync(path, 'utf-8')
|
||||||
|
result[filename] = replaceVars(content, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read template-specific files
|
||||||
|
const templateDir = join(TEMPLATES_DIR, template)
|
||||||
|
for (const path of readDir(templateDir)) {
|
||||||
|
const filename = relative(templateDir, path)
|
||||||
|
const content = readFileSync(path, 'utf-8')
|
||||||
|
result[filename] = replaceVars(content, vars)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
templates/.npmrc
Normal file
1
templates/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry=https://npm.nose.space
|
||||||
7
templates/bare/index.tsx
Normal file
7
templates/bare/index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Hype } from '@because/hype'
|
||||||
|
|
||||||
|
const app = new Hype()
|
||||||
|
|
||||||
|
app.get('/', c => c.text('$$APP_NAME$$'))
|
||||||
|
|
||||||
|
export default app.defaults
|
||||||
25
templates/package.json
Normal file
25
templates/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "$$APP_NAME$$",
|
||||||
|
"module": "index.tsx",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"toes": "bun run --watch index.tsx",
|
||||||
|
"start": "bun toes",
|
||||||
|
"dev": "bun run --hot index.tsx"
|
||||||
|
},
|
||||||
|
"toes": {
|
||||||
|
"icon": "$$APP_EMOJI$$"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@because/hype": "*",
|
||||||
|
"@because/forge": "*",
|
||||||
|
"@because/howl": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
templates/spa/index.tsx
Normal file
1
templates/spa/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './src/server'
|
||||||
BIN
templates/spa/pub/img/bite1.png
Normal file
BIN
templates/spa/pub/img/bite1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
templates/spa/pub/img/bite2.png
Normal file
BIN
templates/spa/pub/img/bite2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
templates/spa/pub/img/burger.png
Normal file
BIN
templates/spa/pub/img/burger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
36
templates/spa/src/client/App.tsx
Normal file
36
templates/spa/src/client/App.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { render, useState } from 'hono/jsx/dom'
|
||||||
|
import { define } from '@because/forge'
|
||||||
|
|
||||||
|
const Wrapper = define({
|
||||||
|
margin: '0 auto',
|
||||||
|
marginTop: 50,
|
||||||
|
width: '50vw',
|
||||||
|
border: '1px solid black',
|
||||||
|
padding: 24,
|
||||||
|
textAlign: 'center'
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<h1>It works!</h1>
|
||||||
|
<h2>Count: {count}</h2>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setCount(c => c + 1)}>+</button>
|
||||||
|
|
||||||
|
<button onClick={() => setCount(c => c && c - 1)}>-</button>
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Render error:', error)
|
||||||
|
return <><h1>Error</h1><pre>{error instanceof Error ? error : new Error(String(error))}</pre></>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.getElementById('root')!
|
||||||
|
render(<App />, root)
|
||||||
40
templates/spa/src/css/main.css
Normal file
40
templates/spa/src/css/main.css
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
section {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hype {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
background: linear-gradient(45deg,
|
||||||
|
#ff00ff 0%,
|
||||||
|
#00ffff 33%,
|
||||||
|
#ffff00 66%,
|
||||||
|
#ff00ff 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 15s ease infinite;
|
||||||
|
color: black;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
31
templates/spa/src/pages/index.tsx
Normal file
31
templates/spa/src/pages/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { $ } from 'bun'
|
||||||
|
|
||||||
|
const GIT_HASH = process.env.RENDER_GIT_COMMIT?.slice(0, 7)
|
||||||
|
|| await $`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => 'unknown')
|
||||||
|
|
||||||
|
export default () => <>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>hype</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
|
||||||
|
<link href={`/css/main.css?${GIT_HASH}`} rel="stylesheet" />
|
||||||
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.GIT_HASH = '${GIT_HASH}';
|
||||||
|
${(process.env.NODE_ENV !== 'production' || process.env.IS_PULL_REQUEST === 'true') ? 'window.DEBUG = true;' : ''}
|
||||||
|
`
|
||||||
|
}} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="viewport">
|
||||||
|
<main>
|
||||||
|
<div id="root" />
|
||||||
|
<script src={`/client/app.js?${GIT_HASH}`} type="module" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</>
|
||||||
8
templates/spa/src/server/index.tsx
Normal file
8
templates/spa/src/server/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Hype } from '@because/hype'
|
||||||
|
|
||||||
|
const app = new Hype({ layout: false })
|
||||||
|
|
||||||
|
// custom routes go here
|
||||||
|
// app.get("/my-custom-routes", (c) => c.text("wild, wild stuff"))
|
||||||
|
|
||||||
|
export default app.defaults
|
||||||
0
templates/spa/src/shared/types.ts
Normal file
0
templates/spa/src/shared/types.ts
Normal file
1
templates/ssr/index.tsx
Normal file
1
templates/ssr/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './src/server'
|
||||||
1
templates/ssr/src/pages/index.tsx
Normal file
1
templates/ssr/src/pages/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export default () => <h1>$$APP_NAME$$</h1>
|
||||||
5
templates/ssr/src/server/index.tsx
Normal file
5
templates/ssr/src/server/index.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Hype } from '@because/hype'
|
||||||
|
|
||||||
|
const app = new Hype()
|
||||||
|
|
||||||
|
export default app.defaults
|
||||||
29
templates/tsconfig.json
Normal file
29
templates/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"$*": ["src/server/*"],
|
||||||
|
"#*": ["src/client/*"],
|
||||||
|
"@*": ["src/shared/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"exclude": ["templates"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
// Environment setup & latest features
|
||||||
"lib": [
|
"lib": [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user