Skip to content
Merged
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 cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as os from 'node:os'
import * as path from 'node:path'
import {expect} from 'vitest'

import {loadConfig, normalizeConfig} from '../lang/config.ts'
import {isServerRunning, stopGrapheneIfRunning} from './background.ts'

interface RunResult {
Expand Down Expand Up @@ -56,6 +57,11 @@ async function createTelemetryProject(prefix: string) {
}

describe('cli package', () => {
it('derives the project name from package.json with a directory fallback', async () => {
expect((await loadConfig(flightDir, () => {})).projectName).toBe('example-flights')
expect(normalizeConfig({root: '/tmp/project-without-package'}).projectName).toBe('project-without-package')
})

it('directly includes every lang and ui runtime dependency with the exact same spec', async () => {
let cli = JSON.parse(await fsp.readFile(path.resolve(dir, '../cli/package.json'), 'utf8'))
let lang = JSON.parse(await fsp.readFile(path.resolve(dir, '../lang/package.json'), 'utf8'))
Expand Down
5 changes: 4 additions & 1 deletion cli/serve2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,10 @@ function updateWorkspacePlugin(telemetry?: CliTelemetry) {
}
}

return `export default ${JSON.stringify(res)}`
return `
export const projectName = ${JSON.stringify(config.projectName)};
export default ${JSON.stringify(res)}
`
},
configureServer: (s: ViteDevServer) => {
let refresh = async () => {
Expand Down
6 changes: 3 additions & 3 deletions cli/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ describe('cli telemetry', () => {
let env = process.env.GRAPHENE_TELEMETRY_DISABLED
try {
delete process.env.GRAPHENE_TELEMETRY_DISABLED
expect(isTelemetryEnabled({dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root: '/tmp'}, 'https://example.com')).toBe(true)
expect(isTelemetryEnabled({dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root: '/tmp', telemetry: false}, 'https://example.com')).toBe(false)
expect(isTelemetryEnabled({dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root: '/tmp', projectName: 'tmp'}, 'https://example.com')).toBe(true)
expect(isTelemetryEnabled({dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root: '/tmp', projectName: 'tmp', telemetry: false}, 'https://example.com')).toBe(false)
process.env.GRAPHENE_TELEMETRY_DISABLED = '1'
expect(isTelemetryEnabled({dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root: '/tmp'}, 'https://example.com')).toBe(false)
expect(isTelemetryEnabled({dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root: '/tmp', projectName: 'tmp'}, 'https://example.com')).toBe(false)
} finally {
if (env === undefined) delete process.env.GRAPHENE_TELEMETRY_DISABLED
else process.env.GRAPHENE_TELEMETRY_DISABLED = env
Expand Down
2 changes: 1 addition & 1 deletion cli/updateNotifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {Config} from '../lang/config.ts'
import {checkForUpdate, detectPackageManager, getUpgradeCommand, isNewerVersion, isUpdateNotifierEnabled, showCachedUpdateNotice} from './updateNotifier.ts'

function testConfig(root: string, overrides: Partial<Config> = {}): Config {
return {dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root, ...overrides}
return {dialect: 'duckdb', envFile: ['.env'], ignoredFiles: [], root, ...overrides, projectName: overrides.projectName || path.basename(root)}
}

function testStderr() {
Expand Down
19 changes: 12 additions & 7 deletions lang/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'path'

export interface Config {
root: string
projectName: string
dialect: string
defaultNamespace?: string
ignoredFiles: string[]
Expand Down Expand Up @@ -69,7 +70,7 @@ export interface Config {
}
}

export type ConfigInput = Omit<Config, 'root' | 'dialect' | 'ignoredFiles' | 'envFile'> & {
export type ConfigInput = Omit<Config, 'root' | 'projectName' | 'dialect' | 'ignoredFiles' | 'envFile'> & {
root?: string
dialect?: Config['dialect']
ignoredFiles?: Config['ignoredFiles']
Expand All @@ -79,13 +80,15 @@ export type ConfigInput = Omit<Config, 'root' | 'dialect' | 'ignoredFiles' | 'en

export let config: Config = {dialect: 'duckdb', root: ''} as Config

export function setGlobalConfig(cfg: ConfigInput) {
export function setGlobalConfig(cfg: ConfigInput | Config, projectName?: string) {
Object.keys(config).forEach(key => delete config[key])
Object.assign(config, normalizeConfig(cfg))
if ('projectName' in cfg) projectName ||= cfg.projectName
Object.assign(config, normalizeConfig(cfg, process.cwd(), projectName))
}

export function normalizeConfig(input: ConfigInput, defaultRoot = process.cwd()): Config {
export function normalizeConfig(input: ConfigInput, defaultRoot = process.cwd(), projectName?: string): Config {
let cfg = {...input}
let root = path.resolve(cfg.root || defaultRoot)
if (cfg.namespace && !cfg.defaultNamespace) cfg.defaultNamespace = cfg.namespace

let dialect = cfg.dialect || 'duckdb'
Expand All @@ -103,7 +106,8 @@ export function normalizeConfig(input: ConfigInput, defaultRoot = process.cwd())
return {
...cfg,
dialect,
root: path.resolve(cfg.root || defaultRoot),
root,
projectName: projectName || path.basename(root),
port: cfg.port || Number(process.env.GRAPHENE_PORT) || 4000,
ignoredFiles: cfg.ignoredFiles || [],
envFile,
Expand All @@ -121,7 +125,8 @@ export async function loadConfig(dir: string, envLoader: (envFiles: string[]) =>
}

let txt = await readFile(path.join(configDir, 'package.json'), 'utf8')
let graphene = JSON.parse(txt).graphene
let pkgJson = JSON.parse(txt)
let graphene = pkgJson.graphene
if (!graphene || typeof graphene != 'object' || Array.isArray(graphene)) {
throw new Error(`No graphene config found in ${path.join(configDir, 'package.json')}`)
}
Expand All @@ -130,6 +135,6 @@ export async function loadConfig(dir: string, envLoader: (envFiles: string[]) =>
let envFiles = Array.isArray(graphene.envFile) ? graphene.envFile : [graphene.envFile || '.env']
envLoader(envFiles.map(file => path.resolve(configDir, file)))

let cfg = normalizeConfig({...graphene, root: configDir}, configDir)
let cfg = normalizeConfig({...graphene, root: configDir}, configDir, pkgJson.name)
return cfg
}
9 changes: 8 additions & 1 deletion ui/internal/LocalApp.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount, tick} from 'svelte'
import {setErrorFor} from './telemetry.ts'
import navFiles from 'virtual:nav'
import navFiles, {projectName} from 'virtual:nav'
import Sidebar from './Sidebar.svelte'
import SidebarToggle from './SidebarToggle.svelte'
import PageNavGroup from './PageNavGroup.svelte'
Expand All @@ -11,6 +11,7 @@
import StyleGallery from './StyleGallery.svelte'
import QueryCacheStatus from './QueryCacheStatus.svelte'
import {type GrapheneError} from '../../lang/index.js'
import {prettyPrintFilename} from './utils.ts'

// Nav sidebar with HMR support for the virtual file list.
let navData = $state(navFiles)
Expand Down Expand Up @@ -47,6 +48,12 @@
let Page = $state<any>(null)
let pageMeta = $state<any>({})
let blankForTests = $state(pathName == '__ct')
let fileName = pathName.split('/').at(-1) + '.md'
let pageTitle = $derived(pageMeta.title || prettyPrintFilename(fileName))

$effect(() => {
document.title = `${pageTitle} - ${projectName}`
})

onMount(async () => {
try {
Expand Down
13 changes: 3 additions & 10 deletions ui/internal/PageNavGroup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import FileChartColumnIncreasing from '@lucide/svelte/icons/file-chart-column-increasing'
import {SvelteSet, SvelteMap} from 'svelte/reactivity'
import {route} from './router.ts'
import {prettyPrintFilename} from './utils.ts'

let {files = [], onNavigate = undefined, baseRoute = '', projectName = ''} = $props()

Expand Down Expand Up @@ -56,7 +57,7 @@
for (let segment of segments) {
parentPath = parentPath ? `${parentPath}/${segment}` : segment
if (!folderMap.has(parentPath)) {
let folderNode = {type: 'folder', name: segment, label: formatLabel(segment, 'folder'), path: parentPath, children: []}
let folderNode = {type: 'folder', name: segment, label: prettyPrintFilename(segment), path: parentPath, children: []}
folderMap.set(parentPath, folderNode)
parentChildren.push(folderNode)
}
Expand All @@ -72,7 +73,7 @@
parentChildren.push({
type: 'file',
name: fileName,
label: formatLabel(fileName, 'file', titleLookup[fullPath]),
label: prettyPrintFilename(fileName, titleLookup[fullPath]),
path: fullPath,
route: pathToRoute(fullPath),
})
Expand Down Expand Up @@ -107,14 +108,6 @@
return next
}

function formatLabel(value, type, explicitTitle = undefined) {
if (explicitTitle) return explicitTitle
let cleaned = type === 'file' ? value.replace(/\.md$/, '') : value
if (cleaned.toLowerCase() === 'index') return 'Home'
return cleaned.split(/[\s_-]+/).filter(Boolean)
.map(c => c.charAt(0).toUpperCase() + c.slice(1)).join(' ')
}

function pathToRoute(path) {
// A nested index.md resolves to its folder's route (path/to/folder/index.md → /path/to/folder).
let clean = path.replace(/\.md$/, '').replace(/\/index$/, '')
Expand Down
11 changes: 11 additions & 0 deletions ui/internal/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Format filenames consistently for navigation and browser titles.
export function prettyPrintFilename(filename: string, explicitTitle?: string) {
if (explicitTitle) return explicitTitle
let cleaned = filename.replace(/\.md$/, '')
if (cleaned.toLowerCase() === 'index') return 'Home'
return cleaned
.split(/[\s_-]+/)
.filter(Boolean)
.map(component => component.charAt(0).toUpperCase() + component.slice(1))
.join(' ')
}
6 changes: 3 additions & 3 deletions ui/tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const test = base.extend<{browser: Browser; page: Page; sharedPage: Page;
let port = await getAvailablePort()
let viteRoot = path.join(fileURLToPath(import.meta.url), '../../../examples/flights')
process.env.GRAPHENE_PORT = String(port)
setGlobalConfig({port, root: viteRoot})
setGlobalConfig({port, root: viteRoot}, 'example-flights')
let server = await serve2()

function cleanup() {
Expand All @@ -118,7 +118,7 @@ export const test = base.extend<{browser: Browser; page: Page; sharedPage: Page;

await use({
url: (options: Partial<Config> = {}) => {
setGlobalConfig({...options, root: options.root || viteRoot, port} as any)
setGlobalConfig({...options, root: options.root || viteRoot, port} as any, options.projectName || 'example-flights')
onTestFinished(cleanup)
return `http://localhost:${port}`
},
Expand Down Expand Up @@ -221,7 +221,7 @@ export const test = base.extend<{browser: Browser; page: Page; sharedPage: Page;

test.beforeEach(() => {
let root = path.join(fileURLToPath(import.meta.url), '../../../examples/flights')
setGlobalConfig({root})
setGlobalConfig({root}, 'example-flights')
clearSvelteWarnings()
})

Expand Down
2 changes: 1 addition & 1 deletion ui/tests/globalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {setGlobalConfig} from '../../lang/config.ts'

export default async function setup(project: TestProject) {
let viteRoot = path.join(fileURLToPath(import.meta.url), '../../../examples/flights')
setGlobalConfig({root: viteRoot})
setGlobalConfig({root: viteRoot}, 'example-flights')

await fs.rm(path.join(import.meta.dirname, 'results'), {force: true, recursive: true})

Expand Down
2 changes: 2 additions & 0 deletions ui/tests/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ test('loads markdown files', async ({server, page}) => {
server.mockFile('delays.md', '---\ntitle: Delay Deep-Dive\n---\n# Delays')

await page.goto(server.url() + '/')
await expect(page).toHaveTitle('Flight Delay Analysis - example-flights')
await expect(page.getByRole('heading', {level: 1, name: 'Flight Delay Analysis'})).toBeVisible()
let nav = page.getByRole('navigation')
await expect(nav).toBeVisible()
Expand Down Expand Up @@ -77,6 +78,7 @@ test('flights simple stacked bar renders', async ({server, page}) => {

await page.goto(server.url() + '/')
await waitForGrapheneLoad(page)
await expect(page).toHaveTitle('Home - example-flights')
await expect(page).screenshot('flights-simple-stacked-bar')
})

Expand Down
Loading