@observablehq/notebook-kit 1.3.0-rc.3 → 1.4.0-rc.1

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": "1.3.0-rc.3",
8
+ "version": "1.4.0-rc.1",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "test": "vitest",
@@ -61,6 +61,7 @@
61
61
  "jsdom": "^26.1.0",
62
62
  "markdown-it": "^14.1.0",
63
63
  "markdown-it-anchor": "^9.2.0",
64
+ "typescript": "^5.8.3",
64
65
  "vite": "^7.0.0"
65
66
  },
66
67
  "devDependencies": {
@@ -77,7 +78,6 @@
77
78
  "postgres": "^3.4.7",
78
79
  "snowflake-sdk": "^2.1.3",
79
80
  "tsx": "^4.20.3",
80
- "typescript": "^5.8.3",
81
81
  "typescript-eslint": "^8.35.0",
82
82
  "vitest": "^3.2.4"
83
83
  },
@@ -1,2 +1,7 @@
1
1
  import type { Identifier, Node } from "acorn";
2
- export declare function checkAssignments(node: Node, references: Identifier[], input: string): void;
2
+ export declare function checkAssignments(node: Node, { input, locals, references, globals }: {
3
+ input: string;
4
+ locals: Map<Node, Set<string>>;
5
+ globals?: Set<string>;
6
+ references: Identifier[];
7
+ }): void;
@@ -1,33 +1,44 @@
1
1
  import { defaultGlobals } from "./globals.js";
2
2
  import { syntaxError } from "./syntaxError.js";
3
- import { simple } from "./walk.js";
4
- export function checkAssignments(node, references, input) {
5
- function checkConst(node) {
3
+ import { ancestor } from "./walk.js";
4
+ export function checkAssignments(node, { input, locals, references, globals = defaultGlobals }) {
5
+ function isLocal({ name }, parents) {
6
+ for (const p of parents)
7
+ if (locals.get(p)?.has(name))
8
+ return true;
9
+ return false;
10
+ }
11
+ function checkConst(node, parents) {
6
12
  switch (node.type) {
7
13
  case "Identifier":
14
+ if (isLocal(node, parents))
15
+ break;
8
16
  if (references.includes(node))
9
17
  throw syntaxError(`Assignment to external variable '${node.name}'`, node, input);
10
- if (defaultGlobals.has(node.name))
18
+ if (globals.has(node.name))
11
19
  throw syntaxError(`Assignment to global '${node.name}'`, node, input);
12
20
  break;
13
- case "ObjectPattern":
14
- node.properties.forEach((node) => checkConst(node.type === "Property" ? node.value : node));
15
- break;
16
21
  case "ArrayPattern":
17
- node.elements.forEach((node) => node && checkConst(node));
22
+ for (const e of node.elements)
23
+ if (e)
24
+ checkConst(e, parents);
25
+ break;
26
+ case "ObjectPattern":
27
+ for (const p of node.properties)
28
+ checkConst(p.type === "Property" ? p.value : p, parents);
18
29
  break;
19
30
  case "RestElement":
20
- checkConst(node.argument);
31
+ checkConst(node.argument, parents);
21
32
  break;
22
33
  }
23
34
  }
24
- function checkConstLeft({ left }) {
25
- checkConst(left);
35
+ function checkConstArgument({ argument }, parents) {
36
+ checkConst(argument, parents);
26
37
  }
27
- function checkConstArgument({ argument }) {
28
- checkConst(argument);
38
+ function checkConstLeft({ left }, parents) {
39
+ checkConst(left, parents);
29
40
  }
30
- simple(node, {
41
+ ancestor(node, {
31
42
  AssignmentExpression: checkConstLeft,
32
43
  AssignmentPattern: checkConstLeft,
33
44
  UpdateExpression: checkConstArgument,
@@ -1,9 +1,9 @@
1
1
  import { assert, test } from "vitest";
2
- import { checkAssignments } from "./assignments.js";
3
2
  import { parseJavaScript } from "./parse.js";
3
+ import { findReferences } from "./references.js";
4
4
  function check(input) {
5
5
  const cell = parseJavaScript(input);
6
- checkAssignments(cell.body, cell.references, input);
6
+ findReferences(cell.body, { input });
7
7
  }
8
8
  test("allows non-external assignments", () => {
9
9
  assert.doesNotThrow(() => check("let foo = 1;\nfoo = 2;"));
@@ -29,5 +29,18 @@ test("does not allow external assignments via for…of or for…in", () => {
29
29
  assert.throws(() => check("for (foo in {});"), /external variable 'foo'/);
30
30
  });
31
31
  test("does not allow global assignments", () => {
32
- assert.throws(() => check("window = 1;"), /global 'window'/);
32
+ assert.throws(() => check("window = 1;"), /Assignment to global 'window'/);
33
+ assert.throws(() => check("const foo = (window = 1);"), /Assignment to global 'window'/);
34
+ });
35
+ test("does not allow conflicting top-level variables", () => {
36
+ assert.throws(() => check("const window = 1;"), /Global 'window' cannot be redefined/);
37
+ assert.throws(() => check("const foo = 1, window = 2;"), /Global 'window' cannot be redefined/);
38
+ assert.throws(() => check("const {window} = {};"), /Global 'window' cannot be redefined/);
39
+ assert.throws(() => check("const {window = 1} = {};"), /Global 'window' cannot be redefined/);
40
+ });
41
+ test("allows conflicting non-top-level variables", () => {
42
+ assert.doesNotThrow(() => check("{ const window = 1; }"));
43
+ assert.doesNotThrow(() => check("{ let window; window = 2; }"));
44
+ assert.doesNotThrow(() => check("{ const {window} = {}; }"));
45
+ assert.doesNotThrow(() => check("{ const {window = 1} = {}; }"));
33
46
  });
@@ -3,7 +3,9 @@ import type { ImportDefaultSpecifier, ImportSpecifier } from "acorn";
3
3
  import type { Sourcemap } from "./sourcemap.js";
4
4
  type NamedImportSpecifier = ImportSpecifier | ImportDefaultSpecifier;
5
5
  /** Throws a syntax error if any export declarations are found. */
6
- export declare function checkExports(body: Node, input: string): void;
6
+ export declare function checkExports(body: Node, { input }: {
7
+ input: string;
8
+ }): void;
7
9
  /** Returns true if the body includes an import declaration. */
8
10
  export declare function hasImportDeclaration(body: Node): boolean;
9
11
  /** Returns true if the given node is a import.meta.resolve(…) call. */
@@ -6,7 +6,7 @@ import { getStringLiteralValue, isStringLiteral } from "./strings.js";
6
6
  import { syntaxError } from "./syntaxError.js";
7
7
  import { simple } from "./walk.js";
8
8
  /** Throws a syntax error if any export declarations are found. */
9
- export function checkExports(body, input) {
9
+ export function checkExports(body, { input }) {
10
10
  function checkExport(child) {
11
11
  throw syntaxError("Unexpected token 'export'", child, input);
12
12
  }
@@ -109,8 +109,10 @@ function renderImport(source, node, input) {
109
109
  return `import(${source}${node.attributes.length > 0
110
110
  ? `, {with: {${input.slice(node.attributes[0].start, node.attributes[node.attributes.length - 1].end)}}}`
111
111
  : ""})${names.length > 0
112
- ? `.then((module) => {${names.map((name) => `
113
- if (!("${name}" in module)) throw new SyntaxError(\`export '${name}' not found\`);`).join("")}
112
+ ? `.then((module) => {${names
113
+ .map((name) => `
114
+ if (!("${name}" in module)) throw new SyntaxError(\`export '${name}' not found\`);`)
115
+ .join("")}
114
116
  return module;
115
117
  })`
116
118
  : ""}`;
@@ -1,7 +1,5 @@
1
1
  import { Parser, tokTypes } from "acorn";
2
- import { checkExports } from "./imports.js";
3
2
  import { findReferences } from "./references.js";
4
- import { checkAssignments } from "./assignments.js";
5
3
  import { findDeclarations } from "./declarations.js";
6
4
  import { findAwaits } from "./awaits.js";
7
5
  export const acornOptions = {
@@ -25,13 +23,10 @@ export function parseJavaScript(input) {
25
23
  if (expression?.type === "FunctionExpression" && expression.id)
26
24
  expression = null; // treat named function as program
27
25
  const body = expression ?? parseProgram(input); // otherwise parse as a program
28
- checkExports(body, input);
29
- const references = findReferences(body);
30
- checkAssignments(body, references, input);
31
26
  return {
32
27
  body,
33
28
  declarations: expression ? null : findDeclarations(body, input),
34
- references,
29
+ references: findReferences(body, { input }),
35
30
  expression: !!expression,
36
31
  async: findAwaits(body).length > 0
37
32
  };
@@ -1,5 +1,6 @@
1
1
  import type { Identifier, Node } from "acorn";
2
- export declare function findReferences(node: Node, { globals, filterReference, filterDeclaration }?: {
2
+ export declare function findReferences(node: Node, { input, globals, filterReference, filterDeclaration }?: {
3
+ input?: string;
3
4
  globals?: Set<string>;
4
5
  filterReference?: (identifier: Identifier) => boolean;
5
6
  filterDeclaration?: (identifier: {
@@ -1,4 +1,6 @@
1
+ import { checkAssignments } from "./assignments.js";
1
2
  import { defaultGlobals } from "./globals.js";
3
+ import { checkExports } from "./imports.js";
2
4
  import { ancestor } from "./walk.js";
3
5
  function isScope(node) {
4
6
  return (node.type === "FunctionExpression" ||
@@ -15,12 +17,11 @@ function isBlockScope(node) {
15
17
  node.type === "ForStatement" ||
16
18
  isScope(node));
17
19
  }
18
- export function findReferences(node, { globals = defaultGlobals, filterReference = (identifier) => !globals.has(identifier.name), filterDeclaration = () => true } = {}) {
20
+ export function findReferences(node, { input, globals = defaultGlobals, filterReference = (identifier) => !globals.has(identifier.name), filterDeclaration = () => true } = {}) {
19
21
  const locals = new Map();
20
22
  const references = [];
21
23
  function hasLocal(node, name) {
22
- const l = locals.get(node);
23
- return l ? l.has(name) : false;
24
+ return locals.get(node)?.has(name) ?? false;
24
25
  }
25
26
  function declareLocal(node, id) {
26
27
  if (!filterDeclaration(id))
@@ -125,5 +126,9 @@ export function findReferences(node, { globals = defaultGlobals, filterReference
125
126
  },
126
127
  Identifier: identifier
127
128
  });
129
+ if (input !== undefined) {
130
+ checkAssignments(node, { locals, references, globals, input });
131
+ checkExports(node, { input });
132
+ }
128
133
  return references;
129
134
  }
@@ -72,12 +72,19 @@ export function parseTemplate(input) {
72
72
  }
73
73
  export function transpileTemplate(input, tag = "", raw = false) {
74
74
  let cell;
75
+ let prefix;
76
+ let suffix;
75
77
  if (typeof input !== "string") {
76
78
  cell = input;
77
79
  input = cell.value;
78
- tag = getTag(cell);
80
+ prefix = getPrefix(cell);
81
+ suffix = getSuffix(cell);
79
82
  raw = getRaw(cell);
80
83
  }
84
+ else {
85
+ prefix = tag;
86
+ suffix = "";
87
+ }
81
88
  if (!input)
82
89
  return input;
83
90
  const source = new Sourcemap(input);
@@ -93,30 +100,29 @@ export function transpileTemplate(input, tag = "", raw = false) {
93
100
  (raw ? escapeRawTemplateElements : escapeTemplateElements)(source, template);
94
101
  node = template;
95
102
  }
96
- source.insertLeft(node.start, "`");
97
- source.insertRight(node.end, "`");
98
- source.insertLeft(node.start, tag);
99
- return String(source) + (cell ? getSuffix(cell) : "");
103
+ source.insertLeft(node.start, `${prefix}\``);
104
+ source.insertRight(node.end, `\`${suffix}`);
105
+ return String(source);
100
106
  }
101
107
  function getRaw(cell) {
102
108
  return cell.mode !== "md";
103
109
  }
104
- function getTag(cell) {
110
+ function getPrefix(cell) {
105
111
  return cell.mode === "tex"
106
112
  ? "tex.block"
107
113
  : cell.mode === "sql"
108
- ? getSqlTag(cell)
114
+ ? getSqlPrefix(cell)
109
115
  : isInterpreter(cell.mode)
110
- ? getInterpreterTag(cell)
116
+ ? getInterpreterPrefix(cell)
111
117
  : cell.mode;
112
118
  }
113
- function getSqlTag(cell) {
119
+ function getSqlPrefix(cell) {
114
120
  const { id, database = "var:db", since } = cell;
115
121
  return database.startsWith("var:")
116
122
  ? `${database.slice("var:".length)}.sql`
117
123
  : `DatabaseClient(${JSON.stringify(database)}, {id: ${id}${since === undefined ? "" : `, since: ${JSON.stringify(since)}`}}).sql`;
118
124
  }
119
- function getInterpreterTag(cell) {
125
+ function getInterpreterPrefix(cell) {
120
126
  const { id, mode, format, since } = cell;
121
127
  return `Interpreter(${JSON.stringify(mode)}, {id: ${id}${format === undefined ? "" : `, format: ${JSON.stringify(format)}`}${since === undefined ? "" : `, since: ${JSON.stringify(since)}`}}).run(`;
122
128
  }
@@ -129,7 +135,7 @@ function getSuffix(cell) {
129
135
  }
130
136
  function getInterpreterSuffix(cell) {
131
137
  const method = getInterpreterMethod(cell.format);
132
- return method ? `).then((file) => file${method})` : "";
138
+ return method ? `).then((file) => file${method})` : ")";
133
139
  }
134
140
  function escapeTemplateElements(source, node) {
135
141
  for (const quasi of node.quasis) {
@@ -1,3 +1,4 @@
1
+ import { ScriptTarget, transpile as transpileTypeScript } from "typescript";
1
2
  import { toCell } from "../lib/notebook.js";
2
3
  import { rewriteFileExpressions } from "./files.js";
3
4
  import { hasImportDeclaration } from "./imports.js";
@@ -18,16 +19,18 @@ export function transpile(input, mode, options) {
18
19
  cell = input;
19
20
  input = cell.value;
20
21
  }
21
- const transpiled = mode === "ojs"
22
- ? transpileObservable(input, options)
23
- : mode !== "js"
24
- ? transpileJavaScript(transpileTemplate(cell), options)
25
- : transpileJavaScript(input, options);
22
+ const transpiled = mode === "ts"
23
+ ? transpileJavaScript(transpileTypeScript(input, { target: ScriptTarget.ESNext }), options)
24
+ : mode === "ojs"
25
+ ? transpileObservable(input, options)
26
+ : mode !== "js"
27
+ ? transpileJavaScript(transpileTemplate(cell), options)
28
+ : transpileJavaScript(input, options);
26
29
  if (transpiled.output === undefined)
27
30
  transpiled.output = cell.output;
28
31
  if (cell.hidden)
29
32
  transpiled.autodisplay = false;
30
- else if (mode !== "js" && mode !== "ojs") {
33
+ else if (mode !== "js" && mode !== "ts" && mode !== "ojs") {
31
34
  transpiled.autodisplay = !!input;
32
35
  transpiled.autoview = mode === "sql" && transpiled.autodisplay && !!transpiled.output;
33
36
  if (transpiled.autoview)
@@ -15,7 +15,9 @@ export function getInterpreterExtension(format) {
15
15
  case "tsv":
16
16
  case "png":
17
17
  case "gif":
18
+ case "svg":
18
19
  case "webp":
20
+ case "xml":
19
21
  return `.${format}`;
20
22
  default:
21
23
  return ".bin";
@@ -28,6 +30,7 @@ export function getInterpreterMethod(format) {
28
30
  case "json":
29
31
  case "blob":
30
32
  case "text":
33
+ case "xml":
31
34
  return `.${format}()`;
32
35
  case "html":
33
36
  return `.text().then((text) => html({raw: [text]}))`;
@@ -36,6 +39,7 @@ export function getInterpreterMethod(format) {
36
39
  case "jpeg":
37
40
  case "png":
38
41
  case "gif":
42
+ case "svg":
39
43
  case "webp":
40
44
  return ".image()";
41
45
  case "csv":
@@ -21,7 +21,7 @@ export interface CellSpec {
21
21
  /** the committed cell value; defaults to empty */
22
22
  value?: string;
23
23
  /** the mode; affects how the value is evaluated; defaults to js */
24
- mode?: "js" | "ojs" | "md" | "html" | "tex" | "dot" | "sql" | "node" | "python";
24
+ mode?: "js" | "ts" | "ojs" | "md" | "html" | "tex" | "dot" | "sql" | "node" | "python";
25
25
  /** if true, the editor will stay open when not focused; defaults to false */
26
26
  pinned?: boolean;
27
27
  /** if true, implicit display will be suppressed; defaults to false */
@@ -29,7 +29,7 @@ export interface CellSpec {
29
29
  /** if present, exposes the cell’s value to the rest of the notebook */
30
30
  output?: string;
31
31
  /** for data loader cells, how the data is represented */
32
- format?: "text" | "blob" | "buffer" | "json" | "csv" | "tsv" | "jpeg" | "gif" | "webp" | "png" | "arrow" | "parquet" | "html";
32
+ format?: "text" | "blob" | "buffer" | "json" | "csv" | "tsv" | "jpeg" | "gif" | "webp" | "png" | "arrow" | "parquet" | "html" | "svg" | "xml";
33
33
  /** for SQL cells, the database to query; use var:<name> to refer to a variable */
34
34
  database?: string;
35
35
  /** for SQL cells, the oldest allowable age of the cached query result */
@@ -24,5 +24,5 @@ function asDate(date) {
24
24
  return date instanceof Date ? date : new Date(date);
25
25
  }
26
26
  export function defaultPinned(mode) {
27
- return mode === "js" || mode === "sql" || isInterpreter(mode) || mode === "ojs";
27
+ return mode === "js" || mode === "ts" || mode === "sql" || isInterpreter(mode) || mode === "ojs";
28
28
  }
@@ -74,6 +74,8 @@ function serializeMode(mode) {
74
74
  return "text/x-python";
75
75
  case "ojs":
76
76
  return "application/vnd.observable.javascript";
77
+ case "ts":
78
+ return "text/x-typescript";
77
79
  default:
78
80
  return "module";
79
81
  }
@@ -96,6 +98,8 @@ function deserializeMode(mode) {
96
98
  return "python";
97
99
  case "application/vnd.observable.javascript":
98
100
  return "ojs";
101
+ case "text/x-typescript":
102
+ return "ts";
99
103
  default:
100
104
  return "js";
101
105
  }
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": "1.3.0-rc.3",
8
+ "version": "1.4.0-rc.1",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "test": "vitest",
@@ -61,6 +61,7 @@
61
61
  "jsdom": "^26.1.0",
62
62
  "markdown-it": "^14.1.0",
63
63
  "markdown-it-anchor": "^9.2.0",
64
+ "typescript": "^5.8.3",
64
65
  "vite": "^7.0.0"
65
66
  },
66
67
  "devDependencies": {
@@ -77,7 +78,6 @@
77
78
  "postgres": "^3.4.7",
78
79
  "snowflake-sdk": "^2.1.3",
79
80
  "tsx": "^4.20.3",
80
- "typescript": "^5.8.3",
81
81
  "typescript-eslint": "^8.35.0",
82
82
  "vitest": "^3.2.4"
83
83
  },