Repository: sql-formatter-org/sql-formatter (ts)

Issue:
Explicitly include a single dialect
We are using this library in production in the browser, so bundle sizes matter. 

When I analyze my bundle app, the sql-formatter library uses about 443kb, but a large part of this is all the dialetcs:

![image](https://user-images.githubusercontent.com/1142915/191344127-46b8a948-1463-41a0-b017-d0f00e55efc3.png)

We only need 1 dialect, so there is no need for the others to be in the bundle.

Would it be possible to have an option to explicitly import a language so that the rest can be tree-shaken from the bundle?

I was thinking, maybe something like this:
`import { format } from 'sql-formatter/sqlite';`

Pull request description:
Single dialect import
New API for supporting import of a single dialect. As discussed in #484, I've added a separate formatting function that works a bit differently than the old one.

The old `format()` function works like before, with the exception that `language` parameter can only be string:

```ts
import { format } from "sql-formatter";

format("SELECT *", { language: "mysql" });
```

The new `formatDialect()` only works with explicitly imported dialect:

```ts
import { formatDialect, mysql } from "sql-formatter";

formatDialect("SELECT *", { dialect: mysql });
```

`formatDialect()` is also used under the hood by the old `format()` function.

I chose to name it `formatDialect` to make it clear that it accepts a `dialect` config option (instead of `language`). This also aligns with the naming of types, where the actual value is a `DialectOptions` object. (Originally I thought of using `language` option for both, but this seemed too confusing - an option expecting one type in one context and another type in another context - so I opted for clearly distinct option names).

Fixes #452 

Patch:
diff --git a/.eslintrc b/.eslintrc
index 72910350..c7932b69 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -45,7 +45,8 @@
       { "avoidEscape": true, "allowTemplateLiterals": true }
     ],
     "@typescript-eslint/semi": "error",
-    "@typescript-eslint/no-non-null-assertion": "error"
+    "@typescript-eslint/no-non-null-assertion": "error",
+    "@typescript-eslint/consistent-type-exports": "error"
   },
   "settings": {
     "import/resolver": {
diff --git a/src/allDialects.ts b/src/allDialects.ts
new file mode 100644
index 00000000..7ba24ffb
--- /dev/null
+++ b/src/allDialects.ts
@@ -0,0 +1,16 @@
+export { bigquery } from './languages/bigquery/bigquery.formatter.js';
+export { db2 } from './languages/db2/db2.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 { 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';
diff --git a/src/index.ts b/src/index.ts
index ed358225..76b57732 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,17 @@
-export * from './sqlFormatter.js';
+export { supportedDialects, format, formatDialect } from './sqlFormatter.js';
+export { expandPhrases } from './expandPhrases.js';
+
+// Intentionally use "export *" syntax here to make sure when adding a new SQL dialect
+// we wouldn't forget to expose it in our public API.
+export * from './allDialects.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 {
   IndentStyle,
   KeywordCase,
@@ -7,4 +20,4 @@ export type {
   FormatOptions,
 } from './FormatOptions.js';
 export type { DialectOptions } from './dialect.js';
-export { expandPhrases } from './expandPhrases.js';
+export type { ConfigError } from './validateConfig.js';
diff --git a/src/sqlFormatter.ts b/src/sqlFormatter.ts
index f9b34d13..abee1b24 100644
--- a/src/sqlFormatter.ts
+++ b/src/sqlFormatter.ts
@@ -1,53 +1,26 @@
-import { bigquery } from './languages/bigquery/bigquery.formatter.js';
-import { db2 } from './languages/db2/db2.formatter.js';
-import { hive } from './languages/hive/hive.formatter.js';
-import { mariadb } from './languages/mariadb/mariadb.formatter.js';
-import { mysql } from './languages/mysql/mysql.formatter.js';
-import { n1ql } from './languages/n1ql/n1ql.formatter.js';
-import { plsql } from './languages/plsql/plsql.formatter.js';
-import { postgresql } from './languages/postgresql/postgresql.formatter.js';
-import { redshift } from './languages/redshift/redshift.formatter.js';
-import { spark } from './languages/spark/spark.formatter.js';
-import { sqlite } from './languages/sqlite/sqlite.formatter.js';
-import { sql } from './languages/sql/sql.formatter.js';
-import { trino } from './languages/trino/trino.formatter.js';
-import { transactsql } from './languages/transactsql/transactsql.formatter.js';
-import { singlestoredb } from './languages/singlestoredb/singlestoredb.formatter.js';
-import { snowflake } from './languages/snowflake/snowflake.formatter.js';
+import * as allDialects from './allDialects.js';
 
 import { FormatOptions } from './FormatOptions.js';
-import { ParamItems } from './formatter/Params.js';
 import { createDialect, DialectOptions } from './dialect.js';
 import Formatter from './formatter/Formatter.js';
+import { ConfigError, validateConfig } from './validateConfig.js';
 
-export const formatters = {
-  bigquery,
-  db2,
-  hive,
-  mariadb,
-  mysql,
-  n1ql,
-  plsql,
-  postgresql,
-  redshift,
-  singlestoredb,
-  snowflake,
-  spark,
-  sql,
-  sqlite,
-  transactsql,
-  trino,
-  tsql: transactsql, // alias for transactsql
+const formatters = {
+  ...allDialects,
+  tsql: allDialects.transactsql, // alias for transactsql
 };
 export type SqlLanguage = keyof typeof formatters;
 export const supportedDialects = Object.keys(formatters);
 
-export interface FormatOptionsWithLanguage extends FormatOptions {
-  language: SqlLanguage | DialectOptions;
-}
+export type FormatOptionsWithLanguage = Partial<FormatOptions> & {
+  language?: SqlLanguage;
+};
+
+export type FormatOptionsWithDialect = Partial<FormatOptions> & {
+  dialect: DialectOptions;
+};
 
-const defaultOptions: FormatOptionsWithLanguage = {
-  language: 'sql',
+const defaultOptions: FormatOptions = {
   tabWidth: 2,
   useTabs: false,
   keywordCase: 'preserve',
@@ -65,10 +38,32 @@ const defaultOptions: FormatOptionsWithLanguage = {
  * Format whitespace in a query to make it easier to read.
  *
  * @param {string} query - input SQL query string
- * @param {Partial<FormatOptionsWithLanguage>} cfg Configuration options (see docs in README)
+ * @param {FormatOptionsWithLanguage} cfg Configuration options (see docs in README)
+ * @return {string} formatted query
+ */
+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}`);
+  }
+
+  return formatDialect(query, {
+    ...cfg,
+    dialect: formatters[cfg.language || 'sql'],
+  });
+};
+
+/**
+ * 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 format = (query: string, cfg: Partial<FormatOptionsWithLanguage> = {}): string => {
+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);
   }
@@ -78,55 +73,7 @@ export const format = (query: string, cfg: Partial<FormatOptionsWithLanguage> =
     ...cfg,
   });
 
-  const dialectOptions: DialectOptions =
-    typeof options.language === 'string' ? formatters[options.language] : options.language;
-
-  return new Formatter(createDialect(dialectOptions), options).format(query);
+  return new Formatter(createDialect(dialect), options).format(query);
 };
 
-export class ConfigError extends Error {}
-
-function validateConfig(cfg: FormatOptionsWithLanguage): FormatOptionsWithLanguage {
-  if (typeof cfg.language === 'string' && !supportedDialects.includes(cfg.language)) {
-    throw new ConfigError(`Unsupported SQL dialect: ${cfg.language}`);
-  }
-
-  if ('multilineLists' in cfg) {
-    throw new ConfigError('multilineLists config is no more supported.');
-  }
-  if ('newlineBeforeOpenParen' in cfg) {
-    throw new ConfigError('newlineBeforeOpenParen config is no more supported.');
-  }
-  if ('newlineBeforeCloseParen' in cfg) {
-    throw new ConfigError('newlineBeforeCloseParen config is no more supported.');
-  }
-  if ('aliasAs' in cfg) {
-    throw new ConfigError('aliasAs config is no more supported.');
-  }
-
-  if (cfg.expressionWidth <= 0) {
-    throw new ConfigError(
-      `expressionWidth config must be positive number. Received ${cfg.expressionWidth} instead.`
-    );
-  }
-
-  if (cfg.commaPosition === 'before' && cfg.useTabs) {
-    throw new ConfigError(
-      'commaPosition: before does not work when tabs are used for indentation.'
-    );
-  }
-
-  if (cfg.params && !validateParams(cfg.params)) {
-    // eslint-disable-next-line no-console
-    console.warn('WARNING: All "params" option values should be strings.');
-  }
-
-  return cfg;
-}
-
-function validateParams(params: ParamItems | string[]): boolean {
-  const paramValues = params instanceof Array ? params : Object.values(params);
-  return paramValues.every(p => typeof p === 'string');
-}
-
 export type FormatFn = typeof format;
diff --git a/src/validateConfig.ts b/src/validateConfig.ts
new file mode 100644
index 00000000..caa4efbf
--- /dev/null
+++ b/src/validateConfig.ts
@@ -0,0 +1,43 @@
+import { FormatOptions } from './FormatOptions.js';
+import { ParamItems } from './formatter/Params.js';
+
+export class ConfigError extends Error {}
+
+export function validateConfig(cfg: FormatOptions): FormatOptions {
+  if ('multilineLists' in cfg) {
+    throw new ConfigError('multilineLists config is no more supported.');
+  }
+  if ('newlineBeforeOpenParen' in cfg) {
+    throw new ConfigError('newlineBeforeOpenParen config is no more supported.');
+  }
+  if ('newlineBeforeCloseParen' in cfg) {
+    throw new ConfigError('newlineBeforeCloseParen config is no more supported.');
+  }
+  if ('aliasAs' in cfg) {
+    throw new ConfigError('aliasAs config is no more supported.');
+  }
+
+  if (cfg.expressionWidth <= 0) {
+    throw new ConfigError(
+      `expressionWidth config must be positive number. Received ${cfg.expressionWidth} instead.`
+    );
+  }
+
+  if (cfg.commaPosition === 'before' && cfg.useTabs) {
+    throw new ConfigError(
+      'commaPosition: before does not work when tabs are used for indentation.'
+    );
+  }
+
+  if (cfg.params && !validateParams(cfg.params)) {
+    // eslint-disable-next-line no-console
+    console.warn('WARNING: All "params" option values should be strings.');
+  }
+
+  return cfg;
+}
+
+function validateParams(params: ParamItems | string[]): boolean {
+  const paramValues = params instanceof Array ? params : Object.values(params);
+  return paramValues.every(p => typeof p === 'string');
+}


Test patch:
diff --git a/test/sqlFormatter.test.ts b/test/sqlFormatter.test.ts
index a6e5ddd8..dabafa2e 100644
--- a/test/sqlFormatter.test.ts
+++ b/test/sqlFormatter.test.ts
@@ -1,7 +1,6 @@
 import dedent from 'dedent-js';
 
-import { format, SqlLanguage } from '../src/sqlFormatter.js';
-import { sqlite } from '../src/languages/sqlite/sqlite.formatter.js';
+import { format, formatDialect, SqlLanguage, sqlite } from '../src/index.js';
 
 describe('sqlFormatter', () => {
   it('throws error when unsupported language parameter specified', () => {
@@ -56,11 +55,13 @@ describe('sqlFormatter', () => {
     }).toThrow('aliasAs config is no more supported.');
   });
 
-  it('allows passing Dialect config object as a language parameter', () => {
-    expect(format('SELECT [foo], `bar`;', { language: sqlite })).toBe(dedent`
-      SELECT
-        [foo],
-        \`bar\`;
-    `);
+  describe('formatDialect()', () => {
+    it('allows passing Dialect config object as a dialect parameter', () => {
+      expect(formatDialect('SELECT [foo], `bar`;', { dialect: sqlite })).toBe(dedent`
+        SELECT
+          [foo],
+          \`bar\`;
+      `);
+    });
   });
 });