@prisma-next/cli 0.5.1 → 0.6.0-dev.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma-next/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.0-dev.10",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -27,27 +27,27 @@
27
27
  "string-width": "^8.2.1",
28
28
  "strip-ansi": "^7.2.0",
29
29
  "wrap-ansi": "^10.0.0",
30
- "@prisma-next/config": "0.5.1",
31
- "@prisma-next/contract": "0.5.1",
32
- "@prisma-next/emitter": "0.5.1",
33
- "@prisma-next/errors": "0.5.1",
34
- "@prisma-next/framework-components": "0.5.1",
35
- "@prisma-next/migration-tools": "0.5.1",
36
- "@prisma-next/psl-printer": "0.5.1",
37
- "@prisma-next/utils": "0.5.1"
30
+ "@prisma-next/config": "0.6.0-dev.10",
31
+ "@prisma-next/contract": "0.6.0-dev.10",
32
+ "@prisma-next/emitter": "0.6.0-dev.10",
33
+ "@prisma-next/errors": "0.6.0-dev.10",
34
+ "@prisma-next/framework-components": "0.6.0-dev.10",
35
+ "@prisma-next/psl-printer": "0.6.0-dev.10",
36
+ "@prisma-next/utils": "0.6.0-dev.10",
37
+ "@prisma-next/migration-tools": "0.6.0-dev.10"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "24.10.4",
41
41
  "tsdown": "0.22.0",
42
42
  "typescript": "5.9.3",
43
43
  "vitest": "4.1.5",
44
- "@prisma-next/sql-contract": "0.5.1",
45
- "@prisma-next/sql-contract-emitter": "0.5.1",
46
- "@prisma-next/sql-contract-ts": "0.5.1",
47
- "@prisma-next/sql-operations": "0.5.1",
44
+ "@prisma-next/sql-contract": "0.6.0-dev.10",
45
+ "@prisma-next/sql-contract-emitter": "0.6.0-dev.10",
46
+ "@prisma-next/sql-operations": "0.6.0-dev.10",
47
+ "@prisma-next/sql-contract-ts": "0.6.0-dev.10",
48
48
  "@prisma-next/test-utils": "0.0.1",
49
- "@prisma-next/sql-runtime": "0.5.1",
50
49
  "@prisma-next/tsconfig": "0.0.0",
50
+ "@prisma-next/sql-runtime": "0.6.0-dev.10",
51
51
  "@prisma-next/tsdown": "0.0.0"
52
52
  },
53
53
  "exports": {
@@ -1,15 +1,39 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { detect } from 'package-manager-detector/detect';
2
+ import { detect, getUserAgent } from 'package-manager-detector/detect';
3
3
  import { join } from 'pathe';
4
4
 
5
5
  export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun' | 'deno';
6
6
 
7
7
  const KNOWN: ReadonlySet<string> = new Set<PackageManager>(['pnpm', 'npm', 'yarn', 'bun', 'deno']);
8
8
 
9
+ /**
10
+ * Resolves the package manager `init` should drive for `add` / `install`
11
+ * commands. Tries, in order:
12
+ *
13
+ * 1. **`detect()`** — walks up from `cwd` looking for a lockfile, the
14
+ * `packageManager` field, the `devEngines.packageManager` field, or
15
+ * install metadata. This is the right answer whenever the user is
16
+ * anywhere inside an existing project, including a deep workspace
17
+ * subdirectory.
18
+ *
19
+ * 2. **`getUserAgent()`** — parses `npm_config_user_agent`, the env var
20
+ * every PM sets when it spawns a script. This catches the
21
+ * bare-directory case where there's no project to walk up to but the
22
+ * user invoked us via `pnpm dlx prisma-next init` / `bunx
23
+ * prisma-next init` / `yarn dlx …`. Same signal used by every
24
+ * `create-*` tool in the ecosystem (`create-vite`, `create-next-app`,
25
+ * `create-astro`, `@antfu/ni`, …).
26
+ *
27
+ * 3. **`npm`** — final fallback. Always present alongside Node.
28
+ */
9
29
  export async function detectPackageManager(cwd: string): Promise<PackageManager> {
10
- const result = await detect({ cwd });
11
- if (result && KNOWN.has(result.name)) {
12
- return result.name as PackageManager;
30
+ const detected = await detect({ cwd });
31
+ if (detected && KNOWN.has(detected.name)) {
32
+ return detected.name as PackageManager;
33
+ }
34
+ const userAgent = getUserAgent();
35
+ if (userAgent !== null && KNOWN.has(userAgent)) {
36
+ return userAgent as PackageManager;
13
37
  }
14
38
  return 'npm';
15
39
  }
@@ -1,21 +1,5 @@
1
1
  import { CliStructuredError } from '../../utils/cli-errors';
2
2
 
3
- /**
4
- * No `package.json` / `deno.json` / `deno.jsonc` in the target directory.
5
- *
6
- * `init` cannot bootstrap a fresh project from a bare directory (NG1) — that
7
- * gap is tracked separately. The fix is for the user to run `npm init` (or
8
- * the equivalent for their package manager) first.
9
- */
10
- export function errorInitMissingManifest(): CliStructuredError {
11
- return new CliStructuredError('5001', 'No project manifest found', {
12
- domain: 'CLI',
13
- why: 'No package.json or deno.json found in the target directory. `prisma-next init` requires an existing project to attach to.',
14
- fix: 'Initialize your project first (e.g. `npm init -y` or `deno init`), then re-run `prisma-next init`.',
15
- docsUrl: 'https://prisma-next.dev/docs/cli/init',
16
- });
17
- }
18
-
19
3
  /**
20
4
  * Re-init in non-interactive mode without `--force`. Distinct from the
21
5
  * decline-the-prompt path (which is `errorInitUserAborted`) because here
@@ -27,9 +27,9 @@ export const INIT_EXIT_INTERNAL_ERROR = 1;
27
27
  /**
28
28
  * Preconditions not met. The caller asked for something we cannot do
29
29
  * without more input or a different environment. Examples:
30
- * - missing `package.json` / `deno.json`
31
30
  * - non-interactive mode without enough flags to proceed
32
31
  * - re-init without `--force` in non-interactive mode
32
+ * - malformed `package.json` / `tsconfig.json`
33
33
  */
34
34
  export const INIT_EXIT_PRECONDITION = 2;
35
35
 
@@ -89,3 +89,80 @@ export function mergePackageScripts(
89
89
  const trailingNewline = existing.endsWith('\n') ? '\n' : '';
90
90
  return { content: `${JSON.stringify(parsed, null, 2)}${trailingNewline}`, warnings };
91
91
  }
92
+
93
+ export interface EsmModuleTypeResult {
94
+ /**
95
+ * The new package.json content. `null` when no change is required —
96
+ * either `"type": "module"` is already set, or the user has explicitly
97
+ * opted into a different module type (in which case `warning` is set).
98
+ */
99
+ readonly content: string | null;
100
+ /**
101
+ * Structured warning raised when `"type"` is already set to a value
102
+ * other than `"module"`. The user's explicit choice is preserved, but
103
+ * the scaffolded `db.ts` uses the ESM-only `with { type: 'json' }`
104
+ * import attribute and will not load under CJS resolution.
105
+ */
106
+ readonly warning: string | null;
107
+ }
108
+
109
+ /**
110
+ * Idempotently sets `"type": "module"` on a `package.json` so the
111
+ * scaffolded `prisma/db.ts` — which uses the ESM-only `with { type: 'json' }`
112
+ * import attribute — loads as ES module under Node's loader (TML-2494).
113
+ *
114
+ * Without this field Node either:
115
+ *
116
+ * - emits `MODULE_TYPELESS_PACKAGE_JSON` and reparses the file as ESM
117
+ * with a perf penalty (Node 22+ with `--experimental-strip-types`), or
118
+ * - hard-fails with `ERR_*` because the CJS loader cannot parse the
119
+ * import-attribute syntax (older Node, or any tool that doesn't
120
+ * reparse).
121
+ *
122
+ * Behaviour:
123
+ *
124
+ * - **Field missing** → set to `"module"`. New entry is inserted right
125
+ * after `"name"` (when present) so the diff lands in a conventional
126
+ * spot for human review; falls through to the natural append position
127
+ * otherwise.
128
+ * - **Field already `"module"`** → no-op (idempotent).
129
+ * - **Field set to anything else** (e.g. `"commonjs"`) → leave it alone
130
+ * and surface a structured warning. The user explicitly opted out of
131
+ * ESM and we don't silently overwrite that.
132
+ */
133
+ export function ensureEsmModuleType(existing: string): EsmModuleTypeResult {
134
+ const parsed = JSON.parse(existing) as Record<string, unknown>;
135
+ const currentType = parsed['type'];
136
+
137
+ if (currentType === 'module') {
138
+ return { content: null, warning: null };
139
+ }
140
+
141
+ if (typeof currentType === 'string' && currentType !== 'module') {
142
+ return {
143
+ content: null,
144
+ warning: `package.json declares "type": "${currentType}" — keeping yours, but the scaffolded prisma/db.ts uses an ESM-only import attribute (\`with { type: 'json' }\`) and will not load under that module type.\nIf you want the default, set "type": "module" in package.json.`,
145
+ };
146
+ }
147
+
148
+ const next: Record<string, unknown> = {};
149
+ let inserted = false;
150
+ for (const [key, value] of Object.entries(parsed)) {
151
+ // A non-string `type` slipped past the early-return guards above
152
+ // (those only fire for `'module'` and other strings). Skip it so the
153
+ // normalised `'module'` we inject below cannot be overwritten when
154
+ // `type` appears after `name` in key order.
155
+ if (key === 'type') continue;
156
+ next[key] = value;
157
+ if (!inserted && key === 'name') {
158
+ next['type'] = 'module';
159
+ inserted = true;
160
+ }
161
+ }
162
+ if (!inserted) {
163
+ next['type'] = 'module';
164
+ }
165
+
166
+ const trailingNewline = existing.endsWith('\n') ? '\n' : '';
167
+ return { content: `${JSON.stringify(next, null, 2)}${trailingNewline}`, warning: null };
168
+ }
@@ -2,7 +2,7 @@ import { execFile } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { promisify } from 'node:util';
4
4
  import * as clack from '@clack/prompts';
5
- import { dirname, isAbsolute, join } from 'pathe';
5
+ import { basename, dirname, isAbsolute, join } from 'pathe';
6
6
  import { CliStructuredError } from '../../utils/cli-errors';
7
7
  import { formatErrorJson, formatErrorOutput } from '../../utils/formatters/errors';
8
8
  import type { GlobalFlags } from '../../utils/global-flags';
@@ -21,7 +21,6 @@ import {
21
21
  errorInitInstallFailed,
22
22
  errorInitInvalidManifest,
23
23
  errorInitInvalidTsconfig,
24
- errorInitMissingManifest,
25
24
  errorInitProbeFailed,
26
25
  } from './errors';
27
26
  import {
@@ -34,7 +33,11 @@ import {
34
33
  } from './exit-codes';
35
34
  import { mergeGitattributes, requiredGitattributesLines } from './hygiene-gitattributes';
36
35
  import { mergeGitignore } from './hygiene-gitignore';
37
- import { mergePackageScripts, REQUIRED_SCRIPTS } from './hygiene-package-scripts';
36
+ import {
37
+ ensureEsmModuleType,
38
+ mergePackageScripts,
39
+ REQUIRED_SCRIPTS,
40
+ } from './hygiene-package-scripts';
38
41
  import { type InitFlagOptions, type ResolvedInitInputs, resolveInitInputs } from './inputs';
39
42
  import {
40
43
  buildNextSteps,
@@ -115,10 +118,6 @@ export async function runInit(
115
118
  clack.intro('prisma-next init', { output: process.stderr });
116
119
  }
117
120
 
118
- if (!hasProjectManifest(baseDir)) {
119
- return emitError(ui, flags, errorInitMissingManifest());
120
- }
121
-
122
121
  let inputs: ResolvedInitInputs;
123
122
  try {
124
123
  inputs = await resolveInitInputs({ baseDir, options, flags, canPrompt });
@@ -244,10 +243,21 @@ export async function runInit(
244
243
  // the FR2.1 `@types/node`-presence check. A malformed manifest is
245
244
  // mapped to a structured precondition error (5010) rather than the
246
245
  // generic INTERNAL_ERROR fallback so CI/agents can branch on it.
246
+ //
247
+ // When neither `package.json` nor a `deno.json[c]` is present, init
248
+ // synthesises a minimal `package.json` (TML-2496) — running
249
+ // `npm init -y` first was friction with no upside, since we always
250
+ // edit the file anyway. A `deno.json[c]` project is left alone:
251
+ // creating a `package.json` next to it would fork the project's
252
+ // dependency graph.
247
253
  const packageJsonPath = join(baseDir, 'package.json');
254
+ const packageJsonExisted = existsSync(packageJsonPath);
255
+ const synthesisePackageJson = !packageJsonExisted && !hasProjectManifest(baseDir);
248
256
  let parsedPackageJson: Record<string, unknown> | null = null;
249
- if (existsSync(packageJsonPath)) {
250
- const pkgRaw = readFileSync(packageJsonPath, 'utf-8');
257
+ if (packageJsonExisted || synthesisePackageJson) {
258
+ const pkgRaw = packageJsonExisted
259
+ ? readFileSync(packageJsonPath, 'utf-8')
260
+ : defaultPackageJsonContent(basename(baseDir));
251
261
  try {
252
262
  parsedPackageJson = JSON.parse(pkgRaw) as Record<string, unknown>;
253
263
  } catch (err) {
@@ -262,11 +272,16 @@ export async function runInit(
262
272
  }
263
273
 
264
274
  // package.json edits are chained: FR9.2 facade-dep removal first
265
- // (so the script merge sees the cleaned `dependencies` and rounds
275
+ // (so the later passes see the cleaned `dependencies` and we round
266
276
  // out a single re-stringification), then FR3.5 / FR9.3 idempotent
267
- // scripts merge with collision detection.
277
+ // scripts merge with collision detection, then `"type": "module"`
278
+ // alignment so the ESM-only `with { type: 'json' }` import attribute
279
+ // in the scaffolded `prisma/db.ts` loads cleanly under Node's
280
+ // loader (TML-2494).
268
281
  let workingPkg = pkgRaw;
269
- let pkgChanged = false;
282
+ // A synthesised manifest is always a write — the file does not
283
+ // exist on disk yet.
284
+ let pkgChanged = synthesisePackageJson;
270
285
  if (inputs.removePreviousFacade !== null) {
271
286
  const next = removeDependency(workingPkg, inputs.removePreviousFacade);
272
287
  if (next !== null) {
@@ -282,10 +297,23 @@ export async function runInit(
282
297
  workingPkg = nextPkg;
283
298
  pkgChanged = true;
284
299
  }
300
+ const { content: typedPkg, warning: typeWarning } = ensureEsmModuleType(workingPkg);
301
+ if (typedPkg !== null) {
302
+ workingPkg = typedPkg;
303
+ pkgChanged = true;
304
+ }
285
305
  if (pkgChanged) {
286
306
  filesToWrite.push({ path: 'package.json', content: workingPkg });
287
307
  }
288
308
  warnings.push(...scriptWarnings);
309
+ if (typeWarning !== null) {
310
+ warnings.push(typeWarning);
311
+ }
312
+ if (synthesisePackageJson) {
313
+ warnings.push(
314
+ 'No package.json found in the target directory; created a minimal one. Edit `name` / `version` to taste.',
315
+ );
316
+ }
289
317
  }
290
318
 
291
319
  // -----------------------------------------------------------------
@@ -481,7 +509,6 @@ function emitError(ui: TerminalUI, flags: GlobalFlags, error: CliStructuredError
481
509
  */
482
510
  export function exitCodeForError(error: { readonly code: string }): number {
483
511
  switch (error.code) {
484
- case '5001': // missing manifest — precondition
485
512
  case '5002': // re-init needs --force — precondition
486
513
  case '5003': // missing flags — precondition
487
514
  case '5004': // invalid flag value — precondition
@@ -823,3 +850,44 @@ function causeMessage(err: unknown): string {
823
850
  if (err instanceof Error) return err.message;
824
851
  return String(err);
825
852
  }
853
+
854
+ /**
855
+ * Minimal `package.json` content used when init runs in a directory
856
+ * that has no project manifest (TML-2496). Mirrors the npm 11 `init -y`
857
+ * defaults, with two deliberate deviations:
858
+ *
859
+ * - `"private": true` so a stray `npm publish` cannot leak the
860
+ * placeholder. Users who want to publish have to opt in by removing
861
+ * the field.
862
+ * - `"type": "module"` so the scaffolded ESM imports in
863
+ * `prisma-next.config.ts` and `db.ts` typecheck and run without
864
+ * additional tsconfig coercion.
865
+ *
866
+ * Exported for unit tests so the canonical shape is asserted in one
867
+ * place rather than re-derived at every call site.
868
+ */
869
+ export function defaultPackageJsonContent(rawName: string): string {
870
+ return `${JSON.stringify(
871
+ {
872
+ name: sanitisePackageName(rawName),
873
+ version: '0.0.0',
874
+ private: true,
875
+ type: 'module',
876
+ },
877
+ null,
878
+ 2,
879
+ )}\n`;
880
+ }
881
+
882
+ /**
883
+ * npm package names are restricted to lowercase, no leading dot/underscore,
884
+ * and a small URL-safe character set. `basename(cwd)` happily returns
885
+ * "My Project" or ".hidden" — both rejected by `npm install` validation.
886
+ * Coerce to a safe fallback rather than emit a manifest npm refuses to
887
+ * read.
888
+ */
889
+ function sanitisePackageName(raw: string): string {
890
+ const lowered = raw.toLowerCase().replace(/[^a-z0-9._~-]/g, '-');
891
+ const trimmed = lowered.replace(/^[._-]+/, '').replace(/-+/g, '-');
892
+ return trimmed.length > 0 ? trimmed : 'my-app';
893
+ }