@prisma-next/cli 0.6.0-dev.7 → 0.6.0-dev.8
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/dist/cli.mjs +1 -1
- package/dist/{init-DETSgw3h.mjs → init-BRKnARU6.mjs} +125 -25
- package/dist/init-BRKnARU6.mjs.map +1 -0
- package/package.json +16 -16
- package/src/commands/init/detect-package-manager.ts +28 -4
- package/src/commands/init/errors.ts +0 -16
- package/src/commands/init/exit-codes.ts +1 -1
- package/src/commands/init/hygiene-package-scripts.ts +77 -0
- package/src/commands/init/init.ts +81 -13
- package/dist/init-DETSgw3h.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/cli",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -27,28 +27,28 @@
|
|
|
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.6.0-dev.
|
|
31
|
-
"@prisma-next/contract": "0.6.0-dev.
|
|
32
|
-
"@prisma-next/
|
|
33
|
-
"@prisma-next/
|
|
34
|
-
"@prisma-next/migration-tools": "0.6.0-dev.
|
|
35
|
-
"@prisma-next/
|
|
36
|
-
"@prisma-next/
|
|
37
|
-
"@prisma-next/
|
|
30
|
+
"@prisma-next/config": "0.6.0-dev.8",
|
|
31
|
+
"@prisma-next/contract": "0.6.0-dev.8",
|
|
32
|
+
"@prisma-next/emitter": "0.6.0-dev.8",
|
|
33
|
+
"@prisma-next/framework-components": "0.6.0-dev.8",
|
|
34
|
+
"@prisma-next/migration-tools": "0.6.0-dev.8",
|
|
35
|
+
"@prisma-next/errors": "0.6.0-dev.8",
|
|
36
|
+
"@prisma-next/psl-printer": "0.6.0-dev.8",
|
|
37
|
+
"@prisma-next/utils": "0.6.0-dev.8"
|
|
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.6.0-dev.
|
|
45
|
-
"@prisma-next/sql-contract-
|
|
46
|
-
"@prisma-next/sql-
|
|
47
|
-
"@prisma-next/sql-
|
|
44
|
+
"@prisma-next/sql-contract": "0.6.0-dev.8",
|
|
45
|
+
"@prisma-next/sql-contract-ts": "0.6.0-dev.8",
|
|
46
|
+
"@prisma-next/sql-operations": "0.6.0-dev.8",
|
|
47
|
+
"@prisma-next/sql-contract-emitter": "0.6.0-dev.8",
|
|
48
|
+
"@prisma-next/sql-runtime": "0.6.0-dev.8",
|
|
48
49
|
"@prisma-next/test-utils": "0.0.1",
|
|
49
|
-
"@prisma-next/
|
|
50
|
-
"@prisma-next/tsconfig": "0.0.0"
|
|
51
|
-
"@prisma-next/tsdown": "0.0.0"
|
|
50
|
+
"@prisma-next/tsdown": "0.0.0",
|
|
51
|
+
"@prisma-next/tsconfig": "0.0.0"
|
|
52
52
|
},
|
|
53
53
|
"exports": {
|
|
54
54
|
".": {
|
|
@@ -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
|
|
11
|
-
if (
|
|
12
|
-
return
|
|
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 {
|
|
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 (
|
|
250
|
-
const pkgRaw =
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|