Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/true-foxes-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-hive/laboratory': patch
'@graphql-hive/render-laboratory': patch
---

Hive Laboratory renders Hive Router query plan if included in response extensions
24 changes: 24 additions & 0 deletions .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ jobs:
uploadJavaScriptArtifacts: true
secrets: inherit

laboratory-static-preview:
name: laboratory static preview
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: setup environment
uses: ./.github/actions/setup
with:
actor: laboratory-static-preview
codegen: false

- name: build static laboratory preview
run: pnpm --filter @graphql-hive/laboratory build:static-preview

- name: upload static laboratory preview artifact
uses: actions/upload-artifact@v7.0.0
with:
name: laboratory-static-preview
path: packages/libraries/laboratory/dist/hive-laboratory.static-preview.html
if-no-files-found: error
archive: false

# Run db migrations tests
db-migration-tests:
name: test
Expand Down
10 changes: 5 additions & 5 deletions packages/libraries/laboratory/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/laboratory/components",
"utils": "@/laboratory/lib/utils",
"ui": "@/laboratory/components/ui",
"lib": "@/laboratory/lib",
"hooks": "@/laboratory/hooks"
"components": "src/components",
"utils": "src/lib/utils",
"ui": "src/components/ui",
"lib": "src/lib",
"hooks": "src/hooks"
},
"registries": {}
}
4 changes: 3 additions & 1 deletion packages/libraries/laboratory/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
],
"scripts": {
"build": "vite build --config vite.lib.config.ts && vite build --config vite.umd.config.ts",
"build:static-preview": "pnpm run build && node scripts/build-static-preview.mjs",
"dev": "vite",
"dev:electron": "VITE_TARGET=electron concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
"lint": "eslint .",
Expand All @@ -38,11 +39,12 @@
"zod": "^4.1.12"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"radix-ui": "^1.4.3",
"uuid": "^13.0.0"
},
"devDependencies": {
"@dagrejs/dagre": "^1.1.8",
"@dagrejs/dagre": "^2.0.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
Expand Down
120 changes: 120 additions & 0 deletions packages/libraries/laboratory/scripts/build-static-preview.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const packageDirectory = path.resolve(scriptDirectory, '..');
const distDirectory = path.resolve(packageDirectory, 'dist');

const resolveDistPath = relativePath => path.resolve(distDirectory, relativePath);

const readRequiredText = relativePath => fs.readFile(resolveDistPath(relativePath), 'utf8');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The readRequiredText function is designed to fetch essential files for the static preview. However, it currently lacks error handling. If any of these files are missing from the dist directory, the script will fail abruptly without a clear message indicating which file caused the issue. Implementing a try-catch block would make the script more resilient and provide better diagnostic information in case of missing build artifacts.

const readRequiredText = async relativePath => {
  try {
    return await fs.readFile(resolveDistPath(relativePath), 'utf8');
  } catch (error) {
    console.error(`Error reading required file ${relativePath}:`, error);
    process.exit(1); // Exit with an error code
  }
};


const readOptionalText = async relativePath => {
try {
return await fs.readFile(resolveDistPath(relativePath), 'utf8');
} catch {
return '';
}
};

const [
bundleSource,
cssSource,
editorWorkerSource,
graphqlWorkerSource,
jsonWorkerSource,
tsWorkerSource,
] = await Promise.all([
readRequiredText('hive-laboratory.umd.js'),
readOptionalText('laboratory.css'),
readRequiredText('monacoeditorwork/editor.worker.bundle.js'),
readRequiredText('monacoeditorwork/graphql.worker.bundle.js'),
readRequiredText('monacoeditorwork/json.worker.bundle.js'),
readRequiredText('monacoeditorwork/ts.worker.bundle.js'),
]);

const serializeForScriptTag = value =>
JSON.stringify(value)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');

const payload = {
cssSource,
bundleSource,
workers: {
editorWorkerService: editorWorkerSource,
graphql: graphqlWorkerSource,
json: jsonWorkerSource,
typescript: tsWorkerSource,
},
};

const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Hive Laboratory Static Preview</title>
<style>
html,
body,
#root {
height: 100%;
}

body {
margin: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script>
const payload = ${serializeForScriptTag(payload)};

const createWorkerUrl = workerContent => {
const blob = new Blob([workerContent], { type: 'application/javascript' });
return URL.createObjectURL(blob);
};

const workerUrls = {
editorWorkerService: createWorkerUrl(payload.workers.editorWorkerService),
typescript: createWorkerUrl(payload.workers.typescript),
json: createWorkerUrl(payload.workers.json),
graphql: createWorkerUrl(payload.workers.graphql),
};

globalThis.MonacoEnvironment = {
globalAPI: false,
getWorkerUrl(_moduleId, label) {
return workerUrls[label] || workerUrls.editorWorkerService;
},
};

if (payload.cssSource) {
const style = document.createElement('style');
style.textContent = payload.cssSource;
document.head.appendChild(style);
}

new Function(payload.bundleSource)();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using new Function(payload.bundleSource)() to execute the main JavaScript bundle, while functional, can introduce minor performance overhead and make debugging more challenging compared to directly embedding the script content within a <script> tag. For a static HTML file, directly inserting the bundle content is often a more straightforward and equally secure approach, assuming the bundleSource is trusted (which it is, coming from a local build).

const script = document.createElement('script');
script.textContent = payload.bundleSource;
document.body.appendChild(script);


const params = new URLSearchParams(globalThis.location.search);
const endpoint = params.get('endpoint') || globalThis.localStorage.getItem('hive-laboratory:endpoint');

globalThis.HiveLaboratory.renderLaboratory(document.getElementById('root'), {
defaultEndpoint: endpoint || globalThis.location.origin + '/graphql',
});
</script>
</body>
</html>
`;

const outputPath = resolveDistPath('hive-laboratory.static-preview.html');
await fs.writeFile(outputPath, html, 'utf8');

console.log(`Created static preview: ${outputPath}`);
Loading
Loading