@observablehq/notebook-kit 2.1.8 → 2.2.0

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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/observablehq/notebook-kit.git"
7
7
  },
8
- "version": "2.1.8",
8
+ "version": "2.2.0",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "test": "TZ=America/Los_Angeles vitest",
@@ -56,12 +56,12 @@
56
56
  "@observablehq/parser": "^6.1.0",
57
57
  "@observablehq/runtime": "^6.0.0",
58
58
  "@sindresorhus/slugify": "^2.2.1",
59
+ "@sveltejs/acorn-typescript": "^1.0.11",
59
60
  "acorn": "^8.16.0",
60
61
  "acorn-walk": "^8.3.5",
61
62
  "jsdom": "^26.1.0",
62
63
  "markdown-it": "^14.1.1",
63
64
  "markdown-it-anchor": "^9.2.0",
64
- "typescript": "^5.9.3",
65
65
  "vite": "^7.0.0 || ^8.0.0"
66
66
  },
67
67
  "devDependencies": {
@@ -80,6 +80,7 @@
80
80
  "postgres": "^3.4.9",
81
81
  "snowflake-sdk": "^2.4.2",
82
82
  "tsx": "^4.22.3",
83
+ "typescript": "^5.9.3",
83
84
  "typescript-eslint": "^8.59.4",
84
85
  "vitest": "^4.1.7"
85
86
  },
@@ -1,3 +1,4 @@
1
+ import { isTypeImport } from "./imports.js";
1
2
  import { defaultGlobals } from "./globals.js";
2
3
  import { syntaxError } from "./syntaxError.js";
3
4
  export function findDeclarations(node, input) {
@@ -14,7 +15,7 @@ export function findDeclarations(node, input) {
14
15
  declareLocal(node);
15
16
  break;
16
17
  case "ObjectPattern":
17
- node.properties.forEach((node) => declarePattern(node.type === "Property" ? node.value : node));
18
+ node.properties.forEach((node) => declarePattern(node.type === "Property" ? node.value : node)); // prettier-ignore
18
19
  break;
19
20
  case "ArrayPattern":
20
21
  node.elements.forEach((node) => node && declarePattern(node));
@@ -37,7 +38,11 @@ export function findDeclarations(node, input) {
37
38
  declareLocal(child.id);
38
39
  break;
39
40
  case "ImportDeclaration":
40
- child.specifiers.forEach((node) => declareLocal(node.local));
41
+ if (isTypeImport(child))
42
+ break;
43
+ child.specifiers
44
+ .filter((node) => !isTypeImport(node))
45
+ .forEach((node) => declareLocal(node.local));
41
46
  break;
42
47
  }
43
48
  }
@@ -13,6 +13,7 @@ export declare function hasImportDeclaration(body: Node): boolean;
13
13
  export declare function isImportMetaResolve(node: CallExpression): boolean;
14
14
  /** Returns true if the given node is an import.meta.url expression. */
15
15
  export declare function isImportMetaUrl(node: MemberExpression): boolean;
16
+ export declare function isTypeImport(node: AnyImportSpecifier | ImportDeclaration): boolean;
16
17
  export type RewriteImportOptions = {
17
18
  /** If true, resolve local imports relative to document.baseURI. */
18
19
  resolveLocalImports?: boolean;
@@ -44,6 +44,9 @@ export function isImportMetaUrl(node) {
44
44
  node.property.type === "Identifier" &&
45
45
  node.property.name === "url");
46
46
  }
47
+ export function isTypeImport(node) {
48
+ return node.importKind === "type";
49
+ }
47
50
  function resolveImport(specifier) {
48
51
  return resolveObservableImport(resolveJsrImport(resolveNpmImport(specifier)));
49
52
  }
@@ -80,7 +83,10 @@ export function rewriteImportDeclarations(output, body, inputs, { resolveLocalIm
80
83
  const declarations = [];
81
84
  simple(body, {
82
85
  ImportDeclaration(node) {
83
- if (isStringLiteral(node.source)) {
86
+ if (isTypeImport(node)) {
87
+ output.delete(node.start, node.end);
88
+ }
89
+ else if (isStringLiteral(node.source)) {
84
90
  declarations.push([node, node.source]);
85
91
  }
86
92
  }
@@ -106,8 +112,11 @@ export function rewriteImportDeclarations(output, body, inputs, { resolveLocalIm
106
112
  }
107
113
  }
108
114
  function renderImport(source, node, input) {
109
- const names = node.specifiers.filter(isNamedSpecifier).map(getImportedName);
110
- return `import(${source}${node.attributes.length > 0
115
+ const names = node.specifiers
116
+ .filter((node) => !isTypeImport(node))
117
+ .filter(isNamedSpecifier)
118
+ .map(getImportedName);
119
+ return `import(${source}${node.attributes?.length > 0
111
120
  ? `, {with: {${input.slice(node.attributes[0].start, node.attributes[node.attributes.length - 1].end)}}}`
112
121
  : ""})${names.length > 0
113
122
  ? `.then((module) => {${names
@@ -140,7 +149,7 @@ function isNamedSpecifier(node) {
140
149
  function rewriteImportSpecifiers(node) {
141
150
  return node.specifiers.some(isNamedSpecifier)
142
151
  ? `{${flatMapImportSpecifiers(node, rewriteImportSpecifier).join(", ")}}`
143
- : (node.specifiers.find(isNamespaceSpecifier)?.local.name ?? "{}");
152
+ : (node.specifiers.find((node) => isNamespaceSpecifier(node) && !isTypeImport(node))?.local.name ?? "{}"); // prettier-ignore
144
153
  }
145
154
  export function flatMapImportSpecifiers(node, f) {
146
155
  return node.specifiers.flatMap((node) => {
@@ -150,7 +159,9 @@ export function flatMapImportSpecifiers(node, f) {
150
159
  ? [f(node), f(toSpecialImportSpecifier(node, "viewof"))]
151
160
  : isMutableImport(node)
152
161
  ? [f(node), f(toSpecialImportSpecifier(node, "mutable"))]
153
- : f(node);
162
+ : isTypeImport(node)
163
+ ? []
164
+ : f(node);
154
165
  });
155
166
  }
156
167
  function rewriteImportSpecifier(node) {
@@ -1,6 +1,5 @@
1
- import type { Expression, Identifier, Options, Program } from "acorn";
1
+ import type { Expression, Identifier, Program } from "acorn";
2
2
  import type { FeatureExpression } from "./features.js";
3
- export declare const acornOptions: Options;
4
3
  export interface JavaScriptCell {
5
4
  body: Program | Expression;
6
5
  declarations: Identifier[] | null;
@@ -11,5 +10,7 @@ export interface JavaScriptCell {
11
10
  expression: boolean;
12
11
  async: boolean;
13
12
  }
14
- export declare function maybeParseJavaScript(input: string): JavaScriptCell | undefined;
15
- export declare function parseJavaScript(input: string): JavaScriptCell;
13
+ type Dialect = "js" | "ts";
14
+ export declare function maybeParseJavaScript(input: string, dialect?: Dialect): JavaScriptCell | undefined;
15
+ export declare function parseJavaScript(input: string, dialect?: Dialect): JavaScriptCell;
16
+ export {};
@@ -1,15 +1,30 @@
1
+ import { tsPlugin } from "@sveltejs/acorn-typescript";
1
2
  import { Parser, tokTypes } from "acorn";
2
3
  import { findAwaits } from "./awaits.js";
3
4
  import { findDeclarations } from "./declarations.js";
4
5
  import { findFeatures } from "./features.js";
5
6
  import { findReferences } from "./references.js";
6
- export const acornOptions = {
7
+ import { checkErasableSyntax } from "./typescript.js";
8
+ const jsOptions = {
7
9
  ecmaVersion: "latest",
8
10
  sourceType: "module"
9
11
  };
10
- export function maybeParseJavaScript(input) {
12
+ const tsOptions = {
13
+ ...jsOptions,
14
+ locations: true,
15
+ preserveParens: true // needed for accurate type stripping
16
+ };
17
+ const JsParser = Parser;
18
+ const TsParser = Parser.extend(tsPlugin());
19
+ function getParser(dialect = "js") {
20
+ return dialect === "ts" ? TsParser : JsParser;
21
+ }
22
+ function getOptions(dialect = "js") {
23
+ return dialect === "ts" ? tsOptions : jsOptions;
24
+ }
25
+ export function maybeParseJavaScript(input, dialect) {
11
26
  try {
12
- return parseJavaScript(input);
27
+ return parseJavaScript(input, dialect);
13
28
  }
14
29
  catch (error) {
15
30
  if (!(error instanceof SyntaxError))
@@ -17,13 +32,15 @@ export function maybeParseJavaScript(input) {
17
32
  return;
18
33
  }
19
34
  }
20
- export function parseJavaScript(input) {
21
- let expression = maybeParseExpression(input); // first attempt to parse as expression
35
+ export function parseJavaScript(input, dialect) {
36
+ let expression = maybeParseExpression(input, dialect); // first attempt to parse as expression
22
37
  if (expression?.type === "ClassExpression" && expression.id)
23
38
  expression = null; // treat named class as program
24
39
  if (expression?.type === "FunctionExpression" && expression.id)
25
40
  expression = null; // treat named function as program
26
- const body = expression ?? parseProgram(input); // otherwise parse as a program
41
+ const body = expression ?? parseProgram(input, dialect); // otherwise parse as a program
42
+ if (dialect === "ts")
43
+ checkErasableSyntax(body, input);
27
44
  return {
28
45
  body,
29
46
  declarations: expression ? null : findDeclarations(body, input),
@@ -35,12 +52,11 @@ export function parseJavaScript(input) {
35
52
  async: findAwaits(body).length > 0
36
53
  };
37
54
  }
38
- function parseProgram(input) {
39
- return Parser.parse(input, acornOptions);
55
+ function parseProgram(input, dialect) {
56
+ return getParser(dialect).parse(input, getOptions(dialect));
40
57
  }
41
- function maybeParseExpression(input) {
42
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
- const parser = new Parser(acornOptions, input, 0); // private constructor
58
+ function maybeParseExpression(input, dialect) {
59
+ const parser = new (getParser(dialect))(getOptions(dialect), input, 0); // eslint-disable-line @typescript-eslint/no-explicit-any -- private constructor
44
60
  parser.nextToken();
45
61
  try {
46
62
  const node = parser.parseExpression();
@@ -1,6 +1,6 @@
1
1
  import { checkAssignments } from "./assignments.js";
2
2
  import { defaultGlobals } from "./globals.js";
3
- import { checkExports } from "./imports.js";
3
+ import { checkExports, isTypeImport } from "./imports.js";
4
4
  import { ancestor } from "./walk.js";
5
5
  function isScope(node) {
6
6
  return (node.type === "FunctionExpression" ||
@@ -102,7 +102,11 @@ export function findReferences(node, { input, globals = defaultGlobals, filterRe
102
102
  ClassExpression: declareClass,
103
103
  CatchClause: declareCatchClause,
104
104
  ImportDeclaration(node, _state, [root]) {
105
- node.specifiers.forEach((specifier) => declareLocal(root, specifier.local));
105
+ if (isTypeImport(node))
106
+ return;
107
+ node.specifiers
108
+ .filter((specifier) => !isTypeImport(specifier))
109
+ .forEach((specifier) => declareLocal(root, specifier.local));
106
110
  }
107
111
  });
108
112
  function identifier(node, _state, parents) {
@@ -3,6 +3,7 @@ interface Position {
3
3
  column: number;
4
4
  }
5
5
  export declare class Sourcemap {
6
+ readonly length: number;
6
7
  readonly input: string;
7
8
  private readonly _edits;
8
9
  constructor(input: string);
@@ -2,6 +2,12 @@
2
2
  import { lineBreakG } from "acorn";
3
3
  export class Sourcemap {
4
4
  constructor(input) {
5
+ Object.defineProperty(this, "length", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: void 0
10
+ });
5
11
  Object.defineProperty(this, "input", {
6
12
  enumerable: true,
7
13
  configurable: true,
@@ -14,6 +20,7 @@ export class Sourcemap {
14
20
  writable: true,
15
21
  value: void 0
16
22
  });
23
+ this.length = input.length;
17
24
  this.input = input;
18
25
  this._edits = [];
19
26
  }
@@ -1,7 +1,10 @@
1
1
  import { TokContext, tokTypes as tt, Parser } from "acorn";
2
2
  import { getInterpreterMethod, isInterpreter } from "../lib/interpreters.js";
3
- import { acornOptions } from "./parse.js";
4
3
  import { Sourcemap } from "./sourcemap.js";
4
+ const options = {
5
+ ecmaVersion: "latest",
6
+ sourceType: "module"
7
+ };
5
8
  const CODE_DOLLAR = 36;
6
9
  const CODE_BACKSLASH = 92;
7
10
  const CODE_BACKTICK = 96;
@@ -68,7 +71,7 @@ class TemplateCellParser extends Parser {
68
71
  }
69
72
  }
70
73
  export function parseTemplate(input) {
71
- return TemplateCellParser.parse(input, acornOptions);
74
+ return TemplateCellParser.parse(input, options);
72
75
  }
73
76
  export function transpileTemplate(input, tag = "", raw = false) {
74
77
  let cell;
@@ -30,4 +30,5 @@ export type TranspileOptions = {
30
30
  /** @deprecated */
31
31
  export declare function transpile(input: string, mode: Cell["mode"], options?: TranspileOptions): TranspiledJavaScript;
32
32
  export declare function transpile(input: Cell, options?: TranspileOptions): TranspiledJavaScript;
33
+ export declare function transpileTypeScript(input: string, options?: TranspileOptions): TranspiledJavaScript;
33
34
  export declare function transpileJavaScript(input: string, options?: TranspileOptions): TranspiledJavaScript;
@@ -6,7 +6,7 @@ import { transpileObservable } from "./observable.js";
6
6
  import { parseJavaScript } from "./parse.js";
7
7
  import { Sourcemap } from "./sourcemap.js";
8
8
  import { transpileTemplate } from "./template.js";
9
- import { transpileTypeScript } from "./typescript.js";
9
+ import { stripTypes } from "./typescript.js";
10
10
  export function transpile(input, mode, options) {
11
11
  let cell;
12
12
  if (typeof input === "string") {
@@ -20,7 +20,7 @@ export function transpile(input, mode, options) {
20
20
  input = cell.value;
21
21
  }
22
22
  const transpiled = mode === "ts"
23
- ? transpileJavaScript(transpileTypeScript(input), options)
23
+ ? transpileTypeScript(input, options)
24
24
  : mode === "ojs"
25
25
  ? transpileObservable(input, options)
26
26
  : mode !== "js"
@@ -38,14 +38,24 @@ export function transpile(input, mode, options) {
38
38
  }
39
39
  return transpiled;
40
40
  }
41
+ export function transpileTypeScript(input, options) {
42
+ const cell = parseJavaScript(input, "ts");
43
+ const output = new Sourcemap(input);
44
+ stripTypes(cell, output);
45
+ return transpileCell(cell, output, options);
46
+ }
41
47
  export function transpileJavaScript(input, options) {
42
- const cell = parseJavaScript(input);
48
+ const cell = parseJavaScript(input, "js");
49
+ const output = new Sourcemap(input);
50
+ return transpileCell(cell, output, options);
51
+ }
52
+ function transpileCell(cell, output, options) {
43
53
  let async = cell.async;
44
54
  const inputs = Array.from(new Set(cell.references.map((r) => r.name)));
45
55
  if (hasImportDeclaration(cell.body))
46
56
  async = true;
47
57
  const outputs = Array.from(new Set(cell.declarations?.map((r) => r.name)));
48
- const output = new Sourcemap(input).trim();
58
+ output.trim();
49
59
  rewriteImportDeclarations(output, cell.body, inputs, options);
50
60
  rewriteImportExpressions(output, cell.body, options);
51
61
  if (options?.resolveFiles)
@@ -54,10 +64,10 @@ export function transpileJavaScript(input, options) {
54
64
  output.insertLeft(0, `return (\n`);
55
65
  output.insertLeft(0, `${async ? "async " : ""}(${inputs.map(deat)}) => {\n`);
56
66
  if (outputs.length > 0)
57
- output.insertRight(input.length, `\nreturn {${outputs}};`);
67
+ output.insertRight(output.length, `\nreturn {${outputs}};`);
58
68
  if (cell.expression)
59
- output.insertRight(input.length, `\n)`);
60
- output.insertRight(input.length, "\n}");
69
+ output.insertRight(output.length, `\n)`);
70
+ output.insertRight(output.length, "\n}");
61
71
  const body = String(output);
62
72
  const autodisplay = cell.expression && !(inputs.includes("display") || inputs.includes("view"));
63
73
  const files = new Set(cell.files.map((f) => f.argument));
@@ -1 +1,7 @@
1
- export declare function transpileTypeScript(input: string): string;
1
+ import type { Node } from "acorn";
2
+ import type { JavaScriptCell } from "./parse.js";
3
+ import type { Sourcemap } from "./sourcemap.js";
4
+ /** Throws a SyntaxError for any non-erasable TypeScript syntax. */
5
+ export declare function checkErasableSyntax(node: Node, input: string): void;
6
+ /** Erases TypeScript-specific syntax from output, leaving equivalent JavaScript. */
7
+ export declare function stripTypes(cell: JavaScriptCell, output: Sourcemap): void;
@@ -1,112 +1,227 @@
1
- import { tokenizer as Tokenizer, tokTypes } from "acorn";
2
- import { ModuleKind, ScriptTarget, transpile } from "typescript";
3
- import { createProgram, createSourceFile } from "typescript";
4
- import { isClassExpression, isFunctionExpression, isParenthesizedExpression } from "typescript";
5
- import { isExpressionStatement } from "typescript";
6
- const tokenizerOptions = {
7
- ecmaVersion: "latest"
1
+ import { tokenizer, tokTypes } from "acorn";
2
+ import { base, recursive } from "acorn-walk";
3
+ import { syntaxError } from "./syntaxError.js";
4
+ import { simple } from "./walk.js";
5
+ // TypeScript-only modifier keywords; static/get/set/async are valid JavaScript
6
+ // and so are deliberately excluded (and preserved).
7
+ const MODIFIERS = new Set([
8
+ "public",
9
+ "private",
10
+ "protected",
11
+ "readonly",
12
+ "override",
13
+ "declare",
14
+ "abstract"
15
+ ]);
16
+ const checkErasableVisitors = {
17
+ TSEnumDeclaration: checkErasable,
18
+ TSModuleDeclaration: checkErasable,
19
+ TSParameterProperty: checkErasable,
20
+ TSImportEqualsDeclaration: checkErasable,
21
+ TSExportAssignment: checkErasable
8
22
  };
9
- const compilerOptions = {
10
- target: ScriptTarget.ESNext,
11
- module: ModuleKind.Preserve,
12
- verbatimModuleSyntax: true
23
+ /** Throws a SyntaxError for any non-erasable TypeScript syntax. */
24
+ export function checkErasableSyntax(node, input) {
25
+ simple(node, checkErasableVisitors, input);
26
+ }
27
+ function checkErasable(node, input) {
28
+ if (node.declare)
29
+ return; // ambient declarations are erasable
30
+ throw syntaxError(`unsupported syntax: ${node.type}`, node, input);
31
+ }
32
+ /** Erases TypeScript-specific syntax from output, leaving equivalent JavaScript. */
33
+ export function stripTypes(cell, output) {
34
+ // The AST is traversed with acorn-walk’s recursive walker. Because acorn-walk’s
35
+ // base visitors only know about (and therefore only recurse into) the value
36
+ // children of each node, type annotations and generics are naturally skipped;
37
+ // we simply delete their source text from the output without descending into
38
+ // them. TypeScript-specific nodes that acorn-walk doesn’t know about (expression
39
+ // wrappers such as `x as T`, type-only declarations, parameter properties) are
40
+ // handled by the dedicated visitors below.
41
+ //
42
+ // Positions are taken from the AST wherever possible. A handful of tokens have
43
+ // no dedicated AST node (modifier keywords, the optional `?` and definite `!`
44
+ // markers), so they are located by re-tokenizing the small, AST-bounded span in
45
+ // which they must occur. Re-tokenizing (rather than string matching) correctly
46
+ // ignores comments and string literals within that span.
47
+ recursive(cell.body, output, stripTypesVisitors);
48
+ }
49
+ const stripTypesVisitors = {
50
+ // Type-only declarations, erased entirely.
51
+ TSTypeAliasDeclaration: deleteStatement,
52
+ TSInterfaceDeclaration: deleteStatement,
53
+ TSDeclareFunction: deleteStatement,
54
+ TSDeclareMethod: deleteStatement,
55
+ TSIndexSignature: deleteStatement,
56
+ TSNamespaceExportDeclaration: deleteStatement,
57
+ // Ambient (declare) enums and namespaces; non-ambient forms, which require
58
+ // runtime support, are rejected earlier by checkErasableSyntax.
59
+ TSEnumDeclaration: deleteStatement,
60
+ TSModuleDeclaration: deleteStatement,
61
+ // TypeScript expression wrappers, unwrapped to their value expression.
62
+ TSAsExpression: unwrapType, // x as T
63
+ TSSatisfiesExpression: unwrapType, // x satisfies T
64
+ TSNonNullExpression: unwrapType, // x!
65
+ TSInstantiationExpression: unwrapType, // f<T>
66
+ TSTypeAssertion: unwrapAssertion, // <T>x
67
+ TSTypeCastExpression: unwrapCast, // (x: T)
68
+ // Standard nodes that may carry type syntax. Function and Class are aggregate
69
+ // types dispatched by acorn-walk’s base walkers, so overriding them covers
70
+ // every function and class form.
71
+ Function: stripFunction,
72
+ Class: stripClass,
73
+ PropertyDefinition: stripMember,
74
+ MethodDefinition: stripMember,
75
+ AccessorProperty: stripMember,
76
+ VariablePattern: stripBindingIdentifier,
77
+ ObjectPattern: stripPattern,
78
+ ArrayPattern: stripPattern,
79
+ RestElement: stripPattern,
80
+ VariableDeclarator: stripVariableDeclarator,
81
+ CallExpression: stripTypeArguments,
82
+ NewExpression: stripTypeArguments,
83
+ TaggedTemplateExpression: stripTypeArguments
13
84
  };
14
- export function transpileTypeScript(input) {
15
- const expr = maybeExpression(input);
16
- if (expr)
17
- return trimTrailingSemicolon(transpile(expr, compilerOptions));
18
- parseTypeScript(input); // enforce valid syntax
19
- return transpile(input, compilerOptions);
20
- }
21
- /** If the given is an expression (not a statement), returns it with parens. */
22
- function maybeExpression(input) {
23
- if (!hasMatchedParens(input))
24
- return; // disallow funny business
25
- const expr = withParens(input);
26
- if (!isSolitaryExpression(expr))
27
- return;
28
- return expr;
29
- }
30
- /** Parses the specified TypeScript input, returning the AST or throwing a SyntaxError. */
31
- function parseTypeScript(input) {
32
- const file = createSourceFile("input.ts", input, compilerOptions.target);
33
- const program = createProgram(["input.ts"], compilerOptions, {
34
- getSourceFile: (path) => (path === "input.ts" ? file : undefined),
35
- getDefaultLibFileName: () => "lib.d.ts",
36
- writeFile: () => { },
37
- getCurrentDirectory: () => "/",
38
- getDirectories: () => [],
39
- getCanonicalFileName: (path) => path,
40
- useCaseSensitiveFileNames: () => true,
41
- getNewLine: () => "\n",
42
- fileExists: (path) => path === "input.ts",
43
- readFile: (path) => (path === "input.ts" ? input : undefined)
85
+ // Deletes the given node, plus any trailing semicolon and newline.
86
+ function deleteStatement(node, output) {
87
+ let end = node.end;
88
+ const input = output.input;
89
+ if (input[end] === ";")
90
+ ++end;
91
+ if (input[end] === "\n")
92
+ ++end;
93
+ output.delete(node.start, end);
94
+ }
95
+ // Unwraps a trailing type wrapper (x as T, x satisfies T, x!, f<T>), keeping the
96
+ // value expression.
97
+ function unwrapType(node, output, next) {
98
+ output.delete(node.expression.end, node.end);
99
+ next(node.expression, output);
100
+ }
101
+ // Unwraps a prefix type assertion (<T>x), keeping the value expression.
102
+ function unwrapAssertion(node, output, next) {
103
+ output.delete(node.start, node.expression.start);
104
+ next(node.expression, output);
105
+ }
106
+ // Unwraps a parenthesized type cast (x: T), keeping the value expression.
107
+ function unwrapCast(node, output, next) {
108
+ output.delete(node.typeAnnotation.start, node.typeAnnotation.end);
109
+ next(node.expression, output);
110
+ }
111
+ // Removes the return type and generic type parameters from a function.
112
+ function stripFunction(node, output, next) {
113
+ deleteType(output, node, "returnType");
114
+ deleteType(output, node, "typeParameters");
115
+ base.Function(node, output, next);
116
+ }
117
+ // Removes generic type parameters, heritage type arguments, the abstract
118
+ // modifier, and the implements clause from a class.
119
+ function stripClass(node, output, next) {
120
+ deleteType(output, node, "typeParameters");
121
+ deleteType(output, node, "superTypeParameters");
122
+ deleteType(output, node, "superTypeArguments");
123
+ if (node.abstract && node.id)
124
+ deleteModifiers(output, findModifiersStart(node), node.id.start);
125
+ if (node.implements?.length)
126
+ deleteImplements(output, node);
127
+ base.Class(node, output, next);
128
+ }
129
+ // Erases abstract/ambient members and overload signatures entirely; otherwise
130
+ // removes modifiers, optional/definite markers, and type annotations.
131
+ function stripMember(node, output, next) {
132
+ if (node.abstract || node.declare || node.value?.type === "TSDeclareMethod")
133
+ return deleteStatement(node, output); // prettier-ignore
134
+ deleteModifiers(output, findModifiersStart(node), node.key.start);
135
+ if (node.optional || node.definite)
136
+ deleteMarker(output, node.key.end, findMarkerEnd(node));
137
+ deleteType(output, node, "typeParameters");
138
+ deleteType(output, node, "typeAnnotation");
139
+ base.PropertyDefinition(node, output, next);
140
+ }
141
+ // Removes the optional marker and type annotation from a binding identifier.
142
+ function stripBindingIdentifier(node, output) {
143
+ if (node.optional)
144
+ deleteMarker(output, node.start, node.typeAnnotation?.start ?? node.end);
145
+ deleteType(output, node, "typeAnnotation");
146
+ }
147
+ // Removes the type annotation from a destructuring pattern, then recurses.
148
+ function stripPattern(node, output, next) {
149
+ deleteType(output, node, "typeAnnotation");
150
+ base[node.type](node, output, next);
151
+ }
152
+ // Removes the definite assignment marker from a variable declarator, then
153
+ // recurses (the annotation on the binding is handled by the pattern visitor).
154
+ function stripVariableDeclarator(node, output, next) {
155
+ if (node.definite && node.id.type === "Identifier")
156
+ deleteMarker(output, node.id.start, node.id.typeAnnotation?.start ?? node.init?.start ?? node.id.end); // prettier-ignore
157
+ base.VariableDeclarator(node, output, next);
158
+ }
159
+ // Removes generic type arguments from a call, construction, or tagged template
160
+ // (e.g. f<T>(), new Map<K, V>()), then recurses.
161
+ function stripTypeArguments(node, output, next) {
162
+ deleteType(output, node, "typeArguments");
163
+ base[node.type](node, output, next);
164
+ }
165
+ // Deletes the given child node from the output, if present.
166
+ function deleteType(output, node, key) {
167
+ const child = node[key];
168
+ if (child)
169
+ output.delete(child.start, child.end);
170
+ }
171
+ // The position after any leading decorators, where modifier keywords begin.
172
+ function findModifiersStart(node) {
173
+ const decorators = node.decorators;
174
+ return decorators?.length ? decorators[decorators.length - 1].end : node.start;
175
+ }
176
+ // The position bounding an optional/definite marker on a class member.
177
+ function findMarkerEnd(node) {
178
+ return node.typeAnnotation?.start ?? node.value?.start ?? node.end;
179
+ }
180
+ // Deletes the `?` or `!` marker token found between start and end.
181
+ function deleteMarker(output, start, end) {
182
+ forEachToken(output.input, start, end, (t) => {
183
+ if (t.type === tokTypes.question || t.type === tokTypes.prefix)
184
+ output.delete(t.start, t.end);
44
185
  });
45
- const diagnostics = program.getSyntacticDiagnostics(file);
46
- if (diagnostics.length > 0) {
47
- const [diagnostic] = diagnostics;
48
- throw new SyntaxError(String(diagnostic.messageText));
49
- }
50
- return file;
51
186
  }
52
- /** Returns true if the specified input is exactly one parenthesized expression statement. */
53
- function isSolitaryExpression(input) {
54
- let file;
55
- try {
56
- file = parseTypeScript(input);
57
- }
58
- catch {
59
- return false;
60
- }
61
- if (file.statements.length !== 1)
62
- return false;
63
- const statement = file.statements[0];
64
- if (!isExpressionStatement(statement))
65
- return false;
66
- const expression = statement.expression;
67
- if (!isParenthesizedExpression(expression))
68
- return false;
69
- const subexpression = expression.expression;
70
- if (isClassExpression(subexpression) && subexpression.name)
71
- return false;
72
- if (isFunctionExpression(subexpression) && subexpression.name)
73
- return false;
74
- return true;
75
- }
76
- function* tokenize(input) {
77
- const tokenizer = Tokenizer(input, tokenizerOptions);
78
- while (true) {
79
- const t = tokenizer.getToken();
187
+ // Deletes TypeScript-only modifier keyword tokens found between start and end,
188
+ // preserving JavaScript keywords such as static, get, set, and async.
189
+ function deleteModifiers(output, start, end) {
190
+ const input = output.input;
191
+ forEachToken(input, start, end, (t) => {
192
+ if (t.type !== tokTypes.name || !MODIFIERS.has(t.value))
193
+ return;
194
+ let e = t.end;
195
+ while (input[e] === " " || input[e] === "\t")
196
+ ++e; // consume trailing space
197
+ output.delete(t.start, e);
198
+ });
199
+ }
200
+ // Deletes an `implements I, J` heritage clause. The clause spans from the end of
201
+ // the preceding heritage node to the end of the last implemented type, so the
202
+ // `implements` keyword itself needs no separate lookup.
203
+ function deleteImplements(output, node) {
204
+ const list = node.implements;
205
+ const start = node.superTypeParameters?.end ??
206
+ node.superTypeArguments?.end ??
207
+ node.superClass?.end ??
208
+ node.id?.end ??
209
+ node.start;
210
+ output.delete(start, list[list.length - 1].end);
211
+ }
212
+ // Invokes fn for each token in the given span, with absolute offsets. The span is
213
+ // re-tokenized with acorn so that comments and strings are handled correctly; any
214
+ // tokenizing error (e.g. an unexpected character) is ignored.
215
+ function forEachToken(input, start, end, fn) {
216
+ if (start >= end)
217
+ return;
218
+ const T = tokenizer(input, { ecmaVersion: "latest" });
219
+ T.pos = start;
220
+ for (const t of T) {
80
221
  if (t.type === tokTypes.eof)
81
222
  break;
82
- yield t;
83
- }
84
- }
85
- /** Returns true if the specified input has matched parens. */
86
- function hasMatchedParens(input) {
87
- let depth = 0;
88
- for (const t of tokenize(input)) {
89
- if (t.type === tokTypes.parenL)
90
- ++depth;
91
- else if (t.type === tokTypes.parenR && --depth < 0)
92
- return false;
93
- }
94
- return depth === 0;
95
- }
96
- /** Wraps the specified input with parentheses. */
97
- function withParens(input) {
98
- let start;
99
- let end;
100
- for (const t of tokenize(input)) {
101
- start ?? (start = t);
102
- end = t;
223
+ if (t.start >= end)
224
+ break;
225
+ fn(t);
103
226
  }
104
- return `(${input.slice(start?.start, end?.end)})`;
105
- }
106
- /** Removes a trailing semicolon, if present. */
107
- function trimTrailingSemicolon(input) {
108
- let end;
109
- for (const t of tokenize(input))
110
- end = t;
111
- return end?.type === tokTypes.semi ? input.slice(0, end.start) : input;
112
227
  }
@@ -1,5 +1,5 @@
1
1
  import type { Node } from "acorn";
2
2
  import type { AncestorVisitors, RecursiveVisitors, SimpleVisitors } from "acorn-walk";
3
- export declare function ancestor<T>(node: Node, visitors: AncestorVisitors<T>): void;
3
+ export declare function ancestor<T>(node: Node, visitors: AncestorVisitors<T>, state?: T): void;
4
4
  export declare function recursive<T>(node: Node, state: T, functions: RecursiveVisitors<T>): void;
5
- export declare function simple<T>(node: Node, visitors: SimpleVisitors<T>): void;
5
+ export declare function simple<T>(node: Node, visitors: SimpleVisitors<T>, state?: T): void;
@@ -1,13 +1,45 @@
1
- import { walk } from "@observablehq/parser";
2
- import { ancestor as _ancestor } from "acorn-walk";
3
- import { recursive as _recursive } from "acorn-walk";
4
- import { simple as _simple } from "acorn-walk";
5
- export function ancestor(node, visitors) {
6
- return _ancestor(node, visitors, walk);
1
+ import { make } from "acorn-walk";
2
+ import { ancestor as baseAncestor } from "acorn-walk";
3
+ import { recursive as baseRecursive } from "acorn-walk";
4
+ import { simple as baseSimple } from "acorn-walk";
5
+ function callbackId(node, state, c) {
6
+ c(node.id, state, "Identifier");
7
+ }
8
+ function callbackExpression(node, state, c) {
9
+ c(node.expression, state, "Expression");
10
+ }
11
+ function callbackParameter(node, state, c) {
12
+ c(node.parameter, state, "Pattern");
13
+ }
14
+ function skip() { }
15
+ const visitors = {
16
+ ViewExpression: callbackId,
17
+ MutableExpression: callbackId,
18
+ TSTypeAliasDeclaration: skip,
19
+ TSInterfaceDeclaration: skip,
20
+ TSDeclareFunction: skip,
21
+ TSDeclareMethod: skip,
22
+ TSIndexSignature: skip,
23
+ TSNamespaceExportDeclaration: skip,
24
+ TSEnumDeclaration: skip,
25
+ TSModuleDeclaration: skip,
26
+ TSImportEqualsDeclaration: skip,
27
+ TSExportAssignment: skip,
28
+ TSAsExpression: callbackExpression,
29
+ TSSatisfiesExpression: callbackExpression,
30
+ TSNonNullExpression: callbackExpression,
31
+ TSTypeAssertion: callbackExpression,
32
+ TSInstantiationExpression: callbackExpression,
33
+ TSTypeCastExpression: callbackExpression,
34
+ TSParameterProperty: callbackParameter
35
+ };
36
+ const walk = make(visitors);
37
+ export function ancestor(node, visitors, state) {
38
+ return baseAncestor(node, visitors, walk, state);
7
39
  }
8
40
  export function recursive(node, state, functions) {
9
- return _recursive(node, state, functions, walk);
41
+ return baseRecursive(node, state, functions, walk);
10
42
  }
11
- export function simple(node, visitors) {
12
- return _simple(node, visitors, walk);
43
+ export function simple(node, visitors, state) {
44
+ return baseSimple(node, visitors, walk, state);
13
45
  }
@@ -203,6 +203,8 @@ async function insertSource(database, name, source) {
203
203
  source = await source;
204
204
  if (isFileAttachment(source))
205
205
  return insertFile(database, name, source);
206
+ if (isFlechetteTable(source))
207
+ return insertFlechetteTable(database, name, source);
206
208
  if (isArrowTable(source))
207
209
  return insertArrowTable(database, name, source);
208
210
  if (Array.isArray(source))
@@ -215,6 +217,8 @@ async function insertSource(database, name, source) {
215
217
  if ("data" in source) {
216
218
  // data + options
217
219
  const { data, ...options } = source;
220
+ if (isFlechetteTable(data))
221
+ return insertFlechetteTable(database, name, data, options);
218
222
  if (isArrowTable(data))
219
223
  return insertArrowTable(database, name, data, options);
220
224
  return insertArray(database, name, data, options);
@@ -301,22 +305,28 @@ async function insertUntypedCSV(connection, file, name) {
301
305
  async function insertArrowTable(database, name, table, options) {
302
306
  const connection = await database.connect();
303
307
  try {
304
- await connection.insertArrowTable(table, {
305
- name,
306
- schema: "main",
307
- ...options
308
- });
308
+ await connection.insertArrowTable(table, { name, schema: "main", ...options });
309
+ }
310
+ finally {
311
+ await connection.close();
312
+ }
313
+ }
314
+ async function insertArrowIpc(database, name, buffer, options) {
315
+ const connection = await database.connect();
316
+ try {
317
+ await connection.insertArrowFromIPCStream(buffer, { name, schema: "main", ...options });
309
318
  }
310
319
  finally {
311
320
  await connection.close();
312
321
  }
313
322
  }
323
+ async function insertFlechetteTable(database, name, table, options) {
324
+ const { tableToIPC } = await import("https://cdn.jsdelivr.net/npm/@uwdata/flechette@2.5.0/+esm");
325
+ return await insertArrowIpc(database, name, tableToIPC(table, { format: "stream" }), options);
326
+ }
314
327
  async function insertArqueroTable(database, name, source) {
315
- // TODO When we have stdlib versioning and can upgrade Arquero to version 5,
316
- // we can then call source.toArrow() directly, with insertArrowTable()
317
- const arrow = await import("https://cdn.jsdelivr.net/npm/apache-arrow@17.0.0/+esm");
318
- const table = arrow.tableFromIPC(source.toArrowBuffer());
319
- return await insertArrowTable(database, name, table);
328
+ const buffer = typeof source.toArrowIPC === "function" ? source.toArrowIPC() : source.toArrowBuffer(); // prettier-ignore
329
+ return await insertArrowIpc(database, name, buffer);
320
330
  }
321
331
  async function insertArray(database, name, array, options) {
322
332
  const arrow = await import("https://cdn.jsdelivr.net/npm/apache-arrow@17.0.0/+esm");
@@ -372,9 +382,17 @@ function isFileAttachment(value) {
372
382
  typeof value.url === "function" &&
373
383
  typeof value.arrayBuffer === "function");
374
384
  }
375
- // Arquero tables have a `toArrowBuffer` function
385
+ // Arquero tables have a `toArrowBuffer` (v5-) or `toArrowIPC` (v6+) function
376
386
  function isArqueroTable(value) {
377
- return value && typeof value.toArrowBuffer === "function";
387
+ return (value && (typeof value.toArrowBuffer === "function" || typeof value.toArrowIPC === "function"));
388
+ }
389
+ // Flechette tables have a `toColumns` method
390
+ function isFlechetteTable(value) {
391
+ return (value &&
392
+ typeof value.getChild === "function" &&
393
+ typeof value.toColumns === "function" &&
394
+ value.schema &&
395
+ Array.isArray(value.schema.fields));
378
396
  }
379
397
  // Returns true if the value is an Apache Arrow table. This uses a “duck” test
380
398
  // (instead of strict instanceof) because we want it to work with a range of
@@ -1 +1 @@
1
- export declare function buffer(file: Blob): Promise<ArrayBuffer>;
1
+ export declare function buffer(blob: Blob): Promise<ArrayBuffer>;
@@ -1,8 +1,3 @@
1
- export function buffer(file) {
2
- return new Promise((resolve, reject) => {
3
- const reader = new FileReader();
4
- reader.onload = () => resolve(reader.result);
5
- reader.onerror = reject;
6
- reader.readAsArrayBuffer(file);
7
- });
1
+ export function buffer(blob) {
2
+ return blob.arrayBuffer();
8
3
  }
@@ -1 +1 @@
1
- export declare function text(file: Blob): Promise<string>;
1
+ export declare function text(blob: Blob): Promise<string>;
@@ -1,8 +1,3 @@
1
- export function text(file) {
2
- return new Promise((resolve, reject) => {
3
- const reader = new FileReader();
4
- reader.onload = () => resolve(reader.result);
5
- reader.onerror = reject;
6
- reader.readAsText(file);
7
- });
1
+ export function text(blob) {
2
+ return blob.text();
8
3
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/observablehq/notebook-kit.git"
7
7
  },
8
- "version": "2.1.8",
8
+ "version": "2.2.0",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "test": "TZ=America/Los_Angeles vitest",
@@ -56,12 +56,12 @@
56
56
  "@observablehq/parser": "^6.1.0",
57
57
  "@observablehq/runtime": "^6.0.0",
58
58
  "@sindresorhus/slugify": "^2.2.1",
59
+ "@sveltejs/acorn-typescript": "^1.0.11",
59
60
  "acorn": "^8.16.0",
60
61
  "acorn-walk": "^8.3.5",
61
62
  "jsdom": "^26.1.0",
62
63
  "markdown-it": "^14.1.1",
63
64
  "markdown-it-anchor": "^9.2.0",
64
- "typescript": "^5.9.3",
65
65
  "vite": "^7.0.0 || ^8.0.0"
66
66
  },
67
67
  "devDependencies": {
@@ -80,6 +80,7 @@
80
80
  "postgres": "^3.4.9",
81
81
  "snowflake-sdk": "^2.4.2",
82
82
  "tsx": "^4.22.3",
83
+ "typescript": "^5.9.3",
83
84
  "typescript-eslint": "^8.59.4",
84
85
  "vitest": "^4.1.7"
85
86
  },