@cognite/dune 0.3.3 → 0.3.5

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.
@@ -0,0 +1,136 @@
1
+ ---
2
+ to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>AGENTS.md'
3
+ ---
4
+ # Coding Standards
5
+
6
+ ## 1. Dependency Injection
7
+
8
+ Inject dependencies via React context (hooks/components) or factory-override pattern (plain functions). Never hard-code dependencies.
9
+
10
+ **React context**
11
+ ```typescript
12
+ const defaultDeps = { useDataSource, useAnalytics };
13
+ export type MyHookContextType = typeof defaultDeps;
14
+ export const MyHookContext = createContext<MyHookContextType>(defaultDeps);
15
+
16
+ export function useMyHook() {
17
+ const { useDataSource } = useContext(MyHookContext);
18
+ }
19
+ ```
20
+
21
+ **Factory overrides**
22
+ ```typescript
23
+ type Deps = { serviceFactory: () => SomeService };
24
+ const defaultDeps: Deps = { serviceFactory: () => new SomeServiceImpl() };
25
+
26
+ export const doWork = async (props: Props, overrides?: Partial<Deps>) => {
27
+ const { serviceFactory } = { ...defaultDeps, ...overrides };
28
+ };
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 2. Interface-Based Services
34
+
35
+ Define an interface; implement with a class. Never reference the concrete class outside its own file.
36
+
37
+ ```typescript
38
+ export interface DataService {
39
+ load(): Promise<Data>;
40
+ save(data: Data): Promise<void>;
41
+ }
42
+
43
+ export class ApiDataService implements DataService { /* ... */ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ## 3. ViewModel Pattern
49
+
50
+ Business logic lives in `use<Name>ViewModel`. Components only render.
51
+
52
+ ```typescript
53
+ export function useTodoViewModel(): TodoViewModel {
54
+ const { useTodoStorage, addTodoCommand } = useContext(TodoViewModelContext);
55
+ const storage = useTodoStorage();
56
+ const addTodo = useCallback((text: string) => addTodoCommand(text, storage), [storage, addTodoCommand]);
57
+ return { todos: storage.listAllTodos(), addTodo };
58
+ }
59
+
60
+ export const TodoView = () => {
61
+ const { todos, addTodo } = useTodoViewModel();
62
+ return <ul>{todos.map(t => <TodoItem key={t.id} todo={t} onAdd={addTodo} />)}</ul>;
63
+ };
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 4. Test-Driven Development
69
+
70
+ ### File creation order
71
+ 1. Integration tests
72
+ 2. Unit tests
73
+ 3. Source files to make tests pass
74
+
75
+ ### Conventions
76
+ - Files: `*.test.ts(x)` — runner: **Vitest** (`pnpm test` to run all, `vitest run` within a package)
77
+ - Structure: Arrange / Act / Assert (explicit comments when test > ~10 statements)
78
+ - One behavior per test; helper functions at the bottom of the file
79
+ - Prefer context injection over `vi.mock`; always add a comment when `vi.mock` is unavoidable
80
+
81
+ ### Type-safe mocks
82
+ ```typescript
83
+ // Preferred: vi.fn(() => ...) for consistent behavior
84
+ mockContext = { useUserInfo: vi.fn(() => ({ data: mockUser, isFetched: true })) };
85
+
86
+ // For per-test reconfiguration
87
+ mockContext = { useUserInfo: vi.fn() };
88
+ vi.mocked(mockContext.useUserInfo).mockReturnValue({ data: undefined, isFetched: true });
89
+ ```
90
+
91
+ For full interface mocks, use `assert.fail` on methods the unit under test should never call — or better, use narrow interfaces that only expose what is needed.
92
+
93
+ ```typescript
94
+ mockStorage = {
95
+ list: vi.fn(),
96
+ retrieve: vi.fn(() => { assert.fail('Not implemented'); }),
97
+ };
98
+ ```
99
+
100
+ ### React hooks
101
+ ```typescript
102
+ describe(useMyHook.name, () => {
103
+ let mockContext: MyContextType;
104
+ let wrapper: ComponentType<{ children: ReactNode }>;
105
+
106
+ beforeEach(() => {
107
+ mockContext = { useUserInfo: vi.fn(() => ({ data: mockUser })) };
108
+ wrapper = ({ children }) => (
109
+ <MyHookContext.Provider value={mockContext}>{children}</MyHookContext.Provider>
110
+ );
111
+ });
112
+
113
+ it('should ...', async () => {
114
+ const { result } = renderHook(() => useMyHook(), { wrapper });
115
+
116
+ await act(async () => { await result.current.someAction(); });
117
+
118
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
119
+ });
120
+ });
121
+ ```
122
+
123
+ ### Type rules
124
+ - Never use `any` — prefer `unknown` or strong types
125
+ - No `as unknown as T` casts; for partial mocks use `{ ...defaults, ...overrides } as T`
126
+
127
+ ```typescript
128
+ function createMockWindow(overrides: Partial<Window> = {}): Window {
129
+ return { postMessage: vi.fn(), ...overrides } as Window;
130
+ }
131
+ ```
132
+
133
+ - Use direct React type imports: `import type { ComponentType, ReactNode } from 'react'`
134
+
135
+ ### Shared mock data
136
+ Place reusable factories in `src/__mocks__/`. Use `.test` TLD for fake URLs (RFC 2606).
@@ -21,9 +21,10 @@ to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>package.json'
21
21
  "deploy-preview": "dune deploy:interactive"
22
22
  },
23
23
  "dependencies": {
24
- "@cognite/aura": "^0.1.1",
24
+ "@cognite/aura": "^0.1.4",
25
25
  "@cognite/sdk": "^10.3.0",
26
- "@cognite/dune": "^0.2.4",
26
+ "@cognite/dune": "^0.3.5",
27
+ "@tabler/icons-react": "^3.35.0",
27
28
  "@tanstack/react-query": "^5.90.10",
28
29
  "clsx": "^2.1.1",
29
30
  "react": "^19.2.0",
@@ -1,8 +1,7 @@
1
1
  ---
2
2
  to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>src/App.test.tsx'
3
3
  ---
4
- import { useDune } from '@cognite/dune';
5
- import { CogniteClient } from '@cognite/sdk';
4
+ import * as duneAuth from '@cognite/dune';
6
5
  import { render, screen } from '@testing-library/react';
7
6
  import { beforeEach, describe, expect, it, vi } from 'vitest';
8
7
 
@@ -11,20 +10,13 @@ import App from './App';
11
10
  // Mock the @cognite/dune module
12
11
  vi.mock(import('@cognite/dune'));
13
12
 
14
- function mockUseDune(project: string, isLoading: boolean): ReturnType<typeof useDune> {
15
- const sdk = new CogniteClient({
16
- appId: 'dune-template-test',
17
- project,
18
- baseUrl: 'https://api.cognitedata.com',
19
- oidcTokenProvider: async () => '',
20
- });
13
+ type UseDuneResult = ReturnType<typeof duneAuth.useDune>;
21
14
 
22
- const contextValue = {
23
- sdk,
15
+ function mockUseDune(project: string, isLoading: boolean): UseDuneResult {
16
+ return {
17
+ sdk: { project } as Partial<UseDuneResult['sdk']> as UseDuneResult['sdk'],
24
18
  isLoading,
25
- } satisfies ReturnType<typeof useDune>;
26
-
27
- return contextValue;
19
+ };
28
20
  }
29
21
 
30
22
  describe('App', () => {
@@ -33,18 +25,29 @@ describe('App', () => {
33
25
  });
34
26
 
35
27
  it('renders loading state', () => {
36
- vi.mocked(useDune).mockReturnValue(mockUseDune('test-project', true));
28
+ vi.mocked(duneAuth.useDune).mockReturnValue(mockUseDune('test-project', true));
37
29
 
38
30
  render(<App />);
39
31
  expect(screen.getByText('Loading project...')).toBeInTheDocument();
40
32
  });
41
33
 
42
- it('renders app with project name', () => {
43
- vi.mocked(useDune).mockReturnValue(mockUseDune('my-test-project', false));
34
+ it('renders splash with deployment targets and checklist copy', () => {
35
+ vi.mocked(duneAuth.useDune).mockReturnValue(mockUseDune('my-test-project', false));
44
36
 
45
37
  render(<App />);
46
- expect(screen.getByText('Welcome to my-test-project')).toBeInTheDocument();
47
- expect(screen.getByText('Open starter config')).toBeInTheDocument();
48
- expect(screen.getByText('<%= org %> / <%= project %>')).toBeInTheDocument();
38
+ expect(screen.getByText('Welcome to Dune')).toBeInTheDocument();
39
+ expect(screen.getByText('App deployment checklist')).toBeInTheDocument();
40
+ expect(screen.getByText('Plan')).toBeInTheDocument();
41
+ expect(screen.getByText('Explore')).toBeInTheDocument();
42
+ expect(screen.getByText('Deploy')).toBeInTheDocument();
43
+ expect(screen.getByText('Support')).toBeInTheDocument();
44
+ expect(screen.getByText('Help & feedback')).toBeInTheDocument();
45
+ expect(screen.getByText('Your app will deploy to')).toBeInTheDocument();
46
+ expect(screen.getByText('org')).toBeInTheDocument();
47
+ expect(screen.getByText('and project')).toBeInTheDocument();
48
+ expect(screen.getByText('<%= org %>')).toBeInTheDocument();
49
+ expect(screen.getByText('<%= project %>')).toBeInTheDocument();
50
+ expect(screen.getByText(/PRD\.md/)).toBeInTheDocument();
51
+ expect(screen.getByText(/deploy:interactive/)).toBeInTheDocument();
49
52
  });
50
53
  });
@@ -2,48 +2,80 @@
2
2
  to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>src/App.tsx'
3
3
  ---
4
4
  import {
5
+ Alert,
6
+ AlertDescription,
5
7
  Badge,
6
- CodeBlock,
7
- Dialog,
8
- DialogContent,
9
- DialogDescription,
10
- DialogFooter,
11
- DialogHeader,
12
- DialogTitle,
13
- DialogTrigger,
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ Collapsible,
14
+ CollapsibleContent,
15
+ CollapsibleTrigger,
14
16
  Loader,
15
17
  Separator,
16
- buttonVariants,
17
18
  } from '@cognite/aura/components';
18
19
  import { useDune } from '@cognite/dune';
20
+ import { IconCaretUpDown, IconRocket } from '@tabler/icons-react';
21
+
19
22
  import appConfig from '../app.json';
20
23
 
21
- const deploymentChecklist = [
22
- 'Confirm org, project, and cluster in app.json.',
23
- 'Replace the starter workflow with your first real query.',
24
- 'Deploy with dune deploy:interactive when the shell is ready.',
25
- ];
24
+ const DUNE_DOCUMENTATION_HREF = 'https://laughing-adventure-r6kwpyy.pages.github.io/';
26
25
 
27
- function getCluster(baseUrl?: string) {
28
- return baseUrl?.replace(/^https?:\/\//, '').replace(/\.cognitedata\.com$/, '') ?? '';
29
- }
26
+ const INTRO_COPY =
27
+ "Build and deploy React apps to Cognite Data Fusion in minutes. Aura, Cognite's AI-native design system, comes pre-configured so your app looks and feels at home from day one. Follow the checklist below to get started.";
28
+
29
+ const CHECKLIST_STEPS = [
30
+ {
31
+ label: 'Plan',
32
+ badge: 'Step 1',
33
+ body: (
34
+ <>
35
+ Describe what you want to build in Cursor and ask the agent to collaborate on a PRD. It will generate a
36
+ structured template in PRD.md, which you can refine in plan mode with as much detail as needed. Keep it simple
37
+ and clear, then move on to building when ready.
38
+ </>
39
+ ),
40
+ },
41
+ {
42
+ label: 'Explore',
43
+ badge: 'Step 2',
44
+ body: (
45
+ <>
46
+ Ask Cursor to review and understand your data model, then answer any follow-up questions it raises. Continue
47
+ refining the app by providing additional input as needed.
48
+ </>
49
+ ),
50
+ },
51
+ {
52
+ label: 'Deploy',
53
+ badge: 'Step 3',
54
+ body: (
55
+ <>
56
+ When ready to deploy, run <code>npx @cognite/dune deploy:interactive</code> in the terminal. Your app will
57
+ appear in the Fusion portal under Custom apps. Run the command again to redeploy new changes.
58
+ </>
59
+ ),
60
+ },
61
+ ] as const;
30
62
 
31
63
  function App() {
32
64
  const { sdk, isLoading } = useDune();
33
65
 
34
66
  if (isLoading) {
35
67
  return (
36
- <main className="min-h-screen bg-[linear-gradient(145deg,var(--background)_0%,var(--mountain-50)_55%,var(--mountain-100)_100%)] text-foreground">
37
- <section className="mx-auto grid min-h-screen w-full max-w-[58rem] place-content-center p-4 sm:p-8">
38
- <div
39
- className="mx-auto grid w-full max-w-sm justify-items-center gap-6 rounded-3xl border border-border bg-card p-5 shadow-xl backdrop-blur-lg sm:p-8"
40
- aria-label="Loading project"
41
- aria-live="polite"
42
- >
43
- <div className="inline-flex items-center gap-3 text-muted-foreground">
44
- <Loader size={20} />
45
- <span>Loading project...</span>
46
- </div>
68
+ <main className="min-h-screen bg-muted/50 text-foreground">
69
+ <section className="mx-auto flex min-h-screen w-full max-w-lg flex-col justify-center p-4 sm:p-8">
70
+ <div className="mx-auto w-full max-w-sm">
71
+ <Card aria-label="Loading project" aria-live="polite">
72
+ <CardContent>
73
+ <div className="inline-flex items-center gap-3 text-muted-foreground">
74
+ <Loader size={20} />
75
+ <span>Loading project...</span>
76
+ </div>
77
+ </CardContent>
78
+ </Card>
47
79
  </div>
48
80
  </section>
49
81
  </main>
@@ -51,80 +83,103 @@ function App() {
51
83
  }
52
84
 
53
85
  const deployment = appConfig.deployments?.[0];
54
- const deploymentLabel = [deployment?.org, deployment?.project ?? sdk.project].filter(Boolean).join(' / ');
55
- const starterConfigPreview = JSON.stringify(
56
- {
57
- org: deployment?.org ?? '',
58
- project: deployment?.project ?? sdk.project,
59
- cluster: getCluster(deployment?.baseUrl),
60
- },
61
- null,
62
- 2
63
- );
86
+ const orgLabel = deployment?.org ?? '';
87
+ const projectLabel = deployment?.project ?? sdk.project;
64
88
 
65
89
  return (
66
- <main className="min-h-screen bg-[linear-gradient(145deg,var(--background)_0%,var(--mountain-50)_55%,var(--mountain-100)_100%)] text-foreground">
67
- <section className="mx-auto grid min-h-screen w-full max-w-[58rem] place-content-center p-4 sm:p-8">
68
- <div className="grid gap-6 rounded-3xl border border-border bg-card p-5 shadow-xl backdrop-blur-lg sm:p-8">
69
- <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
70
- <div>
71
- <h1 className="text-[clamp(2rem,4vw,3rem)] leading-none font-semibold tracking-[-0.04em]">
72
- Welcome to {sdk.project}
73
- </h1>
74
- <div className="mt-3">
75
- <Badge variant="fjord" background>
76
- {deploymentLabel}
77
- </Badge>
78
- </div>
79
- </div>
80
- <Dialog>
81
- <DialogTrigger className={buttonVariants()}>Open starter config</DialogTrigger>
82
- <DialogContent className="max-w-xl">
83
- <DialogHeader>
84
- <DialogTitle>Starter config</DialogTitle>
85
- <DialogDescription>Current starter values as JSON.</DialogDescription>
86
- </DialogHeader>
87
- <div className="grid gap-2">
88
- <div className="overflow-hidden rounded-2xl border border-border bg-background">
89
- <CodeBlock code={starterConfigPreview} language="json" />
90
- </div>
91
- </div>
92
- <DialogFooter>
93
- <Badge variant="fjord" background>{sdk.project}</Badge>
94
- </DialogFooter>
95
- </DialogContent>
96
- </Dialog>
97
- </div>
90
+ <main className="min-h-screen bg-muted/50 text-foreground">
91
+ <section className="mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center p-4 sm:p-8">
92
+ <Card>
93
+ <div className="p-15 gap-16">
94
+ <CardHeader>
95
+ <CardTitle as="h1" className="text-4xl">Welcome to Dune</CardTitle>
96
+ <CardDescription className="text-xl">{INTRO_COPY}</CardDescription>
97
+ </CardHeader>
98
98
 
99
- <Separator />
99
+ <CardContent>
100
+ <Separator />
100
101
 
101
- <div className="grid gap-4">
102
- <div id="deployment-checklist" className="rounded-2xl border border-border bg-muted p-4">
103
- <div className="flex items-center justify-between gap-3">
104
- <div>
105
- <p className="text-sm font-semibold">Deployment checklist</p>
106
- <p className="mt-1 text-sm text-muted-foreground">Use this to turn the starter into a deployable app.</p>
102
+ <div className="flex flex-col gap-6 pt-16">
103
+ <div className="flex items-center gap-2 pt-4">
104
+ <IconRocket aria-hidden />
105
+ <span className="text-2xl font-medium">App deployment checklist</span>
107
106
  </div>
108
- <Badge variant="fjord" background>
109
- 3 steps
110
- </Badge>
111
- </div>
112
- <div className="mt-4 grid gap-2">
113
- {deploymentChecklist.map((item, index) => (
114
- <div
115
- key={item}
116
- className="grid grid-cols-[auto_1fr] items-start gap-3 rounded-xl border border-border bg-card px-3 py-2"
117
- >
118
- <span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-foreground text-xs font-semibold text-background">
119
- {index + 1}
120
- </span>
121
- <p className="text-sm text-card-foreground">{item}</p>
122
- </div>
123
- ))}
107
+
108
+ <div className="flex flex-col gap-4 px-4">
109
+ {CHECKLIST_STEPS.map((step, index) => (
110
+ <Collapsible key={step.label} defaultOpen={index === 0}>
111
+ <CollapsibleTrigger className="w-full">
112
+ <div className="flex w-full min-w-0 items-center justify-between gap-3 text-left">
113
+ <span className="text-lg">{step.label}</span>
114
+ <span className="inline-flex shrink-0 items-center gap-2">
115
+ <Badge variant="mountain" background>
116
+ {step.badge}
117
+ </Badge>
118
+ <IconCaretUpDown aria-hidden className="size-4 text-muted-foreground" />
119
+ </span>
120
+ </div>
121
+ </CollapsibleTrigger>
122
+ <CollapsibleContent className="py-2">
123
+ {step.body}
124
+ </CollapsibleContent>
125
+ </Collapsible>
126
+ ))}
127
+ </div>
128
+
129
+ <Alert className="bg-mountain-50 mb-10">
130
+ <AlertDescription>
131
+ <div className="flex flex-wrap items-center gap-2 text-lg">
132
+ <span>Your app will deploy to</span>
133
+ {orgLabel ? (
134
+ <>
135
+ <span>org</span>
136
+ <Badge variant="nordic" background>
137
+ {orgLabel}
138
+ </Badge>
139
+ <span>and project</span>
140
+ </>
141
+ ) : (
142
+ <span>project</span>
143
+ )}
144
+ <Badge variant="nordic" background>
145
+ {projectLabel}
146
+ </Badge>
147
+ </div>
148
+ </AlertDescription>
149
+ </Alert>
150
+
151
+ <Collapsible>
152
+ <CollapsibleTrigger className="w-full">
153
+ <div className="flex w-full min-w-0 items-center justify-between gap-3 text-left">
154
+ <span className="text-lg">Support</span>
155
+ <span className="inline-flex shrink-0 items-center gap-2">
156
+ <Badge variant="mountain">
157
+ Help & feedback
158
+ </Badge>
159
+ <IconCaretUpDown aria-hidden className="size-4 text-muted-foreground" />
160
+ </span>
161
+ </div>
162
+ </CollapsibleTrigger>
163
+ <CollapsibleContent className="py-2">
164
+ <p>
165
+ For additional support and feedback, please head to{' '}
166
+ <a
167
+ href={DUNE_DOCUMENTATION_HREF}
168
+ rel="noreferrer"
169
+ style={{ color: '#486AED' }}
170
+ target="_blank"
171
+ >
172
+ Dune documentation
173
+ </a>{' '}
174
+ or the{' '}
175
+ <span style={{ color: '#486AED' }}>#Dune Slack channel</span>.
176
+ </p>
177
+ </CollapsibleContent>
178
+ </Collapsible>
124
179
  </div>
125
- </div>
180
+ </CardContent>
126
181
  </div>
127
- </div>
182
+ </Card>
128
183
  </section>
129
184
  </main>
130
185
  );
@@ -1,8 +1,9 @@
1
1
  ---
2
2
  to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>src/styles.css'
3
3
  ---
4
- @import "@cognite/aura/styles.css";
5
- @import "tailwindcss";
4
+ @import '@cognite/aura/styles.source.css';
5
+
6
+ @source '../node_modules/@cognite/aura/dist/components';
6
7
 
7
8
  :root {
8
9
  --font-inter: "Inter", ui-sans-serif, system-ui, sans-serif;
package/bin/cli.js CHANGED
@@ -1,8 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { symlinkSync } from 'node:fs';
3
4
  import { basename, dirname, normalize, resolve } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
 
7
+ /**
8
+ * Creates a CLAUDE.md symlink pointing to AGENTS.md in the given directory.
9
+ * Silently skips if the symlink already exists (EEXIST).
10
+ * @param {string} appDir - Absolute path to the generated app directory
11
+ */
12
+ export function createClaudeMdSymlink(appDir) {
13
+ try {
14
+ symlinkSync('AGENTS.md', resolve(appDir, 'CLAUDE.md'));
15
+ } catch (err) {
16
+ if (err.code !== 'EEXIST') {
17
+ console.warn('⚠️ Could not create CLAUDE.md symlink:', err.message);
18
+ }
19
+ }
20
+ }
21
+
6
22
  import { Logger, runner } from 'hygen';
7
23
 
8
24
  const defaultTemplates = resolve(dirname(fileURLToPath(import.meta.url)), '..', '_templates');
@@ -118,6 +134,9 @@ async function main() {
118
134
  debug: !!process.env.DEBUG,
119
135
  });
120
136
 
137
+ // Create CLAUDE.md symlink -> AGENTS.md for Claude Code support
138
+ createClaudeMdSymlink(isCurrentDir ? process.cwd() : dirName || appName);
139
+
121
140
  // Print success message with next steps
122
141
  const installAndDevSteps = ' pnpm install\n pnpm dev';
123
142
  const deployLabel = 'To deploy your app:';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cognite/dune",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Build and deploy React apps to Cognite Data Fusion",
5
5
  "keywords": ["cognite", "dune", "cdf", "fusion", "react", "scaffold", "deploy"],
6
6
  "license": "Apache-2.0",
@@ -1,11 +0,0 @@
1
- ---
2
- to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>tailwind.config.js'
3
- ---
4
- /** @type {import('tailwindcss').Config} */
5
- export default {
6
- content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
7
- theme: {
8
- extend: {},
9
- },
10
- plugins: [],
11
- };