diff --git a/.eslintrc b/.eslintrc index c7932b6939..040a1e9146 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,7 +16,7 @@ "import/no-extraneous-dependencies": [ "error", { - "devDependencies": ["test/**", "**/*.test.js", "**/*.test.ts"], + "devDependencies": ["test/**", "**/*.test.js", "**/*.test.ts", "tsup.config.ts"], "packageDir": ["./"] } ], diff --git a/docs/dialect.md b/docs/dialect.md index 798f3cc7c5..36835e70ef 100644 --- a/docs/dialect.md +++ b/docs/dialect.md @@ -5,7 +5,8 @@ Specifies the SQL dialect to use. ## Usage ```ts -import { formatDialect, sqlite } from 'sql-formatter'; +import { formatDialect } from 'sql-formatter/dialect'; +import sqlite from 'sql-formatter/languages/sqlite'; const result = formatDialect('SELECT * FROM tbl', { dialect: sqlite }); ``` @@ -49,7 +50,7 @@ Better to always pick something more specific if possible. The `dialect` parameter can also be used to specify a custom SQL dialect configuration: ```ts -import { formatDialect, DialectOptions } from 'sql-formatter'; +import { formatDialect, DialectOptions } from 'sql-formatter/dialect'; const myDialect: DialectOptions { name: 'my_dialect', diff --git a/package.json b/package.json index 596c68e2c0..fda96fb4de 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,14 @@ ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" + }, + "./dialect": { + "import": "./dist/dialect.js", + "require": "./dist/dialect.cjs" + }, + "./languages/*": { + "import": "./dist/languages/*/index.js", + "require": "./dist/languages/*/index.cjs" } }, "bin": { @@ -62,7 +70,7 @@ "prepare": "yarn clean && yarn grammar && yarn fix && yarn check && yarn build", "pre-commit": "npm-run-all --parallel ts:changes lint:changes", "grammar": "nearleyc src/parser/grammar.ne -o src/parser/grammar.ts", - "build:tsup": "tsup src/index.ts --format cjs,esm --sourcemap --dts", + "build:tsup": "tsup", "build:webpack": "webpack --config webpack.prod.js && cp dist/sql-formatter.min.cjs dist/sql-formatter.min.js", "build": "yarn grammar && npm-run-all --parallel build:tsup build:webpack", "release": "release-it" diff --git a/src/FormatOptions.ts b/src/FormatOptions.ts index 4af41184b0..e95ee8bf57 100644 --- a/src/FormatOptions.ts +++ b/src/FormatOptions.ts @@ -12,18 +12,70 @@ export type FunctionCase = KeywordCase; export type LogicalOperatorNewline = 'before' | 'after'; export interface FormatOptions { + /** + * Number of spaces per indentation level. Defaults to `2` + */ tabWidth: number; + /** + * Use tabs for indentation instead of spaces. Defaults to `false` + */ useTabs: boolean; + /** + * Transform case of keywords. Defaults to `"preserve"` + */ keywordCase: KeywordCase; + /** + * Transform case of identifiers. Defaults to `"preserve"` + */ identifierCase: IdentifierCase; + /** + * Transform case of data types. Defaults to `"preserve"` + */ dataTypeCase: DataTypeCase; + /** + * Transform case of function names. Defaults to `"preserve"` + */ functionCase: FunctionCase; + /** + * Indentation style. Defaults to `"standard"` + */ indentStyle: IndentStyle; + /** + * Position of logical operators in multiline conditions. Defaults to `"before"` + */ logicalOperatorNewline: LogicalOperatorNewline; + /** + * Maximum length of expressions before breaking into multiple lines. Defaults to `50` + */ expressionWidth: number; + /** + * Number of blank lines between queries. Defaults to `1` + */ linesBetweenQueries: number; + /** + * Remove spaces around operators. Defaults to `false` + */ denseOperators: boolean; + /** + * Place semicolon on new line. Defaults to `false` + */ newlineBeforeSemicolon: boolean; + params?: ParamItems | string[]; paramTypes?: ParamTypes; } + +export const defaultFormatOptions: FormatOptions = { + tabWidth: 2, + useTabs: false, + keywordCase: 'preserve', + identifierCase: 'preserve', + dataTypeCase: 'preserve', + functionCase: 'preserve', + indentStyle: 'standard', + logicalOperatorNewline: 'before', + expressionWidth: 50, + linesBetweenQueries: 1, + denseOperators: false, + newlineBeforeSemicolon: false, +}; diff --git a/src/allDialects.ts b/src/allDialects.ts index 790ef2095b..416bc0a511 100644 --- a/src/allDialects.ts +++ b/src/allDialects.ts @@ -1,18 +1,19 @@ -export { bigquery } from './languages/bigquery/bigquery.formatter.js'; -export { db2 } from './languages/db2/db2.formatter.js'; -export { db2i } from './languages/db2i/db2i.formatter.js'; -export { hive } from './languages/hive/hive.formatter.js'; -export { mariadb } from './languages/mariadb/mariadb.formatter.js'; -export { mysql } from './languages/mysql/mysql.formatter.js'; -export { tidb } from './languages/tidb/tidb.formatter.js'; -export { n1ql } from './languages/n1ql/n1ql.formatter.js'; -export { plsql } from './languages/plsql/plsql.formatter.js'; -export { postgresql } from './languages/postgresql/postgresql.formatter.js'; -export { redshift } from './languages/redshift/redshift.formatter.js'; -export { spark } from './languages/spark/spark.formatter.js'; -export { sqlite } from './languages/sqlite/sqlite.formatter.js'; -export { sql } from './languages/sql/sql.formatter.js'; -export { trino } from './languages/trino/trino.formatter.js'; -export { transactsql } from './languages/transactsql/transactsql.formatter.js'; -export { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js'; -export { snowflake } from './languages/snowflake/snowflake.formatter.js'; +// When adding a new dialect, be sure to add it to the list of exports below. +export { default as bigquery } from './languages/bigquery/index.js'; +export { default as db2 } from './languages/db2/index.js'; +export { default as db2i } from './languages/db2i/index.js'; +export { default as hive } from './languages/hive/index.js'; +export { default as mariadb } from './languages/mariadb/index.js'; +export { default as mysql } from './languages/mysql/index.js'; +export { default as tidb } from './languages/tidb/index.js'; +export { default as n1ql } from './languages/n1ql/index.js'; +export { default as plsql } from './languages/plsql/index.js'; +export { default as postgresql } from './languages/postgresql/index.js'; +export { default as redshift } from './languages/redshift/index.js'; +export { default as spark } from './languages/spark/index.js'; +export { default as sqlite } from './languages/sqlite/index.js'; +export { default as sql } from './languages/sql/index.js'; +export { default as trino } from './languages/trino/index.js'; +export { default as transactsql } from './languages/transactsql/index.js'; +export { default as singlestoredb } from './languages/singlestoredb/index.js'; +export { default as snowflake } from './languages/snowflake/index.js'; diff --git a/src/dialect.ts b/src/dialect.ts index a4ad930170..63dba3989c 100644 --- a/src/dialect.ts +++ b/src/dialect.ts @@ -5,6 +5,10 @@ import { import Tokenizer from './lexer/Tokenizer.js'; import { TokenizerOptions } from './lexer/TokenizerOptions.js'; +import { defaultFormatOptions, FormatOptions } from './FormatOptions.js'; +import Formatter from './formatter/Formatter.js'; +import { validateConfig } from './validateConfig.js'; + export interface DialectOptions { name: string; tokenizerOptions: TokenizerOptions; @@ -46,3 +50,31 @@ const processDialectFormatOptions = ( (options.tabularOnelineClauses ?? options.onelineClauses).map(name => [name, true]) ), }); + +export type FormatOptionsWithDialect = Partial & { + dialect: DialectOptions; +}; + +/** + * Formats and SQL query string similar to `format` function, however it works directly with dialect objects instead of dialect names. + * Provides better bundling performance and control over the formatting process. + * + * @param {string} query - The SQL query string to format + * @param {FormatOptionsWithDialect} options - Configuration options + * @return {string} formatted query + */ +export const formatDialect = ( + query: string, + { dialect, ...cfg }: FormatOptionsWithDialect +): string => { + if (typeof query !== 'string') { + throw new Error('Invalid query argument. Expected string, instead got ' + typeof query); + } + + const options = validateConfig({ + ...defaultFormatOptions, + ...cfg, + }); + + return new Formatter(createDialect(dialect), options).format(query); +}; diff --git a/src/index.ts b/src/index.ts index d4aab01736..8765906590 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,10 @@ -export { supportedDialects, format, formatDialect } from './sqlFormatter.js'; +export { supportedDialects, format } from './sqlFormatter.js'; export { expandPhrases } from './expandPhrases.js'; export { ConfigError } from './validateConfig.js'; -// When adding a new dialect, be sure to add it to the list of exports below. -export { bigquery } from './languages/bigquery/bigquery.formatter.js'; -export { db2 } from './languages/db2/db2.formatter.js'; -export { db2i } from './languages/db2i/db2i.formatter.js'; -export { hive } from './languages/hive/hive.formatter.js'; -export { mariadb } from './languages/mariadb/mariadb.formatter.js'; -export { mysql } from './languages/mysql/mysql.formatter.js'; -export { tidb } from './languages/tidb/tidb.formatter.js'; -export { n1ql } from './languages/n1ql/n1ql.formatter.js'; -export { plsql } from './languages/plsql/plsql.formatter.js'; -export { postgresql } from './languages/postgresql/postgresql.formatter.js'; -export { redshift } from './languages/redshift/redshift.formatter.js'; -export { spark } from './languages/spark/spark.formatter.js'; -export { sqlite } from './languages/sqlite/sqlite.formatter.js'; -export { sql } from './languages/sql/sql.formatter.js'; -export { trino } from './languages/trino/trino.formatter.js'; -export { transactsql } from './languages/transactsql/transactsql.formatter.js'; -export { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js'; -export { snowflake } from './languages/snowflake/snowflake.formatter.js'; - // NB! To re-export types the "export type" syntax is required by webpack. // Otherwise webpack build will fail. -export type { - SqlLanguage, - FormatOptionsWithLanguage, - FormatOptionsWithDialect, -} from './sqlFormatter.js'; +export type { SqlLanguage, FormatOptionsWithLanguage } from './sqlFormatter.js'; export type { IndentStyle, KeywordCase, @@ -40,4 +16,3 @@ export type { } from './FormatOptions.js'; export type { ParamItems } from './formatter/Params.js'; export type { ParamTypes } from './lexer/TokenizerOptions.js'; -export type { DialectOptions } from './dialect.js'; diff --git a/src/languages/bigquery/index.ts b/src/languages/bigquery/index.ts new file mode 100644 index 0000000000..fff797d1ec --- /dev/null +++ b/src/languages/bigquery/index.ts @@ -0,0 +1,3 @@ +import { bigquery } from './bigquery.formatter.js'; + +export default bigquery; diff --git a/src/languages/db2/index.ts b/src/languages/db2/index.ts new file mode 100644 index 0000000000..d17e8ffb42 --- /dev/null +++ b/src/languages/db2/index.ts @@ -0,0 +1,3 @@ +import { db2 } from './db2.formatter.js'; + +export default db2; diff --git a/src/languages/db2i/index.ts b/src/languages/db2i/index.ts new file mode 100644 index 0000000000..4130f9b678 --- /dev/null +++ b/src/languages/db2i/index.ts @@ -0,0 +1,3 @@ +import { db2i } from './db2i.formatter.js'; + +export default db2i; diff --git a/src/languages/hive/index.ts b/src/languages/hive/index.ts new file mode 100644 index 0000000000..130005c966 --- /dev/null +++ b/src/languages/hive/index.ts @@ -0,0 +1,3 @@ +import { hive } from './hive.formatter.js'; + +export default hive; diff --git a/src/languages/mariadb/index.ts b/src/languages/mariadb/index.ts new file mode 100644 index 0000000000..11e41b10b5 --- /dev/null +++ b/src/languages/mariadb/index.ts @@ -0,0 +1,3 @@ +import { mariadb } from './mariadb.formatter.js'; + +export default mariadb; diff --git a/src/languages/mysql/index.ts b/src/languages/mysql/index.ts new file mode 100644 index 0000000000..e637d2f162 --- /dev/null +++ b/src/languages/mysql/index.ts @@ -0,0 +1,3 @@ +import { mysql } from './mysql.formatter.js'; + +export default mysql; diff --git a/src/languages/n1ql/index.ts b/src/languages/n1ql/index.ts new file mode 100644 index 0000000000..05191c923f --- /dev/null +++ b/src/languages/n1ql/index.ts @@ -0,0 +1,3 @@ +import { n1ql } from './n1ql.formatter.js'; + +export default n1ql; diff --git a/src/languages/plsql/index.ts b/src/languages/plsql/index.ts new file mode 100644 index 0000000000..e4b1b655a8 --- /dev/null +++ b/src/languages/plsql/index.ts @@ -0,0 +1,3 @@ +import { plsql } from './plsql.formatter.js'; + +export default plsql; diff --git a/src/languages/postgresql/index.ts b/src/languages/postgresql/index.ts new file mode 100644 index 0000000000..3a7254a35f --- /dev/null +++ b/src/languages/postgresql/index.ts @@ -0,0 +1,3 @@ +import { postgresql } from './postgresql.formatter.js'; + +export default postgresql; diff --git a/src/languages/redshift/index.ts b/src/languages/redshift/index.ts new file mode 100644 index 0000000000..dfc680e659 --- /dev/null +++ b/src/languages/redshift/index.ts @@ -0,0 +1,3 @@ +import { redshift } from './redshift.formatter.js'; + +export default redshift; diff --git a/src/languages/singlestoredb/index.ts b/src/languages/singlestoredb/index.ts new file mode 100644 index 0000000000..e517fef1e0 --- /dev/null +++ b/src/languages/singlestoredb/index.ts @@ -0,0 +1,3 @@ +import { singlestoredb } from './singlestoredb.formatter.js'; + +export default singlestoredb; diff --git a/src/languages/snowflake/index.ts b/src/languages/snowflake/index.ts new file mode 100644 index 0000000000..275fdeda81 --- /dev/null +++ b/src/languages/snowflake/index.ts @@ -0,0 +1,3 @@ +import { snowflake } from './snowflake.formatter.js'; + +export default snowflake; diff --git a/src/languages/spark/index.ts b/src/languages/spark/index.ts new file mode 100644 index 0000000000..7d6c333739 --- /dev/null +++ b/src/languages/spark/index.ts @@ -0,0 +1,3 @@ +import { spark } from './spark.formatter.js'; + +export default spark; diff --git a/src/languages/sql/index.ts b/src/languages/sql/index.ts new file mode 100644 index 0000000000..163275695b --- /dev/null +++ b/src/languages/sql/index.ts @@ -0,0 +1,3 @@ +import { sql } from './sql.formatter.js'; + +export default sql; diff --git a/src/languages/sqlite/index.ts b/src/languages/sqlite/index.ts new file mode 100644 index 0000000000..f42ba690c3 --- /dev/null +++ b/src/languages/sqlite/index.ts @@ -0,0 +1,3 @@ +import { sqlite } from './sqlite.formatter.js'; + +export default sqlite; diff --git a/src/languages/tidb/index.ts b/src/languages/tidb/index.ts new file mode 100644 index 0000000000..6f5c62e582 --- /dev/null +++ b/src/languages/tidb/index.ts @@ -0,0 +1,3 @@ +import { tidb } from './tidb.formatter.js'; + +export default tidb; diff --git a/src/languages/transactsql/index.ts b/src/languages/transactsql/index.ts new file mode 100644 index 0000000000..f2600e645f --- /dev/null +++ b/src/languages/transactsql/index.ts @@ -0,0 +1,3 @@ +import { transactsql } from './transactsql.formatter.js'; + +export default transactsql; diff --git a/src/languages/trino/index.ts b/src/languages/trino/index.ts new file mode 100644 index 0000000000..0789f1465d --- /dev/null +++ b/src/languages/trino/index.ts @@ -0,0 +1,3 @@ +import { trino } from './trino.formatter.js'; + +export default trino; diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts index b35ac56e22..bfb085f55b 100644 --- a/src/sqlFormatter.ts +++ b/src/sqlFormatter.ts @@ -1,9 +1,8 @@ import * as allDialects from './allDialects.js'; +import { formatDialect } from './dialect.js'; import { FormatOptions } from './FormatOptions.js'; -import { createDialect, DialectOptions } from './dialect.js'; -import Formatter from './formatter/Formatter.js'; -import { ConfigError, validateConfig } from './validateConfig.js'; +import { ConfigError } from './validateConfig.js'; const dialectNameMap: Record = { bigquery: 'bigquery', @@ -31,70 +30,38 @@ export const supportedDialects = Object.keys(dialectNameMap); export type SqlLanguage = keyof typeof dialectNameMap; export type FormatOptionsWithLanguage = Partial & { + /** + * SQL dialect to use (e.g. 'postgresql', 'mysql'). Defaults to 'sql' + */ language?: SqlLanguage; }; -export type FormatOptionsWithDialect = Partial & { - dialect: DialectOptions; -}; - -const defaultOptions: FormatOptions = { - tabWidth: 2, - useTabs: false, - keywordCase: 'preserve', - identifierCase: 'preserve', - dataTypeCase: 'preserve', - functionCase: 'preserve', - indentStyle: 'standard', - logicalOperatorNewline: 'before', - expressionWidth: 50, - linesBetweenQueries: 1, - denseOperators: false, - newlineBeforeSemicolon: false, -}; - /** - * Format whitespace in a query to make it easier to read. + * Formats an SQL query string with consistent whitespace and indentation. * - * @param {string} query - input SQL query string - * @param {FormatOptionsWithLanguage} cfg Configuration options (see docs in README) - * @return {string} formatted query + * @param {string} query - The SQL query string to format + * @param {FormatOptionsWithLanguage} options - Configuration options + * @returns {string} The formatted SQL query + * @throws {ConfigError} If an unsupported SQL dialect is specified + * + * @example + * format('SELECT * FROM users WHERE id = 1'); + * // Returns: + * // SELECT * + * // FROM users + * // WHERE id = 1 */ -export const format = (query: string, cfg: FormatOptionsWithLanguage = {}): string => { - if (typeof cfg.language === 'string' && !supportedDialects.includes(cfg.language)) { - throw new ConfigError(`Unsupported SQL dialect: ${cfg.language}`); +export const format = (query: string, options: FormatOptionsWithLanguage = {}): string => { + if (typeof options.language === 'string' && !supportedDialects.includes(options.language)) { + throw new ConfigError(`Unsupported SQL dialect: ${options.language}`); } - const canonicalDialectName = dialectNameMap[cfg.language || 'sql']; + const canonicalDialectName = dialectNameMap[options.language || 'sql']; return formatDialect(query, { - ...cfg, + ...options, dialect: allDialects[canonicalDialectName], }); }; -/** - * Like the above format(), but language parameter is mandatory - * and must be a Dialect object instead of a string. - * - * @param {string} query - input SQL query string - * @param {FormatOptionsWithDialect} cfg Configuration options (see docs in README) - * @return {string} formatted query - */ -export const formatDialect = ( - query: string, - { dialect, ...cfg }: FormatOptionsWithDialect -): string => { - if (typeof query !== 'string') { - throw new Error('Invalid query argument. Expected string, instead got ' + typeof query); - } - - const options = validateConfig({ - ...defaultOptions, - ...cfg, - }); - - return new Formatter(createDialect(dialect), options).format(query); -}; - export type FormatFn = typeof format; diff --git a/test/sqlFormatter.test.ts b/test/sqlFormatter.test.ts index 87e9cca082..16029c2993 100644 --- a/test/sqlFormatter.test.ts +++ b/test/sqlFormatter.test.ts @@ -1,6 +1,8 @@ import dedent from 'dedent-js'; -import { format, formatDialect, SqlLanguage, sqlite, DialectOptions } from '../src/index.js'; +import { format, SqlLanguage } from '../src/index.js'; +import { formatDialect, DialectOptions } from '../src/dialect.js'; +import sqlite from '../src/languages/sqlite/index.js'; describe('sqlFormatter', () => { it('throws error when unsupported language parameter specified', () => { diff --git a/tsconfig.json b/tsconfig.json index 79b737b98d..1f3ee57314 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ /* Additional Checks */ "noImplicitReturns": true /* Report error when not all code paths in function return a value. */ }, - "include": ["src", "test", "static"], + "include": ["src", "test", "static", "tsup.config.ts"], "exclude": ["node_modules"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000000..d62b8df96d --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts', 'src/dialect.ts', 'src/languages/*/index.ts'], + format: ['cjs', 'esm'], + sourcemap: true, + dts: true, +});