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 push`
|
||||
[x] `toes sync`
|
||||
[ ] `toes new --spa`
|
||||
[ ] `toes new --ssr`
|
||||
[x] `toes new --spa`
|
||||
[x] `toes new --ssr`
|
||||
[x] `toes new --bare`
|
||||
[ ] needs to either check toes.local or take something like TOES_URL
|
||||
|
||||
## webui
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { App } from '@types'
|
||||
import { generateTemplates } from '@templates'
|
||||
import { generateTemplates, type TemplateType } from '@templates'
|
||||
import color from 'kleur'
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
||||
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 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)) {
|
||||
console.error(`Directory already exists: ${name}`)
|
||||
return
|
||||
|
|
@ -71,12 +82,18 @@ export async function newApp(name?: string) {
|
|||
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
|
||||
|
||||
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)) {
|
||||
writeFileSync(join(appPath, filename), content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { program } from 'commander'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
import color from 'kleur'
|
||||
import {
|
||||
getApp,
|
||||
|
|
@ -19,7 +21,7 @@ import {
|
|||
|
||||
program
|
||||
.name('toes')
|
||||
.version('0.0.1', '-v, --version')
|
||||
.version('v0.0.3', '-v, --version')
|
||||
.addHelpText('beforeAll', (ctx) => {
|
||||
if (ctx.command === program) {
|
||||
return color.bold().cyan('\n🐾 Toes') + color.gray(' - personal web appliance\n')
|
||||
|
|
@ -99,6 +101,9 @@ program
|
|||
.command('new')
|
||||
.description('Create a new toes app')
|
||||
.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)
|
||||
|
||||
program
|
||||
|
|
|
|||
|
|
@ -1,69 +1,59 @@
|
|||
import { readdirSync, readFileSync, statSync } from 'fs'
|
||||
import { join, relative } from 'path'
|
||||
import { DEFAULT_EMOJI } from './types'
|
||||
|
||||
export interface AppTemplates {
|
||||
'index.tsx': string
|
||||
'package.json': string
|
||||
'tsconfig.json': string
|
||||
'.npmrc': string
|
||||
'src/pages/index.tsx': string
|
||||
export type TemplateType = 'ssr' | 'bare' | 'spa'
|
||||
|
||||
export type AppTemplates = Record<string, string>
|
||||
|
||||
interface TemplateVars {
|
||||
APP_NAME: string
|
||||
APP_EMOJI: string
|
||||
}
|
||||
|
||||
const tsconfig = {
|
||||
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,
|
||||
},
|
||||
const TEMPLATES_DIR = join(import.meta.dirname, '../../templates')
|
||||
|
||||
function readDir(dir: string): string[] {
|
||||
const files: string[] = []
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const path = join(dir, entry)
|
||||
if (statSync(path).isDirectory()) {
|
||||
files.push(...readDir(path))
|
||||
} else {
|
||||
files.push(path)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export function generateTemplates(appName: string): AppTemplates {
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
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: DEFAULT_EMOJI,
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/bun': 'latest',
|
||||
},
|
||||
peerDependencies: {
|
||||
typescript: '^5.9.2',
|
||||
},
|
||||
dependencies: {
|
||||
'@because/hype': '*',
|
||||
'@because/forge': '*',
|
||||
'@because/howl': '*',
|
||||
},
|
||||
function replaceVars(content: string, vars: TemplateVars): string {
|
||||
return content
|
||||
.replace(/\$\$APP_NAME\$\$/g, vars.APP_NAME)
|
||||
.replace(/\$\$APP_EMOJI\$\$/g, vars.APP_EMOJI)
|
||||
}
|
||||
|
||||
return {
|
||||
'.npmrc': 'registry=https://npm.nose.space',
|
||||
'package.json': JSON.stringify(packageJson, null, 2) + '\n',
|
||||
'src/pages/index.tsx': `export default () => <h1>${appName}</h1>`,
|
||||
'index.tsx': `import { Hype } from '@because/hype'\nconst app = new Hype\nexport default app.defaults`,
|
||||
'tsconfig.json': JSON.stringify(tsconfig, null, 2) + '\n',
|
||||
export function generateTemplates(appName: string, template: TemplateType = 'ssr'): AppTemplates {
|
||||
const vars: TemplateVars = {
|
||||
APP_NAME: appName,
|
||||
APP_EMOJI: DEFAULT_EMOJI,
|
||||
}
|
||||
|
||||
const result: AppTemplates = {}
|
||||
|
||||
// Read shared files from templates/
|
||||
for (const filename of ['.npmrc', 'package.json', 'tsconfig.json']) {
|
||||
const path = join(TEMPLATES_DIR, filename)
|
||||
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": {
|
||||
// Environment setup & latest features
|
||||
"lib": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user