@undp/create-app 0.2.2 โ 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/bin/generateFiles/generateLayoutForNext.js +10 -5
- package/bin/generateFiles/generatePackageJson.js +3 -7
- package/bin/generateFiles/generatePageForNext.js +102 -0
- package/bin/generateFiles/index.js +1 -0
- package/bin/index.js +13 -7
- package/bin/promptUser.js +17 -4
- package/bin/templates/next/auth/app/layout.tsx +2 -1
- package/bin/templates/next/basic/app/layout.tsx +2 -1
- package/bin/templates/queryIntegrationForNext/integration/tanstack-query.tsx +27 -0
- package/bin/templates/vite/query/src/integration/tanstack-query.tsx +2 -1
- package/bin/templates/vite/query+router/src/integration/tanstack-query.tsx +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ๐๏ธ UNDP Create App CLI 
|
|
2
2
|
|
|
3
|
-
**`@undp/create-app`** is UNDPโs official **project scaffolding tool** for quickly bootstrapping frontend applications using **React**, and **TypeScript** โ complete with sensible defaults for ESLint, Prettier
|
|
3
|
+
**`@undp/create-app`** is UNDPโs official **project scaffolding tool** for quickly bootstrapping frontend applications using **React**, and **TypeScript** โ complete with sensible defaults for ESLint, Prettier.
|
|
4
4
|
|
|
5
5
|
[NPM Package](https://www.npmjs.com/package/@undp/create-app)
|
|
6
6
|
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
## ๐ Features
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- ๐ **Vite support** for fast SPA development, including optional **routing**
|
|
11
|
+
- โ๏ธ **React + TypeScript** setup out of the box
|
|
12
|
+
- ๐๏ธ Includes **Tailwind CSS** for styling
|
|
13
|
+
- ๐๏ธ Includes **Zustand** for state management
|
|
14
|
+
- ๐ **Vite support** for fast SPA development, including optional **routing** with **Tanstack router**
|
|
15
15
|
- ๐ **Next.js support** for full-stack apps, including optional **authentication** with **Better Auth**
|
|
16
|
-
-
|
|
16
|
+
- ๐จ Preconfigured for **UNDPโs Design System**
|
|
17
|
+
- โก๏ธ Optional integration with **Tanstack query** for data fetching
|
|
17
18
|
- ๐ Optional integration with **@undp/data-viz** for interactive data visualization
|
|
18
|
-
-
|
|
19
|
+
- โจ **ESLint** + **Prettier** preconfigured for consistent code style
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -26,9 +27,7 @@ All setups include **Tailwind CSS** for styling and **Zustand** for state manage
|
|
|
26
27
|
| Framework | Variants | Description |
|
|
27
28
|
|------------|-----------|-------------|
|
|
28
29
|
| **Vite** | Basic | Minimal React + TypeScript setup |
|
|
29
|
-
| **Vite** | + Query | Adds TanStack Query for data fetching |
|
|
30
30
|
| **Vite** | + Router | Adds TanStack Router for routing |
|
|
31
|
-
| **Vite** | + Router + Query | Full-featured Vite setup |
|
|
32
31
|
| **Next.js** | Basic | SSR and file-based routing |
|
|
33
32
|
| **Next.js** | + Auth | Includes authentication via Better Auth |
|
|
34
33
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export function generateLayoutForNext(dataViz, projectName) {
|
|
1
|
+
export function generateLayoutForNext(dataViz, projectName, query) {
|
|
2
2
|
return `import type { Metadata } from 'next';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
3
4
|
|
|
4
5
|
import HeaderEl from '@/components/Header';
|
|
5
|
-
import FooterEl from '@/components/Footer'
|
|
6
|
+
import FooterEl from '@/components/Footer';${query ? `
|
|
7
|
+
import TanStackQueryProvider from '@/integration/tanstack-query';`:''}
|
|
6
8
|
|
|
7
9
|
import './globals.css';
|
|
8
10
|
import '@undp/design-system-react/style.css';${dataViz ? `
|
|
@@ -16,14 +18,17 @@ export const metadata: Metadata = {
|
|
|
16
18
|
export default function RootLayout({
|
|
17
19
|
children,
|
|
18
20
|
}: Readonly<{
|
|
19
|
-
children:
|
|
21
|
+
children: ReactNode;
|
|
20
22
|
}>) {
|
|
21
23
|
return (
|
|
22
24
|
<html lang='en'>
|
|
23
25
|
<body className='flex flex-col gap-0 min-h-screen'>
|
|
24
26
|
<HeaderEl />
|
|
25
|
-
<main className='grow-1 flex flex-col justify-center'
|
|
26
|
-
<
|
|
27
|
+
<main className='grow-1 flex flex-col justify-center'>${query ? `
|
|
28
|
+
<TanStackQueryProvider>
|
|
29
|
+
<div className='flex flex-col justify-center'>{children}</div>
|
|
30
|
+
</TanStackQueryProvider>` : `
|
|
31
|
+
<div className='flex flex-col justify-center'>{children}</div>`}
|
|
27
32
|
</main>
|
|
28
33
|
<FooterEl />
|
|
29
34
|
</body>
|
|
@@ -38,16 +38,9 @@ export function generatePackageJson(config) {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
switch (config.framework) {
|
|
41
|
-
case 'vite-query':
|
|
42
|
-
dependencies['@tanstack/react-query'] = '^5.90.7';
|
|
43
|
-
break;
|
|
44
41
|
case 'vite-router':
|
|
45
42
|
dependencies['@tanstack/react-router'] = '^1.135.0';
|
|
46
43
|
break;
|
|
47
|
-
case 'vite-full':
|
|
48
|
-
dependencies['@tanstack/react-query'] = '^5.90.7';
|
|
49
|
-
dependencies['@tanstack/react-router'] = '^1.135.0';
|
|
50
|
-
break;
|
|
51
44
|
case 'next-basic':
|
|
52
45
|
dependencies['next'] = '16.0.1';
|
|
53
46
|
break;
|
|
@@ -58,6 +51,9 @@ export function generatePackageJson(config) {
|
|
|
58
51
|
default:
|
|
59
52
|
break;
|
|
60
53
|
}
|
|
54
|
+
if (config.query) {
|
|
55
|
+
dependencies['@tanstack/react-query'] = '^5.90.7';
|
|
56
|
+
}
|
|
61
57
|
if (config.libraries.includes('@undp/data-viz')) {
|
|
62
58
|
const dataVizVer = `^${getLatestVersion('@undp/data-viz')}`
|
|
63
59
|
dependencies['@undp/data-viz'] = dataVizVer;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export function generatePageForNext(query) {
|
|
2
|
+
return `'use client';
|
|
3
|
+
|
|
4
|
+
import { P } from '@undp/design-system-react/Typography';
|
|
5
|
+
import { Button } from '@undp/design-system-react/Button';
|
|
6
|
+
import Image from 'next/image';${query ? `
|
|
7
|
+
import { useQuery } from '@tanstack/react-query';
|
|
8
|
+
import { Spinner } from '@undp/design-system-react/Spinner';` :''}
|
|
9
|
+
|
|
10
|
+
import { useCounterActions, useCounter } from '@/stores/counter';${query ? `
|
|
11
|
+
|
|
12
|
+
function useTodoData() {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: ['todos'],
|
|
15
|
+
queryFn: () =>
|
|
16
|
+
Promise.resolve([
|
|
17
|
+
{ id: 1, name: 'Alice' },
|
|
18
|
+
{ id: 2, name: 'Bob' },
|
|
19
|
+
{ id: 3, name: 'Charlie' },
|
|
20
|
+
]),
|
|
21
|
+
});
|
|
22
|
+
}` :''}
|
|
23
|
+
|
|
24
|
+
export default function Home() {
|
|
25
|
+
const count = useCounter();
|
|
26
|
+
const { increment, decrement } = useCounterActions();${query ? `
|
|
27
|
+
|
|
28
|
+
const { data, isLoading, isError } = useTodoData();
|
|
29
|
+
|
|
30
|
+
if (isLoading) return <Spinner size='lg' className='my-20 m-auto' />;
|
|
31
|
+
|
|
32
|
+
if (isError) return <>Error</>;` : ''}
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<div className='flex gap-4 items-center justify-center my-8 mx-auto'>
|
|
36
|
+
<Image
|
|
37
|
+
className='dark:invert'
|
|
38
|
+
src='/imgs/next.svg'
|
|
39
|
+
alt='Next.js logo'
|
|
40
|
+
width={100}
|
|
41
|
+
height={20}
|
|
42
|
+
priority
|
|
43
|
+
/>
|
|
44
|
+
<P marginBottom='none'>&</P>
|
|
45
|
+
<Image
|
|
46
|
+
className='dark:invert'
|
|
47
|
+
src='/imgs/Tailwind_CSS_Logo.svg'
|
|
48
|
+
alt='Tailwind logo'
|
|
49
|
+
width={40}
|
|
50
|
+
height={20}
|
|
51
|
+
priority
|
|
52
|
+
/>
|
|
53
|
+
<P marginBottom='none'>&</P>
|
|
54
|
+
<Image
|
|
55
|
+
className='dark:invert'
|
|
56
|
+
src='/imgs/Zustand-logo.svg'
|
|
57
|
+
alt='Zustand logo'
|
|
58
|
+
width={40}
|
|
59
|
+
height={20}
|
|
60
|
+
priority
|
|
61
|
+
/>
|
|
62
|
+
<P marginBottom='none'>&</P>
|
|
63
|
+
<Image
|
|
64
|
+
className='dark:invert'
|
|
65
|
+
src='/imgs/undp-logo-blue.svg'
|
|
66
|
+
alt='UNDP logo'
|
|
67
|
+
width={40}
|
|
68
|
+
height={20}
|
|
69
|
+
priority
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<P marginBottom='xl' className='text-center'>
|
|
73
|
+
To get started, edit the App.tsx file.{' '}
|
|
74
|
+
<span className='font-bold'>Count: {count}</span>
|
|
75
|
+
</P>
|
|
76
|
+
<div className='flex gap-4 justify-center mb-8'>
|
|
77
|
+
<Button
|
|
78
|
+
variant='tertiary'
|
|
79
|
+
onClick={() => {
|
|
80
|
+
increment();
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
Increase counter
|
|
84
|
+
</Button>
|
|
85
|
+
<Button
|
|
86
|
+
variant='tertiary'
|
|
87
|
+
onClick={() => {
|
|
88
|
+
decrement();
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
Decrease counter
|
|
92
|
+
</Button>
|
|
93
|
+
</div>${query ? `
|
|
94
|
+
<P marginBottom='xl' className='text-center'>
|
|
95
|
+
Data loaded successfully. {data?.length} elements in the query.
|
|
96
|
+
</P>` : ''}
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
`
|
|
101
|
+
}
|
|
102
|
+
|
|
@@ -6,3 +6,4 @@ export { generateLayoutForNext } from './generateLayoutForNext.js';
|
|
|
6
6
|
export { generateStylesCss } from './generateStylesCss.js';
|
|
7
7
|
export { generateNextEnvTypes } from './generateNextEnvTypes.js';
|
|
8
8
|
export { generateEnv } from './generateEnv.js';
|
|
9
|
+
export { generatePageForNext } from './generatePageForNext.js';
|
package/bin/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
generateLayoutForNext,
|
|
18
18
|
generateNextEnvTypes,
|
|
19
19
|
generateEnv,
|
|
20
|
+
generatePageForNext,
|
|
20
21
|
} from './generateFiles/index.js';
|
|
21
22
|
|
|
22
23
|
function copyFolder(source, destination) {
|
|
@@ -45,14 +46,17 @@ async function main() {
|
|
|
45
46
|
let tertiaryFolder = 'basic';
|
|
46
47
|
|
|
47
48
|
switch (config.framework) {
|
|
48
|
-
case 'vite-
|
|
49
|
-
|
|
49
|
+
case 'vite-basic':
|
|
50
|
+
if (config.query)
|
|
51
|
+
tertiaryFolder = 'query';
|
|
52
|
+
else
|
|
53
|
+
tertiaryFolder = 'basic';
|
|
50
54
|
break;
|
|
51
55
|
case 'vite-router':
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if (config.query)
|
|
57
|
+
tertiaryFolder = 'query+router';
|
|
58
|
+
else
|
|
59
|
+
tertiaryFolder = 'router';
|
|
56
60
|
break;
|
|
57
61
|
case 'next-auth':
|
|
58
62
|
tertiaryFolder = 'auth';
|
|
@@ -73,9 +77,11 @@ async function main() {
|
|
|
73
77
|
}
|
|
74
78
|
} else {
|
|
75
79
|
copyFolder(path.join(__dirname, `./templates/${baseFolder}/${tertiaryFolder}`), projectPath);
|
|
76
|
-
fs.writeFileSync('app/layout.tsx', generateLayoutForNext(config.libraries.includes('@undp/data-viz'), config.projectName));
|
|
80
|
+
fs.writeFileSync('app/layout.tsx', generateLayoutForNext(config.libraries.includes('@undp/data-viz'), config.projectName, config.query));
|
|
77
81
|
fs.writeFileSync('next-env.d.ts', generateNextEnvTypes());
|
|
82
|
+
fs.writeFileSync('app/page.tsx', generatePageForNext(config.query));
|
|
78
83
|
if (config.framework === 'next-auth') fs.writeFileSync('.env.local', generateEnv());
|
|
84
|
+
if (config.query) copyFolder(path.join(__dirname, './templates/queryIntegrationForNext'), projectPath);
|
|
79
85
|
}
|
|
80
86
|
fs.writeFileSync('package.json', JSON.stringify(generatePackageJson(config), null, 2));
|
|
81
87
|
|
package/bin/promptUser.js
CHANGED
|
@@ -25,17 +25,29 @@ export async function promptUser(name) {
|
|
|
25
25
|
loop: false,
|
|
26
26
|
message: chalk.yellow('๐ฆ Choose your setup (all include Tailwind for styling & Zustand for state management):\n'),
|
|
27
27
|
choices: [
|
|
28
|
-
{ name: 'Vite โ Basic React setup', value: 'vite-basic' },
|
|
29
|
-
{ name: 'Vite + Query โ With TanStack Query', value: 'vite-query' },
|
|
28
|
+
{ name: 'Vite โ Basic React setup (ideal for embedding)', value: 'vite-basic' },
|
|
30
29
|
{ name: 'Vite + Router โ With routing support', value: 'vite-router' },
|
|
31
|
-
{ name: 'Vite + Router + Query โ Full Vite setup', value: 'vite-full' },
|
|
32
30
|
{ name: 'Next.js โ SSR and file routing', value: 'next-basic' },
|
|
33
31
|
{ name: 'Next.js + Auth โ With authentication', value: 'next-auth' },
|
|
34
32
|
],
|
|
35
33
|
default: 'vite-basic',
|
|
36
34
|
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const { addQuery } = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'list',
|
|
40
|
+
name: 'addQuery',
|
|
41
|
+
message: chalk.yellow(
|
|
42
|
+
'โ๏ธ Add @tanstack/query for data fetching?'
|
|
43
|
+
),
|
|
44
|
+
choices: ['Yes', 'No'],
|
|
45
|
+
default: 'No',
|
|
46
|
+
},
|
|
37
47
|
]);
|
|
38
48
|
|
|
49
|
+
const query = addQuery === 'Yes';
|
|
50
|
+
|
|
39
51
|
const libraryChoices = [
|
|
40
52
|
{
|
|
41
53
|
name: '@undp/data-viz โ UNDP data visualization components',
|
|
@@ -97,6 +109,7 @@ export async function promptUser(name) {
|
|
|
97
109
|
libraries,
|
|
98
110
|
addStaticWebAppConfig,
|
|
99
111
|
addPostCSSScripts,
|
|
100
|
-
framework,
|
|
112
|
+
framework,
|
|
113
|
+
query,
|
|
101
114
|
};
|
|
102
115
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
import HeaderEl from '@/components/Header';
|
|
4
5
|
import FooterEl from '@/components/Footer';
|
|
@@ -14,7 +15,7 @@ export const metadata: Metadata = {
|
|
|
14
15
|
export default function RootLayout({
|
|
15
16
|
children,
|
|
16
17
|
}: Readonly<{
|
|
17
|
-
children:
|
|
18
|
+
children: ReactNode;
|
|
18
19
|
}>) {
|
|
19
20
|
return (
|
|
20
21
|
<html lang='en'>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
import HeaderEl from '@/components/Header';
|
|
4
5
|
import FooterEl from '@/components/Footer';
|
|
@@ -15,7 +16,7 @@ export const metadata: Metadata = {
|
|
|
15
16
|
export default function RootLayout({
|
|
16
17
|
children,
|
|
17
18
|
}: Readonly<{
|
|
18
|
-
children:
|
|
19
|
+
children: ReactNode;
|
|
19
20
|
}>) {
|
|
20
21
|
return (
|
|
21
22
|
<html lang='en'>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { ReactNode, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export default function TanStackQueryProvider({
|
|
7
|
+
children,
|
|
8
|
+
}: {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}) {
|
|
11
|
+
// Create client only once
|
|
12
|
+
const [queryClient] = useState(
|
|
13
|
+
() =>
|
|
14
|
+
new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: {
|
|
17
|
+
staleTime: 1000 * 60 * 60 * 24, // how long fetched data is considered โfreshโ before it becomes โstaleโ ๐กข 24 hrs
|
|
18
|
+
gcTime: 1000 * 60 * 60 * 24, // how long inactive (unused) query data stays in memory before being deleted ๐กข 24 hrs
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
export function getContext() {
|
|
4
5
|
const queryClient = new QueryClient({
|
|
@@ -18,7 +19,7 @@ export function Provider({
|
|
|
18
19
|
children,
|
|
19
20
|
queryClient,
|
|
20
21
|
}: {
|
|
21
|
-
children:
|
|
22
|
+
children: ReactNode;
|
|
22
23
|
queryClient: QueryClient;
|
|
23
24
|
}) {
|
|
24
25
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
export function getContext() {
|
|
4
5
|
const queryClient = new QueryClient({
|
|
@@ -18,7 +19,7 @@ export function Provider({
|
|
|
18
19
|
children,
|
|
19
20
|
queryClient,
|
|
20
21
|
}: {
|
|
21
|
-
children:
|
|
22
|
+
children: ReactNode;
|
|
22
23
|
queryClient: QueryClient;
|
|
23
24
|
}) {
|
|
24
25
|
return (
|