This commit is contained in:
Chris Wanstrath 2025-12-26 19:11:01 -08:00
commit f92e7af18d
12 changed files with 1030 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

66
README.md Normal file
View File

@ -0,0 +1,66 @@
# forge
Example:
```tsx
import { define } from "forge"
export const Button = define("button", {
layout: {
padding: 20,
},
look: {
background: "blue",
},
variants: {
kind: {
danger: { look: { background: "red" } },
warning: { look: { background: "yellow" } },
}
},
})
// Usage
<Button>Click me</Button>
<Button kind="danger">Click me carefully</Button>
<Button kind="warning">Click me?</Button>
export const Profile = define("div", {
layout: {
padding: 50,
},
look: {
background: "red",
},
parts: {
header: { layout: { display: "flex" } },
avatar: { layout: { width: 50 } },
bio: { look: { color: "gray" } },
},
variants: {
size: {
small: {
parts: { avatar: { layout: { width: 20 }}}
}
}
},
render({ props, parts: { root, header, avatar, bio } }) {
return (
<root>
<header>
<avatar src={props.pic} />
<bio>{props.bio}</bio>
</header>
</root>
)
},
})
// Usage:
import { Profile } from './whatever'
<Profile pic={user.pic} bio={user.bio} />
```

31
bun.lock Normal file
View File

@ -0,0 +1,31 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "forge",
"dependencies": {
"hono": "^4.11.3",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

150
examples/button.tsx Normal file
View File

@ -0,0 +1,150 @@
import { define } from '../src'
import { Layout, ExampleSection } from './helpers'
const Button = define('Button', {
base: 'button',
layout: {
padding: "12px 24px",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
},
look: {
background: "#3b82f6",
color: "white",
border: "none",
borderRadius: 8,
fontSize: 16,
fontWeight: 600,
cursor: "pointer",
transition: "all 0.2s ease",
userSelect: "none",
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
transform: "translateY(0)",
},
states: {
":not(:disabled):hover": {
look: {
transform: 'translateY(-2px) !important',
filter: 'brightness(1.05)'
}
},
":not(:disabled):active": {
look: {
transform: 'translateY(1px) !important',
boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2) !important'
}
},
},
variants: {
intent: {
primary: {
look: {
background: "#3b82f6",
color: "white",
boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
},
},
secondary: {
look: {
background: "#f3f4f6",
color: "#374151",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)",
},
},
danger: {
look: {
background: "#ef4444",
color: "white",
boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)",
},
},
ghost: {
look: {
background: "transparent",
color: "#aaa",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1)",
border: "1px solid #eee",
},
},
},
size: {
small: {
layout: {
padding: "8px 16px",
},
look: {
fontSize: 14,
},
},
large: {
layout: {
padding: "16px 32px",
},
look: {
fontSize: 18,
},
},
},
disabled: {
look: {
opacity: 0.5,
cursor: "not-allowed",
},
},
},
})
const ButtonRow = define('ButtonRow', {
layout: {
display: 'flex',
gap: 16,
flexWrap: 'wrap',
alignItems: 'center',
}
})
export const ButtonExamplesPage = () => (
<Layout title="Forge Button Component Examples">
<ExampleSection title="Intents">
<ButtonRow>
<Button intent="primary">Primary</Button>
<Button intent="secondary">Secondary</Button>
<Button intent="danger">Danger</Button>
<Button intent="ghost">Ghost</Button>
</ButtonRow>
</ExampleSection >
<ExampleSection title="Sizes">
<ButtonRow>
<Button size="small">Small Button</Button>
<Button>Medium Button</Button>
<Button size="large">Large Button</Button>
</ButtonRow>
</ExampleSection>
<ExampleSection title="Sizes">
<ButtonRow>
<Button intent="primary" size="small">Small Primary</Button>
<Button intent="secondary" size="small">Small Secondary</Button>
<Button intent="danger" size="large">Large Danger</Button>
<Button intent="ghost" size="large">Large Ghost</Button>
</ButtonRow>
</ExampleSection>
<ExampleSection title="Disabled">
<ButtonRow>
<Button disabled>Default Disabled</Button>
<Button intent="primary" disabled>Primary Disabled</Button>
<Button intent="secondary" disabled>Secondary Disabled</Button>
<Button intent="danger" disabled>Danger Disabled</Button>
<Button intent="ghost" disabled>Ghost Disabled</Button>
</ButtonRow>
</ExampleSection>
</Layout >
)

78
examples/helpers.tsx Normal file
View File

@ -0,0 +1,78 @@
import { define, Styles } from '../src'
export const Body = define('Body', {
base: 'body',
layout: {
margin: 0,
padding: '40px 20px',
},
look: {
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
background: '#f3f4f6',
}
})
const Container = define('Container', {
layout: {
maxWidth: 1200,
margin: '0 auto'
}
})
export const Header = define('Header', {
base: 'h1',
layout: {
marginBottom: 40,
},
look: {
color: '#111827'
}
})
export const ExampleSection = define('ExampleSection', {
layout: {
marginBottom: 40
},
parts: {
Header: {
base: 'h2',
layout: {
marginBottom: 16
},
look: {
color: '#374151',
fontSize: 18
}
}
},
render({ props, parts: { Root, Header } }) {
return (
<Root>
<Header>{props.title}</Header>
{props.children}
</Root>
)
}
})
export const Layout = define({
render({ props }) {
return (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{props.title}</title>
<Styles />
</head>
<Body>
<Container>
<Header>{props.title}</Header>
{props.children}
</Container>
</Body>
</html>
)
}
})

77
examples/index.tsx Normal file
View File

@ -0,0 +1,77 @@
export const IndexPage = () => (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Forge Examples</title>
<style dangerouslySetInnerHTML={{
__html: `
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px 20px;
background: #f9fafb;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
color: #111827;
margin-bottom: 16px;
}
p {
color: #6b7280;
font-size: 18px;
margin-bottom: 48px;
}
.examples-grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.example-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
text-decoration: none;
transition: all 0.2s ease;
display: block;
}
.example-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.example-card h2 {
color: #111827;
margin: 0 0 8px 0;
font-size: 20px;
}
.example-card p {
color: #6b7280;
margin: 0;
font-size: 14px;
}
`}} />
</head>
<body>
<div class="container">
<h1>Forge Examples</h1>
<p>Explore component examples built with Forge</p>
<div class="examples-grid">
<a href="/profile" class="example-card">
<h2>Profile Card</h2>
<p>User profile component with variants for size, theme, and verified status</p>
</a>
<a href="/buttons" class="example-card">
<h2>Buttons</h2>
<p>Button component with intent, size, and disabled variants</p>
</a>
</div>
</div>
</body>
</html>
)

277
examples/profile.tsx Normal file
View File

@ -0,0 +1,277 @@
import { define } from '../src'
import { Layout, ExampleSection } from './helpers'
const UserProfile = define('UserProfile', {
base: 'div',
layout: {
padding: 24,
maxWidth: 600,
margin: "0 auto",
},
look: {
background: "white",
borderRadius: 12,
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
},
parts: {
Header: {
layout: {
display: "flex",
alignItems: "center",
gap: 16,
marginBottom: 16,
},
},
Avatar: {
base: 'img',
layout: {
width: 64,
height: 64,
},
look: {
borderRadius: "50%",
objectFit: "cover",
border: "3px solid #e5e7eb",
},
},
Info: {
layout: {
flex: 1,
},
},
Name: {
layout: {
marginBottom: 4,
},
look: {
fontSize: 20,
fontWeight: 600,
color: "#111827",
},
},
Handle: {
look: {
fontSize: 14,
color: "#6b7280",
},
},
Bio: {
layout: {
marginBottom: 16,
width: "100%",
},
look: {
fontSize: 14,
lineHeight: 1.6,
color: "#374151",
wordWrap: "break-word",
},
},
Stats: {
layout: {
display: "flex",
gap: 24,
paddingTop: 16,
},
look: {
borderTop: "1px solid #e5e7eb",
},
},
Stat: {
layout: {
display: "flex",
flexDirection: "column",
gap: 4,
},
},
StatValue: {
look: {
fontSize: 18,
fontWeight: 600,
color: "#111827",
},
},
StatLabel: {
look: {
fontSize: 12,
color: "#6b7280",
textTransform: "uppercase",
},
},
},
variants: {
size: {
compact: {
layout: { padding: 16, maxWidth: 300 },
parts: {
Avatar: { layout: { width: 48, height: 48 } },
Name: { look: { fontSize: 16 } },
Bio: { look: { fontSize: 13 } },
},
},
skinny: {
layout: { padding: 20, maxWidth: 125 },
parts: {
Header: {
layout: {
flexDirection: "column",
alignItems: "center",
gap: 12,
}
},
Avatar: { layout: { width: 80, height: 80 } },
Info: { layout: { flex: 0, width: "100%" } },
Name: { look: { textAlign: "center", fontSize: 18 } },
Handle: { look: { textAlign: "center" } },
Bio: { look: { textAlign: "center", fontSize: 13 } },
Stats: {
layout: {
flexDirection: "column",
gap: 16,
}
},
Stat: { layout: { alignItems: "center" } },
},
},
large: {
layout: { padding: 32, maxWidth: 800 },
parts: {
Avatar: { layout: { width: 96, height: 96 } },
Name: { look: { fontSize: 24 } },
},
},
},
verified: {
parts: {
Avatar: {
look: {
border: "3px solid #3b82f6",
},
},
},
},
theme: {
dark: {
look: {
background: "#1f2937",
},
parts: {
Name: { look: { color: "#f9fafb" } },
Handle: { look: { color: "#9ca3af" } },
Bio: { look: { color: "#d1d5db" } },
Stats: { look: { borderTop: "1px solid #374151" } },
StatValue: { look: { color: "#f9fafb" } },
},
},
},
},
render({ props, parts: { Root, Header, Avatar, Info, Name, Handle, Bio, Stats, Stat, StatValue, StatLabel } }) {
return (
<Root>
<Header>
<Avatar src={props.avatarUrl} alt={props.name} />
<Info>
<Name>
{props.name}
{props.verified && " ✓"}
</Name>
<Handle>@{props.username}</Handle>
</Info>
</Header>
<Bio>{props.bio}</Bio>
<Stats>
<Stat>
<StatValue>{props.followers?.toLocaleString() || 0}</StatValue>
<StatLabel>Followers</StatLabel>
</Stat>
<Stat>
<StatValue>{props.following?.toLocaleString() || 0}</StatValue>
<StatLabel>Following</StatLabel>
</Stat>
<Stat>
<StatValue>{props.posts?.toLocaleString() || 0}</StatValue>
<StatLabel>Posts</StatLabel>
</Stat>
</Stats>
</Root>
)
},
})
// Export the full example page
export const ProfileExamplesPage = () => (
<Layout title="Forge Profile Examples">
<ExampleSection title="Default Profile">
<UserProfile
name="Sarah Chen"
username="sarahc"
avatarUrl="https://i.pravatar.cc/150?img=5"
bio="Designer & developer. Building beautiful interfaces. Based in SF."
followers={1234}
following={567}
posts={89}
/>
</ExampleSection>
<ExampleSection title="Compact Variant">
<UserProfile
size="compact"
name="Alex Rivera"
username="arivera"
avatarUrl="https://i.pravatar.cc/150?img=12"
bio="Creative director. Coffee enthusiast."
followers={456}
following={234}
posts={45}
/>
</ExampleSection>
<ExampleSection title="Skinny Variant">
<UserProfile
size="skinny"
name="Taylor Kim"
username="tkim"
avatarUrl="https://i.pravatar.cc/150?img=3"
bio="Minimalist designer."
followers={2890}
following={125}
posts={312}
/>
</ExampleSection>
<ExampleSection title="Verified User (Dark Theme)">
<UserProfile
verified={true}
theme="dark"
name="Jordan Smith"
username="jordansmith"
avatarUrl="https://i.pravatar.cc/150?img=8"
bio="Tech entrepreneur. Angel investor. Sharing insights on startups and innovation."
followers={52300}
following={892}
posts={1240}
/>
</ExampleSection>
<ExampleSection title="Large Verified Profile">
<UserProfile
size="large"
verified={true}
name="Morgan Taylor"
username="mtaylor"
avatarUrl="https://i.pravatar.cc/150?img=20"
bio="Product designer with 10 years of experience. Passionate about accessibility and inclusive design. Speaker at design conferences."
followers={8900}
following={234}
posts={567}
/>
</ExampleSection>
</Layout>
)

17
package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "forge",
"module": "src/index.ts",
"type": "module",
"scripts": {
"dev": "bun run --hot server.tsx"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"hono": "^4.11.3"
}
}

23
server.tsx Normal file
View File

@ -0,0 +1,23 @@
import { Hono } from 'hono'
import { IndexPage } from './examples/index'
import { ProfileExamplesPage } from './examples/profile'
import { ButtonExamplesPage } from './examples/button'
const app = new Hono()
app.get('/', (c) => {
return c.html(<IndexPage />)
})
app.get('/profile', (c) => {
return c.html(<ProfileExamplesPage />)
})
app.get('/buttons', (c) => {
return c.html(<ButtonExamplesPage />)
})
export default {
port: 3300,
fetch: app.fetch,
}

126
src/index.tsx Normal file
View File

@ -0,0 +1,126 @@
import type { JSX } from 'hono/jsx'
import { type TagDef, UnitlessProps } from './types'
const styles: Record<string, Record<string, string>> = {}
export const Styles = () => <style dangerouslySetInnerHTML={{ __html: stylesToCSS(styles) }} />
// turns style object into string CSS definition
function stylesToCSS(styles: Record<string, Record<string, string>>): string {
let out: string[] = []
for (const [selector, style] of Object.entries(styles)) {
out.push(`.${selector} {`)
for (const [name, value] of Object.entries(style)) {
out.push(` ${name}: ${value};`)
}
out.push(`}\n`)
}
return out.join('\n')
}
// creates a CSS class name
function makeClassName(baseName: string, partName?: string, variantName?: string, variantKey?: string): string {
const cls = partName ? `${baseName}_${partName}` : baseName
if (variantName && variantKey) {
return cls + `.${variantName}-${variantKey}`
} else {
return cls
}
}
// 'fontSize' => 'font-size'
function camelToDash(name: string): string {
let out = ''
for (const letter of name.split(''))
out += letter.toUpperCase() === letter ? `-${letter.toLowerCase()}` : letter
return out
}
// turns a TagDef into a JSX style object
function makeStyle(def: TagDef) {
const style: Record<string, string> = {}
for (const [name, value] of Object.entries(Object.assign({}, def.layout ?? {}, def.look ?? {})))
style[camelToDash(name)] = `${typeof value === 'number' && !UnitlessProps.has(name) ? `${value}px` : value}`
return style
}
// turns a TagDef into a JSX component
function makeComponent(baseName: string, rootDef: TagDef, rootProps: Record<string, any>, partName?: string) {
const def = partName ? rootDef.parts?.[partName]! : rootDef
const base = def.base ?? 'div'
const Tag = (base) as keyof JSX.IntrinsicElements
const classNames = [makeClassName(baseName, partName)]
for (const [key, value] of Object.entries(rootProps)) {
const variantConfig = rootDef.variants?.[key]
if (!variantConfig) continue
const variantName = key
const variantKey = value
let variantDef: TagDef | undefined
if ('parts' in variantConfig || 'layout' in variantConfig || 'look' in variantConfig) {
if (value === true) variantDef = variantConfig as TagDef
} else {
variantDef = (variantConfig as Record<string, TagDef>)[value as string]
}
if (!variantDef) continue
classNames.push(`${variantName}-${variantKey}`)
}
return ({ children, ...props }: { children: any, [key: string]: any }) =>
<Tag class={classNames.join(' ')} {...props}>{children}</Tag>
}
// adds CSS styles for tag definition
function registerStyles(name: string, def: TagDef) {
const rootClassName = makeClassName(name)
styles[rootClassName] ??= makeStyle(def)
for (const [partName, partDef] of Object.entries(def.parts ?? {})) {
const partClassName = makeClassName(name, partName)
styles[partClassName] ??= makeStyle(partDef)
}
for (const [variantName, variantConfig] of Object.entries(def.variants ?? {})) {
for (const [variantKey, variantDef] of Object.entries(variantConfig as Record<string, TagDef>)) {
const className = makeClassName(name, undefined, variantName, variantKey)
styles[className] ??= makeStyle({ layout: variantDef.layout, look: variantDef.look })
for (const [partName, partDef] of Object.entries(variantDef.parts ?? {})) {
const partClassName = makeClassName(name, partName, variantName, variantKey)
styles[partClassName] ??= makeStyle(partDef)
}
}
}
}
let anonComponents = 1
// the main event
export function define(nameOrDef: string | TagDef, defIfNamed?: TagDef) {
const name = defIfNamed ? (nameOrDef as string) : `Def${anonComponents++}`
const def = defIfNamed ?? nameOrDef as TagDef
registerStyles(name, def)
return (props: Record<string, any>) => {
const parts: Record<string, Function> = {}
for (const [part] of Object.entries(def.parts ?? {}))
parts[part] = makeComponent(name, def, props, part)
parts.Root = makeComponent(name, def, props)
return def.render ? def.render({ props, parts }) : <parts.Root {...props}>{props.children}</parts.Root>
}
}
define.Styles = Styles

121
src/types.ts Normal file
View File

@ -0,0 +1,121 @@
export type TagDef = {
className?: string
base?: string
layout?: Layout
look?: Look
states?: Record<string, TagDef>,
parts?: Record<string, TagDef>
variants?: Record<string, TagDef | Record<string, TagDef>>
render?: (obj: any) => any
}
type Layout = {
alignContent?: string
alignItems?: string
alignSelf?: string
bottom?: number | string
display?: string
flex?: number | string
flexBasis?: number | string
flexDirection?: string
flexGrow?: number
flexShrink?: number
flexWrap?: string
gap?: number | string
gridAutoFlow?: string
gridColumn?: string
gridGap?: number | string
gridRow?: string
gridTemplateColumns?: string
gridTemplateRows?: string
height?: number | string
justifyContent?: string
justifyItems?: string
justifySelf?: string
left?: number | string
margin?: number | string
marginBottom?: number | string
marginLeft?: number | string
marginRight?: number | string
marginTop?: number | string
maxHeight?: number | string
maxWidth?: number | string
minHeight?: number | string
minWidth?: number | string
order?: number
overflow?: string
overflowX?: string
overflowY?: string
padding?: number | string
paddingBottom?: number | string
paddingLeft?: number | string
paddingRight?: number | string
paddingTop?: number | string
position?: string
right?: number | string
top?: number | string
width?: number | string
zIndex?: number
}
type Look = {
animation?: string
backdropFilter?: string
background?: string
backgroundColor?: string
border?: string
borderBottom?: string
borderColor?: string
borderLeft?: string
borderRadius?: number | string
borderRight?: string
borderStyle?: string
borderTop?: string
borderWidth?: number | string
boxShadow?: string
color?: string
cursor?: string
filter?: string
fontFamily?: string
fontSize?: number | string
fontStyle?: string
fontWeight?: number | string
letterSpacing?: number | string
lineHeight?: number | string
mixBlendMode?: string
objectFit?: string
opacity?: number
outline?: string
outlineColor?: string
outlineOffset?: number | string
outlineStyle?: string
outlineWidth?: number | string
pointerEvents?: string
textAlign?: string
textDecoration?: string
textOverflow?: string
textShadow?: string
textTransform?: string
transform?: string
transition?: string
userSelect?: string
visibility?: string
whiteSpace?: string
wordWrap?: string
overflowWrap?: string
}
export const UnitlessProps = new Set([
'animationIterationCount',
'columnCount',
'flex',
'flexGrow',
'flexShrink',
'fontWeight',
'lineHeight',
'opacity',
'order',
'orphans',
'widows',
'zIndex'
])

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}