forked from prisma/prisma
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): implement generic sub-commands (+ policy) (prisma#26134)
- Loading branch information
Showing
8 changed files
with
173 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { getCommand } from '@antfu/ni' | ||
import type { Command } from '@prisma/internals' | ||
import { command } from 'execa' | ||
import { existsSync } from 'fs' | ||
import { tmpdir } from 'os' | ||
|
||
/** | ||
* Sub-CLIs that are installed on demand need to implement this interface | ||
*/ | ||
type Runnable = { | ||
run: (args: string[]) => Promise<void> | ||
} | ||
|
||
/** | ||
* Generic SubCommand that installs a package on demand and runs it | ||
*/ | ||
export class SubCommand implements Command { | ||
pkg: string | ||
|
||
constructor(pkg: string) { | ||
this.pkg = pkg | ||
} | ||
|
||
async parse(argv: string[]): Promise<string | Error> { | ||
// we accept forcing a version with @, eg. prisma policy @1.0.0 --help | ||
const [version, ...args] = argv[0]?.startsWith('@') ? argv : ['@latest', ...argv] | ||
const pkg = `${this.pkg}${version}` | ||
|
||
// when version defaults to @latest, we cache it for the current day only | ||
const dayMillis = new Date().setHours(0, 0, 0, 0) | ||
const cacheKey = version === '@latest' ? `-${dayMillis}` : '' | ||
const prefix = `${tmpdir()}/${pkg}${cacheKey}` | ||
|
||
// if the package is not installed yet, we install it otherwise we skip | ||
if (existsSync(prefix) === false) { | ||
const installCmd = getCommand('npm', 'install', [pkg, '--no-save', '--prefix', prefix]) | ||
await command(installCmd, { stdout: 'ignore', stderr: 'inherit', env: process.env }) | ||
} | ||
|
||
// load the module and run it via the Runnable interface | ||
const module: Runnable = await import(`${prefix}/node_modules/${this.pkg}`) | ||
await module.run(args) | ||
|
||
return '' | ||
} | ||
|
||
public help() {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import * as ni from '@antfu/ni' | ||
import * as execa from 'execa' | ||
import { rm } from 'fs/promises' | ||
import { copy } from 'fs-extra' | ||
import { tmpdir } from 'os' | ||
import { join } from 'path' | ||
|
||
import { SubCommand } from '../../SubCommand' | ||
|
||
jest.mock('@antfu/ni') | ||
jest.mock('execa') | ||
|
||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01')) | ||
|
||
const getDayMillis = () => new Date().setHours(0, 0, 0, 0) | ||
|
||
beforeEach(async () => { | ||
await rm(join(tmpdir(), `[email protected]`), { recursive: true, force: true }) | ||
await rm(join(tmpdir(), `sub-command@latest-${getDayMillis()}`), { recursive: true, force: true }) | ||
}) | ||
|
||
test('@<version>', async () => { | ||
const cmd = new SubCommand('sub-command') | ||
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) | ||
|
||
const copySrc = join(__dirname, '..', 'fixtures', 'sub-command') | ||
const copyDest = join(tmpdir(), `[email protected]`) | ||
await copy(copySrc, copyDest, { recursive: true }) | ||
|
||
await cmd.parse(['@0.0.0', '--help']) | ||
|
||
expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(` | ||
[ | ||
[ | ||
"sub-command", | ||
[ | ||
[ | ||
"--help", | ||
], | ||
], | ||
], | ||
] | ||
`) | ||
|
||
consoleLogSpy.mockRestore() | ||
}) | ||
|
||
test('@latest', async () => { | ||
const cmd = new SubCommand('sub-command') | ||
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) | ||
|
||
const copySrc = join(__dirname, '..', 'fixtures', 'sub-command') | ||
const copyDest = join(tmpdir(), `sub-command@latest-${getDayMillis()}`) | ||
await copy(copySrc, copyDest, { recursive: true }) | ||
|
||
await cmd.parse(['--help']) | ||
|
||
expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(` | ||
[ | ||
[ | ||
"sub-command", | ||
[ | ||
[ | ||
"--help", | ||
], | ||
], | ||
], | ||
] | ||
`) | ||
|
||
consoleLogSpy.mockRestore() | ||
}) | ||
|
||
test('autoinstall', async () => { | ||
const cmd = new SubCommand('sub-command') | ||
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) | ||
|
||
const copySrc = join(__dirname, '..', 'fixtures', 'sub-command') | ||
const copyDest = join(tmpdir(), '[email protected]') | ||
|
||
jest.mocked(ni.getCommand).mockReturnValue('npm install sub-command --no-save --prefix /tmp/[email protected]') | ||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
jest.mocked(execa.command).mockImplementation((async () => { | ||
await copy(copySrc, copyDest, { recursive: true }) | ||
}) as () => any) | ||
|
||
await cmd.parse(['@0.0.0', '--help']) | ||
|
||
expect(consoleLogSpy.mock.calls).toMatchInlineSnapshot(` | ||
[ | ||
[ | ||
"sub-command", | ||
[ | ||
[ | ||
"--help", | ||
], | ||
], | ||
], | ||
] | ||
`) | ||
|
||
// eslint-disable-next-line @typescript-eslint/unbound-method | ||
expect(execa.command).toHaveBeenCalled() | ||
expect(ni.getCommand).toHaveBeenCalled() | ||
|
||
consoleLogSpy.mockRestore() | ||
}) |
3 changes: 3 additions & 0 deletions
3
packages/cli/src/__tests__/fixtures/sub-command/node_modules/sub-command/index.mjs
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
5 changes: 5 additions & 0 deletions
5
packages/cli/src/__tests__/fixtures/sub-command/node_modules/sub-command/package.json
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ import { Platform } from './platform/_Platform' | |
prisma:cli - /Users/j42/Dev/prisma-meow/node_modules/.pnpm/@[email protected]/node_modules/@prisma/studio-pcw/dist/index.js | ||
*/ | ||
import { Studio } from './Studio' | ||
import { SubCommand } from './SubCommand' | ||
import { Telemetry } from './Telemetry' | ||
import { redactCommandArray, runCheckpointClientCheck } from './utils/checkpoint' | ||
import { detectPrisma1 } from './utils/detectPrisma1' | ||
|
@@ -46,7 +47,6 @@ import { Version } from './Version' | |
|
||
const debug = Debug('prisma:cli:bin') | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment | ||
const packageJson = require('../package.json') | ||
|
||
const commandArray = process.argv.slice(2) | ||
|
@@ -86,6 +86,8 @@ async function main(): Promise<number> { | |
{ | ||
init: Init.new(), | ||
platform: Platform.$.new({ | ||
policy: new SubCommand('@prisma/cli-policy'), | ||
|
||
workspace: Platform.Workspace.$.new({ | ||
show: Platform.Workspace.Show.new(), | ||
}), | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.