@stjoseph/prumo 0.0.1
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/.prumo-templates/api/auth.md +130 -0
- package/.prumo-templates/api/config.md +85 -0
- package/.prumo-templates/api/errors.md +82 -0
- package/.prumo-templates/api/logging.md +79 -0
- package/.prumo-templates/api/modules.md +117 -0
- package/.prumo-templates/api/observability.md +91 -0
- package/.prumo-templates/api/pagination.md +71 -0
- package/.prumo-templates/api/routes.md +89 -0
- package/.prumo-templates/client/data.md +124 -0
- package/.prumo-templates/client/forms.md +88 -0
- package/.prumo-templates/core/conventions.md +74 -0
- package/.prumo-templates/core/testing.md +100 -0
- package/.prumo-templates/core/tooling.md +118 -0
- package/.prumo-templates/database/entities.md +97 -0
- package/.prumo-templates/database/migrations.md +74 -0
- package/.prumo-templates/database/queries.md +79 -0
- package/.prumo-templates/database/testing.md +82 -0
- package/.prumo-templates/database/transactions.md +68 -0
- package/.prumo-templates/mobile/components.md +83 -0
- package/.prumo-templates/mobile/config.md +84 -0
- package/.prumo-templates/mobile/routing.md +74 -0
- package/.prumo-templates/mobile/storage.md +84 -0
- package/.prumo-templates/mobile/structure.md +79 -0
- package/.prumo-templates/monorepo/dependencies.md +84 -0
- package/.prumo-templates/monorepo/layout.md +89 -0
- package/.prumo-templates/monorepo/tasks.md +76 -0
- package/.prumo-templates/multi-tenancy/client.md +83 -0
- package/.prumo-templates/multi-tenancy/isolation.md +104 -0
- package/.prumo-templates/site/rendering.md +83 -0
- package/.prumo-templates/site/structure.md +68 -0
- package/.prumo-templates/web/components.md +106 -0
- package/.prumo-templates/web/routing.md +82 -0
- package/.prumo-templates/web/structure.md +92 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/asker.js +28 -0
- package/dist/cli.js +62 -0
- package/dist/compose.js +222 -0
- package/dist/context.js +38 -0
- package/dist/generate.js +60 -0
- package/dist/jsonc.js +76 -0
- package/dist/names.js +10 -0
- package/dist/questions.js +63 -0
- package/package.json +55 -0
- package/templates/api/.env.example +12 -0
- package/templates/api/.githooks/install.mjs +18 -0
- package/templates/api/.githooks/pre-commit +2 -0
- package/templates/api/.githooks/pre-push +2 -0
- package/templates/api/README.md +35 -0
- package/templates/api/biome.jsonc +18 -0
- package/templates/api/docker/init/01-auth-schema.sql +1 -0
- package/templates/api/docker-compose.yml +20 -0
- package/templates/api/gitignore +5 -0
- package/templates/api/migrations/Migration20260914123239_create_profile.ts +13 -0
- package/templates/api/nest-cli.json +10 -0
- package/templates/api/package.json +69 -0
- package/templates/api/pnpm-workspace.yaml +9 -0
- package/templates/api/src/app.module.ts +46 -0
- package/templates/api/src/auth/auth.constants.ts +2 -0
- package/templates/api/src/auth/auth.controller.ts +19 -0
- package/templates/api/src/auth/auth.factory.spec.ts +29 -0
- package/templates/api/src/auth/auth.factory.ts +52 -0
- package/templates/api/src/auth/auth.guard.ts +43 -0
- package/templates/api/src/auth/auth.module.ts +30 -0
- package/templates/api/src/auth/auth.ts +4 -0
- package/templates/api/src/auth/current-user.decorator.ts +9 -0
- package/templates/api/src/auth/express.d.ts +10 -0
- package/templates/api/src/auth/public.decorator.ts +4 -0
- package/templates/api/src/config/env.module.ts +11 -0
- package/templates/api/src/config/env.ts +81 -0
- package/templates/api/src/errors/problem-details.filter.ts +86 -0
- package/templates/api/src/errors/validation-exception.factory.ts +30 -0
- package/templates/api/src/health/health.controller.ts +32 -0
- package/templates/api/src/health/health.module.ts +9 -0
- package/templates/api/src/main.ts +43 -0
- package/templates/api/src/mikro-orm.config.ts +4 -0
- package/templates/api/src/mikro-orm.factory.ts +14 -0
- package/templates/api/src/request-context/request-context.module.ts +9 -0
- package/templates/api/src/request-context/request-context.service.ts +17 -0
- package/templates/api/src/request-context/request-id.middleware.ts +19 -0
- package/templates/api/src/users/dto/update-profile.dto.ts +20 -0
- package/templates/api/src/users/entities/profile.entity.ts +27 -0
- package/templates/api/src/users/users.controller.ts +23 -0
- package/templates/api/src/users/users.module.ts +10 -0
- package/templates/api/src/users/users.service.spec.ts +44 -0
- package/templates/api/src/users/users.service.ts +36 -0
- package/templates/api/test/factories/profile.factory.ts +26 -0
- package/templates/api/test/factories/user.factory.ts +21 -0
- package/templates/api/test/global-setup.ts +20 -0
- package/templates/api/test/setup.ts +80 -0
- package/templates/api/tsconfig.build.json +8 -0
- package/templates/api/tsconfig.json +29 -0
- package/templates/api/vitest.config.mts +17 -0
- package/templates/mobile/.env.example +1 -0
- package/templates/mobile/.githooks/install.mjs +18 -0
- package/templates/mobile/.githooks/pre-commit +2 -0
- package/templates/mobile/.githooks/pre-push +2 -0
- package/templates/mobile/README.md +30 -0
- package/templates/mobile/app.config.ts +13 -0
- package/templates/mobile/app.json +39 -0
- package/templates/mobile/assets/expo.icon/Assets/expo-symbol 2.svg +3 -0
- package/templates/mobile/assets/expo.icon/Assets/grid.png +0 -0
- package/templates/mobile/assets/expo.icon/icon.json +35 -0
- package/templates/mobile/assets/images/android-icon-background.png +0 -0
- package/templates/mobile/assets/images/android-icon-foreground.png +0 -0
- package/templates/mobile/assets/images/android-icon-monochrome.png +0 -0
- package/templates/mobile/assets/images/icon.png +0 -0
- package/templates/mobile/assets/images/splash-icon.png +0 -0
- package/templates/mobile/babel.config.js +6 -0
- package/templates/mobile/biome.jsonc +21 -0
- package/templates/mobile/gitignore +11 -0
- package/templates/mobile/global.d.ts +1 -0
- package/templates/mobile/jest.config.js +21 -0
- package/templates/mobile/metro.config.js +4 -0
- package/templates/mobile/nativewind-env.d.ts +1 -0
- package/templates/mobile/package.json +63 -0
- package/templates/mobile/src/api-contract/api-error.ts +51 -0
- package/templates/mobile/src/api-contract/client.ts +26 -0
- package/templates/mobile/src/api-contract/index.ts +10 -0
- package/templates/mobile/src/api-contract/profile.ts +27 -0
- package/templates/mobile/src/app/(app)/_layout.tsx +18 -0
- package/templates/mobile/src/app/(app)/index.tsx +18 -0
- package/templates/mobile/src/app/(auth)/sign-in.tsx +24 -0
- package/templates/mobile/src/app/(auth)/sign-up.tsx +23 -0
- package/templates/mobile/src/app/_layout.tsx +54 -0
- package/templates/mobile/src/components/ui/button.tsx +36 -0
- package/templates/mobile/src/components/ui/field.tsx +30 -0
- package/templates/mobile/src/config.ts +3 -0
- package/templates/mobile/src/features/auth/auth-client.ts +27 -0
- package/templates/mobile/src/features/auth/intended-route.ts +11 -0
- package/templates/mobile/src/features/auth/session.ts +17 -0
- package/templates/mobile/src/features/auth/sign-in-form.spec.tsx +37 -0
- package/templates/mobile/src/features/auth/sign-in-form.tsx +80 -0
- package/templates/mobile/src/features/auth/sign-up-form.tsx +99 -0
- package/templates/mobile/src/features/clients/clients-context.tsx +21 -0
- package/templates/mobile/src/features/forms/form-error.tsx +16 -0
- package/templates/mobile/src/features/forms/server-errors.ts +33 -0
- package/templates/mobile/src/features/profile/profile-form.spec.tsx +58 -0
- package/templates/mobile/src/features/profile/profile-form.tsx +96 -0
- package/templates/mobile/src/features/storage/query-persister.ts +15 -0
- package/templates/mobile/src/features/storage/storage.ts +11 -0
- package/templates/mobile/src/global.css +3 -0
- package/templates/mobile/tailwind.config.js +9 -0
- package/templates/mobile/test/fake-transport.ts +23 -0
- package/templates/mobile/tsconfig.json +21 -0
- package/templates/site/.githooks/install.mjs +18 -0
- package/templates/site/.githooks/pre-commit +2 -0
- package/templates/site/.githooks/pre-push +2 -0
- package/templates/site/README.md +28 -0
- package/templates/site/biome.jsonc +12 -0
- package/templates/site/components.json +25 -0
- package/templates/site/gitignore +7 -0
- package/templates/site/next.config.ts +7 -0
- package/templates/site/package.json +40 -0
- package/templates/site/pnpm-workspace.yaml +5 -0
- package/templates/site/postcss.config.mjs +7 -0
- package/templates/site/src/app/globals.css +129 -0
- package/templates/site/src/app/layout.tsx +13 -0
- package/templates/site/src/app/page.spec.ts +10 -0
- package/templates/site/src/app/page.tsx +17 -0
- package/templates/site/src/lib/utils.ts +1 -0
- package/templates/site/tsconfig.json +34 -0
- package/templates/site/vitest.config.mts +8 -0
- package/templates/web/.env.example +1 -0
- package/templates/web/.githooks/install.mjs +18 -0
- package/templates/web/.githooks/pre-commit +2 -0
- package/templates/web/.githooks/pre-push +2 -0
- package/templates/web/README.md +29 -0
- package/templates/web/biome.jsonc +24 -0
- package/templates/web/components.json +25 -0
- package/templates/web/gitignore +6 -0
- package/templates/web/index.html +12 -0
- package/templates/web/package.json +55 -0
- package/templates/web/src/api-contract/api-error.ts +51 -0
- package/templates/web/src/api-contract/client.ts +26 -0
- package/templates/web/src/api-contract/index.ts +10 -0
- package/templates/web/src/api-contract/profile.ts +27 -0
- package/templates/web/src/app.spec.tsx +12 -0
- package/templates/web/src/app.tsx +14 -0
- package/templates/web/src/components/ui/button.tsx +57 -0
- package/templates/web/src/components/ui/field.tsx +224 -0
- package/templates/web/src/components/ui/input.tsx +19 -0
- package/templates/web/src/components/ui/label.tsx +17 -0
- package/templates/web/src/components/ui/separator.tsx +20 -0
- package/templates/web/src/config.ts +7 -0
- package/templates/web/src/features/auth/auth-client.ts +12 -0
- package/templates/web/src/features/auth/session.ts +17 -0
- package/templates/web/src/features/auth/sign-in-form.spec.tsx +30 -0
- package/templates/web/src/features/auth/sign-in-form.tsx +73 -0
- package/templates/web/src/features/auth/sign-up-form.tsx +79 -0
- package/templates/web/src/features/forms/form-error.tsx +15 -0
- package/templates/web/src/features/forms/server-errors.ts +33 -0
- package/templates/web/src/features/profile/profile-form.spec.tsx +58 -0
- package/templates/web/src/features/profile/profile-form.tsx +74 -0
- package/templates/web/src/index.css +130 -0
- package/templates/web/src/lib/utils.ts +1 -0
- package/templates/web/src/main.tsx +28 -0
- package/templates/web/src/routeTree.gen.ts +125 -0
- package/templates/web/src/router.tsx +21 -0
- package/templates/web/src/routes/__root.tsx +10 -0
- package/templates/web/src/routes/_authenticated/index.tsx +21 -0
- package/templates/web/src/routes/_authenticated.tsx +15 -0
- package/templates/web/src/routes/sign-in.tsx +20 -0
- package/templates/web/src/routes/sign-up.tsx +17 -0
- package/templates/web/test/fake-transport.ts +23 -0
- package/templates/web/test/render-app.tsx +17 -0
- package/templates/web/test/setup.ts +7 -0
- package/templates/web/tsconfig.json +21 -0
- package/templates/web/vite.config.ts +18 -0
- package/templates/workspace/.githooks/install.mjs +18 -0
- package/templates/workspace/.githooks/pre-commit +2 -0
- package/templates/workspace/.githooks/pre-push +2 -0
- package/templates/workspace/README.md +25 -0
- package/templates/workspace/biome.jsonc +8 -0
- package/templates/workspace/gitignore +5 -0
- package/templates/workspace/package.json +17 -0
- package/templates/workspace/packages/api-contract/package.json +20 -0
- package/templates/workspace/packages/api-contract/tsconfig.json +11 -0
- package/templates/workspace/pnpm-workspace.yaml +3 -0
- package/templates/workspace/tsconfig.base.json +10 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Rendering and data
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Use the App Router. Do not use the Pages Router, and do not mix the two.
|
|
6
|
+
|
|
7
|
+
Keep every route static by default. Use revalidation where content changes without a deploy. Make a route
|
|
8
|
+
dynamic only when the response depends on who asked, and write one comment line saying why.
|
|
9
|
+
|
|
10
|
+
Fetch data on the server: in a Server Component or in the static generation function. Pass it to client
|
|
11
|
+
components as props.
|
|
12
|
+
|
|
13
|
+
Do not call the API from the browser to render a page. A browser call exists only for interaction after
|
|
14
|
+
load, such as submitting a form.
|
|
15
|
+
|
|
16
|
+
Treat calls from the site's server as anonymous. They carry no user cookie.
|
|
17
|
+
|
|
18
|
+
**Before writing Next code, read the guide for the installed version in `node_modules/next/dist/docs/`.** It wins over
|
|
19
|
+
anything remembered. Keep `agentRules: false` in `next.config.ts`.
|
|
20
|
+
|
|
21
|
+
## Rationale
|
|
22
|
+
|
|
23
|
+
This type exists because of SEO, so a page that does not arrive rendered defeats it. Fetching through
|
|
24
|
+
`useQuery` would leave the generated HTML empty and show the crawler a shell, and then there was no
|
|
25
|
+
reason to separate `site` from `web` at all.
|
|
26
|
+
|
|
27
|
+
**The default must be static because in the App Router a route becomes dynamic by accident.** Reading a
|
|
28
|
+
cookie, a header or a search param inside a Server Component switches the whole route to per-request
|
|
29
|
+
rendering, with no error and no warning: a page that was instant starts waiting on a server. With a static
|
|
30
|
+
default that is a departure from a written rule and shows up in review; with a dynamic default nobody
|
|
31
|
+
notices.
|
|
32
|
+
|
|
33
|
+
Making everything static forever breaks the first time somebody wants to publish a paragraph without
|
|
34
|
+
calling a developer, which is what revalidation is for. Making everything dynamic inverts the cost: an
|
|
35
|
+
institutional page that rarely changes would pay for a server on every visit, including the crawler's, and
|
|
36
|
+
crawlers visit often.
|
|
37
|
+
|
|
38
|
+
The site's server has no user, so there is nobody to authenticate as. A page needing authenticated data is
|
|
39
|
+
neither static nor public, and probably does not belong to this type.
|
|
40
|
+
|
|
41
|
+
**The installed documentation wins because Next says its own APIs moved.** Next 16 ships its guides inside the package
|
|
42
|
+
and, when `next dev` detects an AI coding agent, writes a block into `AGENTS.md` telling it to read them, creating
|
|
43
|
+
`AGENTS.md` and `CLAUDE.md` where none exist, which in a workspace means inside `apps/site`, beside the root's. The
|
|
44
|
+
advice is kept here, where every other rule lives, and the automatic write is turned off so no second context file
|
|
45
|
+
appears on its own.
|
|
46
|
+
|
|
47
|
+
## Applies to
|
|
48
|
+
|
|
49
|
+
Every route under `app/`.
|
|
50
|
+
|
|
51
|
+
**This area uses half of `client/data.md`.** The wire contract (base URL, `/api/v1`, hand-written types,
|
|
52
|
+
the `ApiError`) applies. The TanStack Query conventions do not: there is no client-side cache to key,
|
|
53
|
+
invalidate or paginate.
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
Where a fetch happens:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
✅ // app/posts/page.tsx: a Server Component
|
|
61
|
+
const posts = await getPosts()
|
|
62
|
+
❌ 'use client'
|
|
63
|
+
const { data } = useQuery(postsQuery())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Going dynamic:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
✅ // dynamic: the response depends on the visitor's country header
|
|
70
|
+
❌ const country = headers().get('x-country') // no comment, whole route now dynamic
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Enforcement
|
|
74
|
+
|
|
75
|
+
**Review only, and this is the line that matters: nothing warns when a route slips into being dynamic.**
|
|
76
|
+
The clue is indirect: somebody added a cookie or header read three components down, and the page quietly
|
|
77
|
+
stopped being static. The symptom is latency, not an error.
|
|
78
|
+
|
|
79
|
+
Also review: that no page renders its content from a browser fetch, and that a dynamic route carries its
|
|
80
|
+
reason.
|
|
81
|
+
|
|
82
|
+
**Worth knowing:** most written material about Next describes the Pages Router, so an assistant without
|
|
83
|
+
this document mixes `getServerSideProps` with Server Components. They do not compose.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Structure
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Organise by feature. Put what is reused across routes in `features/<name>/`.
|
|
6
|
+
|
|
7
|
+
Keep a route's content in its own `page.tsx` or `layout.tsx`, alongside its `generateMetadata` and
|
|
8
|
+
`generateStaticParams`. **Do not push a page's content into a feature.**
|
|
9
|
+
|
|
10
|
+
Put shared code in one of four places and nowhere else:
|
|
11
|
+
|
|
12
|
+
- `components/ui/`: shadcn primitives, where its CLI installs them
|
|
13
|
+
- `api-contract/`: the wire contract with the API, as `client/data.md` describes, once a page calls the API
|
|
14
|
+
- `lib/`: pure functions, with no state and no React
|
|
15
|
+
- a named feature: anything with behaviour
|
|
16
|
+
|
|
17
|
+
Do not create a `shared/` or `common/` folder.
|
|
18
|
+
|
|
19
|
+
Style with Tailwind and shadcn/ui. Express variants with `cva`. Extract a repeated class list on its third
|
|
20
|
+
occurrence, as a component.
|
|
21
|
+
|
|
22
|
+
Resolve `@/` to the app's own `src/`.
|
|
23
|
+
|
|
24
|
+
## Rationale
|
|
25
|
+
|
|
26
|
+
**Metadata is part of a page's content, not of its navigation.** A title, a description and a share image
|
|
27
|
+
describe that specific text, so separating them makes it easy to change one without the other, and a page
|
|
28
|
+
with the wrong title is a defect that surfaces only when somebody shares the link.
|
|
29
|
+
|
|
30
|
+
That is why this area departs from `web` and `mobile`, which both keep thin route files pointing at
|
|
31
|
+
features. The departure is not preference: only in the App Router does a route file carry SEO metadata,
|
|
32
|
+
and that is precisely what distinguishes this type.
|
|
33
|
+
|
|
34
|
+
Everything else carries over from the web area for the reasons given there. A `shared/` folder has no
|
|
35
|
+
admission criterion, so it only grows; not everything shared is a component, and a formatting helper is a
|
|
36
|
+
function in a file. shadcn's plumbing (`cn()`, `cva`, the theme tokens) is used from the first
|
|
37
|
+
hand-written component onward, whether or not any component is installed, which is why it is here even on
|
|
38
|
+
a site with four screens.
|
|
39
|
+
|
|
40
|
+
## Applies to
|
|
41
|
+
|
|
42
|
+
Every file under `src/` and `app/`.
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
Where a page lives:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
✅ app/pricing/page.tsx → the content and generateMetadata together
|
|
50
|
+
❌ app/pricing/page.tsx → renders <PricingPage/> from features/pricing
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Shared code:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
✅ features/newsletter/ // reused on three routes
|
|
57
|
+
lib/format-date.ts
|
|
58
|
+
❌ shared/components/cta.tsx
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Enforcement
|
|
62
|
+
|
|
63
|
+
**Review only.** That a page's metadata sits with its content, that no `shared/` folder appeared, and that
|
|
64
|
+
a third repetition was extracted.
|
|
65
|
+
|
|
66
|
+
**An asymmetry worth knowing:** `web` and `mobile` keep route files thin and push screens into features.
|
|
67
|
+
This area does the opposite, and only for the metadata reason above. It is deliberate rather than an
|
|
68
|
+
inconsistency to tidy up.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Edit a shadcn component in place. Do not wrap it, and do not keep a parallel component around it.
|
|
6
|
+
|
|
7
|
+
Extract a repeated Tailwind class list on its **third** occurrence, and extract it as a component, never
|
|
8
|
+
as a string constant.
|
|
9
|
+
|
|
10
|
+
Express variants with `cva`, in every component, including ones that did not come from shadcn.
|
|
11
|
+
|
|
12
|
+
Do not sort classes automatically, and do not add a Prettier plugin to do it.
|
|
13
|
+
|
|
14
|
+
Components are built on Base UI. **Change the rendered element with the `render` prop, never `asChild`.** Pass
|
|
15
|
+
`nativeButton={false}` when a button renders something that is not a `<button>`.
|
|
16
|
+
|
|
17
|
+
## Rationale
|
|
18
|
+
|
|
19
|
+
shadcn copies files into the project rather than shipping a dependency, so the component is yours to edit.
|
|
20
|
+
Wrapping instead would produce a parallel set (a `Button` around `ui/Button`), and then every screen has
|
|
21
|
+
to know which to import; half will import the wrong one, and the wrapper stops applying exactly where it
|
|
22
|
+
mattered.
|
|
23
|
+
|
|
24
|
+
The third occurrence is the threshold because the second is often not a repetition: two things that look
|
|
25
|
+
alike today and diverge next week. Extracting then couples them, and the next change needs a prop to tell
|
|
26
|
+
them apart. A number is arbitrary, and having one stops two developers and an assistant from each using a
|
|
27
|
+
different threshold. A component rather than a string constant, because a constant shares appearance
|
|
28
|
+
without structure: every site still writes its own wrapper, and there is nowhere to put the extra one
|
|
29
|
+
when it is needed.
|
|
30
|
+
|
|
31
|
+
`cva` is already present and already how every installed component is written. Using conditionals
|
|
32
|
+
alongside it would leave two variant grammars in one project, divided by where a file came from, a
|
|
33
|
+
boundary made of history rather than nature. It also declares the combinations, so TypeScript knows which
|
|
34
|
+
values exist and a typo does not compile, where a concatenated string lets two conflicting variants
|
|
35
|
+
coexist and the last class win by accident.
|
|
36
|
+
|
|
37
|
+
**`render`, because `asChild` does not exist here** and is what almost every tutorial and every assistant's
|
|
38
|
+
training shows: shadcn ran on Radix for years. The compiler rejects `asChild` on these components, and that is
|
|
39
|
+
where the real risk starts: an assistant meeting the error tends to satisfy it the wrong way, by dropping the
|
|
40
|
+
`Button` for a styled `Link` or by wrapping it. Base UI's own documentation states the difference in one line:
|
|
41
|
+
Radix uses `asChild`, Base UI uses `render`.
|
|
42
|
+
|
|
43
|
+
Class order in the attribute changes nothing: Tailwind generates the CSS in its own order, so sorting is
|
|
44
|
+
cosmetic. Biome's rule for it is experimental, its fix is classified unsafe and therefore does not run in
|
|
45
|
+
the pre-commit hook, and it does not yet handle screen variants or plugin utilities.
|
|
46
|
+
|
|
47
|
+
## Applies to
|
|
48
|
+
|
|
49
|
+
Every component under `src/`.
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
Changing a primitive:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
✅ edit components/ui/button.tsx
|
|
57
|
+
❌ components/button.tsx exporting a wrapper around ui/Button
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Extraction:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
✅ third time the same list appears → <Card> component
|
|
64
|
+
❌ second time → const cardClasses = "flex items-center gap-2 …"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Rendering a link as a button:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
✅ <Button render={<Link to="/sign-up" />} nativeButton={false}>Sign up</Button>
|
|
71
|
+
❌ <Button asChild><Link to="/sign-up">Sign up</Link></Button> // Radix's idiom
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Variants:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
✅ cva('inline-flex …', { variants: { size: { sm: …, lg: … } } })
|
|
78
|
+
❌ className={`inline-flex ${size === 'sm' ? 'h-8' : 'h-10'} ${tone}`}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Enforcement
|
|
82
|
+
|
|
83
|
+
**Compiler.** `cva` makes variant names typed, so a misspelled variant does not build. `asChild` on a Base UI
|
|
84
|
+
component does not build either.
|
|
85
|
+
|
|
86
|
+
**Review only.** That a third repetition was extracted, and that nobody wrapped a shadcn primitive instead
|
|
87
|
+
of editing it.
|
|
88
|
+
|
|
89
|
+
**Two things to know rather than discover:**
|
|
90
|
+
|
|
91
|
+
`cn` is imported from the npm package **`cn`**, published by shadcn, which the CLI now installs in place of
|
|
92
|
+
`clsx` and `tailwind-merge`. Code importing either of those is copied from older material.
|
|
93
|
+
|
|
94
|
+
Biome's recommended preset rejects two things in generated primitives that are correct there (a label that
|
|
95
|
+
receives `htmlFor` through props, and `role="group"` on a field), so those two rules are off for
|
|
96
|
+
`components/ui/` only, as `core/tooling.md` records. Anything else it reports in generated code is fixed in
|
|
97
|
+
place.
|
|
98
|
+
|
|
99
|
+
`shadcn init` can stop on an interactive prompt even with `-y`. Pass every choice as a flag
|
|
100
|
+
(`shadcn init -b base -p nova --no-monorepo -y`) rather than waiting on it.
|
|
101
|
+
|
|
102
|
+
Re-running the shadcn CLI for a component **overwrites** your edits: it neither versions nor merges.
|
|
103
|
+
Reinstalling one is a deliberate act with the diff inspected, not part of updating dependencies.
|
|
104
|
+
|
|
105
|
+
Automatic class sorting is worth revisiting when Biome's `useSortedClasses` leaves the nursery group and
|
|
106
|
+
its fix becomes safe. It then runs inside the existing hook with nothing else changing.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Define routes as files and let TanStack Router's Vite plugin generate the route tree. **Commit the
|
|
6
|
+
generated tree.**
|
|
7
|
+
|
|
8
|
+
Wrap everything requiring a session in an authenticated layout route that redirects in its `beforeLoad`.
|
|
9
|
+
A public route is one placed outside that layout.
|
|
10
|
+
|
|
11
|
+
Validate every search param with a Zod schema in the route's `validateSearch`. Let path params take the
|
|
12
|
+
types the router derives from the file name.
|
|
13
|
+
|
|
14
|
+
Let TanStack Query own the cache. Use a route loader only to call `ensureQueryData`, and read the data in
|
|
15
|
+
the component through the Query hook. Never pass loader data down as props.
|
|
16
|
+
|
|
17
|
+
## Rationale
|
|
18
|
+
|
|
19
|
+
The generated route tree is committed because application code imports it, so without it `tsc` fails on a
|
|
20
|
+
clean clone, breaking CI and the editor of whoever just cloned, and making *run the dev server first* a
|
|
21
|
+
prerequisite for any task. The cost is merge conflicts on a generated file, always resolved by
|
|
22
|
+
regenerating. A broken clone is worse than a predictable conflict.
|
|
23
|
+
|
|
24
|
+
The authenticated layout means **protection is expressed by where a file lives**. For a route to be born
|
|
25
|
+
unprotected, somebody has to place it outside the layout deliberately; forgetting is not enough.
|
|
26
|
+
|
|
27
|
+
**Client-side protection is not security.** The API refuses through its own guard, so a forgotten
|
|
28
|
+
redirect leaks nothing; it costs a flash of broken interface while the page mounts, fires a request and
|
|
29
|
+
receives 401. This is stated so nobody treats the client as the boundary and relaxes on the server.
|
|
30
|
+
|
|
31
|
+
A search param is untrusted input that does not pass through the API on its way in. Left unvalidated, a
|
|
32
|
+
component feeding `?limit` into a request becomes the path by which a junk value reaches the server,
|
|
33
|
+
which refuses it, but only after a round trip and with an error the user cannot read.
|
|
34
|
+
|
|
35
|
+
The loader prefetches rather than fetches because two caches holding the same data is how *why did the
|
|
36
|
+
screen not update* becomes unanswerable. Data handed down as props sits in no cache, so invalidating
|
|
37
|
+
after a mutation never reaches it.
|
|
38
|
+
|
|
39
|
+
## Applies to
|
|
40
|
+
|
|
41
|
+
Every file under `routes/`.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
Protection by placement:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
✅ routes/_authenticated/orders.tsx → protected
|
|
49
|
+
routes/login.tsx → public
|
|
50
|
+
❌ routes/orders.tsx with an if (!user) inside the component
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Search params:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
✅ validateSearch: z.object({ limit: z.number().max(100).default(20) })
|
|
57
|
+
❌ validateSearch: (s) => s as { limit: number }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Loading data:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
✅ loader: ({ context }) => context.queryClient.ensureQueryData(ordersQuery())
|
|
64
|
+
// component: useSuspenseQuery(ordersQuery())
|
|
65
|
+
❌ loader: () => fetchOrders()
|
|
66
|
+
// component: props.orders
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Enforcement
|
|
70
|
+
|
|
71
|
+
**Structural.** A route inside the authenticated layout cannot skip the redirect, because the layout runs
|
|
72
|
+
first.
|
|
73
|
+
|
|
74
|
+
**Compiler.** `validateSearch` makes the search params' types reflect what survived validation, so a
|
|
75
|
+
component cannot read a field the schema does not produce.
|
|
76
|
+
|
|
77
|
+
**Review only.** That the loader prefetches rather than returning data the component consumes, and that a
|
|
78
|
+
route needing a session was not placed outside the layout.
|
|
79
|
+
|
|
80
|
+
**A tie worth knowing:** the query key is shared between the loader and the component. If they differ, the
|
|
81
|
+
prefetch fills one cache entry and the component reads another, paying for the request twice and gaining
|
|
82
|
+
nothing. Where that key lives is settled in `data.md`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Structure
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Organise by feature. Put a feature's components, hooks and queries together in `features/<name>/`. Keep
|
|
6
|
+
`routes/` for thin route definitions that point at features.
|
|
7
|
+
|
|
8
|
+
**Do not organise by type.** There is no root `components/`, `hooks/` or `pages/` holding every
|
|
9
|
+
feature's files.
|
|
10
|
+
|
|
11
|
+
Put shared code in one of four places and nowhere else:
|
|
12
|
+
|
|
13
|
+
- `components/ui/`: shadcn primitives, where its CLI installs them
|
|
14
|
+
- `api-contract/`: the wire contract with the API, as `client/data.md` describes
|
|
15
|
+
- `lib/`: pure functions, with no state and no React
|
|
16
|
+
- a named feature: anything with behaviour
|
|
17
|
+
|
|
18
|
+
Do not create a `shared/` or `common/` folder.
|
|
19
|
+
|
|
20
|
+
Keep client state in React context, one context per subject, living in the feature that owns it. Do not
|
|
21
|
+
create a single application context.
|
|
22
|
+
|
|
23
|
+
Resolve `@/` to the app's own `src/`.
|
|
24
|
+
|
|
25
|
+
## Rationale
|
|
26
|
+
|
|
27
|
+
Feature-first is the same axis the API uses for modules, so *where do I go to change orders* has one
|
|
28
|
+
answer at both ends of the project. A change then touches one folder instead of five. Organising by type
|
|
29
|
+
optimises for *show me every hook*, which nobody asks; organising by feature optimises for *change this
|
|
30
|
+
feature*, which is every task. This rule is written negatively because `components/`, `hooks/`, `pages/`
|
|
31
|
+
is the layout of nearly every React tutorial and therefore the default an assistant reaches for.
|
|
32
|
+
|
|
33
|
+
`components/ui/` is not a preference: the shadcn CLI installs there, and moving it would mean editing
|
|
34
|
+
every install afterwards.
|
|
35
|
+
|
|
36
|
+
A `shared/` folder has no admission criterion: *is this shared?* answers yes for anything used twice, so
|
|
37
|
+
it only grows. Requiring a name forces the question *what is this*, and something nobody can name is not
|
|
38
|
+
ready to be promoted. Not everything shared is a component, either: formatting a date is a function in a
|
|
39
|
+
file. **If it uses React, it does not belong in `lib/`.**
|
|
40
|
+
|
|
41
|
+
One context per subject exists because context re-renders **every** consumer when its value changes. A
|
|
42
|
+
context bundling theme, sidebar and current user makes a theme toggle re-render the whole tree: invisible
|
|
43
|
+
with three components, expensive when the screen grows, and never an error.
|
|
44
|
+
|
|
45
|
+
## Applies to
|
|
46
|
+
|
|
47
|
+
Every file under `src/`.
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
Where a feature's code lives:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
✅ features/orders/order-list.tsx
|
|
55
|
+
features/orders/use-orders.ts
|
|
56
|
+
routes/orders.tsx → renders features/orders
|
|
57
|
+
|
|
58
|
+
❌ components/orders/order-list.tsx
|
|
59
|
+
hooks/use-orders.ts
|
|
60
|
+
pages/orders.tsx
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Shared code:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
✅ lib/format-currency.ts // pure
|
|
67
|
+
features/notifications/ // has behaviour, has a name
|
|
68
|
+
❌ shared/utils.ts
|
|
69
|
+
shared/hooks/use-notifications.ts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Context:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
✅ features/theme/theme-context.tsx
|
|
76
|
+
❌ app-context.tsx // theme + sidebar + user
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Enforcement
|
|
80
|
+
|
|
81
|
+
**Review only.** Nothing prevents a root `components/` folder from appearing, or a `shared/` from being
|
|
82
|
+
created, or two contexts from being merged into one for tidiness.
|
|
83
|
+
|
|
84
|
+
**Two things worth knowing rather than discovering:**
|
|
85
|
+
|
|
86
|
+
Zustand was deliberately left out because TanStack Query owns server state and what remains is usually
|
|
87
|
+
context-sized. If a project accumulates real client state and contexts start multiplying or re-rendering
|
|
88
|
+
too widely, Zustand is the right answer again and installing it costs nothing. Context is not claimed to
|
|
89
|
+
scale indefinitely.
|
|
90
|
+
|
|
91
|
+
In a monorepo, `@/` means something different in each app, so a file moved between apps can compile while
|
|
92
|
+
pointing somewhere else.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonardo Freitas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/stjosephworks/prumo/main/docs/brand/brand.png" alt="Prumo" width="300">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@stjoseph/prumo"><img src="https://img.shields.io/npm/v/%40stjoseph%2Fprumo?color=1e3a5f&label=npm" alt="npm"></a>
|
|
7
|
+
<a href="https://github.com/stjosephworks/prumo/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/%40stjoseph%2Fprumo?color=1e3a5f" alt="MIT"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
Prumo is a set of choices a team has already made, packaged so that starting a new TypeScript
|
|
11
|
+
project does not mean making them all over again. Which framework, how a module is laid out, where
|
|
12
|
+
the database rules live, how authentication is wired, what a test is allowed to assume: each of
|
|
13
|
+
those was decided once, written down with the reasoning and with what the choice costs, and then
|
|
14
|
+
turned into something that actually runs.
|
|
15
|
+
|
|
16
|
+
Put briefly, Prumo is the validation of a well built project without heavy architectural
|
|
17
|
+
bureaucracy, done up front instead of discovered three months in. Well built here does not mean
|
|
18
|
+
heavily layered. Every choice follows what the framework itself already indicates, so there are no
|
|
19
|
+
ports, no adapters, and no in house architecture to learn before writing the first endpoint.
|
|
20
|
+
|
|
21
|
+
A new project receives two things. The first is code that already works: installed, wired together,
|
|
22
|
+
and proven by starting rather than by compiling. The second is a `.prumo/` folder holding the
|
|
23
|
+
conventions that code follows. Any AI assistant working in that repository reads the folder and
|
|
24
|
+
knows the rules without anyone explaining them, which is the whole point: **the conventions live in
|
|
25
|
+
the project, not in someone's head.**
|
|
26
|
+
|
|
27
|
+
Applications differ, so the choices are not one fixed bundle. Three answers shape what arrives:
|
|
28
|
+
which artefacts the project has, whether they sit alone or in one workspace, and whether the
|
|
29
|
+
application serves several tenants. The same body of decisions covers an API on its own and a
|
|
30
|
+
workspace holding four applications, without either one carrying rules it has no use for.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
Node 22.17 or later, pnpm, and Docker for anything with an API.
|
|
35
|
+
|
|
36
|
+
## Running it
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pnpm dlx @stjoseph/prumo new my-app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It asks what to build, writes the project into `my-app/`, runs `git init`, and installs. Nothing is
|
|
43
|
+
committed, so the first commit is yours and the diff shows everything Prumo wrote.
|
|
44
|
+
|
|
45
|
+
Every question also has a flag, which is how CI runs it without a terminal. Outside an interactive
|
|
46
|
+
terminal a missing answer is an error rather than a default, so nothing important gets decided by
|
|
47
|
+
silence:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pnpm dlx @stjoseph/prumo new my-app --types api,web --single-tenant
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Flag | What it answers |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `--types api,web,mobile,site` | What the project contains. More than one type makes a workspace |
|
|
56
|
+
| `--alone` or `--monorepo` | How a single type is laid out |
|
|
57
|
+
| `--multi-tenant` or `--single-tenant` | Whether the application serves several tenants |
|
|
58
|
+
| `--skip-install` | Stops after writing the files |
|
|
59
|
+
|
|
60
|
+
## What it generates
|
|
61
|
+
|
|
62
|
+
| Type | What you get |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `api` | NestJS, MikroORM on PostgreSQL, Better Auth in a schema of its own, Swagger, Vitest with Testcontainers |
|
|
65
|
+
| `web` | Vite, React, TanStack Router and Query, Tailwind, shadcn/ui, forms validated with Zod |
|
|
66
|
+
| `mobile` | Expo with Expo Router, NativeWind, MMKV, and the session kept in `expo-secure-store` |
|
|
67
|
+
| `site` | Next on the App Router, static by default, for pages that have to be indexed |
|
|
68
|
+
|
|
69
|
+
One type produces that project on its own. Several produce a pnpm workspace: each application under
|
|
70
|
+
`apps/`, the wire contract shared through `packages/api-contract` so a change on one side breaks the
|
|
71
|
+
other at typecheck, and one version per dependency held in a catalog.
|
|
72
|
+
|
|
73
|
+
Every template is committed here in full, and none of them is generated by calling somebody else's
|
|
74
|
+
scaffolder at install time. Each one installs, passes lint and typecheck, builds, starts, and passes
|
|
75
|
+
its own tests before it ships. Versions are pinned exactly, with no carets, so two projects generated
|
|
76
|
+
a month apart are the same project.
|
|
77
|
+
|
|
78
|
+
## What lands in the project
|
|
79
|
+
|
|
80
|
+
| File | What it is |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `.prumo/` | The conventions, in nine areas. Only the areas the answers call for are copied |
|
|
83
|
+
| `.prumo/INDEX.md` | One line per document. Generated by the tool, and safe to regenerate |
|
|
84
|
+
| `AGENTS.md` | The pointer into `.prumo/`, read by Codex, Cursor, Copilot, Gemini CLI and others |
|
|
85
|
+
| `CLAUDE.md` | An `@AGENTS.md` import, for Claude Code |
|
|
86
|
+
|
|
87
|
+
The nine areas are `core`, `api`, `database`, `client`, `web`, `mobile`, `site`, `monorepo` and
|
|
88
|
+
`multi-tenancy`. Only `core` ships unconditionally, because everything in it is true of every type;
|
|
89
|
+
the rest follow the answers, since advice about a platform the project does not have is worse than
|
|
90
|
+
no advice at all.
|
|
91
|
+
|
|
92
|
+
Once written, those documents belong to the team. Prumo does not come back and rewrite them.
|
|
93
|
+
|
|
94
|
+
## Where the rest is written
|
|
95
|
+
|
|
96
|
+
| Document | Holds |
|
|
97
|
+
|---|---|
|
|
98
|
+
| [`docs/stack.md`](https://github.com/stjosephworks/prumo/blob/main/docs/stack.md) | What is locked, what was deliberately dropped, and the version floors |
|
|
99
|
+
| [`docs/structure.md`](https://github.com/stjosephworks/prumo/blob/main/docs/structure.md) | The `.prumo/` layout, and the rule deciding which areas a project receives |
|
|
100
|
+
| [`docs/DECISIONS.md`](https://github.com/stjosephworks/prumo/blob/main/docs/DECISIONS.md) | Every decision, why it won, and what it costs |
|
|
101
|
+
| [`docs/OPEN-QUESTIONS.md`](https://github.com/stjosephworks/prumo/blob/main/docs/OPEN-QUESTIONS.md) | What has been raised and is still unresolved |
|
|
102
|
+
|
|
103
|
+
## Status
|
|
104
|
+
|
|
105
|
+
Prumo is in beta. `0.0.1` is the first published version: every template runs and every generated
|
|
106
|
+
project is verified in CI, and what a generated project looks like can still change before 1.0.
|
|
107
|
+
What is known to be missing is listed in [`docs/OPEN-QUESTIONS.md`](https://github.com/stjosephworks/prumo/blob/main/docs/OPEN-QUESTIONS.md), and
|
|
108
|
+
`prumo new` is the only command. Updating a project's `.prumo/` in place comes after 1.0.
|
|
109
|
+
|
|
110
|
+
MIT licensed. See [`LICENSE`](https://github.com/stjosephworks/prumo/blob/main/LICENSE).
|
package/dist/asker.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as prompt from '@clack/prompts';
|
|
2
|
+
const TYPES = ['api', 'web', 'mobile', 'site'];
|
|
3
|
+
function answered(value) {
|
|
4
|
+
if (prompt.isCancel(value)) {
|
|
5
|
+
prompt.cancel('Nothing was generated.');
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function terminalAsker(validateName) {
|
|
11
|
+
return {
|
|
12
|
+
name: async () => answered(await prompt.text({ message: 'Project name', validate: (v) => validateName(v ?? '') })),
|
|
13
|
+
types: async () => answered(await prompt.multiselect({
|
|
14
|
+
message: 'What does the project have?',
|
|
15
|
+
options: TYPES.map((type) => ({ value: type, label: type })),
|
|
16
|
+
required: true,
|
|
17
|
+
})),
|
|
18
|
+
architecture: async () => answered(await prompt.select({
|
|
19
|
+
message: 'One project, or a workspace ready to grow?',
|
|
20
|
+
options: [
|
|
21
|
+
{ value: 'alone', label: 'alone' },
|
|
22
|
+
{ value: 'monorepo', label: 'monorepo' },
|
|
23
|
+
],
|
|
24
|
+
initialValue: 'alone',
|
|
25
|
+
})),
|
|
26
|
+
multiTenant: async () => answered(await prompt.confirm({ message: 'Is it multi-tenant?', initialValue: false })),
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import * as prompt from '@clack/prompts';
|
|
6
|
+
import { terminalAsker } from "./asker.js";
|
|
7
|
+
import { generate } from "./generate.js";
|
|
8
|
+
import { validateProjectName } from "./names.js";
|
|
9
|
+
import { resolveAnswers } from "./questions.js";
|
|
10
|
+
const USAGE = `Usage: prumo new [name] [--types api,web,mobile,site] [--alone | --monorepo]
|
|
11
|
+
[--multi-tenant | --single-tenant] [--skip-install]`;
|
|
12
|
+
function firstExisting(...candidates) {
|
|
13
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
14
|
+
if (found === undefined) {
|
|
15
|
+
throw new Error(`Cannot find ${candidates.join(' or ')}`);
|
|
16
|
+
}
|
|
17
|
+
return found;
|
|
18
|
+
}
|
|
19
|
+
async function main() {
|
|
20
|
+
const { values, positionals } = parseArgs({
|
|
21
|
+
allowPositionals: true,
|
|
22
|
+
options: {
|
|
23
|
+
types: { type: 'string' },
|
|
24
|
+
alone: { type: 'boolean', default: false },
|
|
25
|
+
monorepo: { type: 'boolean', default: false },
|
|
26
|
+
'multi-tenant': { type: 'boolean', default: false },
|
|
27
|
+
'single-tenant': { type: 'boolean', default: false },
|
|
28
|
+
'skip-install': { type: 'boolean', default: false },
|
|
29
|
+
help: { type: 'boolean', default: false },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const [command, name] = positionals;
|
|
33
|
+
if (values.help || command !== 'new') {
|
|
34
|
+
console.log(USAGE);
|
|
35
|
+
process.exit(values.help ? 0 : 1);
|
|
36
|
+
}
|
|
37
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
38
|
+
const answers = await resolveAnswers({
|
|
39
|
+
name,
|
|
40
|
+
types: values.types,
|
|
41
|
+
alone: values.alone,
|
|
42
|
+
monorepo: values.monorepo,
|
|
43
|
+
multiTenant: values['multi-tenant'],
|
|
44
|
+
singleTenant: values['single-tenant'],
|
|
45
|
+
}, interactive ? terminalAsker(validateProjectName) : undefined);
|
|
46
|
+
if (answers.types.length > 1) {
|
|
47
|
+
prompt.log.info(`${answers.types.length} types make a workspace: apps/ and packages/ under one root.`);
|
|
48
|
+
}
|
|
49
|
+
const here = import.meta.dirname;
|
|
50
|
+
await generate({
|
|
51
|
+
templates: firstExisting(join(here, '../templates'), join(here, '../../templates')),
|
|
52
|
+
knowledge: firstExisting(join(here, '../.prumo-templates'), join(here, '../../.prumo-templates')),
|
|
53
|
+
target: resolve(answers.name),
|
|
54
|
+
answers,
|
|
55
|
+
install: !values['skip-install'],
|
|
56
|
+
});
|
|
57
|
+
prompt.outro(`Created ${answers.name}.`);
|
|
58
|
+
}
|
|
59
|
+
main().catch((error) => {
|
|
60
|
+
console.error(error instanceof Error ? error.message : error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|