@malloydata/render 0.0.202-dev241013162457 → 0.0.202-dev241018164632

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.
@@ -1,6 +1,10 @@
1
1
  import {join, dirname} from 'path';
2
2
  import {mergeConfig, InlineConfig} from 'vite';
3
3
  import {StorybookConfig} from '@storybook/html-vite';
4
+ import {
5
+ malloyStoriesIndexer,
6
+ viteMalloyStoriesPlugin,
7
+ } from './malloy-stories-indexer';
4
8
 
5
9
  /**
6
10
  * This function is used to resolve the absolute path of a package.
@@ -13,21 +17,28 @@ function getAbsolutePath(value) {
13
17
  const config: StorybookConfig = {
14
18
  'stories': [
15
19
  '../src/stories/*.mdx',
20
+ '../src/**/*.stories.malloy',
16
21
  '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
17
22
  ],
23
+ experimental_indexers: async existingIndexers => [
24
+ ...(existingIndexers ?? []),
25
+ malloyStoriesIndexer,
26
+ ],
18
27
  staticDirs: ['../src/stories/static'],
19
28
  'addons': [
20
29
  getAbsolutePath('@storybook/addon-links'),
21
30
  getAbsolutePath('@storybook/addon-essentials'),
22
31
  getAbsolutePath('@storybook/addon-interactions'),
23
32
  ],
33
+ core: {
34
+ builder: '@storybook/builder-vite',
35
+ disableTelemetry: true,
36
+ },
24
37
  'framework': {
25
38
  'name': '@storybook/html-vite',
26
39
  'options': {},
27
40
  },
28
- 'docs': {
29
- 'autodocs': 'tag',
30
- },
41
+ 'docs': {},
31
42
  async viteFinal(config, {configType}) {
32
43
  if (configType === 'DEVELOPMENT') {
33
44
  // Your development configuration goes here
@@ -54,6 +65,7 @@ const config: StorybookConfig = {
54
65
  'process.env': {},
55
66
  },
56
67
  assetsInclude: ['/sb-preview/runtime.js'],
68
+ plugins: [viteMalloyStoriesPlugin()],
57
69
  };
58
70
  const finalConfig = mergeConfig(config, configOverride);
59
71
  return finalConfig;
@@ -0,0 +1,172 @@
1
+ import path from 'path';
2
+ import {PluginOption} from 'vite';
3
+ import type {IndexInput, Indexer} from '@storybook/types';
4
+ import fs from 'fs';
5
+ import {DuckDBConnection} from '@malloydata/db-duckdb';
6
+ import {Model, SingleConnectionRuntime, URLReader} from '@malloydata/malloy';
7
+
8
+ const STORY_MODEL_PREFIX = /##\(story\)\s/;
9
+ const STORY_PREFIX = /#\(story\)\s/;
10
+
11
+ async function createConnection() {
12
+ // TODO: figure out how to get duckdb.table to load based on taht path, rather than from workingDirectory. so that malloy story files can be anywhere
13
+ const workingDirectory = path.join(__dirname, '../src/stories');
14
+ const connection = new DuckDBConnection(
15
+ 'duckdb',
16
+ undefined,
17
+ workingDirectory,
18
+ {
19
+ rowLimit: 1000,
20
+ }
21
+ );
22
+ await connection.connecting;
23
+ return connection;
24
+ }
25
+
26
+ async function getMaterializedModel(fileName: string) {
27
+ const connection = await createConnection();
28
+ const modelCode = fs.readFileSync(fileName, 'utf-8');
29
+ const runtime = new SingleConnectionRuntime(connection);
30
+ const model = runtime.loadModel(modelCode);
31
+
32
+ const materializedModel = await model.getModel();
33
+ return materializedModel;
34
+ }
35
+
36
+ type ModelIndexInput = IndexInput & {
37
+ sourceName: string;
38
+ queryName: string;
39
+ };
40
+
41
+ function fileNameToComponentName(fileName: string) {
42
+ const regex = /\/([^\/]+)\.stories\.malloy$/;
43
+ const match = fileName.match(regex);
44
+ const name = match && match[1];
45
+ if (name) {
46
+ return name
47
+ .split(/[-_]/)
48
+ .map(part => {
49
+ return part.charAt(0).toUpperCase() + part.slice(1);
50
+ })
51
+ .join(' ');
52
+ }
53
+ }
54
+
55
+ function getModelStories(materializedModel: Model, fileName: string) {
56
+ const models = materializedModel.explores;
57
+ let modelStories: ModelIndexInput[] = [];
58
+
59
+ const isLegacy = materializedModel.tagParse().tag.has('renderer_legacy');
60
+ const componentName =
61
+ materializedModel
62
+ .tagParse({prefix: STORY_MODEL_PREFIX})
63
+ .tag.text('component') ?? fileNameToComponentName(fileName);
64
+
65
+ models.forEach(model => {
66
+ model.allFields
67
+ .filter(f => f.isQueryField() && f.getTaglines(STORY_PREFIX).length > 0)
68
+ .forEach(query => {
69
+ modelStories.push({
70
+ type: 'story',
71
+ importPath: fileName,
72
+ title: `Malloy ${isLegacy ? 'Legacy' : 'Next'}/${componentName}`,
73
+ exportName: query.name,
74
+ name: query.tagParse({prefix: STORY_PREFIX}).tag.text('story'),
75
+ sourceName: model.name,
76
+ queryName: query.name,
77
+ });
78
+ });
79
+ });
80
+ return {
81
+ componentName,
82
+ isLegacy,
83
+ stories: modelStories,
84
+ };
85
+ }
86
+
87
+ export const malloyStoriesIndexer: Indexer = {
88
+ test: /stories\.malloy$/,
89
+ createIndex: async fileName => {
90
+ const connection = await createConnection();
91
+ const modelCode = fs.readFileSync(fileName, 'utf-8');
92
+ const urlReader: URLReader = {
93
+ async readURL(url) {
94
+ const pathToUrl = path.join(fileName, '..', url.href);
95
+ const contents = fs.readFileSync(pathToUrl, 'utf-8');
96
+ return contents;
97
+ },
98
+ };
99
+ const runtime = new SingleConnectionRuntime(urlReader, connection);
100
+ const model = runtime.loadModel(modelCode);
101
+ const materializedModel = await model.getModel();
102
+ const modelStories = getModelStories(materializedModel, fileName).stories;
103
+ return modelStories;
104
+ },
105
+ };
106
+
107
+ export function viteMalloyStoriesPlugin(): PluginOption {
108
+ return {
109
+ name: 'vite-plugin-storybook-malloy-stories',
110
+ async transform(code, id) {
111
+ if (id.endsWith('.stories.malloy')) {
112
+ const model = await getMaterializedModel(id);
113
+ const modelStoriesMeta = getModelStories(model, id);
114
+
115
+ const storyExports = modelStoriesMeta.stories
116
+ .map(
117
+ meta => `
118
+ export const ${meta.exportName} = {
119
+ args: {
120
+ source: '${meta.sourceName}',
121
+ view: '${meta.queryName}',
122
+ },
123
+ };
124
+ `
125
+ )
126
+ .join('\n');
127
+
128
+ const header = modelStoriesMeta.isLegacy
129
+ ? `
130
+ import script from '${id}?raw';
131
+ import {renderMalloyLegacy} from './util';
132
+
133
+ export default {
134
+ title: 'Malloy Legacy/Basic',
135
+ render: ({source, view}, {globals: {getConnection}}) => {
136
+ return renderMalloyLegacy({script, source, view, connection: getConnection()});
137
+ },
138
+ argTypes: {},
139
+ };`
140
+ : `
141
+ import script from '${id}?raw';
142
+ import {createLoader} from './util';
143
+ import './themes.css';
144
+ import '../component/render-webcomponent';
145
+
146
+ const meta = {
147
+ title: "Malloy Next/${modelStoriesMeta.componentName}",
148
+ render: ({classes}, context) => {
149
+ const parent = document.createElement('div');
150
+ parent.style.height = 'calc(100vh - 40px)';
151
+ parent.style.position = 'relative';
152
+ const el = document.createElement('malloy-render');
153
+ if (classes) el.classList.add(classes);
154
+ el.result = context.loaded['result'];
155
+ parent.appendChild(el);
156
+ return parent;
157
+ },
158
+ loaders: [createLoader(script)],
159
+ argTypes: {},
160
+ };
161
+ export default meta;
162
+ `;
163
+
164
+ const generatedCode = `
165
+ ${header}
166
+ ${storyExports}
167
+ `;
168
+ return generatedCode;
169
+ }
170
+ },
171
+ };
172
+ }
@@ -3,7 +3,9 @@ import {Preview} from '@storybook/html';
3
3
  import registeredData from './registered_data.json';
4
4
  import theme from './theme';
5
5
 
6
+ let memoConnection: DuckDBWASMConnection | null = null;
6
7
  async function createConnection() {
8
+ if (memoConnection) return memoConnection;
7
9
  const connection = new DuckDBWASMConnection('duckdb', null, undefined, {
8
10
  rowLimit: 1000,
9
11
  });
@@ -15,12 +17,15 @@ async function createConnection() {
15
17
  new window.URL(fullTableName, window.location.href).toString()
16
18
  );
17
19
  }
20
+ memoConnection = connection;
18
21
  return connection;
19
22
  }
20
23
 
21
24
  const preview: Preview = {
22
25
  parameters: {
23
- actions: {argTypesRegex: '^on[A-Z].*'},
26
+ actions: {
27
+ // argTypesRegex: '^on[A-Z].*'
28
+ },
24
29
  controls: {
25
30
  matchers: {
26
31
  color: /(background|color)$/i,
@@ -31,8 +36,10 @@ const preview: Preview = {
31
36
  theme,
32
37
  },
33
38
  },
34
- globals: {
35
- connection: createConnection(),
39
+ initialGlobals: {
40
+ // Doing this with a lazy callback in the context because otherwise
41
+ // Storybook was providing an empty Promise on first render when trying to directly createConnection here
42
+ getConnection: () => createConnection(),
36
43
  },
37
44
  };
38
45