diff --git a/packages/schema/src/cli/actions/format.ts b/packages/schema/src/cli/actions/format.ts new file mode 100644 index 000000000..0d717343c --- /dev/null +++ b/packages/schema/src/cli/actions/format.ts @@ -0,0 +1,21 @@ +import { getVersion } from '@zenstackhq/runtime'; +import { formatDocument } from '../cli-util'; +import colors from 'colors'; +import ora from 'ora'; +import { writeFile } from 'fs/promises'; + +export async function format(projectPath: string, options: { schema: string }) { + const version = getVersion(); + console.log(colors.bold(`⌛️ ZenStack CLI v${version}`)); + + const schemaFile = options.schema; + const spinner = ora(`Formatting ${schemaFile}`).start(); + try { + const formattedDoc = await formatDocument(schemaFile); + await writeFile(schemaFile, formattedDoc); + spinner.succeed(); + } catch (e) { + spinner.fail(); + throw e; + } +} diff --git a/packages/schema/src/cli/actions/index.ts b/packages/schema/src/cli/actions/index.ts index 0795902d7..e56e4797c 100644 --- a/packages/schema/src/cli/actions/index.ts +++ b/packages/schema/src/cli/actions/index.ts @@ -2,3 +2,4 @@ export * from './generate'; export * from './info'; export * from './init'; export * from './repl'; +export * from './format'; diff --git a/packages/schema/src/cli/cli-util.ts b/packages/schema/src/cli/cli-util.ts index f7e090990..04a64af24 100644 --- a/packages/schema/src/cli/cli-util.ts +++ b/packages/schema/src/cli/cli-util.ts @@ -13,6 +13,8 @@ import { createZModelServices, ZModelServices } from '../language-server/zmodel- import { mergeBaseModel, resolveImport, resolveTransitiveImports } from '../utils/ast-utils'; import { getVersion } from '../utils/version-utils'; import { CliError } from './cli-error'; +import { ZModelFormatter } from '../language-server/zmodel-formatter'; +import { TextDocument } from 'vscode-languageserver-textdocument'; // required minimal version of Prisma export const requiredPrismaVersion = '4.8.0'; @@ -251,3 +253,26 @@ export async function checkNewVersion() { console.log(`A newer version ${colors.cyan(latestVersion)} is available.`); } } + +export async function formatDocument(fileName: string) { + const services = createZModelServices(NodeFileSystem).ZModel; + const extensions = services.LanguageMetaData.fileExtensions; + if (!extensions.includes(path.extname(fileName))) { + console.error(colors.yellow(`Please choose a file with extension: ${extensions}.`)); + throw new CliError('invalid schema file'); + } + + const langiumDocuments = services.shared.workspace.LangiumDocuments; + const document = langiumDocuments.getOrCreateDocument(URI.file(path.resolve(fileName))); + + const formatter = services.lsp.Formatter as ZModelFormatter; + + const identifier = { uri: document.uri.toString() }; + const options = formatter.getFormatOptions() ?? { + insertSpaces: true, + tabSize: 4, + }; + + const edits = await formatter.formatDocument(document, { options, textDocument: identifier }); + return TextDocument.applyEdits(document.textDocument, edits); +} diff --git a/packages/schema/src/cli/index.ts b/packages/schema/src/cli/index.ts index b57a1446f..84861bdd4 100644 --- a/packages/schema/src/cli/index.ts +++ b/packages/schema/src/cli/index.ts @@ -50,6 +50,16 @@ export const replAction = async (options: Parameters[1]): P ); }; +export const formatAction = async (options: Parameters[1]): Promise => { + await telemetry.trackSpan( + 'cli:command:start', + 'cli:command:complete', + 'cli:command:error', + { command: 'format' }, + () => actions.format(process.cwd(), options) + ); +}; + export function createProgram() { const program = new Command('zenstack'); @@ -116,6 +126,12 @@ export function createProgram() { .option('--table', 'enable table format output') .action(replAction); + program + .command('format') + .description('Format a ZenStack schema file.') + .addOption(schemaOption) + .action(formatAction); + // make sure config is loaded before actions run program.hook('preAction', async (_, actionCommand) => { let configFile: string | undefined = actionCommand.opts().config; diff --git a/packages/schema/tests/schema/formatter.test.ts b/packages/schema/tests/schema/formatter.test.ts index c0f296216..35b08707d 100644 --- a/packages/schema/tests/schema/formatter.test.ts +++ b/packages/schema/tests/schema/formatter.test.ts @@ -7,12 +7,11 @@ const services = createZModelServices({ ...EmptyFileSystem }).ZModel; const formatting = expectFormatting(services); describe('ZModelFormatter', () => { - // eslint-disable-next-line jest/no-disabled-tests - test.skip('declaration formatting', async () => { + it('declaration formatting', async () => { await formatting({ before: `datasource db { provider = 'postgresql' url = env('DATABASE_URL')} generator js {provider = 'prisma-client-js'} plugin swrHooks {provider = '@zenstackhq/swr'output = 'lib/hooks'} - model User {id:id String @id name String? } + model User {id String @id name String? } enum Role {ADMIN USER}`, after: `datasource db { provider = 'postgresql' @@ -26,7 +25,6 @@ plugin swrHooks { output = 'lib/hooks' } model User { - id id String @id name String? } diff --git a/tests/integration/tests/cli/format.test.ts b/tests/integration/tests/cli/format.test.ts new file mode 100644 index 000000000..781d6c917 --- /dev/null +++ b/tests/integration/tests/cli/format.test.ts @@ -0,0 +1,43 @@ +import * as fs from 'fs'; +import * as tmp from 'tmp'; +import { createProgram } from '../../../../packages/schema/src/cli'; +describe('CLI format test', () => { + let origDir: string; + + beforeEach(() => { + origDir = process.cwd(); + const r = tmp.dirSync({ unsafeCleanup: true }); + console.log(`Project dir: ${r.name}`); + process.chdir(r.name); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('basic format', async () => { + const model = ` + datasource db {provider="sqlite" url="file:./dev.db"} + generator client {provider = "prisma-client-js"} + model Post {id Int @id() @default(autoincrement())users User[]}`; + + const formattedModel = ` +datasource db { + provider="sqlite" + url="file:./dev.db" +} +generator client { + provider = "prisma-client-js" +} +model Post { + id Int @id() @default(autoincrement()) + users User[] +}`; + // set up schema + fs.writeFileSync('schema.zmodel', model, 'utf-8'); + const program = createProgram(); + await program.parseAsync(['format'], { from: 'user' }); + + expect(fs.readFileSync('schema.zmodel', 'utf-8')).toEqual(formattedModel); + }); +});