Repository navigation
test: check the built packages in plain Node, and turn the Next.js server-util test on again #3239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| import { afterEach, beforeEach } from "vite-plus/test"; | ||
|
|
||
| // No `window`, as in the Node apps that use this package. The editor reads | ||
| // `__TEST_OPTIONS` off `globalThis` when there is no `window`. | ||
| beforeEach(() => { | ||
| globalThis.window = globalThis.window || ({} as any); | ||
| (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {}; | ||
| (globalThis as { __TEST_OPTIONS?: object }).__TEST_OPTIONS = {}; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| delete (window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS; | ||
| delete (globalThis as { __TEST_OPTIONS?: object }).__TEST_OPTIONS; | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,4 @@ node_modules | |
| .tarballs | ||
| package-lock.json | ||
| next-env.d.ts | ||
| AGENTS.md | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { execFileSync } from "node:child_process"; | ||
| import { existsSync, readdirSync, readFileSync } from "node:fs"; | ||
| import { join, resolve } from "node:path"; | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
|
||
| // These tests check the build output as an app installs it: in plain Node, | ||
| // without the aliases to the sources that the other tests use. They cannot find | ||
| // a missing dependency, as the workspace installs all of them (see nextjs/). | ||
| const packagesDir = resolve(__dirname, "../../../../packages"); | ||
|
|
||
| function publishedPackages() { | ||
| return readdirSync(packagesDir).flatMap((dir) => { | ||
| const path = join(packagesDir, dir, "package.json"); | ||
| if (!existsSync(path)) { | ||
| return []; | ||
| } | ||
| const pkg = JSON.parse(readFileSync(path, "utf8")); | ||
| return pkg.private ? [] : [{ dir, pkg }]; | ||
| }); | ||
| } | ||
|
|
||
| function exportTargets(exports: unknown): string[] { | ||
| if (typeof exports === "string") { | ||
| return [exports]; | ||
| } | ||
| if (exports && typeof exports === "object") { | ||
| return Object.values(exports).flatMap(exportTargets); | ||
| } | ||
| return []; | ||
| } | ||
|
|
||
| function runNode(script: string, env: Record<string, string> = {}) { | ||
| return execFileSync(process.execPath, [join(__dirname, script)], { | ||
|
Check failure on line 33 in tests/src/unit/builtPackages/builtPackages.test.ts
|
||
| encoding: "utf8", | ||
| env: { ...process.env, ...env }, | ||
| timeout: 120_000, | ||
| }); | ||
| } | ||
|
|
||
| // CI runs this after `vp run -r build`. Locally, it waits for a build. | ||
| describe.skipIf(!existsSync(join(packagesDir, "core/dist")) && !process.env.CI)( | ||
| "Built packages", | ||
| () => { | ||
| it("point to files that exist", () => { | ||
| const missing = publishedPackages().flatMap(({ dir, pkg }) => | ||
| [pkg.main, pkg.module, pkg.types, ...exportTargets(pkg.exports)] | ||
| .filter((target): target is string => typeof target === "string") | ||
| .filter((target) => !existsSync(resolve(packagesDir, dir, target))) | ||
| .map((target) => `${pkg.name}: ${target}`), | ||
| ); | ||
| expect([...new Set(missing)]).toEqual([]); | ||
|
Check failure on line 51 in tests/src/unit/builtPackages/builtPackages.test.ts
|
||
| }); | ||
|
|
||
| it("load in plain Node", { timeout: 60_000 }, () => { | ||
| expect(JSON.parse(runNode("loadEntries.mjs"))).toEqual([]); | ||
|
Check failure on line 55 in tests/src/unit/builtPackages/builtPackages.test.ts
|
||
| }); | ||
|
|
||
| // React's development build crashed the process after the export (#3230). | ||
| it.each(["development", "production"])( | ||
| "export React and optional blocks on a server (NODE_ENV=%s)", | ||
| { timeout: 60_000 }, | ||
| (nodeEnv) => { | ||
| expect(runNode("serverExport.mjs", { NODE_ENV: nodeEnv })).toContain( | ||
| "exported", | ||
| ); | ||
| }, | ||
| ); | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // Imports every entry of every published package from its build output, in | ||
| // plain Node (no bundler, no Vitest), and prints the entries that fail as JSON. | ||
| import { existsSync, readdirSync, readFileSync } from "node:fs"; | ||
| import { join, resolve } from "node:path"; | ||
| import { pathToFileURL } from "node:url"; | ||
|
|
||
| const packagesDir = resolve(import.meta.dirname, "../../../../packages"); | ||
|
|
||
| const failures = []; | ||
| for (const dir of readdirSync(packagesDir)) { | ||
| const packageJSONPath = join(packagesDir, dir, "package.json"); | ||
| if (!existsSync(packageJSONPath)) { | ||
| continue; | ||
| } | ||
| const pkg = JSON.parse(readFileSync(packageJSONPath, "utf8")); | ||
| if (pkg.private || !pkg.exports) { | ||
| continue; | ||
| } | ||
| for (const [subpath, target] of Object.entries(pkg.exports)) { | ||
| const file = | ||
| typeof target === "string" ? target : (target.import ?? target.default); | ||
| // Stylesheets and WebAssembly files are for bundlers, not for Node. | ||
| if (!file || !/\.m?js$/.test(file)) { | ||
| continue; | ||
| } | ||
| try { | ||
| await import(pathToFileURL(resolve(packagesDir, dir, file)).href); | ||
| } catch (error) { | ||
| failures.push({ | ||
| entry: pkg.name + subpath.slice(1), | ||
| error: String(error).split("\n")[0], | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log(JSON.stringify(failures)); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| // Exports a document with React and optional blocks through | ||
| // `ServerBlockNoteEditor`, in plain Node from the build output, as a server | ||
| // app does. | ||
| import { readFileSync } from "node:fs"; | ||
| import { createRequire } from "node:module"; | ||
| import { join, resolve } from "node:path"; | ||
| import { pathToFileURL } from "node:url"; | ||
|
|
||
| const packagesDir = resolve(import.meta.dirname, "../../../../packages"); | ||
|
|
||
| function entry(dir) { | ||
| const pkg = JSON.parse( | ||
| readFileSync(join(packagesDir, dir, "package.json"), "utf8"), | ||
| ); | ||
| return pathToFileURL(resolve(packagesDir, dir, pkg.exports["."].import)).href; | ||
| } | ||
|
|
||
| const { BlockNoteSchema } = await import(entry("core")); | ||
| const { createReactBlockSpec } = await import(entry("react")); | ||
| const { ServerBlockNoteEditor } = await import(entry("server-util")); | ||
| const { createReactInlineMathSpec, createReactMathBlockSpec } = await import( | ||
| entry("math-block") | ||
| ); | ||
| const { createReactDiagramBlockSpec } = await import(entry("diagram-block")); | ||
| const { withMultiColumn } = await import(entry("xl-multi-column")); | ||
| // The React that `@blocknote/react` uses, so that its hooks work. | ||
| const { createElement, useEffect } = createRequire( | ||
| join(packagesDir, "react/package.json"), | ||
| )("react"); | ||
|
|
||
| const reactBlock = createReactBlockSpec( | ||
| { type: "reactBlock", propSchema: {}, content: "inline" }, | ||
| { | ||
| render: (props) => { | ||
| useEffect(() => {}); | ||
| return createElement("p", { ref: props.contentRef }); | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| const schema = withMultiColumn(BlockNoteSchema.create()).extend({ | ||
| blockSpecs: { | ||
| mathBlock: createReactMathBlockSpec(), | ||
| diagram: createReactDiagramBlockSpec(), | ||
| reactBlock: reactBlock(), | ||
| }, | ||
| inlineContentSpecs: { math: createReactInlineMathSpec() }, | ||
| }); | ||
|
|
||
| const blocks = [ | ||
| { | ||
| type: "paragraph", | ||
| content: ["Inline ", { type: "math", content: "x^2" }, " math"], | ||
| }, | ||
| { type: "mathBlock", content: "a^2 + b^2 = c^2" }, | ||
| { type: "diagram", content: "graph TD\n A --> B" }, | ||
| { type: "reactBlock", content: "React block" }, | ||
| { | ||
| type: "columnList", | ||
| children: [ | ||
| { type: "column", children: [{ type: "paragraph", content: "Left" }] }, | ||
| { type: "column", children: [{ type: "paragraph", content: "Right" }] }, | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| const editor = ServerBlockNoteEditor.create({ schema }); | ||
| await editor.blocksToMarkdownLossy(blocks); | ||
| await editor.blocksToHTMLLossy(blocks); | ||
| await editor.blocksToFullHTML(blocks); | ||
| // Node runs React's queued work before it exits. A crash there (#3230) gives a | ||
| // non-zero exit code, which fails the test. | ||
| console.log("exported"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: TypeCellOS/BlockNote
Length of output: 158
🏁 Script executed:
Repository: TypeCellOS/BlockNote
Length of output: 4240
Handle string root exports before iterating entries.
If a published package declares
"exports": "./dist/index.js",Object.entries(pkg.exports)iterates the string's characters. The loader can then skip every character and report success without importing the entry. Normalize a string export to a"."entry before iterating.🐛 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents