@jacobknightley/fabric-format 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -50,13 +50,11 @@ Format Fabric notebooks directly in your browser with a single click.
50
50
 
51
51
  1. Download `fabric-format-chromium.zip` from the [latest release](https://github.com/jacobknightley/fabric-format/releases)
52
52
  2. Extract the zip file
53
- 3. Open your browser's extension page:
54
- - **Chrome:** `chrome://extensions`
55
- - **Edge:** `edge://extensions`
56
- 4. Enable **Developer mode** (toggle in top-right)
57
- 5. Click **Load unpacked** and select the extracted folder
53
+ 3. Load the unpacked extension in your browser:
54
+ - **Chrome:** [Install an unpacked extension](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#load-unpacked)
55
+ - **Edge:** [Sideload an extension](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/getting-started/extension-sideloading)
58
56
 
59
- > **Note:** Plan to eventually support directly in Chrome and Edge extension stores.
57
+ > **Note:** Plan to eventually publish to the Chrome Web Store and Edge Add-ons.
60
58
 
61
59
  ### Usage
62
60
 
@@ -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
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  export { formatSql, needsFormatting } from './formatters/sparksql/index.js';
13
13
  export { getFormatterRegistry, detectLanguage, SqlFormatter, getSqlFormatter, isSqlCode, PythonFormatter, getPythonFormatter, isPythonCode, type LanguageFormatter, type FormatterOptions, type FormatResult, type FormatterConfig, type FormatterRegistry, } from './formatters/index.js';
14
14
  export { formatCell, formatCellSync, initializePythonFormatter, isPythonFormatterReady, type FormatCellResult, type CellType, } from './cell-formatter.js';
15
- export { parseNotebook, formatNotebook, type NotebookCell, type FabricNotebook, type FormatStats, } from './notebook-formatter.js';
15
+ export { parseNotebook, formatNotebook, NotebookStructureError, type NotebookCell, type FabricNotebook, type FormatStats, } from './notebook-formatter.js';
16
16
  export { DEFAULT_RUFF_CONFIG, RUFF_WASM_CONFIG, type RuffConfig, type RuffFormatConfig, type WasmInitOptions, } from './formatters/python/index.js';
17
17
  export { hasFormatOff, detectCollapseDirectives, hasCollapseDirective, type FormatDirectiveInfo } from './formatters/sparksql/index.js';
18
18
  export type { AnalyzerResult, FormattingState, MultiArgFunctionInfo, WindowDefInfo, TokenContext, PendingComment, ExpandedFunction, ExpandedWindow } from './formatters/sparksql/types.js';
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ export { formatCell, formatCellSync, initializePythonFormatter, isPythonFormatte
30
30
  // ============================================================================
31
31
  // Notebook Formatter (High-level API)
32
32
  // ============================================================================
33
- export { parseNotebook, formatNotebook, } from './notebook-formatter.js';
33
+ export { parseNotebook, formatNotebook, NotebookStructureError, } from './notebook-formatter.js';
34
34
  // ============================================================================
35
35
  // Configuration (Python/Ruff)
36
36
  // ============================================================================
@@ -79,6 +79,14 @@ export interface FormatStats {
79
79
  cellsSkipped: number;
80
80
  errors: string[];
81
81
  }
82
+ /** Error thrown when a notebook cell has invalid structure */
83
+ export declare class NotebookStructureError extends Error {
84
+ readonly cellIndex: number;
85
+ readonly lineNumber: number;
86
+ readonly metadataLanguage: string;
87
+ readonly fileDefaultLanguage: string;
88
+ constructor(message: string, cellIndex: number, lineNumber: number, metadataLanguage: string, fileDefaultLanguage: string);
89
+ }
82
90
  /**
83
91
  * Parse a Fabric notebook file into cells.
84
92
  * @param content The file content
@@ -66,12 +66,68 @@ const R_CONFIG = {
66
66
  fabricHeader: '# Fabric notebook source', // Check if R has different header
67
67
  defaultLanguage: 'r',
68
68
  };
69
+ /** Error thrown when a notebook cell has invalid structure */
70
+ export class NotebookStructureError extends Error {
71
+ cellIndex;
72
+ lineNumber;
73
+ metadataLanguage;
74
+ fileDefaultLanguage;
75
+ constructor(message, cellIndex, lineNumber, metadataLanguage, fileDefaultLanguage) {
76
+ super(message);
77
+ this.cellIndex = cellIndex;
78
+ this.lineNumber = lineNumber;
79
+ this.metadataLanguage = metadataLanguage;
80
+ this.fileDefaultLanguage = fileDefaultLanguage;
81
+ this.name = 'NotebookStructureError';
82
+ }
83
+ }
69
84
  // ============================================================================
70
85
  // INTERNAL UTILITIES
71
86
  // ============================================================================
72
- // ============================================================================
73
- // INTERNAL UTILITIES
74
- // ============================================================================
87
+ /**
88
+ * Get the valid raw (uncommented) languages for a file type.
89
+ * Raw cells must match the file's native language - other languages must use MAGIC prefix.
90
+ */
91
+ function getValidRawLanguages(defaultLanguage) {
92
+ switch (defaultLanguage) {
93
+ case 'python':
94
+ // Python files: raw cells can be python or pyspark
95
+ return new Set(['python', 'pyspark']);
96
+ case 'sparksql':
97
+ // SQL files: raw cells must be sparksql
98
+ return new Set(['sparksql']);
99
+ case 'scala':
100
+ // Scala files: raw cells must be scala
101
+ return new Set(['scala']);
102
+ case 'r':
103
+ // R files: raw cells must be r
104
+ return new Set(['r']);
105
+ default:
106
+ return new Set([defaultLanguage]);
107
+ }
108
+ }
109
+ /**
110
+ * Validate that a raw cell's metadata language matches the file type.
111
+ * Throws NotebookStructureError if there's a mismatch.
112
+ */
113
+ function validateRawCellLanguage(metadataLanguage, isRawCell, config, cellIndex, lineNumber) {
114
+ if (!isRawCell || !metadataLanguage) {
115
+ // MAGIC cells can have any language, and cells without metadata are fine
116
+ return;
117
+ }
118
+ const validRawLanguages = getValidRawLanguages(config.defaultLanguage);
119
+ const normalizedLanguage = metadataLanguage.toLowerCase();
120
+ if (!validRawLanguages.has(normalizedLanguage)) {
121
+ const fileType = config.defaultLanguage === 'python' ? '.py' :
122
+ config.defaultLanguage === 'sparksql' ? '.sql' :
123
+ config.defaultLanguage === 'scala' ? '.scala' :
124
+ config.defaultLanguage === 'r' ? '.r' :
125
+ `.${config.defaultLanguage}`;
126
+ throw new NotebookStructureError(`Invalid notebook structure: Cell ${cellIndex + 1} (line ${lineNumber + 1}) has metadata language "${metadataLanguage}" ` +
127
+ `but is not wrapped with MAGIC prefix. In ${fileType} files, only ${config.defaultLanguage} cells can be raw/uncommented. ` +
128
+ `Other languages (like ${metadataLanguage}) must use the "${config.magicPrefix}%%" prefix.`, cellIndex, lineNumber, metadataLanguage, config.defaultLanguage);
129
+ }
130
+ }
75
131
  /**
76
132
  * Line ending constant - this library standardizes on LF.
77
133
  */
@@ -291,6 +347,9 @@ export function parseNotebook(content, fileExtension) {
291
347
  const magicCommand = extractMagicCommand(originalLines, config);
292
348
  // Check if it's a MAGIC cell
293
349
  const isMagicCell = originalLines.some(l => l.trim().startsWith(config.magicPrefix.trim()));
350
+ // Validate that raw cells have a language matching the file type
351
+ const cellIndex = result.cells.length;
352
+ validateRawCellLanguage(metadataLanguage, !isMagicCell, config, cellIndex, j);
294
353
  // Extract content
295
354
  let content;
296
355
  let contentStartLine = j;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacobknightley/fabric-format",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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",