chore: scaffold twitter lite

This commit is contained in:
2026-07-13 09:41:34 +09:00
parent 245d8d743f
commit c4382c1669
17 changed files with 3459 additions and 0 deletions
+2
View File
@@ -1,2 +1,4 @@
.superpowers/
.worktrees/
.output/
node_modules/
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
"files": {
"includes": ["**", "!src/routeTree.gen.ts", "!!**/.output"]
},
"formatter": { "enabled": true, "indentStyle": "space" },
"linter": { "enabled": true, "rules": { "recommended": true } },
"javascript": {
"formatter": { "quoteStyle": "single", "semicolons": "asNeeded" }
}
}
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1783776592,
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+22
View File
@@ -0,0 +1,22 @@
{
description = "Intentional X reader";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { nixpkgs, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
devShells = forAllSystems (system:
let pkgs = import nixpkgs { inherit system; };
in {
default = pkgs.mkShell {
packages = with pkgs; [ nodejs_22 pnpm chromium ];
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1";
PLAYWRIGHT_CHROMIUM_EXECUTABLE =
"${pkgs.chromium}/bin/chromium";
};
});
};
}
+53
View File
@@ -0,0 +1,53 @@
{
"name": "twitter-lite",
"private": true,
"type": "module",
"packageManager": "[email protected]",
"engines": {
"node": ">=22.12.0"
},
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "vite dev --host 127.0.0.1 --port 3000",
"dev:tailscale": "vite dev --host 0.0.0.0 --port 3000",
"generate-routes": "tsr generate",
"build": "vite build",
"start": "HOST=127.0.0.1 node .output/server/index.mjs",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"format": "biome check --write .",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:live": "TWITTER_LITE_LIVE=1 vitest run tests/live/relay.test.ts"
},
"dependencies": {
"@tanstack/react-query": "5.101.2",
"@tanstack/react-router": "1.170.17",
"@tanstack/react-router-ssr-query": "1.167.1",
"@tanstack/react-start": "1.168.27",
"@yuta/bird": "link:../bird",
"nitro": "3.0.260610-beta",
"react": "19.2.7",
"react-dom": "19.2.7",
"zod": "4.4.3"
},
"devDependencies": {
"@biomejs/biome": "2.5.3",
"@playwright/test": "1.61.1",
"@tanstack/router-cli": "1.167.18",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@types/node": "26.1.1",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "6.0.3",
"jsdom": "29.1.1",
"typescript": "7.0.2",
"vite": "8.1.4",
"vitest": "4.1.10"
}
}
+3106
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
esbuild: true
+68
View File
@@ -0,0 +1,68 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
+27
View File
@@ -0,0 +1,27 @@
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: 0, gcTime: 0 },
},
})
const router = createRouter({
routeTree,
context: { queryClient },
scrollRestoration: true,
defaultPreload: false,
})
setupRouterSsrQueryIntegration({ router, queryClient })
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
+38
View File
@@ -0,0 +1,38 @@
import type { QueryClient } from '@tanstack/react-query'
import {
createRootRouteWithContext,
HeadContent,
Scripts,
} from '@tanstack/react-router'
import appCss from '../styles.css?url'
type RouterContext = { queryClient: QueryClient }
export const Route = createRootRouteWithContext<RouterContext>()({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{ title: 'Twitter Lite' },
],
links: [{ rel: 'stylesheet', href: appCss }],
}),
shellComponent: RootDocument,
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
+12
View File
@@ -0,0 +1,12 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { Home } from './index'
describe('Home', () => {
it('starts without ambient post content', () => {
render(<Home />)
expect(screen.getByText('目的を決めてから開く')).toBeInTheDocument()
expect(screen.queryByRole('article')).not.toBeInTheDocument()
})
})
+13
View File
@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/')({ component: Home })
export function Home() {
return (
<main className="landing">
<p className="brand">TWITTER LITE</p>
<h1>目的を決めてから開く</h1>
<p>ユーザー名か検索語を入力するまで、投稿は表示しません。</p>
</main>
)
}
+35
View File
@@ -0,0 +1,35 @@
:root {
color: #1e3238;
background: #dfe7e9;
font-family: Inter, "Noto Sans JP", ui-sans-serif, system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
}
button,
input {
font: inherit;
}
.landing {
width: min(42rem, calc(100% - 2rem));
margin: 15vh auto;
padding: 2rem;
border: 1px solid #c1cfd2;
border-radius: 1rem;
background: #f8faf9;
}
.brand {
color: #70858a;
font-size: 0.7rem;
letter-spacing: 0.2em;
}
+5
View File
@@ -0,0 +1,5 @@
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
afterEach(cleanup)
+19
View File
@@ -0,0 +1,19 @@
{
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client", "node"],
"paths": { "#/*": ["./src/*"] },
"allowImportingTsExtensions": true,
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
+8
View File
@@ -0,0 +1,8 @@
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { nitro } from 'nitro/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [nitro(), tanstackStart(), viteReact()],
})
+11
View File
@@ -0,0 +1,11 @@
import path from 'node:path'
import { configDefaults, defineConfig } from 'vitest/config'
export default defineConfig({
resolve: { alias: { '#': path.resolve(import.meta.dirname, 'src') } },
test: {
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
exclude: [...configDefaults.exclude, 'tests/e2e/**'],
},
})