@jacobknightley/fabric-format 0.0.1 → 0.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.
package/dist/cli.js CHANGED
@@ -86,30 +86,12 @@ async function formatFile(content, filePath) {
86
86
  return formatted;
87
87
  }
88
88
  /**
89
- * Detect the line ending style used in content.
90
- */
91
- function detectLineEnding(content) {
92
- if (content.includes('\r\n')) {
93
- return '\r\n';
94
- }
95
- return '\n';
96
- }
97
- /**
98
- * Normalize line endings to Unix style.
89
+ * Normalize line endings to LF (Unix style).
90
+ * This library standardizes on LF for all output.
99
91
  */
100
92
  function normalizeLineEndings(content) {
101
93
  return content.replace(/\r\n/g, '\n');
102
94
  }
103
- /**
104
- * Convert line endings to the specified style.
105
- */
106
- function convertLineEndings(content, lineEnding) {
107
- const normalized = normalizeLineEndings(content);
108
- if (lineEnding === '\r\n') {
109
- return normalized.replace(/\n/g, '\r\n');
110
- }
111
- return normalized;
112
- }
113
95
  /** Print main help */
114
96
  function printHelp() {
115
97
  console.log(`fabfmt - Fabric Notebook Formatter (Spark SQL & Python)
@@ -307,12 +289,10 @@ async function cmdFormat(args) {
307
289
  for (const file of files) {
308
290
  try {
309
291
  const content = fs.readFileSync(file, 'utf-8');
310
- const originalLineEnding = detectLineEnding(content);
311
292
  const normalizedContent = normalizeLineEndings(content);
312
293
  const formatted = await formatFile(normalizedContent, file);
313
- const formattedWithOriginalEndings = convertLineEndings(formatted, originalLineEnding);
314
- if (formattedWithOriginalEndings !== content) {
315
- fs.writeFileSync(file, formattedWithOriginalEndings, 'utf-8');
294
+ if (formatted !== normalizedContent) {
295
+ fs.writeFileSync(file, formatted, 'utf-8');
316
296
  console.log(`Formatted ${file}`);
317
297
  formattedCount++;
318
298
  }
@@ -8,6 +8,56 @@ import { RUFF_WASM_CONFIG } from './config.js';
8
8
  // Dynamic import for ruff WASM (loaded on demand)
9
9
  let ruffModule = null;
10
10
  let workspace = null;
11
+ /**
12
+ * Detect if we're running in Node.js
13
+ */
14
+ function isNodeEnvironment() {
15
+ return typeof process !== 'undefined' &&
16
+ process.versions != null &&
17
+ process.versions.node != null;
18
+ }
19
+ /**
20
+ * Find the WASM file path relative to the ruff-wasm-web package in Node.js.
21
+ * Uses indirect dynamic imports to avoid bundler static analysis.
22
+ */
23
+ async function findWasmFileForNode() {
24
+ // Use Function constructor to create dynamic import that bundlers can't statically analyze
25
+ // This is intentional - these modules only exist in Node.js, not in browsers
26
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
27
+ const { createRequire } = await dynamicImport('module');
28
+ const { dirname, join } = await dynamicImport('path');
29
+ const { readFile } = await dynamicImport('fs/promises');
30
+ // Get the path to ruff-wasm-web package
31
+ // We need import.meta.url to create a require function
32
+ // Use a fallback for bundled environments (though this path shouldn't be hit in browsers)
33
+ let ruffWasmDir;
34
+ try {
35
+ const require = createRequire(import.meta.url);
36
+ const ruffWasmPath = require.resolve('@astral-sh/ruff-wasm-web');
37
+ ruffWasmDir = dirname(ruffWasmPath);
38
+ }
39
+ catch {
40
+ // Fallback: try to find it via node_modules traversal
41
+ const { fileURLToPath } = await dynamicImport('url');
42
+ const currentDir = dirname(fileURLToPath(import.meta.url));
43
+ // Walk up to find node_modules
44
+ let searchDir = currentDir;
45
+ const { existsSync } = await dynamicImport('fs');
46
+ while (searchDir !== dirname(searchDir)) {
47
+ const candidate = join(searchDir, 'node_modules', '@astral-sh', 'ruff-wasm-web');
48
+ if (existsSync(candidate)) {
49
+ ruffWasmDir = candidate;
50
+ break;
51
+ }
52
+ searchDir = dirname(searchDir);
53
+ }
54
+ if (!ruffWasmDir) {
55
+ throw new Error('Could not locate @astral-sh/ruff-wasm-web package');
56
+ }
57
+ }
58
+ const wasmPath = join(ruffWasmDir, 'ruff_wasm_bg.wasm');
59
+ return readFile(wasmPath);
60
+ }
11
61
  /**
12
62
  * Python formatter using Ruff WASM.
13
63
  */
@@ -43,9 +93,13 @@ export class PythonFormatter {
43
93
  // Use async initialization with provided URL
44
94
  await ruffModule.default({ module_or_path: this.wasmOptions.wasmUrl });
45
95
  }
96
+ else if (isNodeEnvironment()) {
97
+ // Node.js: Load WASM file from disk
98
+ const wasmBinary = await findWasmFileForNode();
99
+ ruffModule.initSync({ module: wasmBinary });
100
+ }
46
101
  else {
47
- // Default: let ruff-wasm-web use import.meta.url to find the WASM file
48
- // This works in Node.js and ESM environments but may fail in bundled IIFE
102
+ // Browser: let ruff-wasm-web use import.meta.url to find the WASM file
49
103
  await ruffModule.default();
50
104
  }
51
105
  // Create workspace with config
@@ -37,7 +37,7 @@ const PYTHON_CONFIG = {
37
37
  metadataMarker: '# METADATA ********************',
38
38
  magicPrefix: '# MAGIC ',
39
39
  magicSqlCommand: '# MAGIC %%sql',
40
- emptyMagic: '# MAGIC',
40
+ emptyMagic: '# MAGIC ',
41
41
  supportsRawSql: false,
42
42
  defaultLanguage: 'python',
43
43
  };
@@ -47,7 +47,7 @@ const SCALA_CONFIG = {
47
47
  metadataMarker: '// METADATA ********************',
48
48
  magicPrefix: '// MAGIC ',
49
49
  magicSqlCommand: '// MAGIC %%sql',
50
- emptyMagic: '// MAGIC',
50
+ emptyMagic: '// MAGIC ',
51
51
  supportsRawSql: false,
52
52
  defaultLanguage: 'scala',
53
53
  };
@@ -57,7 +57,7 @@ const SPARKSQL_CONFIG = {
57
57
  metadataMarker: '-- METADATA ********************',
58
58
  magicPrefix: '-- MAGIC ',
59
59
  magicSqlCommand: '-- MAGIC %%sql',
60
- emptyMagic: '-- MAGIC',
60
+ emptyMagic: '-- MAGIC ',
61
61
  supportsRawSql: true,
62
62
  defaultLanguage: 'sparksql',
63
63
  };
@@ -73,13 +73,9 @@ const R_CONFIG = {
73
73
  // INTERNAL UTILITIES
74
74
  // ============================================================================
75
75
  /**
76
- * Detect line ending style.
76
+ * Line ending constant - this library standardizes on LF.
77
77
  */
78
- function detectLineEnding(content) {
79
- if (content.includes('\r\n'))
80
- return '\r\n';
81
- return '\n';
82
- }
78
+ const LINE_ENDING = '\n';
83
79
  /**
84
80
  * Get language config based on file extension.
85
81
  * The file extension determines the default language and comment syntax.
@@ -203,7 +199,9 @@ function stripMagicPrefix(line, config) {
203
199
  if (line.startsWith(config.magicPrefix)) {
204
200
  return line.slice(config.magicPrefix.length);
205
201
  }
206
- if (line.trim() === config.emptyMagic) {
202
+ // Check for empty magic line (with or without trailing space)
203
+ const trimmed = line.trim();
204
+ if (trimmed === config.emptyMagic.trim()) {
207
205
  return '';
208
206
  }
209
207
  return line;
@@ -332,7 +330,7 @@ export function parseNotebook(content, fileExtension) {
332
330
  /**
333
331
  * Replace a cell's content in the file.
334
332
  */
335
- function replaceCell(fileContent, cell, formattedContent, config, lineEnding) {
333
+ function replaceCell(fileContent, cell, formattedContent, config) {
336
334
  const lines = fileContent.split(/\r?\n/);
337
335
  let newLines;
338
336
  if (cell.isMagicCell) {
@@ -343,7 +341,7 @@ function replaceCell(fileContent, cell, formattedContent, config, lineEnding) {
343
341
  }
344
342
  const before = lines.slice(0, cell.contentStartLine);
345
343
  const after = lines.slice(cell.contentEndLine + 1);
346
- return [...before, ...newLines, ...after].join(lineEnding);
344
+ return [...before, ...newLines, ...after].join(LINE_ENDING);
347
345
  }
348
346
  /**
349
347
  * Format all cells in a Fabric notebook.
@@ -370,7 +368,6 @@ export async function formatNotebook(content, fileExtension, options) {
370
368
  if (!notebook.isFabricNotebook || notebook.cells.length === 0 || !notebook.config) {
371
369
  return { content, stats };
372
370
  }
373
- const lineEnding = detectLineEnding(content);
374
371
  // Initialize Python formatter if needed
375
372
  if (formatPythonCells) {
376
373
  try {
@@ -397,7 +394,7 @@ export async function formatNotebook(content, fileExtension, options) {
397
394
  const formatResult = formatCell(cell.content, 'sparksql');
398
395
  if (formatResult.changed) {
399
396
  // replaceCell will add back MAGIC prefixes if needed
400
- result = replaceCell(result, cell, formatResult.formatted, notebook.config, lineEnding);
397
+ result = replaceCell(result, cell, formatResult.formatted, notebook.config);
401
398
  stats.sparkSqlCellsFormatted++;
402
399
  }
403
400
  if (formatResult.error) {
@@ -409,7 +406,7 @@ export async function formatNotebook(content, fileExtension, options) {
409
406
  const formatResult = formatCell(cell.content, 'python');
410
407
  if (formatResult.changed) {
411
408
  // replaceCell will add back MAGIC prefixes if needed
412
- result = replaceCell(result, cell, formatResult.formatted, notebook.config, lineEnding);
409
+ result = replaceCell(result, cell, formatResult.formatted, notebook.config);
413
410
  stats.pythonCellsFormatted++;
414
411
  }
415
412
  if (formatResult.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobknightley/fabric-format",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "A fast, opinionated formatter for Microsoft Fabric notebooks with Spark SQL and Python support",
6
6
  "main": "dist/index.js",