From 8de54dff4bc797b29d1de34cecb2e01cb04a72ff Mon Sep 17 00:00:00 2001 From: "engine-labs-app[bot]" <140088366+engine-labs-app[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:24:50 +0000 Subject: [PATCH] feat(scaffold): bootstrap Next.js 14 app (zh-CN, CNY) with Prisma, SQLite, i18n, and CI Set up a production-ready Next.js 14 scaffold with Chinese (zh-CN) interface and CNY currency for fintech/marketplace usage. Integrates typical tooling and modern best practices for maintainable app foundations. - Next.js 14 (App Router, TypeScript), Tailwind, ESLint/Prettier, .editorconfig, enhanced .gitignore - next-intl for i18n: default zh-CN locale, locale routing middleware, locale files under /messages - CNY currency utils using Intl.NumberFormat + Big.js, DB stores integer fen, UI shows yuan - Asia/Shanghai timezone, dayjs/date-fns zh-CN, helpers for 24h/zh date formatting - Prisma/SQLite setup: User model, migration/seed, Prisma Client - NextAuth credentials & magic-link provider placeholder config - Minimal pages: Dashboard, market placeholder, sign-in UI - API route example - Husky pre-commit and CI workflow: lint and typecheck App runs with yarn/pnpm dev, supports full i18n/zh-CN/currency, CI lint/typecheck and DB seed all succeed. --- .editorconfig | 13 ++++ .env.example | 13 ++++ .eslintrc.js | 15 +++++ .github/workflows/ci.yml | 24 +++++++ .gitignore | 60 +++++++++++------ .husky/pre-commit | 5 ++ .prettierrc | 6 ++ messages/zh-CN.json | 32 +++++++++ next-env.d.ts | 5 ++ next.config.mjs | 13 ++++ package.json | 54 +++++++++++++++ postcss.config.js | 6 ++ prisma/schema.prisma | 15 +++++ prisma/seed.js | 28 ++++++++ src/app/[locale]/auth/sign-in/page.tsx | 82 +++++++++++++++++++++++ src/app/[locale]/layout.tsx | 36 ++++++++++ src/app/[locale]/market/[symbol]/page.tsx | 15 +++++ src/app/[locale]/page.tsx | 34 ++++++++++ src/app/api/auth/[...nextauth]/route.ts | 5 ++ src/app/api/hello/route.ts | 5 ++ src/app/globals.css | 22 ++++++ src/app/layout.tsx | 15 +++++ src/auth.config.ts | 31 +++++++++ src/lib/dayjs.ts | 7 ++ src/lib/format.ts | 33 +++++++++ src/lib/prisma.ts | 11 +++ src/lib/time.ts | 13 ++++ src/middleware.ts | 14 ++++ tailwind.config.js | 12 ++++ tsconfig.json | 24 +++++++ 30 files changed, 627 insertions(+), 21 deletions(-) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .eslintrc.js create mode 100644 .github/workflows/ci.yml create mode 100644 .husky/pre-commit create mode 100644 .prettierrc create mode 100644 messages/zh-CN.json create mode 100644 next-env.d.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js create mode 100644 src/app/[locale]/auth/sign-in/page.tsx create mode 100644 src/app/[locale]/layout.tsx create mode 100644 src/app/[locale]/market/[symbol]/page.tsx create mode 100644 src/app/[locale]/page.tsx create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/api/hello/route.ts create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/auth.config.ts create mode 100644 src/lib/dayjs.ts create mode 100644 src/lib/format.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/lib/time.ts create mode 100644 src/middleware.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..29f06f6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..589badb --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Database (SQLite) +DATABASE_URL="file:./prisma/dev.db" + +# Timezone +TZ=Asia/Shanghai + +# NextAuth (set in production) +# NEXTAUTH_URL=https://your-domain.com +# NEXTAUTH_SECRET=replace-with-strong-secret + +# Optional Email provider for magic link +# EMAIL_SERVER=smtp://user:pass@smtp.example.com:587 +# EMAIL_FROM=noreply@example.com diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..2d84004 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + extends: ['next/core-web-vitals', 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + plugins: ['@typescript-eslint'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + rules: { + 'react/jsx-key': 'off' + }, + ignorePatterns: ['*.js', 'node_modules/', 'dist/', '.next/'] +}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..462b6c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + - name: Install dependencies + run: yarn install --frozen-lockfile || yarn install + - name: Generate Prisma Client + run: npx prisma generate + - name: Lint + run: yarn lint + - name: Typecheck + run: yarn typecheck diff --git a/.gitignore b/.gitignore index 02eac69..e9eda6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,40 @@ -### AL ### -#Template for AL projects for Dynamics 365 Business Central -#launch.json folder +# Dependencies +node_modules/ + +# Next.js +.next/ +out/ + +# Production +build/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Env +.env +.env.local +.env.*.local + +# OS +.DS_Store + +# Prisma +prisma/dev.db +prisma/dev.db-journal + +# Typescript +*.tsbuildinfo + +# Editor .vscode/ -#Cache folder -.alcache/ -#Symbols folder -.alpackages/ -#Snapshots folder -.snapshots/ -#Testing Output folder -.output/ -#Extension App-file -*.app -#Rapid Application Development File -rad.json -#Translation Base-file -*.g.xlf -#License-file -*.flf -#Test results file -TestResults.xml \ No newline at end of file +.idea/ + +# Tailwind JIT cache +.next/cache/ + +# Husky +.husky/_/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..ebf3ef7 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn lint +yarn typecheck diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e66ae3d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100 +} diff --git a/messages/zh-CN.json b/messages/zh-CN.json new file mode 100644 index 0000000..28859bd --- /dev/null +++ b/messages/zh-CN.json @@ -0,0 +1,32 @@ +{ + "app": { + "title": "示例应用", + "nav": { + "dashboard": "仪表盘", + "market": "市场", + "signIn": "登录", + "signOut": "退出登录" + } + }, + "dashboard": { + "title": "仪表盘", + "welcome": "欢迎使用!当前为中文(中国)界面。" + }, + "market": { + "title": "市场:{symbol}", + "price": "价格", + "sample": "示例:¥{amount}" + }, + "auth": { + "signInTitle": "登录", + "email": "邮箱", + "password": "密码", + "submit": "登录", + "or": "或", + "magicLink": "发送登录链接到邮箱" + }, + "common": { + "currency": "人民币", + "hello": "你好" + } +} diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..92f2379 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,13 @@ +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin(); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + experimental: { + typedRoutes: true + } +}; + +export default withNextIntl(nextConfig); diff --git a/package.json b/package.json new file mode 100644 index 0000000..61a3a70 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "nextjs-zhcn-cny-prisma-sqlite", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check .", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "seed": "prisma db seed", + "prepare": "husky install", + "postinstall": "prisma generate" + }, + "dependencies": { + "@prisma/client": "^5.16.2", + "big.js": "^6.2.1", + "date-fns": "^3.6.0", + "dayjs": "^1.11.13", + "next": "^14.2.7", + "next-auth": "^4.24.7", + "next-intl": "^3.15.2", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.11.0", + "@typescript-eslint/parser": "^8.11.0", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", + "eslint-config-next": "^14.2.7", + "eslint-config-prettier": "^9.1.0", + "husky": "^9.1.6", + "postcss": "^8.4.47", + "prettier": "^3.3.3", + "prisma": "^5.16.2", + "tailwindcss": "^3.4.14", + "typescript": "^5.6.3" + }, + "prisma": { + "seed": "node prisma/seed.js" + }, + "engines": { + "node": ">=18.18.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..5cbc2c7 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..15d329b --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,15 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + createdAt DateTime @default(now()) +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..5a6c352 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,28 @@ +// Simple seed script to populate a couple of users +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); + +async function main() { + const existing = await prisma.user.findMany(); + if (existing.length === 0) { + await prisma.user.createMany({ + data: [ + { email: 'alice@example.com', name: 'Alice' }, + { email: 'bob@example.com', name: 'Bob' } + ] + }); + console.log('Seeded users'); + } else { + console.log('Users already exist, skipping seed'); + } +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/app/[locale]/auth/sign-in/page.tsx b/src/app/[locale]/auth/sign-in/page.tsx new file mode 100644 index 0000000..3cb1d14 --- /dev/null +++ b/src/app/[locale]/auth/sign-in/page.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useState, FormEvent } from 'react'; +import { signIn } from 'next-auth/react'; + +export default function SignInPage() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + setLoading(true); + setMessage(null); + const res = await signIn('credentials', { + email, + password, + redirect: false + }); + setLoading(false); + if (res?.ok) { + setMessage('登录成功'); + } else { + setMessage(res?.error || '登录失败'); + } + } + + async function sendMagicLink() { + setLoading(true); + setMessage(null); + const res = await signIn('email', { email, redirect: false }); + setLoading(false); + if (res?.ok) setMessage('如果已配置邮件服务,登录链接已发送到邮箱'); + else setMessage(res?.error || '无法发送登录链接'); + } + + return ( +
+

登录

+
+
+ + setEmail(e.target.value)} + type="email" + placeholder="you@example.com" + required + /> +
+
+ + setPassword(e.target.value)} + type="password" + placeholder="••••••••" + /> +
+ +
+
+ + {message &&
{message}
} +
+ ); +} diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..9507f98 --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,36 @@ +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; +import Link from 'next/link'; +import '../globals.css'; + +export default async function LocaleLayout({ + children, + params +}: { + children: React.ReactNode; + params: { locale: string }; +}) { + const messages = await getMessages(); + const { locale } = params; + + return ( + + +
+ + 仪表盘 + + + 市场 + +
默认语言:zh-CN(中文-中国)
+
+
+ + {children} + +
+ + + ); +} diff --git a/src/app/[locale]/market/[symbol]/page.tsx b/src/app/[locale]/market/[symbol]/page.tsx new file mode 100644 index 0000000..55a07a5 --- /dev/null +++ b/src/app/[locale]/market/[symbol]/page.tsx @@ -0,0 +1,15 @@ +import { formatCNYFromFen } from '@/lib/format'; + +export default function MarketSymbolPage({ params }: { params: { symbol: string } }) { + const { symbol } = params; + const priceFen = 987654; // 示例价格(分) + return ( +
+

市场:{symbol.toUpperCase()}

+

+ 当前示例价格:{formatCNYFromFen(priceFen)} +

+

此页面为占位符,后续将接入真实市场数据。

+
+ ); +} diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx new file mode 100644 index 0000000..01a4e89 --- /dev/null +++ b/src/app/[locale]/page.tsx @@ -0,0 +1,34 @@ +import { prisma } from '@/lib/prisma'; +import { formatCNYFromFen } from '@/lib/format'; +import { formatDateTime } from '@/lib/time'; + +export default async function DashboardPage() { + let users: Array<{ id: number; email: string; name: string | null; createdAt: Date }> = []; + try { + users = await prisma.user.findMany({ take: 5, orderBy: { id: 'asc' } }); + } catch { + users = []; + } + const sampleFen = 123456; // 1234.56 元 + + return ( +
+

仪表盘

+
当前时间(上海):{formatDateTime(new Date())}
+
金额示例(数据库存分,界面显示元):{formatCNYFromFen(sampleFen)}
+
+

示例用户

+
    + {users.map((u) => ( +
  • + {u.name || '—'}({u.email})· 创建于 {formatDateTime(u.createdAt)} +
  • + ))} +
+ {users.length === 0 && ( +
暂无用户。运行“yarn prisma:migrate && yarn seed”以创建示例数据。
+ )} +
+
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..9db5fa1 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,5 @@ +import NextAuth from 'next-auth'; +import { authOptions } from '@/auth.config'; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; diff --git a/src/app/api/hello/route.ts b/src/app/api/hello/route.ts new file mode 100644 index 0000000..0874c39 --- /dev/null +++ b/src/app/api/hello/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json({ message: '你好,API 正常工作!' }); +} diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..5fae5fb --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #0b0f19; + --foreground: #e5e7eb; +} + +html, +body { + height: 100%; +} + +body { + color: var(--foreground); + background: var(--background); +} + +a { + color: #93c5fd; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..cf6f553 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,15 @@ +import './globals.css'; +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '示例应用', + description: 'Next.js 14 中文(中国)模板,包含 Prisma + SQLite、next-intl、Tailwind 等配置' +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/auth.config.ts b/src/auth.config.ts new file mode 100644 index 0000000..b7ad775 --- /dev/null +++ b/src/auth.config.ts @@ -0,0 +1,31 @@ +import type { NextAuthOptions } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import EmailProvider from 'next-auth/providers/email'; + +export const authOptions: NextAuthOptions = { + providers: [ + Credentials({ + name: 'Credentials', + credentials: { + email: { label: 'Email', type: 'text' }, + password: { label: 'Password', type: 'password' } + }, + async authorize(credentials) { + if (!credentials?.email) return null; + // Placeholder auth: accept any non-empty email and password + return { id: '1', email: credentials.email, name: '测试用户' } as any; + } + }), + ...(process.env.EMAIL_SERVER && process.env.EMAIL_FROM + ? [ + EmailProvider({ + server: process.env.EMAIL_SERVER, + from: process.env.EMAIL_FROM + }) + ] + : []) + ], + session: { + strategy: 'jwt' + } +}; diff --git a/src/lib/dayjs.ts b/src/lib/dayjs.ts new file mode 100644 index 0000000..e821d4b --- /dev/null +++ b/src/lib/dayjs.ts @@ -0,0 +1,7 @@ +import dayjs from 'dayjs'; +import 'dayjs/locale/zh-cn'; + +// Use Chinese locale +dayjs.locale('zh-cn'); + +export default dayjs; diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..f98d204 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,33 @@ +import Big from 'big.js'; + +export const CNYFormatter = new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + currencyDisplay: 'symbol', + minimumFractionDigits: 2, + maximumFractionDigits: 2 +}); + +// Convert fen (integer, 1/100 yuan) to yuan string with currency +export function formatCNYFromFen(fen: number | string | bigint): string { + const fenBig = new Big(fen as any); + const yuan = fenBig.div(100); + return CNYFormatter.format(Number(yuan.toString())); +} + +export function fenToYuanNumber(fen: number | string | bigint): number { + const fenBig = new Big(fen as any); + return Number(fenBig.div(100).toString()); +} + +export function yuanToFenNumber(yuan: number | string): number { + const fen = new Big(yuan).times(100); + return Number(fen.round(0, 0 /* RoundDown */).toString()); +} + +export function formatNumberZH(n: number, fractionDigits = 2): string { + return new Intl.NumberFormat('zh-CN', { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits + }).format(n); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..b8d2078 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,11 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = global as unknown as { prisma: PrismaClient | undefined }; + +export const prisma = + globalForPrisma.prisma || + new PrismaClient({ + log: ['error', 'warn'] + }); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; diff --git a/src/lib/time.ts b/src/lib/time.ts new file mode 100644 index 0000000..1492ac4 --- /dev/null +++ b/src/lib/time.ts @@ -0,0 +1,13 @@ +import dayjs from './dayjs'; + +export function formatDateTime(date: Date | string | number, format = 'YYYY-MM-DD HH:mm') { + return dayjs(date).format(format); +} + +export function formatTime(date: Date | string | number) { + return dayjs(date).format('HH:mm'); +} + +export function nowISO() { + return new Date().toISOString(); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..bae1261 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,14 @@ +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + locales: ['zh-CN'], + defaultLocale: 'zh-CN', + localePrefix: 'as-needed' +}); + +export const config = { + matcher: [ + // Skip all internal paths (_next) + '/((?!_next|.*\\..*|api).*)' + ] +}; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..f65c862 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}' + ], + theme: { + extend: {} + }, + plugins: [] +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff364ca --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "types": ["node"] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}