@stjoseph/prumo 0.4.1 → 0.4.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.
@@ -55,11 +55,13 @@ been published since January 2024. Every other rule here (placement, no database
55
55
  behaviour) holds unchanged. **What it costs:** two runners in one workspace with near-identical APIs, so `vi.fn`
56
56
  written in a mobile test is a mistake an assistant will make.
57
57
 
58
- **Three things a mobile test meets, verified with Expo SDK 57:** `render` from `@testing-library/react-native` 14
58
+ **Four things a mobile test meets, verified with Expo SDK 57:** `render` from `@testing-library/react-native` 14
59
59
  is **asynchronous** and its matchers need no setup. `expo-secure-store` needs the native Keychain or Keystore,
60
60
  which Jest does not have, so a test builds the auth client with an in-memory token store, which is why a form
61
61
  asks only for the auth methods it calls. And `jest-expo` leaves the app manifest empty, so `expo-linking` cannot
62
- know the app's scheme: a test that reaches `Linking.createURL` mocks it with the scheme it expects.
62
+ know the app's scheme: a test that reaches `Linking.createURL` mocks it with the scheme it expects. A test rendering a `useMutation` builds
63
+ its `QueryClient` with `mutations: { gcTime: Infinity }`: once unmounted, the mutation otherwise starts TanStack
64
+ Query's five-minute garbage-collection timer, and Jest force-exits the worker still waiting on it.
63
65
 
64
66
  A coverage threshold turns a proxy into a target. Whoever is below writes tests to raise the number rather
65
67
  than to check anything; whoever is above stops thinking. With no database mocks a test costs more to write,
@@ -11,6 +11,8 @@ boot, and never by hand.
11
11
  Make every migration compatible with the **previous** release. Removing a column is two deployments:
12
12
  one that stops using it, one that removes it.
13
13
 
14
+ Keep the `package.json` in the migrations directory. It holds only `"type": "module"`.
15
+
14
16
  Delete the generated `down`. Undoing a schema change is a new migration going forward.
15
17
 
16
18
  Keep DDL and data apart. A migration performs DDL only. Filling existing rows is a separate task, run
@@ -31,6 +33,12 @@ migration fails. It would also mean **the process serving requests holds DDL per
31
33
  injection or compromise could drop a table. A separate step lets the deploy use a privileged user for
32
34
  thirty seconds while the application runs as one that only reads and writes rows.
33
35
 
36
+ The migrations are TypeScript that Node loads directly, by type stripping; the build never compiles
37
+ them. They use `import`, and without a declared module type Node guesses: it fails to parse them as
38
+ CommonJS, reparses them as ES modules, and warns `MODULE_TYPELESS_PACKAGE_JSON` in every process that
39
+ runs them. The nearest `package.json` decides a file's type, so the one in the migrations directory
40
+ settles it for those files alone and leaves the CommonJS build untouched.
41
+
34
42
  During a rolling deploy the previous version is still running against the new schema, so a migration
35
43
  that removes something the old code reads breaks production before the new version finishes arriving.
36
44
 
package/README.md CHANGED
@@ -54,17 +54,18 @@ A project cannot be named `api`, `web`, `mobile` or `site`: those are the names
54
54
  a workspace.
55
55
 
56
56
  Every question also has a flag, which is how CI runs it without a terminal. Outside an interactive
57
- terminal a missing answer is an error rather than a default, so nothing important gets decided by
58
- silence:
57
+ terminal every question that applies needs its flag, the layout of a single type included: a missing
58
+ answer is an error rather than a default, so nothing important gets decided by silence:
59
59
 
60
60
  ```sh
61
61
  npx @stjoseph/prumo new my-app --types api,web --single-tenant --no-mcp --email --no-social
62
+ npx @stjoseph/prumo new my-api --types api --alone --single-tenant --email
62
63
  ```
63
64
 
64
65
  | Flag | What it answers |
65
66
  |---|---|
66
67
  | `--types api,web,mobile,site` | What the project contains. More than one type makes a workspace |
67
- | `--alone` or `--monorepo` | How a single type is laid out |
68
+ | `--alone` or `--monorepo` | How a single type is laid out, so one type needs one of them. Several types always make a workspace and refuse `--alone` |
68
69
  | `--multi-tenant` or `--single-tenant` | Whether the application serves several tenants. Multi-tenant ships the `multi-tenancy` conventions; the generated code itself is not tenant-aware yet |
69
70
  | `--mcp` or `--no-mcp` | With an api and a web: whether AI assistants reach the API through MCP, signed in as the user |
70
71
  | `--email` or `--no-email` | With an api: email verification and password reset, by a 6-digit code sent through the `Mailer` port, which writes to the log until you give it a provider |
package/dist/commands.js CHANGED
@@ -9,7 +9,10 @@ export const COMMANDS = [
9
9
  summary: 'Generate a project and the .prumo/ context it follows; `.` generates into this empty directory',
10
10
  options: [
11
11
  { flag: '--types <list>', description: 'Comma-separated: api, web, mobile, site' },
12
- { flag: '--alone', description: 'One project of one type' },
12
+ {
13
+ flag: '--alone',
14
+ description: 'One project of one type; without a terminal, one type needs this or --monorepo',
15
+ },
13
16
  { flag: '--monorepo', description: 'A workspace, even for a single type' },
14
17
  {
15
18
  flag: '--multi-tenant',
package/dist/questions.js CHANGED
@@ -69,19 +69,22 @@ export async function resolveAnswers(flags, asker, cwd = process.cwd()) {
69
69
  else {
70
70
  missing('--types');
71
71
  }
72
- let architecture = 'alone';
72
+ let architecture;
73
73
  if (types.length > 1) {
74
74
  if (flags.alone) {
75
75
  throw new CliError('invalid_input', '--alone holds a single type; several types make a workspace.');
76
76
  }
77
77
  architecture = 'monorepo';
78
78
  }
79
- else if (flags.monorepo) {
80
- architecture = 'monorepo';
79
+ else if (flags.alone || flags.monorepo) {
80
+ architecture = flags.monorepo ? 'monorepo' : 'alone';
81
81
  }
82
- else if (!flags.alone && asker !== undefined) {
82
+ else if (asker !== undefined) {
83
83
  architecture = await asker.architecture();
84
84
  }
85
+ else {
86
+ missing('--alone or --monorepo');
87
+ }
85
88
  let multiTenant = false;
86
89
  if (!types.some((type) => TENANT_AWARE.includes(type))) {
87
90
  if (flags.multiTenant) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stjoseph/prumo",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Scaffolds a TypeScript project and ships it the conventions it follows, in a .prumo/ folder any AI assistant reads.",
5
5
  "keywords": [
6
6
  "scaffolding",
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -1,7 +1,7 @@
1
1
  import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql'
2
2
  import type { TestProject } from 'vitest/node'
3
3
 
4
- let container: StartedPostgreSqlContainer
4
+ let container: StartedPostgreSqlContainer | undefined
5
5
 
6
6
  export async function setup(project: TestProject): Promise<void> {
7
7
  container = await new PostgreSqlContainer('postgres:18-alpine').start()
@@ -9,8 +9,10 @@ export async function setup(project: TestProject): Promise<void> {
9
9
  project.provide('postgresUrl', container.getConnectionUri())
10
10
  }
11
11
 
12
+ // Vitest still tears down when setup threw, as when the image could not be pulled; there is no container then, and
13
+ // failing here would print a second error over the one that matters.
12
14
  export async function teardown(): Promise<void> {
13
- await container.stop()
15
+ await container?.stop()
14
16
  }
15
17
 
16
18
  declare module 'vitest' {
@@ -36,8 +36,12 @@ describe('ProfileForm', () => {
36
36
  }),
37
37
  })
38
38
 
39
+ // A mutation left without an observer starts a five-minute garbage-collection timer, and Jest's worker would wait
40
+ // it out. Infinity schedules none, as TanStack Query's testing guide advises.
41
+ const queryClient = new QueryClient({ defaultOptions: { mutations: { gcTime: Infinity } } })
42
+
39
43
  await render(
40
- <QueryClientProvider client={new QueryClient()}>
44
+ <QueryClientProvider client={queryClient}>
41
45
  <ProfileForm api={api} profile={profile} />
42
46
  </QueryClientProvider>,
43
47
  )