diff --git a/.gitignore b/.gitignore index 4da7741a..85f13062 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage # Editor .vscode +.idea # System .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index d2bf4082..43bede40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Unreleased +## [0.21.7](https://github.com/o1-labs/zkapp-cli/compare/0.21.6...0.21.7) - 2024-09-19 + +### Changed + +- Updated Next.js project starter to use app router and skip `src` directory +- Forces TS in Next.js projects + ## [0.21.6](https://github.com/o1-labs/zkapp-cli/compare/0.21.5...0.21.6) - 2024-09-03 ### Fixed diff --git a/package.json b/package.json index 77400f60..cecd704e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zkapp-cli", - "version": "0.21.6", + "version": "0.21.7", "description": "CLI to create zkApps (zero-knowledge apps) for Mina Protocol", "homepage": "https://github.com/o1-labs/zkapp-cli/", "repository": { diff --git a/src/bin/index.js b/src/bin/index.js index db207252..296aa259 100755 --- a/src/bin/index.js +++ b/src/bin/index.js @@ -48,7 +48,7 @@ yargs(hideBin(process.argv)) // chalk.reset is a hack to force the terminal to retain a line break below chalk.reset( ` - + __ _ /_ | | | ___ | | __ _| |__ ___ diff --git a/src/lib/project.js b/src/lib/project.js index d1d41313..c7683b0d 100644 --- a/src/lib/project.js +++ b/src/lib/project.js @@ -7,7 +7,8 @@ import url from 'node:url'; import util from 'node:util'; import ora from 'ora'; import shell from 'shelljs'; -import customNextIndex from '../lib/ui/next/customNextIndex.js'; +import customNextPage from '../lib/ui/next/customNextPage.js'; +import customNextLayout from '../lib/ui/next/customNextLayout.js'; import customNuxtIndex from '../lib/ui/nuxt/customNuxtIndex.js'; import nuxtGradientBackground from '../lib/ui/nuxt/nuxtGradientBackground.js'; import customLayoutSvelte from '../lib/ui/svelte/customLayoutSvelte.js'; @@ -314,12 +315,13 @@ async function scaffoldNext(projectName) { // set the project name and default flags // https://nextjs.org/docs/api-reference/create-next-app#options let args = [ - 'create-next-app@14.2.3', + 'create-next-app@14.2.12', 'ui', '--use-npm', - '--src-dir', + '--no-src-dir', + '--ts', '--import-alias "@/*"', - '--no-app', + '--app', ]; spawnSync('npx', args, { @@ -328,19 +330,12 @@ async function scaffoldNext(projectName) { }); shell.rm('-rf', path.join('ui', '.git')); // Remove NextJS' .git; we will init .git in our monorepo's root. - // Read in the NextJS config file and add the middleware. - let useTypescript = true; - try { - // Determine if generated project is a ts project by looking for a tsconfig file - fs.readFileSync(path.join('ui', 'tsconfig.json')); - } catch (err) { - if (err.code !== 'ENOENT') { - console.error(err); - } - useTypescript = false; - } + // Removes create-next-app assets + fs.emptyDirSync(path.join('ui', 'public')); + fs.emptyDirSync(path.join('ui', 'app')); + // Read in the NextJS config file and add the middleware. const nextConfig = fs.readFileSync( path.join('ui', 'next.config.mjs'), 'utf8' @@ -354,8 +349,9 @@ const __dirname = path.dirname(__filename); `; newNextConfig += nextConfig.replace( - /^};(.*?)$/gm, // Search for the last '};' in the file. + '};', ` + reactStrictMode: false, webpack(config, { isServer }) { if (!isServer) { config.resolve.alias = { @@ -390,37 +386,36 @@ const __dirname = path.dirname(__filename); };` ); - // This prevents usEffect from running twice on initial mount. - newNextConfig = newNextConfig.replace( - 'reactStrictMode: true', - 'reactStrictMode: false' - ); - fs.writeFileSync(path.join('ui', 'next.config.mjs'), newNextConfig); - const indexFileName = useTypescript ? 'index.tsx' : 'index.js'; + const pageFileName = 'page.tsx'; fs.writeFileSync( - path.join('ui', 'src/pages', indexFileName), - customNextIndex, + path.join('ui', 'app', pageFileName), + customNextPage, + 'utf8' + ); + + const layoutFileName = 'layout.tsx'; + + fs.writeFileSync( + path.join('ui', 'app', layoutFileName), + customNextLayout, 'utf8' ); // Adds landing page components directory and files to NextJS project. fs.copySync( path.join(__dirname, 'ui', 'next', 'components'), - path.join('ui', 'src', 'components') + path.join('ui', 'components') ); // Adds landing page style directory and files to NextJS project. fs.copySync( path.join(__dirname, 'ui', 'next', 'styles'), - path.join('ui', 'src', 'styles') + path.join('ui', 'styles') ); - // Removes create-next-app assets - fs.emptyDirSync(path.join('ui', 'public')); - // Adds landing page assets directory and files to NextJS project. fs.copySync( path.join(__dirname, 'ui', 'next', 'assets'), @@ -452,23 +447,26 @@ const __dirname = path.dirname(__filename); "jsx": "preserve", "paths": { "@/*": ["./src/*"] - } + }, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } `; - if (useTypescript) { - fs.writeFileSync(path.join('ui', 'tsconfig.json'), tsconfig); + fs.writeFileSync(path.join('ui', 'tsconfig.json'), tsconfig); - // Add a script to the package.json - let x = fs.readJsonSync(path.join('ui', 'package.json')); - x.scripts['ts-watch'] = 'tsc --noEmit --incremental --watch'; - x.scripts['build'] = 'next build --no-lint'; - x.type = 'module'; - fs.writeJSONSync(path.join('ui', 'package.json'), x, { spaces: 2 }); - } + // Add a script to the package.json + let x = fs.readJsonSync(path.join('ui', 'package.json')); + x.scripts['ts-watch'] = 'tsc --noEmit --incremental --watch'; + x.scripts['build'] = 'next build --no-lint'; + x.type = 'module'; + fs.writeJSONSync(path.join('ui', 'package.json'), x, { spaces: 2 }); if (useGHPages) { const isWindows = process.platform === 'win32'; @@ -510,13 +508,6 @@ const __dirname = path.dirname(__filename); return config;` ); - // update page extensions - newNextConfig = newNextConfig.replace( - 'reactStrictMode: false,', - `reactStrictMode: false, - pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],` - ); - fs.writeFileSync(path.join('ui', 'next.config.mjs'), newNextConfig); // Add some scripts to the package.json @@ -548,42 +539,23 @@ const __dirname = path.dirname(__filename); ); shell.cd('..'); - const appFileName = useTypescript ? '_app.tsx' : '_app.js'; - const appPagesFileName = useTypescript ? '_app.page.tsx' : '_app.page.js'; - const indexFileName = useTypescript ? 'index.tsx' : 'index.js'; - const indexPagesFileName = useTypescript - ? 'index.page.tsx' - : 'index.page.js'; - const reactCOIServiceWorkerFileName = useTypescript - ? 'reactCOIServiceWorker.tsx' - : 'reactCOIServiceWorker.js'; - shell.mv( - path.join('ui', 'src/pages', appFileName), - path.join('ui', 'src/pages', appPagesFileName) - ); - shell.mv( - path.join('ui', 'src/pages', indexFileName), - path.join('ui', 'src/pages', indexPagesFileName) - ); + const reactCOIServiceWorkerFileName = 'reactCOIServiceWorker.tsx'; - let appFile = fs.readFileSync( - path.join('ui', 'src', 'pages', appPagesFileName), + let pageFile = fs.readFileSync( + path.join('ui', 'app', pageFileName), 'utf8' ); - appFile = appFile.replace( + pageFile = pageFile.replace( 'export default function', `import './reactCOIServiceWorker'; export default function` ); - fs.writeFileSync( - path.join('ui', 'src', 'pages', appPagesFileName), - appFile - ); + fs.writeFileSync(path.join('ui', 'app', pageFileName), pageFile); fs.writeFileSync( - path.join('ui', 'src', 'pages', reactCOIServiceWorkerFileName), + path.join('ui', 'app', reactCOIServiceWorkerFileName), `export {}; function loadCOIServiceWorker() { diff --git a/src/lib/project.test.js b/src/lib/project.test.js index c497d707..c7272b56 100644 --- a/src/lib/project.test.js +++ b/src/lib/project.test.js @@ -205,12 +205,13 @@ describe('project.js', () => { expect(spawnSync).toHaveBeenCalledWith( 'npx', [ - 'create-next-app@14.2.3', + 'create-next-app@14.2.12', 'ui', '--use-npm', - '--src-dir', + '--no-src-dir', + '--ts', '--import-alias "@/*"', - '--no-app', + '--app', ], { stdio: 'inherit', @@ -621,12 +622,13 @@ describe('project.js', () => { expect(spawnSync).toHaveBeenCalledWith( 'npx', [ - 'create-next-app@14.2.3', + 'create-next-app@14.2.12', 'ui', '--use-npm', - '--src-dir', + '--no-src-dir', + '--ts', '--import-alias "@/*"', - '--no-app', + '--app', ], { stdio: 'inherit', diff --git a/src/lib/ui/next/components/GradientBG.js b/src/lib/ui/next/components/GradientBG.js index bc434017..10ae2f62 100644 --- a/src/lib/ui/next/components/GradientBG.js +++ b/src/lib/ui/next/components/GradientBG.js @@ -1,5 +1,5 @@ // @ts-nocheck -import styles from '@/styles/Home.module.css'; +import styles from '../styles/Home.module.css'; import { useEffect, useState, useRef } from 'react'; export default function GradientBG({ children }) { diff --git a/src/lib/ui/next/customNextLayout.js b/src/lib/ui/next/customNextLayout.js new file mode 100644 index 00000000..b1ec8e65 --- /dev/null +++ b/src/lib/ui/next/customNextLayout.js @@ -0,0 +1,18 @@ +export default `import "../styles/globals.css"; + +export const metadata = { + title: 'Mina zkApp UI', + description: 'built with o1js', + icons: { + icon: '/assets/favicon.ico', + }, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +`; diff --git a/src/lib/ui/next/customNextIndex.js b/src/lib/ui/next/customNextPage.js similarity index 90% rename from src/lib/ui/next/customNextIndex.js rename to src/lib/ui/next/customNextPage.js index 483ed7f1..c5ef9dea 100644 --- a/src/lib/ui/next/customNextIndex.js +++ b/src/lib/ui/next/customNextPage.js @@ -1,26 +1,26 @@ -export default ` +export default `'use client'; import Head from 'next/head'; import Image from 'next/image'; import { useEffect } from 'react'; import GradientBG from '../components/GradientBG.js'; import styles from '../styles/Home.module.css'; -import heroMinaLogo from '../../public/assets/hero-mina-logo.svg'; -import arrowRightSmall from '../../public/assets/arrow-right-small.svg'; +import heroMinaLogo from '../public/assets/hero-mina-logo.svg'; +import arrowRightSmall from '../public/assets/arrow-right-small.svg'; export default function Home() { useEffect(() => { (async () => { const { Mina, PublicKey } = await import('o1js'); - const { Add } = await import('../../../contracts/build/src/'); + const { Add } = await import('../../contracts/build/src/'); // Update this to use the address (public key) for your zkApp account. // To try it out, you can try this address for an example "Add" smart contract that we've deployed to - // Testnet B62qkwohsqTBPsvhYE8cPZSpzJMgoKn4i1LQRuBAtVXWpaT4dgH6WoA. + // Testnet B62qnTDEeYtBHBePA4yhCt4TCgDtA4L2CGvK7PirbJyX4pKH8bmtWe5. const zkAppAddress = ''; // This should be removed once the zkAppAddress is updated. if (!zkAppAddress) { console.error( - 'The following error is caused because the zkAppAddress has an empty string as the public key. Update the zkAppAddress with the public key for your zkApp account, or try this address for an example "Add" smart contract that we deployed to Testnet: B62qkwohsqTBPsvhYE8cPZSpzJMgoKn4i1LQRuBAtVXWpaT4dgH6WoA' + 'The following error is caused because the zkAppAddress has an empty string as the public key. Update the zkAppAddress with the public key for your zkApp account, or try this address for an example "Add" smart contract that we deployed to Testnet: B62qnTDEeYtBHBePA4yhCt4TCgDtA4L2CGvK7PirbJyX4pKH8bmtWe5' ); } //const zkApp = new Add(PublicKey.fromBase58(zkAppAddress)) @@ -58,7 +58,7 @@ export default function Home() {

Get started by editing - src/pages/index.js or src/pages/index.tsx + app/page.tsx