@coldge.com/gitbase 1.0.2 → 1.0.3

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.
@@ -1,24 +1,41 @@
1
1
  import { runQuery } from '../api/supabase.js';
2
2
  import * as Queries from './queries.js';
3
3
  export async function extractSchema(projectRef) {
4
- const tables = await extractTables(projectRef);
5
- const functions = await extractFunctions(projectRef);
6
- const views = await extractViews(projectRef);
7
- const triggers = await extractTriggers(projectRef);
8
- const policies = await extractPolicies(projectRef);
9
- const types = await extractTypes(projectRef);
10
- return { tables, functions, views, triggers, policies, types };
4
+ // All extractors run in parallel for maximum performance
5
+ const [extensions, types, sequences, tables, matviews, views, functions, triggers, policies, grants, publications] = await Promise.all([
6
+ extractExtensions(projectRef),
7
+ extractTypes(projectRef),
8
+ extractSequences(projectRef),
9
+ extractTables(projectRef),
10
+ extractMaterializedViews(projectRef),
11
+ extractViews(projectRef),
12
+ extractFunctions(projectRef),
13
+ extractTriggers(projectRef),
14
+ extractPolicies(projectRef),
15
+ extractGrants(projectRef),
16
+ extractPublications(projectRef),
17
+ ]);
18
+ return {
19
+ extensions, types, sequences, tables, matviews,
20
+ views, functions, triggers, policies, grants, publications
21
+ };
11
22
  }
12
23
  async function extractTables(projectRef) {
13
24
  const tableList = await runQuery(projectRef, Queries.LIST_TABLES);
14
25
  if (!Array.isArray(tableList))
15
26
  return {};
16
27
  const tables = {};
17
- for (const row of tableList) {
28
+ // Process all tables concurrently instead of sequentially
29
+ await Promise.all(tableList.map(async (row) => {
18
30
  const tableName = row.table_name;
19
31
  const rlsEnabled = row.rls_enabled;
20
- const columns = await runQuery(projectRef, Queries.GET_COLUMNS(tableName));
21
- let sql = `CREATE TABLE public."${tableName}" (\n`;
32
+ // Fetch columns, constraints, and indexes in parallel per table
33
+ const [columns, constraints, indexes] = await Promise.all([
34
+ runQuery(projectRef, Queries.GET_COLUMNS(tableName)),
35
+ runQuery(projectRef, Queries.GET_TABLE_CONSTRAINTS(tableName)),
36
+ runQuery(projectRef, Queries.GET_TABLE_INDEXES(tableName)),
37
+ ]);
38
+ // Column definitions
22
39
  const colDefs = columns.map(c => {
23
40
  let line = ` "${c.column_name}" ${c.data_type}`;
24
41
  if (c.is_nullable === 'NO')
@@ -27,13 +44,23 @@ async function extractTables(projectRef) {
27
44
  line += ` DEFAULT ${c.column_default}`;
28
45
  return line;
29
46
  });
30
- sql += colDefs.join(',\n');
47
+ // Constraint definitions (PK, FK, UNIQUE, CHECK)
48
+ const constraintDefs = constraints.map(c => {
49
+ return ` CONSTRAINT "${c.name}" ${c.definition}`;
50
+ });
51
+ const allDefs = [...colDefs, ...constraintDefs];
52
+ let sql = `CREATE TABLE public."${tableName}" (\n`;
53
+ sql += allDefs.join(',\n');
31
54
  sql += '\n);';
55
+ // Standalone index statements (non-primary, appended after CREATE TABLE)
56
+ for (const idx of indexes) {
57
+ sql += `\n${idx.definition};`;
58
+ }
32
59
  if (rlsEnabled) {
33
60
  sql += `\nALTER TABLE public."${tableName}" ENABLE ROW LEVEL SECURITY;`;
34
61
  }
35
62
  tables[tableName] = sql;
36
- }
63
+ }));
37
64
  return tables;
38
65
  }
39
66
  async function extractFunctions(projectRef) {
@@ -56,6 +83,16 @@ async function extractViews(projectRef) {
56
83
  }
57
84
  return result;
58
85
  }
86
+ async function extractMaterializedViews(projectRef) {
87
+ const matviews = await runQuery(projectRef, Queries.LIST_MAT_VIEWS);
88
+ if (!Array.isArray(matviews))
89
+ return {};
90
+ const result = {};
91
+ for (const v of matviews) {
92
+ result[v.name] = `CREATE MATERIALIZED VIEW public."${v.name}" AS\n${v.definition}`;
93
+ }
94
+ return result;
95
+ }
59
96
  async function extractTriggers(projectRef) {
60
97
  const triggers = await runQuery(projectRef, Queries.LIST_TRIGGERS);
61
98
  if (!Array.isArray(triggers))
@@ -73,7 +110,6 @@ async function extractPolicies(projectRef) {
73
110
  const result = {};
74
111
  for (const p of policies) {
75
112
  let sql = `CREATE POLICY "${p.name}"\nON public."${p.tablename}"\nFOR ${p.cmd}`;
76
- // Handle roles
77
113
  let roles = p.roles;
78
114
  if (Array.isArray(roles)) {
79
115
  roles = roles.join(', ');
@@ -81,9 +117,8 @@ async function extractPolicies(projectRef) {
81
117
  else if (typeof roles === 'string' && roles.startsWith('{')) {
82
118
  roles = roles.replace(/^\{|\}$/g, '').split(',').join(', ');
83
119
  }
84
- if (roles) {
120
+ if (roles)
85
121
  sql += `\nTO ${roles}`;
86
- }
87
122
  if (p.qual)
88
123
  sql += `\nUSING (${p.qual})`;
89
124
  if (p.with_check)
@@ -94,20 +129,142 @@ async function extractPolicies(projectRef) {
94
129
  return result;
95
130
  }
96
131
  async function extractTypes(projectRef) {
97
- const types = await runQuery(projectRef, Queries.LIST_TYPES);
98
- if (!Array.isArray(types))
99
- return {};
132
+ const [enums, composites, domains] = await Promise.all([
133
+ runQuery(projectRef, Queries.LIST_ENUM_TYPES),
134
+ runQuery(projectRef, Queries.LIST_COMPOSITE_TYPES),
135
+ runQuery(projectRef, Queries.LIST_DOMAIN_TYPES),
136
+ ]);
100
137
  const result = {};
101
- // Group by type name since list returns rows of (name, label) for enums
102
- const grouped = {};
103
- for (const t of types) {
104
- if (!grouped[t.name])
105
- grouped[t.name] = [];
106
- grouped[t.name].push(t.label);
107
- }
108
- for (const [name, labels] of Object.entries(grouped)) {
138
+ // Enum types
139
+ const enumGrouped = {};
140
+ for (const t of enums) {
141
+ if (!enumGrouped[t.name])
142
+ enumGrouped[t.name] = [];
143
+ enumGrouped[t.name].push(t.label);
144
+ }
145
+ for (const [name, labels] of Object.entries(enumGrouped)) {
109
146
  const labelsStr = labels.map(l => `'${l}'`).join(', ');
110
147
  result[name] = `CREATE TYPE public."${name}" AS ENUM (${labelsStr});`;
111
148
  }
149
+ // Composite types
150
+ const compGrouped = {};
151
+ for (const c of composites) {
152
+ if (!compGrouped[c.name])
153
+ compGrouped[c.name] = [];
154
+ compGrouped[c.name].push({ col_name: c.col_name, col_type: c.col_type });
155
+ }
156
+ for (const [name, cols] of Object.entries(compGrouped)) {
157
+ const colDefs = cols.map(c => ` "${c.col_name}" ${c.col_type}`).join(',\n');
158
+ result[name] = `CREATE TYPE public."${name}" AS (\n${colDefs}\n);`;
159
+ }
160
+ // Domain types
161
+ for (const d of domains) {
162
+ let sql = `CREATE DOMAIN public."${d.name}" AS ${d.base_type}`;
163
+ if (d.default_val)
164
+ sql += `\n DEFAULT ${d.default_val}`;
165
+ if (d.not_null)
166
+ sql += `\n NOT NULL`;
167
+ if (d.check_expr)
168
+ sql += `\n ${d.check_expr}`;
169
+ sql += ';';
170
+ result[d.name] = sql;
171
+ }
172
+ return result;
173
+ }
174
+ async function extractExtensions(projectRef) {
175
+ const exts = await runQuery(projectRef, Queries.LIST_EXTENSIONS);
176
+ if (!Array.isArray(exts))
177
+ return {};
178
+ const result = {};
179
+ for (const e of exts) {
180
+ // Always use IF NOT EXISTS so applying this snapshot is safe on any project
181
+ result[e.name] = `CREATE EXTENSION IF NOT EXISTS "${e.name}" WITH SCHEMA ${e.schema};`;
182
+ }
183
+ return result;
184
+ }
185
+ async function extractGrants(projectRef) {
186
+ const [tableGrants, functionGrants] = await Promise.all([
187
+ runQuery(projectRef, Queries.LIST_TABLE_GRANTS),
188
+ runQuery(projectRef, Queries.LIST_FUNCTION_GRANTS),
189
+ ]);
190
+ // Group table grants by table name
191
+ const tableMap = {};
192
+ for (const g of tableGrants) {
193
+ if (!tableMap[g.table_name])
194
+ tableMap[g.table_name] = [];
195
+ const grantOption = g.is_grantable === 'YES' ? ' WITH GRANT OPTION' : '';
196
+ tableMap[g.table_name].push(`GRANT ${g.privileges} ON TABLE public."${g.table_name}" TO ${g.grantee}${grantOption};`);
197
+ }
198
+ // Group function grants by function name
199
+ const funcMap = {};
200
+ for (const g of functionGrants) {
201
+ if (!funcMap[g.routine_name])
202
+ funcMap[g.routine_name] = [];
203
+ const grantOption = g.is_grantable === 'YES' ? ' WITH GRANT OPTION' : '';
204
+ funcMap[g.routine_name].push(`GRANT ${g.privileges} ON FUNCTION public."${g.routine_name}" TO ${g.grantee}${grantOption};`);
205
+ }
206
+ const result = {};
207
+ // One file per table: "grants/table__tablename"
208
+ for (const [tableName, stmts] of Object.entries(tableMap)) {
209
+ result[`table__${tableName}`] = stmts.join('\n');
210
+ }
211
+ // One file per function: "grants/function__funcname"
212
+ for (const [funcName, stmts] of Object.entries(funcMap)) {
213
+ result[`function__${funcName}`] = stmts.join('\n');
214
+ }
215
+ return result;
216
+ }
217
+ async function extractPublications(projectRef) {
218
+ const pubs = await runQuery(projectRef, Queries.LIST_PUBLICATIONS);
219
+ if (!Array.isArray(pubs))
220
+ return {};
221
+ const result = {};
222
+ for (const p of pubs) {
223
+ const ops = [];
224
+ if (p.pubinsert)
225
+ ops.push('insert');
226
+ if (p.pubupdate)
227
+ ops.push('update');
228
+ if (p.pubdelete)
229
+ ops.push('delete');
230
+ if (p.pubtruncate)
231
+ ops.push('truncate');
232
+ const withClause = ops.length > 0 ? ` WITH (publish = '${ops.join(', ')}')` : '';
233
+ let sql;
234
+ if (p.puballtables) {
235
+ sql = `CREATE PUBLICATION "${p.pubname}" FOR ALL TABLES${withClause};`;
236
+ }
237
+ else {
238
+ // Fetch the specific tables in this publication
239
+ const tables = await runQuery(projectRef, Queries.LIST_PUBLICATION_TABLES(p.pubname));
240
+ if (tables.length === 0) {
241
+ sql = `CREATE PUBLICATION "${p.pubname}"${withClause};`;
242
+ }
243
+ else {
244
+ const tableList = tables.map(t => `public."${t.table_name}"`).join(', ');
245
+ sql = `CREATE PUBLICATION "${p.pubname}" FOR TABLE ${tableList}${withClause};`;
246
+ }
247
+ }
248
+ result[p.pubname] = sql;
249
+ }
250
+ return result;
251
+ }
252
+ async function extractSequences(projectRef) {
253
+ const seqs = await runQuery(projectRef, Queries.LIST_SEQUENCES);
254
+ if (!Array.isArray(seqs))
255
+ return {};
256
+ const result = {};
257
+ for (const s of seqs) {
258
+ const cycle = s.cycle_option === 'YES' ? 'CYCLE' : 'NO CYCLE';
259
+ result[s.name] = [
260
+ `CREATE SEQUENCE IF NOT EXISTS public."${s.name}"`,
261
+ ` AS ${s.data_type}`,
262
+ ` START WITH ${s.start_value}`,
263
+ ` INCREMENT BY ${s.increment}`,
264
+ ` MINVALUE ${s.minimum_value}`,
265
+ ` MAXVALUE ${s.maximum_value}`,
266
+ ` ${cycle};`,
267
+ ].join('\n');
268
+ }
112
269
  return result;
113
270
  }
@@ -1,17 +1,49 @@
1
+ // All dynamic queries return { text, values } objects to prevent SQL injection.
2
+ // Static queries (no user input) remain plain strings.
1
3
  export const LIST_TABLES = `
2
4
  SELECT c.relname as table_name, c.relrowsecurity as rls_enabled
3
5
  FROM pg_class c
4
6
  JOIN pg_namespace n ON n.oid = c.relnamespace
5
- WHERE n.nspname = 'public'
7
+ WHERE n.nspname = 'public'
6
8
  AND c.relkind = 'r'
7
9
  ORDER BY c.relname;
8
10
  `;
9
- export const GET_COLUMNS = (tableName) => `
10
- SELECT column_name, data_type, is_nullable, column_default
11
- FROM information_schema.columns
12
- WHERE table_schema = 'public' AND table_name = '${tableName}'
13
- ORDER BY ordinal_position;
14
- `;
11
+ /** Parameterized safe against SQL injection */
12
+ export const GET_COLUMNS = (tableName) => ({
13
+ text: `
14
+ SELECT column_name, data_type, is_nullable, column_default
15
+ FROM information_schema.columns
16
+ WHERE table_schema = 'public' AND table_name = $1
17
+ ORDER BY ordinal_position
18
+ `,
19
+ values: [tableName]
20
+ });
21
+ /** All constraints (PK, FK, UNIQUE, CHECK) for a table — parameterized */
22
+ export const GET_TABLE_CONSTRAINTS = (tableName) => ({
23
+ text: `
24
+ SELECT c.conname as name, pg_get_constraintdef(c.oid) as definition, c.contype
25
+ FROM pg_constraint c
26
+ JOIN pg_class t ON t.oid = c.conrelid
27
+ JOIN pg_namespace n ON n.oid = t.relnamespace
28
+ WHERE n.nspname = 'public' AND t.relname = $1
29
+ ORDER BY c.contype, c.conname
30
+ `,
31
+ values: [tableName]
32
+ });
33
+ /** Non-primary indexes for a table — parameterized */
34
+ export const GET_TABLE_INDEXES = (tableName) => ({
35
+ text: `
36
+ SELECT pg_get_indexdef(i.indexrelid) as definition
37
+ FROM pg_index i
38
+ JOIN pg_class t ON t.oid = i.indrelid
39
+ JOIN pg_class ix ON ix.oid = i.indexrelid
40
+ JOIN pg_namespace n ON n.oid = t.relnamespace
41
+ WHERE n.nspname = 'public' AND t.relname = $1
42
+ AND NOT i.indisprimary
43
+ ORDER BY ix.relname
44
+ `,
45
+ values: [tableName]
46
+ });
15
47
  export const LIST_FUNCTIONS = `
16
48
  SELECT proname as name, pg_get_functiondef(oid) as definition
17
49
  FROM pg_proc
@@ -24,6 +56,12 @@ FROM information_schema.views
24
56
  WHERE table_schema = 'public'
25
57
  ORDER BY table_name;
26
58
  `;
59
+ export const LIST_MAT_VIEWS = `
60
+ SELECT matviewname as name, pg_get_viewdef(matviewname::regclass) as definition
61
+ FROM pg_matviews
62
+ WHERE schemaname = 'public'
63
+ ORDER BY matviewname;
64
+ `;
27
65
  export const LIST_TRIGGERS = `
28
66
  SELECT tgname as name, pg_get_triggerdef(oid) as definition
29
67
  FROM pg_trigger
@@ -37,7 +75,8 @@ FROM pg_policies
37
75
  WHERE schemaname = 'public'
38
76
  ORDER BY tablename, policyname;
39
77
  `;
40
- export const LIST_TYPES = `
78
+ /** Enum types only */
79
+ export const LIST_ENUM_TYPES = `
41
80
  SELECT t.typname as name, e.enumlabel as label
42
81
  FROM pg_type t
43
82
  JOIN pg_enum e ON t.oid = e.enumtypid
@@ -45,3 +84,116 @@ JOIN pg_namespace n ON n.oid = t.typnamespace
45
84
  WHERE n.nspname = 'public'
46
85
  ORDER BY t.typname, e.enumsortorder;
47
86
  `;
87
+ /** Composite types */
88
+ export const LIST_COMPOSITE_TYPES = `
89
+ SELECT t.typname as name, a.attname as col_name,
90
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as col_type
91
+ FROM pg_type t
92
+ JOIN pg_class c ON c.oid = t.typrelid
93
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
94
+ JOIN pg_namespace n ON n.oid = t.typnamespace
95
+ WHERE n.nspname = 'public' AND t.typtype = 'c'
96
+ ORDER BY t.typname, a.attnum;
97
+ `;
98
+ /** All sequences in the public schema */
99
+ export const LIST_SEQUENCES = `
100
+ SELECT sequence_name as name, data_type, start_value, minimum_value,
101
+ maximum_value, increment, cycle_option
102
+ FROM information_schema.sequences
103
+ WHERE sequence_schema = 'public'
104
+ ORDER BY sequence_name;
105
+ `;
106
+ /**
107
+ * All installed extensions (excluding the always-present plpgsql).
108
+ * Applied with CREATE EXTENSION IF NOT EXISTS — never dropped during revert.
109
+ */
110
+ export const LIST_EXTENSIONS = `
111
+ SELECT e.extname as name, e.extversion as version,
112
+ n.nspname as schema
113
+ FROM pg_extension e
114
+ JOIN pg_namespace n ON n.oid = e.extnamespace
115
+ WHERE e.extname != 'plpgsql'
116
+ ORDER BY e.extname;
117
+ `;
118
+ /**
119
+ * All explicit grants on tables in the public schema.
120
+ * Excludes internal Supabase/Postgres system roles.
121
+ * Grouped so each row = one grantee + one table + all their privileges combined.
122
+ */
123
+ export const LIST_TABLE_GRANTS = `
124
+ SELECT
125
+ table_name,
126
+ grantee,
127
+ string_agg(privilege_type, ', ' ORDER BY privilege_type) as privileges,
128
+ is_grantable
129
+ FROM information_schema.role_table_grants
130
+ WHERE table_schema = 'public'
131
+ AND grantee NOT IN (
132
+ 'postgres', 'supabase_admin', 'supabase_auth_admin',
133
+ 'supabase_storage_admin', 'dashboard_user', 'pg_monitor',
134
+ 'pg_read_all_data', 'pg_write_all_data'
135
+ )
136
+ GROUP BY table_name, grantee, is_grantable
137
+ ORDER BY table_name, grantee;
138
+ `;
139
+ /**
140
+ * All explicit grants on functions in the public schema.
141
+ */
142
+ export const LIST_FUNCTION_GRANTS = `
143
+ SELECT
144
+ routine_name,
145
+ grantee,
146
+ string_agg(privilege_type, ', ' ORDER BY privilege_type) as privileges,
147
+ is_grantable
148
+ FROM information_schema.role_routine_grants
149
+ WHERE routine_schema = 'public'
150
+ AND grantee NOT IN (
151
+ 'postgres', 'supabase_admin', 'supabase_auth_admin',
152
+ 'supabase_storage_admin', 'dashboard_user', 'pg_monitor',
153
+ 'pg_read_all_data', 'pg_write_all_data'
154
+ )
155
+ GROUP BY routine_name, grantee, is_grantable
156
+ ORDER BY routine_name, grantee;
157
+ `;
158
+ /**
159
+ * All publications (used by Supabase Realtime).
160
+ */
161
+ export const LIST_PUBLICATIONS = `
162
+ SELECT
163
+ p.pubname as name,
164
+ p.puballtables,
165
+ p.pubinsert,
166
+ p.pubupdate,
167
+ p.pubdelete,
168
+ p.pubtruncate
169
+ FROM pg_publication p
170
+ ORDER BY p.pubname;
171
+ `;
172
+ /** Tables enrolled in a specific publication — parameterized */
173
+ export const LIST_PUBLICATION_TABLES = (pubname) => ({
174
+ text: `
175
+ SELECT c.relname as table_name
176
+ FROM pg_publication p
177
+ JOIN pg_publication_rel pr ON pr.prpubid = p.oid
178
+ JOIN pg_class c ON c.oid = pr.prrelid
179
+ WHERE p.pubname = $1
180
+ ORDER BY c.relname
181
+ `,
182
+ values: [pubname]
183
+ });
184
+ /**
185
+ * Domain types in the public schema (CREATE DOMAIN ... AS base_type CHECK ...).
186
+ */
187
+ export const LIST_DOMAIN_TYPES = `
188
+ SELECT
189
+ t.typname as name,
190
+ pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
191
+ t.typnotnull as not_null,
192
+ t.typdefault as default_val,
193
+ pg_get_constraintdef(c.oid) as check_expr
194
+ FROM pg_type t
195
+ LEFT JOIN pg_constraint c ON c.contypid = t.oid AND c.contype = 'c'
196
+ JOIN pg_namespace n ON n.oid = t.typnamespace
197
+ WHERE n.nspname = 'public' AND t.typtype = 'd'
198
+ ORDER BY t.typname;
199
+ `;
@@ -1,9 +1,58 @@
1
1
  import crypto from 'crypto';
2
+ import { parseSync, deparseSync } from 'pgsql-parser';
2
3
  export function hashString(content) {
3
4
  return crypto.createHash('sha1').update(content).digest('hex');
4
5
  }
6
+ /**
7
+ * Canonicalizes a SQL string using the actual PostgreSQL parser (libpg_query via WASM).
8
+ *
9
+ * Strategy:
10
+ * 1. Strip SQL comments (they are semantically empty).
11
+ * 2. Parse the SQL into an AST via pgsql-parser (the real PG C parser).
12
+ * 3. Deparse the AST back to SQL — this produces a structurally normalized form.
13
+ * 4. Apply aggressive whitespace normalization post-deparse:
14
+ * - Lowercase everything
15
+ * - Collapse all whitespace runs to a single space
16
+ * - Remove spaces adjacent to ( ) , ; = operators
17
+ * This eliminates the residual whitespace variants that pgsql-parser's
18
+ * deparser still allows through (e.g. "foo ( id" vs "foo(id").
19
+ *
20
+ * If the SQL is not a full parseable statement (e.g. a raw expression fragment
21
+ * stored in an old commit), the function falls back to best-effort text
22
+ * normalization and never throws.
23
+ */
5
24
  export function canonicalize(sql) {
6
- // Basic normalization: trim
7
- // In a real implementation, we would parse the SQL and reprint it deterministically.
8
- return sql.trim();
25
+ if (!sql || sql.trim() === '')
26
+ return '';
27
+ // Strip comments first — they are semantically empty
28
+ const stripped = sql
29
+ .replace(/--[^\n]*/g, '') // single-line comments
30
+ .replace(/\/\*[\s\S]*?\*\//g, ''); // block comments
31
+ try {
32
+ const stmts = parseSync(stripped);
33
+ const deparsed = deparseSync(stmts);
34
+ return normalizeWhitespace(deparsed);
35
+ }
36
+ catch {
37
+ // Fallback for fragments / non-parseable SQL (e.g. raw expressions)
38
+ return normalizeWhitespace(stripped);
39
+ }
40
+ }
41
+ /**
42
+ * Applies deterministic whitespace normalization to a SQL string.
43
+ * After this step, two strings that are semantically identical will be equal
44
+ * regardless of original indentation, casing, or punctuation spacing.
45
+ */
46
+ function normalizeWhitespace(sql) {
47
+ return sql
48
+ .toLowerCase()
49
+ .replace(/\s+/g, ' ') // collapse all whitespace
50
+ .replace(/\s*\(\s*/g, '(') // no spaces around open-paren
51
+ .replace(/\s*\)\s*/g, ')') // no spaces around close-paren
52
+ .replace(/\s*,\s*/g, ',') // no spaces around comma
53
+ .replace(/\s*;\s*/g, ';') // no spaces around semicolon
54
+ .replace(/\s*=\s*/g, '=') // no spaces around equals
55
+ .replace(/\(\s+/g, '(') // extra pass: leading space in parens
56
+ .replace(/\s+\)/g, ')') // extra pass: trailing space in parens
57
+ .trim();
9
58
  }