@stjoseph/prumo 0.4.3 → 0.5.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.
Files changed (34) hide show
  1. package/.prumo-templates/core/testing.md +14 -4
  2. package/README.md +23 -3
  3. package/dist/command.js +17 -0
  4. package/dist/compose.js +4 -1
  5. package/dist/doctor.js +18 -11
  6. package/dist/generate.js +2 -4
  7. package/dist/names.js +13 -1
  8. package/dist/pnpm-extended.js +159 -0
  9. package/package.json +5 -2
  10. package/templates/api/.gitattributes +2 -0
  11. package/templates/api/README.md +2 -0
  12. package/templates/api/scripts/database.mjs +2 -1
  13. package/templates/api/scripts/ports.mjs +249 -36
  14. package/templates/mobile/.gitattributes +2 -0
  15. package/templates/mobile/scripts/ports.mjs +249 -36
  16. package/templates/mobile/test/features/auth/authenticated-layout.spec.tsx +3 -2
  17. package/templates/mobile/test/features/auth/email-flows.spec.tsx +3 -2
  18. package/templates/mobile/test/features/auth/sign-in-form.spec.tsx +3 -2
  19. package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +3 -2
  20. package/templates/mobile/test/features/auth/social-buttons.spec.tsx +3 -2
  21. package/templates/mobile/test/features/profile/profile-form.spec.tsx +3 -4
  22. package/templates/mobile/test/support/query-client.ts +14 -0
  23. package/templates/site/.gitattributes +2 -0
  24. package/templates/site/scripts/ports.mjs +249 -36
  25. package/templates/web/.gitattributes +2 -0
  26. package/templates/web/scripts/ports.mjs +249 -36
  27. package/templates/web/test/features/oauth/consent-form.spec.tsx +3 -2
  28. package/templates/web/test/features/profile/profile-form.spec.tsx +3 -2
  29. package/templates/web/test/support/query-client.ts +14 -0
  30. package/templates/web/test/support/render-app.tsx +2 -2
  31. package/templates/web/vite.config.ts +10 -3
  32. package/templates/workspace/.gitattributes +2 -0
  33. package/templates/workspace/README.md +2 -0
  34. package/templates/workspace/scripts/ports.mjs +249 -36
@@ -15,7 +15,8 @@ Do not mock `EntityManager` or any concrete class from a library.
15
15
 
16
16
  **In a client, replace the network through the injected transport**: pass a fake `fetch` to `createClient`.
17
17
  Do not add a request-mocking library. Test a component that shows an error by rendering it and asserting
18
- **where** the error appears.
18
+ **where** the error appears. Build every test's `QueryClient` with `createTestQueryClient()` from
19
+ `@test/support/query-client`, never with `new QueryClient()`: the defaults a test needs live there, once.
19
20
 
20
21
  Measure coverage and report it. Do not block a merge on a coverage threshold.
21
22
 
@@ -59,9 +60,18 @@ written in a mobile test is a mistake an assistant will make.
59
60
  is **asynchronous** and its matchers need no setup. `expo-secure-store` needs the native Keychain or Keystore,
60
61
  which Jest does not have, so a test builds the auth client with an in-memory token store, which is why a form
61
62
  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. 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
+ know the app's scheme: a test that reaches `Linking.createURL` mocks it with the scheme it expects. And a query
64
+ or mutation left without an observer once a test unmounts starts TanStack Query's five-minute garbage-collection
65
+ timer, which Jest's worker waits on until it is force-exited. `createTestQueryClient()` sets `gcTime: Infinity`
66
+ for both, so no timer starts, and `retry: false` for queries, so a failure shows at once: the two defaults TanStack
67
+ Query's testing guide gives. Vitest does not wait on the timer, and `web` uses the same helper so that one rule
68
+ holds in both.
69
+
70
+ **`web` turns route code splitting off under Vitest**, in `vite.config.ts`. Split, a route's component is a module
71
+ Vite transforms the first time a test renders it, inside the one second a `find*` query waits, and on a loaded CI
72
+ runner the first page of a test file missed it. The wait is not the problem and is not raised: with the transform
73
+ gone the same load passes, and past that load a longer wait fails the same tests anyway. **What it costs:** the
74
+ tests never load a route's lazy chunk; the build and its preview do.
65
75
 
66
76
  A coverage threshold turns a proxy into a target. Whoever is below writes tests to raise the number rather
67
77
  than to check anything; whoever is above stops thinking. With no database mocks a test costs more to write,
package/README.md CHANGED
@@ -33,7 +33,16 @@ workspace holding four applications, without either one carrying rules it has no
33
33
  ## Requirements
34
34
 
35
35
  Node 22.18 or later, pnpm 10.26 or later, and Docker for anything with an API. `prumo doctor` checks
36
- all of it. Prumo runs on macOS and Linux; Windows is not supported yet.
36
+ all of it. Prumo runs on macOS, Linux and Windows.
37
+
38
+ On Windows it runs natively: the project lives in a Windows folder, and the Windows Node and pnpm run it.
39
+ Docker Desktop, with its WSL2 backend and set to Linux containers, is only where the database and the API's
40
+ tests run.
41
+
42
+ Inside a WSL distribution, with the distribution's own terminal, Node, pnpm and Docker, Prumo is on Linux, which is
43
+ supported, though not tested on WSL specifically. Mixing the two is not supported: Windows tools, Prumo Desktop
44
+ included, working on a project inside the WSL filesystem (`\\wsl$` or `\\wsl.localhost`).
45
+
37
46
 
38
47
  ## Running it
39
48
 
@@ -51,7 +60,9 @@ committed, so the first commit is yours and the diff shows everything Prumo wrot
51
60
  writes into the current directory instead, which must be empty, and names the project after it. If
52
61
  anything fails or is interrupted, Prumo removes what it wrote, so the same command can run again.
53
62
  A project cannot be named `api`, `web`, `mobile` or `site`: those are the names its apps take inside
54
- a workspace.
63
+ a workspace. Nor can it take the name of a package that pnpm's compatibility database extends, such as `ws`:
64
+ pnpm applies the extension to the project's own `package.json` and installs what it adds. Prumo names
65
+ the packages and suggests another name.
55
66
 
56
67
  Every question also has a flag, which is how CI runs it without a terminal. Outside an interactive
57
68
  terminal every question that applies needs its flag, the layout of a single type included: a missing
@@ -79,7 +90,7 @@ npx @stjoseph/prumo new my-api --types api --alone --single-tenant --email
79
90
  | `prumo new [name \| .]` | Generates a project |
80
91
  | `prumo db` | Inside a project with an API: creates the development database in Docker and writes its URL into `.env` |
81
92
  | `prumo clean` | Removes what the project needed only once, today the database setup, once the database exists. Only the Prumo version that generated the project cleans it |
82
- | `prumo doctor` | Checks Node, pnpm, git and Docker on this machine |
93
+ | `prumo doctor` | Checks Node, pnpm, git and Docker on this machine, and that Docker runs Linux containers |
83
94
  | `prumo version` · `--version` · `-v` | Prints the CLI version |
84
95
  | `prumo help [command]` · `--help` | Lists the commands, or describes one |
85
96
 
@@ -129,6 +140,7 @@ Once written, those documents belong to the team. Prumo does not come back and r
129
140
  | [`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 |
130
141
  | [`docs/document-template.md`](https://github.com/stjosephworks/prumo/blob/main/docs/document-template.md) | The shape every convention document fills |
131
142
  | [`docs/maintaining-templates.md`](https://github.com/stjosephworks/prumo/blob/main/docs/maintaining-templates.md) | How a template is changed, and how the change is proved |
143
+ | [`docs/upgrading.md`](https://github.com/stjosephworks/prumo/blob/main/docs/upgrading.md) | What each release changed in the files it generates, and how an existing project carries it over |
132
144
 
133
145
  ## Releasing
134
146
 
@@ -136,6 +148,14 @@ Once written, those documents belong to the team. Prumo does not come back and r
136
148
  registry does not have yet, the `publish` job sends it to npm through npm's trusted publishing, with provenance and
137
149
  without a token in the repository. Any other push to `main` publishes nothing.
138
150
 
151
+ ## Upgrading a project
152
+
153
+ Prumo does not update a project it generated, and there is no `prumo upgrade`. For each release that changed
154
+ the generated files, [`docs/upgrading.md`](https://github.com/stjosephworks/prumo/blob/main/docs/upgrading.md)
155
+ lists what to copy or change and why, and gives one method: generate a reference project with the same answers
156
+ at the old and the new version, and diff the files listed. A file your team has edited is merged by hand, never
157
+ overwritten.
158
+
139
159
  ## Status
140
160
 
141
161
  Prumo is in beta: every template runs and every generated project is verified in CI, and what a generated project looks like can still change before 1.0.
@@ -0,0 +1,17 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ const windows = process.platform === 'win32';
3
+ // Only what the CLI itself writes: no space, quote or shell operator, so one line means the same in any shell.
4
+ const PLAIN = /^[\w@.:/={}-]+$/;
5
+ // Runs a tool the CLI names, such as pnpm, git or docker. On Windows pnpm is a .cmd shim, which Node starts only
6
+ // through a shell, and a shell given an argument list is deprecated (DEP0190), so there the command goes as one line.
7
+ export function runCommand(command, args, options = {}) {
8
+ if (!windows) {
9
+ return spawnSync(command, args, options);
10
+ }
11
+ for (const part of [command, ...args]) {
12
+ if (!PLAIN.test(part)) {
13
+ throw new Error(`${part} cannot be passed through a shell unquoted`);
14
+ }
15
+ }
16
+ return spawnSync([command, ...args].join(' '), { ...options, shell: true });
17
+ }
package/dist/compose.js CHANGED
@@ -49,7 +49,10 @@ export async function copyTemplate(source, destination) {
49
49
  async function copyApp(templates, target, type) {
50
50
  const destination = join(target, 'apps', type);
51
51
  await copyTemplate(join(templates, type), destination);
52
- await rm(join(destination, '.githooks'), { recursive: true, force: true });
52
+ // The workspace root carries both, for every app below it.
53
+ for (const rootOnly of ['.githooks', '.gitattributes']) {
54
+ await rm(join(destination, rootOnly), { recursive: true, force: true });
55
+ }
53
56
  return destination;
54
57
  }
55
58
  async function contractFiles(directory) {
package/dist/doctor.js CHANGED
@@ -1,14 +1,15 @@
1
- import { spawnSync } from 'node:child_process';
1
+ import { runCommand } from "./command.js";
2
2
  const NODE_FLOOR = [22, 18, 0];
3
3
  // Every generated workspace approves dependency builds with `allowBuilds`, which pnpm added in 10.26.0.
4
4
  const PNPM_FLOOR = [10, 26, 0];
5
- // On Windows pnpm is a .cmd shim, and Node starts one only through a shell.
6
- export const SHELL = process.platform === 'win32';
7
5
  export const systemProbe = {
8
6
  nodeVersion: process.versions.node,
9
7
  run: (command, args) => {
10
- const result = spawnSync(command, args, { encoding: 'utf8', timeout: 10_000, shell: SHELL });
11
- return { ok: result.error === undefined && result.status === 0, stdout: result.stdout ?? '' };
8
+ const result = runCommand(command, args, { encoding: 'utf8', timeout: 10_000 });
9
+ return {
10
+ ok: result.error === undefined && result.status === 0,
11
+ stdout: String(result.stdout ?? ''),
12
+ };
12
13
  },
13
14
  };
14
15
  function atLeast(version, floor) {
@@ -59,19 +60,25 @@ export async function doctor(probe = systemProbe) {
59
60
  });
60
61
  checks.push(...toolChecks(probe, ['pnpm', 'git']));
61
62
  const docker = probe.run('docker', ['--version']);
62
- const dockerRunning = docker.ok && probe.run('docker', ['info']).ok;
63
+ const engine = docker.ok ? probe.run('docker', ['info', '--format', '{{.OSType}}']) : undefined;
64
+ const os = firstLine(engine?.stdout ?? '');
63
65
  checks.push({
64
66
  id: 'docker',
65
67
  label: 'Docker',
66
68
  required: false,
67
- ...(dockerRunning
68
- ? { status: 'ok', detail: firstLine(docker.stdout) }
69
- : {
69
+ ...(engine?.ok !== true
70
+ ? {
70
71
  status: 'warn',
71
72
  detail: docker.ok
72
73
  ? 'installed but not running; open Docker Desktop. The API runs its database and tests in it'
73
- : 'not found; install Docker Desktop. The API runs its database and tests in it',
74
- }),
74
+ : 'not found; install Docker Desktop, on Windows with the WSL2 backend. The API runs its database and tests in it',
75
+ }
76
+ : os !== 'linux'
77
+ ? {
78
+ status: 'warn',
79
+ detail: `running ${os || 'unknown'} containers; switch Docker Desktop to Linux containers. The API's Postgres image is a Linux one`,
80
+ }
81
+ : { status: 'ok', detail: firstLine(docker.stdout) }),
75
82
  });
76
83
  return { ready: checks.every((check) => !check.required || check.status === 'ok'), checks };
77
84
  }
package/dist/generate.js CHANGED
@@ -1,11 +1,10 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import { randomBytes } from 'node:crypto';
3
2
  import { existsSync } from 'node:fs';
4
3
  import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
5
4
  import { join } from 'node:path';
5
+ import { runCommand } from "./command.js";
6
6
  import { composeWorkspace, copyTemplate } from "./compose.js";
7
7
  import { writeContext } from "./context.js";
8
- import { SHELL } from "./doctor.js";
9
8
  import { applyFeatures, enabledFor } from "./features.js";
10
9
  import { setJsonc } from "./jsonc.js";
11
10
  import { schemeFor, targetProblem } from "./names.js";
@@ -78,10 +77,9 @@ function stopIfInterrupted() {
78
77
  function run(command, args, cwd, output) {
79
78
  stopIfInterrupted();
80
79
  // Under --json stdout belongs to the result document, so a child's output goes to stderr instead.
81
- const result = spawnSync(command, args, {
80
+ const result = runCommand(command, args, {
82
81
  cwd,
83
82
  stdio: output === 'inherit' ? 'inherit' : ['ignore', 2, 2],
84
- shell: SHELL,
85
83
  });
86
84
  // spawnSync holds the event loop, so the listener has not run yet; the child says it instead, either by dying of
87
85
  // the signal or, as pnpm does, by catching it and exiting with 130.
package/dist/names.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, statSync } from 'node:fs';
2
2
  import { basename, resolve } from 'node:path';
3
+ import { EXTENDED_BY_PNPM } from "./pnpm-extended.js";
3
4
  const PROJECT_NAME = /^[a-z][a-z0-9-]*$/;
4
5
  // npm refuses a longer package name.
5
6
  const MAX_LENGTH = 214;
@@ -15,7 +16,18 @@ export function validateProjectName(name) {
15
16
  if (RESERVED.includes(name)) {
16
17
  return 'That is the name an app takes inside a workspace; choose another.';
17
18
  }
18
- return undefined;
19
+ return extendedByPnpm(name);
20
+ }
21
+ function extendedByPnpm(name) {
22
+ const added = Object.hasOwn(EXTENDED_BY_PNPM, name) ? EXTENDED_BY_PNPM[name] : undefined;
23
+ if (added === undefined) {
24
+ return undefined;
25
+ }
26
+ const shown = added.length > 3
27
+ ? `${added.slice(0, 3).join(', ')} and ${added.length - 3} more`
28
+ : added.join(', ');
29
+ return (`pnpm extends the npm package "${name}" with ${shown}, and would install them into a project of that name. ` +
30
+ `Choose another, such as "${name}-app".`);
19
31
  }
20
32
  // Why a directory cannot receive a project, or undefined when it can: it does not exist yet, or it is empty.
21
33
  export function targetProblem(target) {
@@ -0,0 +1,159 @@
1
+ // pnpm applies a compatibility database, @yarnpkg/extensions, to every manifest it reads: the registry's packages and
2
+ // the project's own package.json alike. An entry matches by name and version range, and the template version 0.0.1
3
+ // falls inside ranges like `ws@<7.2.1`. A project carrying one of these names therefore gains the dependencies the
4
+ // entry adds, installed into its root, and a package among them with a build script stops `pnpm install` with
5
+ // ERR_PNPM_IGNORED_BUILDS. Each name maps to the packages it would add.
6
+ //
7
+ // Derived from @yarnpkg/extensions, pinned in devDependencies, by test/names.spec.ts, which fails when this list and
8
+ // the database part ways. Only names a project can take are kept, and only entries that add a package: one that sets
9
+ // nothing but peerDependenciesMeta changes no install.
10
+ export const EXTENDED_BY_PNPM = {
11
+ 'apollo-upload-client': ['graphql'],
12
+ 'auto-relay': ['reflect-metadata'],
13
+ 'babel-plugin-graphql-tag': ['graphql'],
14
+ 'babel-plugin-remove-graphql-queries': ['@babel/runtime', '@babel/types', 'gatsby-core-utils'],
15
+ 'babel-plugin-transform-typescript-metadata': ['@babel/core', '@babel/traverse'],
16
+ 'babel-preset-gatsby-package': ['@babel/runtime'],
17
+ 'bin-links': ['mkdirp-infer-owner'],
18
+ 'cacheable-lookup': ['@types/keyv'],
19
+ 'clipanion-v3-codemod': ['jscodeshift'],
20
+ 'connect-mongo': ['express-session'],
21
+ consolidate: [
22
+ 'arc-templates',
23
+ 'atpl',
24
+ 'babel-core',
25
+ 'bracket-template',
26
+ 'coffee-script',
27
+ 'dot',
28
+ 'dust',
29
+ 'dustjs-helpers',
30
+ 'dustjs-linkedin',
31
+ 'eco',
32
+ 'ect',
33
+ 'ejs',
34
+ 'haml-coffee',
35
+ 'hamlet',
36
+ 'hamljs',
37
+ 'handlebars',
38
+ 'hogan.js',
39
+ 'htmling',
40
+ 'jade',
41
+ 'jazz',
42
+ 'jqtpl',
43
+ 'just',
44
+ 'liquid-node',
45
+ 'liquor',
46
+ 'lodash',
47
+ 'marko',
48
+ 'mote',
49
+ 'mustache',
50
+ 'nunjucks',
51
+ 'plates',
52
+ 'pug',
53
+ 'qejs',
54
+ 'ractive',
55
+ 'razor-tmpl',
56
+ 'react',
57
+ 'react-dom',
58
+ 'slm',
59
+ 'squirrelly',
60
+ 'swig',
61
+ 'swig-templates',
62
+ 'teacup',
63
+ 'templayed',
64
+ 'then-jade',
65
+ 'then-pug',
66
+ 'tinyliquid',
67
+ 'toffee',
68
+ 'twig',
69
+ 'twing',
70
+ 'underscore',
71
+ 'vash',
72
+ 'velocityjs',
73
+ 'walrus',
74
+ 'whiskers',
75
+ ],
76
+ 'cordova-ios': ['underscore'],
77
+ 'cordova-lib': ['underscore'],
78
+ 'create-gatsby': ['@babel/runtime'],
79
+ 'elm-webpack-loader': ['temp'],
80
+ 'eslint-import-resolver-vite': ['debug', 'resolve'],
81
+ fdir: ['picomatch'],
82
+ 'fork-ts-checker-webpack-plugin': ['eslint', 'typescript', 'vue-template-compiler', 'webpack'],
83
+ 'gatsby-admin': ['@babel/runtime'],
84
+ 'gatsby-cli': ['@babel/runtime'],
85
+ 'gatsby-core-utils': ['@babel/runtime', 'got'],
86
+ 'gatsby-design-tokens': ['@babel/runtime'],
87
+ 'gatsby-legacy-polyfills': ['@babel/runtime'],
88
+ 'gatsby-plugin-benchmark-reporting': ['@babel/runtime'],
89
+ 'gatsby-plugin-favicon': ['webpack'],
90
+ 'gatsby-plugin-gatsby-cloud': ['gatsby-core-utils', 'webpack'],
91
+ 'gatsby-plugin-graphql-config': ['@babel/runtime'],
92
+ 'gatsby-plugin-i18n': ['ramda'],
93
+ 'gatsby-plugin-image': ['@babel/runtime'],
94
+ 'gatsby-plugin-mdx': ['@babel/runtime', 'mkdirp'],
95
+ 'gatsby-plugin-netlify-cms': ['@babel/runtime'],
96
+ 'gatsby-plugin-no-sourcemaps': ['@babel/runtime'],
97
+ 'gatsby-plugin-page-creator': ['@babel/runtime', 'fs-extra'],
98
+ 'gatsby-plugin-preact': ['@babel/runtime'],
99
+ 'gatsby-plugin-preload-fonts': ['@babel/runtime'],
100
+ 'gatsby-plugin-schema-snapshot': ['@babel/runtime'],
101
+ 'gatsby-plugin-sharp': ['debug'],
102
+ 'gatsby-plugin-styletron': ['@babel/runtime'],
103
+ 'gatsby-plugin-subfont': ['@babel/runtime'],
104
+ 'gatsby-plugin-utils': ['@babel/runtime', 'fastq', 'graphql'],
105
+ 'gatsby-react-router-scroll': ['prop-types'],
106
+ 'gatsby-recipes': ['@babel/runtime'],
107
+ 'gatsby-remark-prismjs': ['lodash'],
108
+ 'gatsby-source-apiserver': ['babel-polyfill'],
109
+ 'gatsby-source-shopify': ['@babel/runtime'],
110
+ 'gatsby-source-wikipedia': ['@babel/runtime'],
111
+ 'gatsby-transformer-screenshot': ['@babel/runtime'],
112
+ 'gatsby-worker': ['@babel/runtime'],
113
+ 'git-node-fs': ['js-git'],
114
+ got: ['@types/keyv', '@types/responselike'],
115
+ 'http-link-dataloader': ['graphql'],
116
+ 'ink-select-input': ['react'],
117
+ 'jest-vue-preprocessor': ['@babel/core', '@babel/template', 'pug'],
118
+ 'jss-plugin-rule-value-function': ['tiny-warning'],
119
+ mqtt: ['duplexify'],
120
+ 'query-ast': ['lodash'],
121
+ 'rc-animate': ['react', 'react-dom'],
122
+ 'react-bootstrap-table2-paginator': ['classnames'],
123
+ 'react-color': ['react'],
124
+ 'react-dev-utils': ['typescript', 'webpack'],
125
+ 'react-draggable': ['react', 'react-dom'],
126
+ 'react-github-btn': ['react'],
127
+ 'react-instantsearch-core': ['algoliasearch'],
128
+ 'react-instantsearch-dom': ['react-fast-compare'],
129
+ 'react-live': ['react', 'react-dom'],
130
+ 'react-portal': ['react-dom'],
131
+ 'react-rnd': ['react', 'react-dom'],
132
+ 'react-scripts': ['eslint', 'react'],
133
+ reactcss: ['react'],
134
+ rebass: ['react'],
135
+ 'redux-persist': ['react'],
136
+ 'redux-thunk': ['redux'],
137
+ 'rollup-plugin-polyfill-node': ['rollup'],
138
+ 'scss-parser': ['lodash'],
139
+ skypack: ['tar'],
140
+ snowpack: ['magic-string'],
141
+ terser: ['acorn'],
142
+ testcafe: ['@babel/plugin-transform-for-of', '@babel/runtime'],
143
+ 'testcafe-legacy-api': ['read-file-relative', 'testcafe-hammerhead'],
144
+ 'typescript-language-server': ['vscode-jsonrpc', 'vscode-languageserver-protocol'],
145
+ unified: ['@types/unist'],
146
+ 'unplugin-vue2-script-setup': ['@vue/composition-api', '@vue/runtime-dom'],
147
+ 'vite-plugin-vuetify': ['vue'],
148
+ 'volar-service-typescript': ['typescript'],
149
+ 'volar-service-typescript-twoslash-queries': ['typescript'],
150
+ 'vue-cli-plugin-vuetify': ['null-loader', 'semver'],
151
+ 'vue-i18n': ['vue'],
152
+ 'vue-loader': ['@vue/compiler-sfc', 'webpack'],
153
+ 'vue-router': ['vue'],
154
+ 'vue-template-babel-compiler': ['vue-template-compiler'],
155
+ 'webpack-plugin-vuetify': ['vue'],
156
+ 'winston-transport': ['logform'],
157
+ ws: ['bufferutil', 'utf-8-validate'],
158
+ xo: ['webpack'],
159
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stjoseph/prumo",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
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",
@@ -36,7 +36,7 @@
36
36
  "scripts": {
37
37
  "lint": "biome check",
38
38
  "typecheck": "tsc --noEmit",
39
- "test": "vitest run --dir test",
39
+ "test": "vitest run --dir test --test-timeout 30000",
40
40
  "prumo": "node src/cli.ts",
41
41
  "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
42
42
  "prepack": "pnpm build"
@@ -48,6 +48,9 @@
48
48
  "devDependencies": {
49
49
  "@biomejs/biome": "2.5.13",
50
50
  "@types/node": "24.13.4",
51
+ "@types/semver": "7.8.0",
52
+ "@yarnpkg/extensions": "2.0.7",
53
+ "semver": "7.8.5",
51
54
  "typescript": "6.0.3",
52
55
  "vitest": "4.1.11"
53
56
  },
@@ -0,0 +1,2 @@
1
+ # LF in every checkout, Windows included: Biome formats to LF and would otherwise flag every file.
2
+ * text=auto eol=lf
@@ -3,6 +3,8 @@
3
3
  ## Requirements
4
4
 
5
5
  Node 22.18 or later, pnpm 10.26 or later, and Docker Desktop running: the database and the tests both run in it.
6
+ On Windows, Docker Desktop needs its WSL2 backend and Linux containers. Keep the project in a Windows folder, or
7
+ work entirely inside a WSL distribution; Windows tools on a project inside the WSL filesystem are not supported.
6
8
 
7
9
  ## Running it
8
10
 
@@ -267,7 +267,8 @@ async function main() {
267
267
 
268
268
  if (!values['skip-migrate']) {
269
269
  say('Migrating…')
270
- if (spawnSync('pnpm', ['db:migrate'], { cwd: api, stdio: childStdio }).status !== 0) {
270
+ // One line through the shell: on Windows pnpm is a .cmd shim, which Node starts only that way.
271
+ if (spawnSync('pnpm db:migrate', { cwd: api, stdio: childStdio, shell: true }).status !== 0) {
271
272
  throw new Failure(
272
273
  'migrate_failed',
273
274
  '`pnpm db:migrate` failed. The database exists and .env points at it.',