From fe1aa15d8947ed3e0c6414326473cb21d9dac551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 11 Dec 2023 11:36:10 +0100 Subject: [PATCH 1/6] support typescript --- docs/sum.ts | 3 + docs/typescript.md | 64 ++++++++++++++ src/build.ts | 2 +- src/javascript/imports.ts | 4 +- src/markdown.ts | 6 +- src/preview.ts | 2 +- src/tag.ts | 13 +++ test/input/build/typescript/sum.ts | 3 + test/input/build/typescript/typescript.md | 22 +++++ test/javascript/imports-test.ts | 2 +- test/output/build/fetches/_import/foo/foo.js | 2 +- test/output/build/fetches/_import/top.js | 4 +- test/output/build/imports/_import/bar/bar.js | 2 +- test/output/build/imports/_import/bar/baz.js | 3 +- test/output/build/imports/_import/foo/foo.js | 5 +- .../output/build/typescript/_import/sum.ts.js | 3 + test/output/build/typescript/typescript.html | 87 +++++++++++++++++++ test/output/single-quote-expression.json | 2 +- test/typescript-test.ts | 23 +++++ 19 files changed, 236 insertions(+), 16 deletions(-) create mode 100644 docs/sum.ts create mode 100644 docs/typescript.md create mode 100644 test/input/build/typescript/sum.ts create mode 100644 test/input/build/typescript/typescript.md create mode 100644 test/output/build/typescript/_import/sum.ts.js create mode 100644 test/output/build/typescript/typescript.html create mode 100644 test/typescript-test.ts diff --git a/docs/sum.ts b/docs/sum.ts new file mode 100644 index 000000000..7372ed029 --- /dev/null +++ b/docs/sum.ts @@ -0,0 +1,3 @@ +export function sum(a: number, b: number): number { + return a + b; +} diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 000000000..0e5746884 --- /dev/null +++ b/docs/typescript.md @@ -0,0 +1,64 @@ +# TypeScript ([#79](https://github.com/observablehq/cli/issues/79)) + +[Observable Markdown](./markdown) supports TypeScript — an extension of JavaScript adding types to the language — in fenced code blocks and inline expressions. + +`ts` code blocks use [esbuild](https://esbuild.github.io/) to parse TypeScript syntax and discard the type annotations. Inline expressions that fail to parse with JavaScript are likewise passed to esbuild, and replaced with the transform if it succeeds. + +Note however that this support is limited to parsing the code and converting it to JavaScript; esbuild does not do any type checking. + +TypeScript is also supported in [data loaders](./loaders). + +````md +```ts +TypeScript code here… +``` +```` + +try this one: + +```ts echo +import {FileAttachment} from "npm:@observablehq/stdlib"; +FileAttachment(`./test.txt`) +``` + +```ts echo +function add(a: number, b: number): number { + return a + b; +} +``` + +The resulting function can be called from js cells as well as other ts cells: + +```js echo +add(1, 3) +``` + +```ts echo +add(1 as number, 3) +``` + +Inline expressions are also converted when they appear to be written in TypeScript: + +```md echo +1 + 2 = ${add(1 as number, 2)}. +``` + +1 + 2 = ${add(1 as number, 2)}. + +Errors echo up as expected: + +```ts echo +function bad() ::: { + return a + b; +} +``` + +Imports are transpiled too: + +```ts echo +import {sum} from "./sum.ts"; +``` + +```js +sum(1, 2) +``` diff --git a/src/build.ts b/src/build.ts index 2938ca748..6982e370d 100644 --- a/src/build.ts +++ b/src/build.ts @@ -124,7 +124,7 @@ export async function build( const importResolver = createImportResolver(root); for (const file of imports) { const sourcePath = join(root, file); - const outputPath = join("_import", file); + const outputPath = join("_import", file.replace(/\.ts$/, ".ts.js")); if (!existsSync(sourcePath)) { effects.logger.error("missing referenced file", sourcePath); continue; diff --git a/src/javascript/imports.ts b/src/javascript/imports.ts index 40fdf61c2..c5a208e51 100644 --- a/src/javascript/imports.ts +++ b/src/javascript/imports.ts @@ -9,6 +9,7 @@ import {isEnoent} from "../error.js"; import {type Feature, type ImportReference, type JavaScriptNode} from "../javascript.js"; import {parseOptions} from "../javascript.js"; import {Sourcemap} from "../sourcemap.js"; +import {transpileTypeScript} from "../tag.js"; import {relativeUrl, resolvePath} from "../url.js"; import {getFeature, getStringLiteralValue, isStringLiteral} from "./features.js"; import {defaultGlobals} from "./globals.js"; @@ -200,6 +201,7 @@ export function findImportFeatures(node: Node, path: string, input: string): Fea /** Rewrites import specifiers and FileAttachment calls in the specified ES module source. */ export async function rewriteModule(input: string, path: string, resolver: ImportResolver): Promise { + input = transpileTypeScript(input); const body = Parser.parse(input, parseOptions); const featureMap = getFeatureReferenceMap(body); const output = new Sourcemap(input); @@ -306,7 +308,7 @@ export type ImportResolver = (path: string, specifier: string) => Promise { return isLocalImport(specifier, path) - ? relativeUrl(path, resolvePath(base, path, resolveImportHash(root, path, specifier))) + ? relativeUrl(path, resolvePath(base, path, resolveImportHash(root, path, specifier.replace(/\.ts$/, ".ts.js")))) : specifier === "npm:@observablehq/runtime" ? resolveBuiltin(base, path, "runtime.js") : specifier === "npm:@observablehq/stdlib" diff --git a/src/markdown.ts b/src/markdown.ts index d694f4b79..c7b3ca978 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -17,7 +17,7 @@ import {computeHash} from "./hash.js"; import {parseInfo} from "./info.js"; import type {FileReference, ImportReference, PendingTranspile, Transpile} from "./javascript.js"; import {transpileJavaScript} from "./javascript.js"; -import {transpileTag} from "./tag.js"; +import {transpileTag, transpileTypeScript} from "./tag.js"; import {resolvePath} from "./url.js"; export interface ReadMarkdownResult { @@ -91,6 +91,8 @@ function isFalse(attribute: string | undefined): boolean { function getLiveSource(content: string, tag: string): string | undefined { return tag === "js" ? content + : tag === "ts" + ? transpileTypeScript(content) : tag === "tex" ? transpileTag(content, "tex.block", true) : tag === "dot" @@ -267,7 +269,7 @@ function makePlaceholderRenderer(root: string, sourcePath: string): RenderRule { return (tokens, idx, options, context: ParseContext) => { const id = uniqueCodeId(context, tokens[idx].content); const token = tokens[idx]; - const transpile = transpileJavaScript(token.content, { + const transpile = transpileJavaScript(transpileTypeScript(token.content), { id, root, sourcePath, diff --git a/src/preview.ts b/src/preview.ts index a23e0b632..d11bbc734 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -93,7 +93,7 @@ export class PreviewServer { } else if (pathname.startsWith("/_observablehq/")) { send(req, pathname.slice("/_observablehq".length), {root: publicRoot}).pipe(res); } else if (pathname.startsWith("/_import/")) { - const file = pathname.slice("/_import".length); + const file = pathname.slice("/_import".length).replace(/\.ts\.js$/, ".ts"); let js: string; try { js = await readFile(join(root, file), "utf-8"); diff --git a/src/tag.ts b/src/tag.ts index de066164a..21a996fa7 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,6 +1,7 @@ import type {Options, TemplateElement, TemplateLiteral} from "acorn"; // @ts-expect-error TokContext is private import {Parser, TokContext, tokTypes as tt} from "acorn"; +import {transformSync} from "esbuild"; import {Sourcemap} from "./sourcemap.js"; const CODE_DOLLAR = 36; @@ -17,6 +18,18 @@ export function transpileTag(input: string, tag = "", raw = false): string { return String(source); } +export function transpileTypeScript(input) { + try { + const js = transformSync(input, { + loader: "ts", + tsconfigRaw: '{"compilerOptions": {"verbatimModuleSyntax": true}}' + }).code; + // preserve the absence of a trailing semi-colon, to display + return input.trim().at(-1) !== ";" ? js.replace(/;[\s\n]*$/, "") : js; + } catch { + return input; + } +} class TemplateParser extends (Parser as any) { constructor(options: Options, input: string, startPos?: number) { super(options, input, startPos); diff --git a/test/input/build/typescript/sum.ts b/test/input/build/typescript/sum.ts new file mode 100644 index 000000000..7372ed029 --- /dev/null +++ b/test/input/build/typescript/sum.ts @@ -0,0 +1,3 @@ +export function sum(a: number, b: number): number { + return a + b; +} diff --git a/test/input/build/typescript/typescript.md b/test/input/build/typescript/typescript.md new file mode 100644 index 000000000..d8847a823 --- /dev/null +++ b/test/input/build/typescript/typescript.md @@ -0,0 +1,22 @@ +```ts echo +function add(a: number, b: number): number { + return a + b; +} +``` + +```js echo +add(1, 3) +``` + +```ts echo +add(1 as number, 3) +``` + +```ts echo +import {sum} from "./sum.ts"; +``` + +```ts echo +sum(1, 2) +``` + diff --git a/test/javascript/imports-test.ts b/test/javascript/imports-test.ts index 291b326ac..cad834696 100644 --- a/test/javascript/imports-test.ts +++ b/test/javascript/imports-test.ts @@ -120,7 +120,7 @@ describe("rewriteModule(input, path, resolver)", () => { it("ignores FileAttachment if masked by a reference", async () => { const input = 'import {FileAttachment} from "npm:@observablehq/stdlib";\n((FileAttachment) => FileAttachment("./test.txt"))(eval)'; // prettier-ignore const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, '((FileAttachment) => FileAttachment("./test.txt"))(eval)'); + assert.strictEqual(output, '((FileAttachment2) => FileAttachment2("./test.txt"))(eval)'); }); it("ignores FileAttachment if not imported", async () => { const input = 'import {Generators} from "npm:@observablehq/stdlib";\nFileAttachment("./test.txt")'; diff --git a/test/output/build/fetches/_import/foo/foo.js b/test/output/build/fetches/_import/foo/foo.js index 130f9d719..1a22ff230 100644 --- a/test/output/build/fetches/_import/foo/foo.js +++ b/test/output/build/fetches/_import/foo/foo.js @@ -1,3 +1,3 @@ -import {FileAttachment} from "../../_observablehq/stdlib.js"; +import { FileAttachment } from "../../_observablehq/stdlib.js"; export const fooJsonData = await FileAttachment("../../foo/foo-data.json", import.meta.url).json(); export const fooCsvData = await FileAttachment("../../foo/foo-data.csv", import.meta.url).text(); diff --git a/test/output/build/fetches/_import/top.js b/test/output/build/fetches/_import/top.js index 94d5dba46..92d939c84 100644 --- a/test/output/build/fetches/_import/top.js +++ b/test/output/build/fetches/_import/top.js @@ -1,4 +1,4 @@ -import {FileAttachment} from "../_observablehq/stdlib.js"; -export {fooCsvData, fooJsonData} from "./foo/foo.js?sha=ddc538dfc10d83a59458d5893c89191ef3b2c9b1c02ef6da055423f37388ecf4"; +import { FileAttachment } from "../_observablehq/stdlib.js"; +export { fooCsvData, fooJsonData } from "./foo/foo.js?sha=ddc538dfc10d83a59458d5893c89191ef3b2c9b1c02ef6da055423f37388ecf4"; export const topJsonData = await FileAttachment("../top-data.json", import.meta.url).json(); export const topCsvData = await FileAttachment("../top-data.csv", import.meta.url).text(); diff --git a/test/output/build/imports/_import/bar/bar.js b/test/output/build/imports/_import/bar/bar.js index b45bcb384..a6c1c9864 100644 --- a/test/output/build/imports/_import/bar/bar.js +++ b/test/output/build/imports/_import/bar/bar.js @@ -1 +1 @@ -export {bar} from "./baz.js?sha=e48bbd08d9d69efb7c743b54db47ff7efb1f6bf5f59648c861d5cb8fa0096ce6"; +export { bar } from "./baz.js?sha=e48bbd08d9d69efb7c743b54db47ff7efb1f6bf5f59648c861d5cb8fa0096ce6"; diff --git a/test/output/build/imports/_import/bar/baz.js b/test/output/build/imports/_import/bar/baz.js index eef58ffa8..472364554 100644 --- a/test/output/build/imports/_import/bar/baz.js +++ b/test/output/build/imports/_import/bar/baz.js @@ -1,4 +1,3 @@ -import {foo} from "../foo/foo.js?sha=056eae1b54d6d0e3046f38c8e7b3af2796b70e6505afb47427357e415005d997"; - +import { foo } from "../foo/foo.js?sha=056eae1b54d6d0e3046f38c8e7b3af2796b70e6505afb47427357e415005d997"; export const bar = "bar"; export const foobar = foo + "bar"; diff --git a/test/output/build/imports/_import/foo/foo.js b/test/output/build/imports/_import/foo/foo.js index 697142f00..945347454 100644 --- a/test/output/build/imports/_import/foo/foo.js +++ b/test/output/build/imports/_import/foo/foo.js @@ -1,6 +1,5 @@ import "https://cdn.jsdelivr.net/npm/d3@7.8.5/+esm"; -import {bar} from "../bar/bar.js?sha=2e71da6918681d51fd9e7ed79b03aed514771c2e1583af42783665aff3f5ef5e"; -export {top} from "../top.js?sha=43e37c2a4fe9ca94e62d0d8acd5dbd8bdd5f0ec845503964f465979c4c82c2a3"; - +import { bar } from "../bar/bar.js?sha=2e71da6918681d51fd9e7ed79b03aed514771c2e1583af42783665aff3f5ef5e"; +export { top } from "../top.js?sha=43e37c2a4fe9ca94e62d0d8acd5dbd8bdd5f0ec845503964f465979c4c82c2a3"; export const foo = "foo"; export const foobar = "foo" + bar; diff --git a/test/output/build/typescript/_import/sum.ts.js b/test/output/build/typescript/_import/sum.ts.js new file mode 100644 index 000000000..506a41343 --- /dev/null +++ b/test/output/build/typescript/_import/sum.ts.js @@ -0,0 +1,3 @@ +export function sum(a, b) { + return a + b; +} diff --git a/test/output/build/typescript/typescript.html b/test/output/build/typescript/typescript.html new file mode 100644 index 000000000..55b281ea7 --- /dev/null +++ b/test/output/build/typescript/typescript.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + +
+
+
+
function add(a: number, b: number): number {
+  return a + b;
+}
+
+
+
add(1, 3)
+
+
+
add(1 as number, 3)
+
+
+
import {sum} from "./sum.ts";
+
+
+
sum(1, 2)
+
+
+
+ +
© 2023 Observable, Inc.
+
+
diff --git a/test/output/single-quote-expression.json b/test/output/single-quote-expression.json index 8f5ed85ef..93c796245 100644 --- a/test/output/single-quote-expression.json +++ b/test/output/single-quote-expression.json @@ -21,7 +21,7 @@ "display" ], "inline": true, - "body": "async (display) => {\ndisplay(await(\n'}\"'\n))\n}" + "body": "async (display) => {\ndisplay(await(\n\"}\\\"\"\n))\n}" } ], "hash": "b48a5eea909f2e4be0d1f34e9df66c5a03e8dfac2529c179451d3ea674988c60" diff --git a/test/typescript-test.ts b/test/typescript-test.ts new file mode 100644 index 000000000..cdf1c4b7d --- /dev/null +++ b/test/typescript-test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert"; +import {transpileTypeScript} from "../src/tag.js"; + +describe("transpileTypeScript(input)", () => { + it("basic", () => { + assert.strictEqual(transpileTypeScript("1 + 2"), "1 + 2"); + }); + it("empty", () => { + assert.strictEqual(transpileTypeScript(""), ""); + }); + it("function call", () => { + assert.strictEqual(transpileTypeScript("sum(1, 2)"), "sum(1, 2)"); + }); + it("single import", () => { + assert.strictEqual(transpileTypeScript('import {sum} from "./sum.ts";'), 'import { sum } from "./sum.ts";\n'); + }); + it("import and value", () => { + assert.strictEqual( + transpileTypeScript('import {sum} from "./sum.ts"; sum(1, 2)'), + 'import { sum } from "./sum.ts";\nsum(1, 2)' + ); + }); +}); From 21b6c58e7c76026228f1acd38e246c91465bec52 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 12 Mar 2024 21:26:04 -0700 Subject: [PATCH 2/6] mostly working --- docs/sum.ts | 2 +- docs/typescript.md | 14 ++++++-------- src/build.ts | 2 +- src/dataloader.ts | 10 ++++++++-- src/javascript/module.ts | 13 ++++++++++--- src/javascript/parse.ts | 2 +- src/preview.ts | 13 ++++++++----- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/sum.ts b/docs/sum.ts index fc351b346..7372ed029 100644 --- a/docs/sum.ts +++ b/docs/sum.ts @@ -1,3 +1,3 @@ export function sum(a: number, b: number): number { - return a + b + 2; + return a + b; } diff --git a/docs/typescript.md b/docs/typescript.md index 0a85fd619..92505fcb7 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -42,13 +42,13 @@ add(1 as number, 3) Inline expressions are also converted when they appear to be written in TypeScript: -```md echo 1 + 2 = ${add(1 as number, 2)}. -``` +```md echo 1 + 2 = ${add(1 as number, 2)}. +``` -Errors echo up as expected: +Syntax errors are shown as expected: ```ts echo function bad() ::: { @@ -56,12 +56,10 @@ function bad() ::: { } ``` -Imports are transpiled too: +Imports are transpiled too (here `sum.js` refers to `sum.ts`; by convention, TypeScript files have the `.ts` extension but are imported using `.js`): ```ts echo -import {sum} from "./sum.ts"; // TODO import sum.ts as sum.js -``` +import {sum} from "./sum.js"; -```js -sum(1, 2) +display(sum(1, 2)); ``` diff --git a/src/build.ts b/src/build.ts index 3d0c6c307..c9fae07c2 100644 --- a/src/build.ts +++ b/src/build.ts @@ -188,7 +188,7 @@ export async function build( const resolveImportAlias = (path: string): string => { const hash = getModuleHash(root, path).slice(0, 8); const ext = extname(path); - return `/${join("_import", dirname(path), basename(path, ext))}.${hash}${ext === "ts" ? "js" : "ts"}`; + return `/${join("_import", dirname(path), basename(path, ext))}.${hash}${ext === "ts" ? "js" : ext}`; }; for (const path of localImports) { const sourcePath = join(root, path); diff --git a/src/dataloader.ts b/src/dataloader.ts index 18e41b674..ffc385b9b 100644 --- a/src/dataloader.ts +++ b/src/dataloader.ts @@ -1,6 +1,6 @@ import {type WriteStream, createReadStream, existsSync, statSync} from "node:fs"; import {mkdir, open, readFile, rename, unlink} from "node:fs/promises"; -import {dirname, extname, join, relative} from "node:path/posix"; +import {basename, dirname, extname, join, relative} from "node:path/posix"; import {createGunzip} from "node:zlib"; import {spawn} from "cross-spawn"; import JSZip from "jszip"; @@ -122,7 +122,13 @@ export class LoaderResolver { getWatchPath(path: string): string | undefined { const exactPath = join(this.root, path); - return existsSync(exactPath) ? exactPath : this.find(path)?.path; + if (existsSync(exactPath)) return exactPath; + if (path.endsWith(".js")) { + // TODO consolidate this resolving + const tspath = join(this.root, dirname(path), basename(path, ".js") + ".ts"); + if (existsSync(tspath)) return tspath; + } + return this.find(path)?.path; } watchFiles(path: string, watchPaths: Iterable, callback: (name: string) => void) { diff --git a/src/javascript/module.ts b/src/javascript/module.ts index 67842176a..e1aa0abe4 100644 --- a/src/javascript/module.ts +++ b/src/javascript/module.ts @@ -1,8 +1,9 @@ import {createHash} from "node:crypto"; -import {readFileSync, statSync} from "node:fs"; -import {join} from "node:path/posix"; +import {existsSync, readFileSync, statSync} from "node:fs"; +import {basename, dirname, join} from "node:path/posix"; import type {Program} from "acorn"; import {resolvePath} from "../path.js"; +import {transpileTypeScript} from "../typescript.js"; import {findFiles} from "./files.js"; import {findImports} from "./imports.js"; import {parseProgram} from "./parse.js"; @@ -70,7 +71,12 @@ export function getModuleHash(root: string, path: string): string { * source root, or undefined if the module does not exist or has invalid syntax. */ export function getModuleInfo(root: string, path: string): ModuleInfo | undefined { - const key = join(root, path); + let key = join(root, path); + // TODO Shouldn’t the path always end with .js? + if (!existsSync(key) && path.endsWith(".js")) { + const tskey = join(dirname(key), basename(key, ".js") + ".ts"); + if (existsSync(tskey)) key = tskey; + } let mtimeMs: number; try { ({mtimeMs} = statSync(key)); @@ -84,6 +90,7 @@ export function getModuleInfo(root: string, path: string): ModuleInfo | undefine let body: Program; try { source = readFileSync(key, "utf-8"); + source = transpileTypeScript(source); // TODO conditional body = parseProgram(source); } catch { moduleInfoCache.delete(key); // delete stale entry diff --git a/src/javascript/parse.ts b/src/javascript/parse.ts index 77545e6f6..b8151a05f 100644 --- a/src/javascript/parse.ts +++ b/src/javascript/parse.ts @@ -40,7 +40,7 @@ export interface JavaScriptNode { * the specified inline JavaScript expression. */ export function parseJavaScript(input: string, options: ParseOptions): JavaScriptNode { - input = transpileTypeScript(input); + input = transpileTypeScript(input); // TODO conditional const {inline = false, path} = options; let expression = maybeParseExpression(input); // first attempt to parse as expression if (expression?.type === "ClassExpression" && expression.id) expression = null; // treat named class as program diff --git a/src/preview.ts b/src/preview.ts index 8e456624d..0e399b457 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -1,5 +1,5 @@ import {createHash} from "node:crypto"; -import {watch} from "node:fs"; +import {existsSync, watch} from "node:fs"; import type {FSWatcher, WatchEventType} from "node:fs"; import {access, constants, readFile} from "node:fs/promises"; import {createServer} from "node:http"; @@ -30,7 +30,6 @@ import {bundleStyles, rollupClient} from "./rollup.js"; import {searchIndex} from "./search.js"; import {Telemetry} from "./telemetry.js"; import {bold, faint, green, link} from "./tty.js"; -import {transpileTypeScript} from "./typescript.js"; export interface PreviewOptions { config: Config; @@ -112,14 +111,18 @@ export class PreviewServer { send(req, pathname, {root: join(root, ".observablehq", "cache")}).pipe(res); } else if (pathname.startsWith("/_import/")) { const path = pathname.slice("/_import".length); - const filepath = join(root, path); + let filepath = join(root, path); try { if (pathname.endsWith(".css")) { await access(filepath, constants.R_OK); end(req, res, await bundleStyles({path: filepath}), "text/css"); return; - } else if (pathname.endsWith(".js") || pathname.endsWith(".ts")) { - const input = await readFile(join(root, path), "utf-8"); + } else if (pathname.endsWith(".js")) { + if (!existsSync(filepath)) { + const tspath = join(dirname(filepath), basename(filepath, ".js") + ".ts"); + if (existsSync(tspath)) filepath = tspath; + } + const input = await readFile(filepath, "utf-8"); const output = await transpileModule(input, {root, path}); end(req, res, output, "text/javascript"); return; From e6c61e13c16940a39ac7ec84984e865c0b11fd1c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 12 Mar 2024 21:32:38 -0700 Subject: [PATCH 3/6] partial fixes --- src/markdown.ts | 2 +- test/javascript/imports-test.ts | 80 --------------------------------- test/typescript-test.ts | 2 +- 3 files changed, 2 insertions(+), 82 deletions(-) diff --git a/src/markdown.ts b/src/markdown.ts index 0bb6ea09b..df2c2bdc4 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -17,8 +17,8 @@ import {relativePath} from "./path.js"; import {transpileSql} from "./sql.js"; import {transpileTag} from "./tag.js"; import {InvalidThemeError} from "./theme.js"; -import {transpileTypeScript} from "./typescript.js"; import {red} from "./tty.js"; +import {transpileTypeScript} from "./typescript.js"; export interface MarkdownCode { id: string; diff --git a/test/javascript/imports-test.ts b/test/javascript/imports-test.ts index ec5a5139d..23f25f0e3 100644 --- a/test/javascript/imports-test.ts +++ b/test/javascript/imports-test.ts @@ -108,89 +108,9 @@ describe("hasImportDeclaration(body)", () => { it("returns true if the body has import declarations", () => { assert.strictEqual(hasImportDeclaration(parse("import 'foo.js';")), true); }); -<<<<<<< HEAD - it("does not require paths to start with ./, ../, or /", async () => { - assert.strictEqual(await testFile("test.txt", "test.js"), 'FileAttachment("../test.txt", import.meta.url)'); // prettier-ignore - assert.strictEqual(await testFile("sub/test.txt", "test.js"), 'FileAttachment("../sub/test.txt", import.meta.url)'); // prettier-ignore - assert.strictEqual(await testFile("test.txt", "sub/test.js"), 'FileAttachment("../../sub/test.txt", import.meta.url)'); // prettier-ignore - }); - it("rewrites absolute files with meta", async () => { - assert.strictEqual(await testFile("/test.txt", "test.js"), 'FileAttachment("../test.txt", import.meta.url)'); // prettier-ignore - assert.strictEqual(await testFile("/sub/test.txt", "test.js"), 'FileAttachment("../sub/test.txt", import.meta.url)'); // prettier-ignore - assert.strictEqual(await testFile("/test.txt", "sub/test.js"), 'FileAttachment("../../test.txt", import.meta.url)'); // prettier-ignore - }); - it("ignores FileAttachment if masked by a reference", async () => { - const input = 'import {FileAttachment} from "npm:@observablehq/stdlib";\n((FileAttachment) => FileAttachment("./test.txt"))(eval)'; // prettier-ignore - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, '((FileAttachment2) => FileAttachment2("./test.txt"))(eval)'); - }); - it("ignores FileAttachment if not imported", async () => { - const input = 'import {Generators} from "npm:@observablehq/stdlib";\nFileAttachment("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'FileAttachment("./test.txt")'); - }); - it("ignores FileAttachment if a comma expression", async () => { - const input = 'import {FileAttachment} from "npm:@observablehq/stdlib";\n(1, FileAttachment)("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, '(1, FileAttachment)("./test.txt")'); - }); - it("ignores FileAttachment if not imported from @observablehq/stdlib", async () => { - const input = 'import {FileAttachment} from "npm:@observablehq/not-stdlib";\nFileAttachment("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'FileAttachment("./test.txt")'); - }); - it("rewrites FileAttachment when aliased", async () => { - const input = 'import {FileAttachment as F} from "npm:@observablehq/stdlib";\nF("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'F("../test.txt", import.meta.url)'); - }); - it("rewrites FileAttachment when aliased to a global", async () => { - const input = 'import {FileAttachment as File} from "npm:@observablehq/stdlib";\nFile("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'File("../test.txt", import.meta.url)'); - }); - it.skip("rewrites FileAttachment when imported as a namespace", async () => { - const input = 'import * as O from "npm:@observablehq/stdlib";\nO.FileAttachment("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'O.FileAttachment("../test.txt", import.meta.url)'); - }); - it("ignores non-FileAttachment calls", async () => { - const input = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFile("./test.txt")'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'File("./test.txt")'); - }); - it("rewrites single-quoted literals", async () => { - const input = "import {FileAttachment} from \"npm:@observablehq/stdlib\";\nFileAttachment('./test.txt')"; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'FileAttachment("../test.txt", import.meta.url)'); - }); - it("rewrites template-quoted literals", async () => { - const input = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFileAttachment(`./test.txt`)'; - const output = (await rewriteModule(input, "test.js", async (path, specifier) => specifier)).split("\n").pop()!; - assert.strictEqual(output, 'FileAttachment("../test.txt", import.meta.url)'); - }); - it("throws a syntax error with non-literal calls", async () => { - const input = "import {FileAttachment} from \"npm:@observablehq/stdlib\";\nFileAttachment(`./${'test'}.txt`)"; - await assert.rejects(() => rewriteModule(input, "test.js", async (path, specifier) => specifier), /FileAttachment requires a single literal string/); // prettier-ignore - }); - it("throws a syntax error with URL fetches", async () => { - const input = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFileAttachment("https://example.com")'; - await assert.rejects(() => rewriteModule(input, "test.js", async (path, specifier) => specifier), /non-local file path/); // prettier-ignore - }); - it("ignores non-local path fetches", async () => { - const input1 = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFileAttachment("../test.txt")'; - const input2 = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFileAttachment("./../test.txt")'; - const input3 = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFileAttachment("../../test.txt")'; - const input4 = 'import {FileAttachment} from "npm:@observablehq/stdlib";\nFileAttachment("./../../test.txt")'; - await assert.rejects(() => rewriteModule(input1, "test.js", async (path, specifier) => specifier), /non-local file path/); // prettier-ignore - await assert.rejects(() => rewriteModule(input2, "test.js", async (path, specifier) => specifier), /non-local file path/); // prettier-ignore - await assert.rejects(() => rewriteModule(input3, "sub/test.js", async (path, specifier) => specifier), /non-local file path/); // prettier-ignore - await assert.rejects(() => rewriteModule(input4, "sub/test.js", async (path, specifier) => specifier), /non-local file path/); // prettier-ignore -======= it("returns false if the body does not have import declarations", () => { assert.strictEqual(hasImportDeclaration(parse("1 + 2;")), false); assert.strictEqual(hasImportDeclaration(parse("import('foo.js');")), false); ->>>>>>> main }); }); diff --git a/test/typescript-test.ts b/test/typescript-test.ts index cdf1c4b7d..9e1ab7e16 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import {transpileTypeScript} from "../src/tag.js"; +import {transpileTypeScript} from "../src/typescript.js"; describe("transpileTypeScript(input)", () => { it("basic", () => { From 77da1c34550d5f806507d2496feeaf590f3e34c4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Tue, 12 Mar 2024 21:43:47 -0700 Subject: [PATCH 4/6] fix tests --- src/build.ts | 4 ++- src/javascript/module.ts | 2 +- src/javascript/parse.ts | 2 -- src/javascript/transpile.ts | 2 -- src/markdown.ts | 4 ++- src/preview.ts | 4 ++- .../build/fetches/_import/foo/foo.6fd063d5.js | 2 +- .../build/fetches/_import/top.d8f5cc36.js | 5 --- test/output/build/imports/_import/bar/bar.js | 1 - test/output/build/imports/_import/bar/baz.js | 3 -- test/output/build/imports/_import/foo/foo.js | 5 --- .../_import/{sum.ts.js => sum.fd55756b.ts} | 0 test/output/build/typescript/typescript.html | 36 ++++++++++--------- test/output/single-quote-expression.json | 28 --------------- test/output/single-quote-expression.md.json | 6 ++-- 15 files changed, 33 insertions(+), 71 deletions(-) delete mode 100644 test/output/build/imports/_import/bar/bar.js delete mode 100644 test/output/build/imports/_import/bar/baz.js delete mode 100644 test/output/build/imports/_import/foo/foo.js rename test/output/build/typescript/_import/{sum.ts.js => sum.fd55756b.ts} (100%) delete mode 100644 test/output/single-quote-expression.json diff --git a/src/build.ts b/src/build.ts index c9fae07c2..da1c94909 100644 --- a/src/build.ts +++ b/src/build.ts @@ -20,6 +20,7 @@ import {bundleStyles, rollupClient} from "./rollup.js"; import {searchIndex} from "./search.js"; import {Telemetry} from "./telemetry.js"; import {faint, yellow} from "./tty.js"; +import {transpileTypeScript} from "./typescript.js"; export interface BuildOptions { config: Config; @@ -198,7 +199,8 @@ export async function build( } effects.output.write(`${faint("copy")} ${sourcePath} ${faint("→")} `); const resolveImport = getModuleResolver(root, path); - const input = await readFile(sourcePath, "utf-8"); + let input = await readFile(sourcePath, "utf-8"); + if (path.endsWith(".ts")) input = transpileTypeScript(input); const contents = await transpileModule(input, { root, path, diff --git a/src/javascript/module.ts b/src/javascript/module.ts index e1aa0abe4..08b7fbda4 100644 --- a/src/javascript/module.ts +++ b/src/javascript/module.ts @@ -90,7 +90,7 @@ export function getModuleInfo(root: string, path: string): ModuleInfo | undefine let body: Program; try { source = readFileSync(key, "utf-8"); - source = transpileTypeScript(source); // TODO conditional + if (key.endsWith(".ts")) source = transpileTypeScript(source); body = parseProgram(source); } catch { moduleInfoCache.delete(key); // delete stale entry diff --git a/src/javascript/parse.ts b/src/javascript/parse.ts index b8151a05f..d93f2b8bd 100644 --- a/src/javascript/parse.ts +++ b/src/javascript/parse.ts @@ -1,6 +1,5 @@ import {Parser, tokTypes} from "acorn"; import type {Expression, Identifier, Options, Program} from "acorn"; -import {transpileTypeScript} from "../typescript.js"; import {checkAssignments} from "./assignments.js"; import {findAwaits} from "./awaits.js"; import {findDeclarations} from "./declarations.js"; @@ -40,7 +39,6 @@ export interface JavaScriptNode { * the specified inline JavaScript expression. */ export function parseJavaScript(input: string, options: ParseOptions): JavaScriptNode { - input = transpileTypeScript(input); // TODO conditional const {inline = false, path} = options; let expression = maybeParseExpression(input); // first attempt to parse as expression if (expression?.type === "ClassExpression" && expression.id) expression = null; // treat named class as program diff --git a/src/javascript/transpile.ts b/src/javascript/transpile.ts index 586226f60..ff1ffe05a 100644 --- a/src/javascript/transpile.ts +++ b/src/javascript/transpile.ts @@ -5,7 +5,6 @@ import {simple} from "acorn-walk"; import {isPathImport, relativePath, resolvePath} from "../path.js"; import {getModuleResolver} from "../resolvers.js"; import {Sourcemap} from "../sourcemap.js"; -import {transpileTypeScript} from "../typescript.js"; import {findFiles} from "./files.js"; import type {ExportNode, ImportNode} from "./imports.js"; import {hasImportDeclaration, isImportMetaResolve} from "./imports.js"; @@ -51,7 +50,6 @@ export async function transpileModule( input: string, {root, path, resolveImport = getModuleResolver(root, path)}: TranspileModuleOptions ): Promise { - input = transpileTypeScript(input); // TODO sourcemap; conditional const servePath = `/${join("_import", path)}`; const body = parseProgram(input); // TODO ignore syntax error? const output = new Sourcemap(input); diff --git a/src/markdown.ts b/src/markdown.ts index df2c2bdc4..b6f8cf9fe 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -103,6 +103,7 @@ function makeFenceRenderer(baseRenderer: RenderRule): RenderRule { if (source != null) { const id = uniqueCodeId(context, source); // TODO const sourceLine = context.startLine + context.currentLine; + if (tag === "ts") source = transpileTypeScript(source); const node = parseJavaScript(source, {path}); context.code.push({id, node}); html += `
`; } catch (error) { diff --git a/src/preview.ts b/src/preview.ts index 0e399b457..91818963e 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -30,6 +30,7 @@ import {bundleStyles, rollupClient} from "./rollup.js"; import {searchIndex} from "./search.js"; import {Telemetry} from "./telemetry.js"; import {bold, faint, green, link} from "./tty.js"; +import {transpileTypeScript} from "./typescript.js"; export interface PreviewOptions { config: Config; @@ -122,7 +123,8 @@ export class PreviewServer { const tspath = join(dirname(filepath), basename(filepath, ".js") + ".ts"); if (existsSync(tspath)) filepath = tspath; } - const input = await readFile(filepath, "utf-8"); + let input = await readFile(filepath, "utf-8"); + if (filepath.endsWith(".ts")) input = transpileTypeScript(input); const output = await transpileModule(input, {root, path}); end(req, res, output, "text/javascript"); return; diff --git a/test/output/build/fetches/_import/foo/foo.6fd063d5.js b/test/output/build/fetches/_import/foo/foo.6fd063d5.js index 1a22ff230..130f9d719 100644 --- a/test/output/build/fetches/_import/foo/foo.6fd063d5.js +++ b/test/output/build/fetches/_import/foo/foo.6fd063d5.js @@ -1,3 +1,3 @@ -import { FileAttachment } from "../../_observablehq/stdlib.js"; +import {FileAttachment} from "../../_observablehq/stdlib.js"; export const fooJsonData = await FileAttachment("../../foo/foo-data.json", import.meta.url).json(); export const fooCsvData = await FileAttachment("../../foo/foo-data.csv", import.meta.url).text(); diff --git a/test/output/build/fetches/_import/top.d8f5cc36.js b/test/output/build/fetches/_import/top.d8f5cc36.js index b6ab5872b..a94611de3 100644 --- a/test/output/build/fetches/_import/top.d8f5cc36.js +++ b/test/output/build/fetches/_import/top.d8f5cc36.js @@ -1,9 +1,4 @@ -<<<<<<< HEAD:test/output/build/fetches/_import/top.js -import { FileAttachment } from "../_observablehq/stdlib.js"; -export { fooCsvData, fooJsonData } from "./foo/foo.js?sha=ddc538dfc10d83a59458d5893c89191ef3b2c9b1c02ef6da055423f37388ecf4"; -======= import {FileAttachment} from "../_observablehq/stdlib.js"; export {fooCsvData, fooJsonData} from "./foo/foo.6fd063d5.js"; ->>>>>>> main:test/output/build/fetches/_import/top.d8f5cc36.js export const topJsonData = await FileAttachment("../top-data.json", import.meta.url).json(); export const topCsvData = await FileAttachment("../top-data.csv", import.meta.url).text(); diff --git a/test/output/build/imports/_import/bar/bar.js b/test/output/build/imports/_import/bar/bar.js deleted file mode 100644 index a6c1c9864..000000000 --- a/test/output/build/imports/_import/bar/bar.js +++ /dev/null @@ -1 +0,0 @@ -export { bar } from "./baz.js?sha=e48bbd08d9d69efb7c743b54db47ff7efb1f6bf5f59648c861d5cb8fa0096ce6"; diff --git a/test/output/build/imports/_import/bar/baz.js b/test/output/build/imports/_import/bar/baz.js deleted file mode 100644 index 472364554..000000000 --- a/test/output/build/imports/_import/bar/baz.js +++ /dev/null @@ -1,3 +0,0 @@ -import { foo } from "../foo/foo.js?sha=056eae1b54d6d0e3046f38c8e7b3af2796b70e6505afb47427357e415005d997"; -export const bar = "bar"; -export const foobar = foo + "bar"; diff --git a/test/output/build/imports/_import/foo/foo.js b/test/output/build/imports/_import/foo/foo.js deleted file mode 100644 index 945347454..000000000 --- a/test/output/build/imports/_import/foo/foo.js +++ /dev/null @@ -1,5 +0,0 @@ -import "https://cdn.jsdelivr.net/npm/d3@7.8.5/+esm"; -import { bar } from "../bar/bar.js?sha=2e71da6918681d51fd9e7ed79b03aed514771c2e1583af42783665aff3f5ef5e"; -export { top } from "../top.js?sha=43e37c2a4fe9ca94e62d0d8acd5dbd8bdd5f0ec845503964f465979c4c82c2a3"; -export const foo = "foo"; -export const foobar = "foo" + bar; diff --git a/test/output/build/typescript/_import/sum.ts.js b/test/output/build/typescript/_import/sum.fd55756b.ts similarity index 100% rename from test/output/build/typescript/_import/sum.ts.js rename to test/output/build/typescript/_import/sum.fd55756b.ts diff --git a/test/output/build/typescript/typescript.html b/test/output/build/typescript/typescript.html index 55b281ea7..8f9105a58 100644 --- a/test/output/build/typescript/typescript.html +++ b/test/output/build/typescript/typescript.html @@ -2,19 +2,19 @@ - - + - + + - + -