@lightdash/cli 2.153.0 → 2.154.0

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 +1 @@
1
- {"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"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"}
@@ -274,6 +274,11 @@ const downloadAppsToDir = async (args) => {
274
274
  await (0, appCodeFiles_1.writeFilesToDir)(appDir, (0, scaffolding_1.buildStaticAuthoringFiles)({
275
275
  appName: code.manifest.name,
276
276
  sdkVersion: cliVersion,
277
+ // Downloaded chart types get the viz authoring docs, not
278
+ // the app SDK skills a viz must not use.
279
+ flavor: code.manifest.template === common_1.DATA_APP_VIZ_TEMPLATE
280
+ ? 'chart-type'
281
+ : 'app',
277
282
  }));
278
283
  // Server-provided deps override the scaffold's
279
284
  // template package.json so re-uploads round-trip.
@@ -507,6 +507,28 @@ describe('downloadAppsToDir', () => {
507
507
  });
508
508
  expect(fs.existsSync(path.join(appsDir, 'revenue-explorer', 'lightdash-app.yml'))).toBe(true);
509
509
  });
510
+ it('gives downloaded chart types the chart-type authoring flavor', async () => {
511
+ const appsDir = tmpDir();
512
+ const vizCode = codeFor('my-chart-type');
513
+ vizCode.manifest.template = 'data_app_viz';
514
+ const outcome = await (0, appsDownload_1.downloadAppsToDir)({
515
+ appRefs: ['my-chart-type'],
516
+ projectId: 'project-uuid',
517
+ appsDir,
518
+ takenFolders: new Set(),
519
+ cliVersion: '0.0.0-test',
520
+ fetchApp: async () => vizCode,
521
+ });
522
+ expect(outcome.successCount).toBe(1);
523
+ const skillsDir = path.join(appsDir, 'my-chart-type', '.claude/skills');
524
+ expect(fs.existsSync(path.join(skillsDir, 'developing-chart-types-locally/SKILL.md'))).toBe(true);
525
+ // The app SDK skills would only mislead — a viz must not query.
526
+ expect(fs.existsSync(path.join(skillsDir, 'lightdash-data-app/SKILL.md'))).toBe(false);
527
+ expect(fs.existsSync(path.join(skillsDir, 'developing-data-apps-locally/SKILL.md'))).toBe(false);
528
+ expect(fs
529
+ .readFileSync(path.join(appsDir, 'my-chart-type', 'AGENTS.md'), 'utf8')
530
+ .toString()).toContain('custom chart type');
531
+ });
510
532
  it('skips bundles the skipBundle guard rejects without writing them', async () => {
511
533
  const appsDir = tmpDir();
512
534
  const vizCode = codeFor('my-chart-type');
@@ -1,12 +1,14 @@
1
- import { type DataAppManifest } from '@lightdash/common';
1
+ import { type DataAppManifest, type DataAppVizSchema } from '@lightdash/common';
2
2
  export type CreateAppHandlerOptions = {
3
3
  assumeYes?: boolean;
4
+ chartType?: boolean;
4
5
  description?: string;
5
6
  path?: string;
6
7
  project?: string;
7
8
  slug?: string;
8
9
  verbose: boolean;
9
10
  };
11
+ export declare const CHART_TYPE_STARTER_VIZ_SCHEMA: DataAppVizSchema;
10
12
  export declare const SHADCN_VERSION = "2.3.0";
11
13
  export declare const SHADCN_COMPONENTS: readonly ['button', 'badge', 'card', 'table', 'dialog', 'tabs', 'select', 'input', 'label', 'popover', 'tooltip', 'separator', 'skeleton', 'dropdown-menu', 'sheet', 'scroll-area', 'switch', 'checkbox', 'avatar', 'alert', 'progress', 'resizable'];
12
14
  export declare const resolveLocalAppSlug: (name: string, explicitSlug?: string) => string;
@@ -16,6 +18,7 @@ export declare const buildLocalAppManifest: (args: {
16
18
  projectUuid: string;
17
19
  slug: string;
18
20
  now: Date;
21
+ chartType?: boolean;
19
22
  }) => DataAppManifest;
20
23
  export declare const createAppHandler: (name: string, options: CreateAppHandlerOptions) => Promise<void>;
21
24
  //# sourceMappingURL=createApp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createApp.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/createApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAOH,KAAK,eAAe,EACvB,MAAM,mBAAmB,CAAC;AAuB3B,MAAM,MAAM,uBAAuB,GAAG;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,eAAO,MAAM,iBAAiB,YAC1B,QAAQ,EACR,OAAO,EACP,MAAM,EACN,OAAO,EACP,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,OAAO,EACP,aAAa,EACb,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,OAAO,EACP,UAAU,EACV,WAAW,CACL,CAAC;AAEX,eAAO,MAAM,mBAAmB,SACtB,MAAM,iBACG,MAAM,KACtB,MAWF,CAAC;AAEF,eAAO,MAAM,qBAAqB,SAAU;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,IAAI,CAAC;CACb,KAAG,eAUF,CAAC;AAiKH,eAAO,MAAM,gBAAgB,SACnB,MAAM,WACH,uBAAuB,KACjC,OAAO,CAAC,IAAI,CA2Gd,CAAC"}
1
+ {"version":3,"file":"createApp.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/createApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAQH,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACxB,MAAM,mBAAmB,CAAC;AAwB3B,MAAM,MAAM,uBAAuB,GAAG;IAClC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAKF,eAAO,MAAM,6BAA6B,EAAE,gBA4B3C,CAAC;AAEF,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,eAAO,MAAM,iBAAiB,YAC1B,QAAQ,EACR,OAAO,EACP,MAAM,EACN,OAAO,EACP,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,EACT,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,OAAO,EACP,aAAa,EACb,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,OAAO,EACP,UAAU,EACV,WAAW,CACL,CAAC;AAEX,eAAO,MAAM,mBAAmB,SACtB,MAAM,iBACG,MAAM,KACtB,MAWF,CAAC;AAEF,eAAO,MAAM,qBAAqB,SAAU;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,IAAI,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;CACvB,KAAG,eAaF,CAAC;AAqLH,eAAO,MAAM,gBAAgB,SACnB,MAAM,WACH,uBAAuB,KACjC,OAAO,CAAC,IAAI,CAoId,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAppHandler = exports.buildLocalAppManifest = exports.resolveLocalAppSlug = exports.SHADCN_COMPONENTS = exports.SHADCN_VERSION = void 0;
3
+ exports.createAppHandler = exports.buildLocalAppManifest = exports.resolveLocalAppSlug = exports.SHADCN_COMPONENTS = exports.SHADCN_VERSION = exports.CHART_TYPE_STARTER_VIZ_SCHEMA = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
6
  const execa_1 = tslib_1.__importDefault(require("execa"));
@@ -17,6 +17,38 @@ const apiClient_1 = require("../dbt/apiClient");
17
17
  const selectProject_1 = require("../selectProject");
18
18
  const appCodeFiles_1 = require("./appCodeFiles");
19
19
  const scaffolding_1 = require("./scaffolding");
20
+ // Declaration matching the starter component in authoring/chart-type/App.jsx —
21
+ // the two must stay in lockstep (every key the component reads from
22
+ // fieldMapping/options is declared here, and everything declared is read).
23
+ exports.CHART_TYPE_STARTER_VIZ_SCHEMA = {
24
+ fields: [
25
+ {
26
+ name: 'category',
27
+ label: 'Category',
28
+ type: 'dimension',
29
+ required: true,
30
+ },
31
+ { name: 'value', label: 'Value', type: 'metric', required: true },
32
+ ],
33
+ configOptions: [
34
+ {
35
+ type: 'boolean',
36
+ name: 'showLabels',
37
+ label: 'Show value labels',
38
+ group: 'Labels',
39
+ default: true,
40
+ },
41
+ {
42
+ type: 'number',
43
+ name: 'maxBars',
44
+ label: 'Max bars',
45
+ default: 10,
46
+ min: 1,
47
+ max: 50,
48
+ },
49
+ ],
50
+ colorPalette: {},
51
+ };
20
52
  exports.SHADCN_VERSION = '2.3.0';
21
53
  exports.SHADCN_COMPONENTS = [
22
54
  'button',
@@ -60,7 +92,10 @@ const buildLocalAppManifest = (args) => ({
60
92
  version: 1,
61
93
  name: args.name,
62
94
  description: args.description,
63
- template: null,
95
+ template: args.chartType ? common_1.DATA_APP_VIZ_TEMPLATE : null,
96
+ // vizSchema round-trips through upload — without it the uploaded chart
97
+ // type never appears in the explorer's chart type picker.
98
+ ...(args.chartType ? { vizSchema: exports.CHART_TYPE_STARTER_VIZ_SCHEMA } : {}),
64
99
  downloadedAt: args.now.toISOString(),
65
100
  scaffoldingVersion: env_1.CLI_VERSION,
66
101
  });
@@ -98,17 +133,19 @@ const assertNpmAvailable = async () => {
98
133
  throw new common_1.ParameterError('npm is required to create a data app because Lightdash generates its shadcn UI locally. Install Node.js (which includes npm), then run this command again.');
99
134
  }
100
135
  };
101
- const confirmLocalPackageTooling = async (assumeYes) => {
136
+ const confirmLocalPackageTooling = async (assumeYes, entityLabel, withShadcn) => {
102
137
  globalState_1.default.log(styles.warning([
103
138
  '⚠ Local package installation',
104
- `Creating a data app downloads third-party packages through npm into the new app folder and runs shadcn@${exports.SHADCN_VERSION} to generate UI files.`,
105
- 'Dependency lifecycle scripts are disabled, but npm and shadcn will access the network and write files on this machine.',
139
+ `Creating a ${entityLabel} downloads third-party packages through npm into the new folder${withShadcn
140
+ ? ` and runs shadcn@${exports.SHADCN_VERSION} to generate UI files`
141
+ : ''}.`,
142
+ `Dependency lifecycle scripts are disabled, but ${withShadcn ? 'npm and shadcn' : 'npm'} will access the network and write files on this machine.`,
106
143
  'Only continue if you trust these package sources and are comfortable running this tooling locally.',
107
144
  ].join('\n')));
108
145
  if (assumeYes)
109
146
  return;
110
147
  if (globalState_1.default.isNonInteractive()) {
111
- throw new common_1.ParameterError('Creating a data app requires approval to download and install npm packages. Rerun with --assume-yes to approve in non-interactive mode.');
148
+ throw new common_1.ParameterError(`Creating a ${entityLabel} requires approval to download and install npm packages. Rerun with --assume-yes to approve in non-interactive mode.`);
112
149
  }
113
150
  const { confirmed } = await inquirer_1.default.prompt([
114
151
  {
@@ -122,24 +159,28 @@ const confirmLocalPackageTooling = async (assumeYes) => {
122
159
  throw new common_1.ParameterError('Data app creation cancelled.');
123
160
  }
124
161
  };
125
- const confirmDependencyInstall = async (directPackages, assumeYes) => {
162
+ const confirmDependencyInstall = async (directPackages, assumeYes, entityLabel, withShadcn) => {
126
163
  globalState_1.default.log([
127
- 'Creating this data app will install these direct npm packages:',
164
+ `Creating this ${entityLabel} will install these direct npm packages:`,
128
165
  ...directPackages.map((packageSpec) => ` - ${packageSpec}`),
129
- `\nIt will also run shadcn@${exports.SHADCN_VERSION} to generate:`,
130
- ` ${exports.SHADCN_COMPONENTS.join(', ')}`,
166
+ ...(withShadcn
167
+ ? [
168
+ `\nIt will also run shadcn@${exports.SHADCN_VERSION} to generate:`,
169
+ ` ${exports.SHADCN_COMPONENTS.join(', ')}`,
170
+ ]
171
+ : []),
131
172
  "\nDependency lifecycle scripts will be disabled. React 19 peer dependencies will use npm's legacy-peer-deps mode.",
132
173
  ].join('\n'));
133
174
  if (assumeYes)
134
175
  return;
135
176
  if (globalState_1.default.isNonInteractive()) {
136
- throw new common_1.ParameterError('Creating a data app requires approval to install npm packages. Rerun with --assume-yes to approve in non-interactive mode.');
177
+ throw new common_1.ParameterError(`Creating a ${entityLabel} requires approval to install npm packages. Rerun with --assume-yes to approve in non-interactive mode.`);
137
178
  }
138
179
  const { confirmed } = await inquirer_1.default.prompt([
139
180
  {
140
181
  type: 'confirm',
141
182
  name: 'confirmed',
142
- message: 'Install these packages and create the app?',
183
+ message: `Install these packages and create the ${entityLabel}?`,
143
184
  default: false,
144
185
  },
145
186
  ]);
@@ -147,7 +188,7 @@ const confirmDependencyInstall = async (directPackages, assumeYes) => {
147
188
  throw new common_1.ParameterError('Data app creation cancelled.');
148
189
  }
149
190
  };
150
- const installDependenciesAndShadcn = async (appDir) => {
191
+ const installDependenciesAndShadcn = async (appDir, withShadcn) => {
151
192
  const npmEnv = {
152
193
  ...process.env,
153
194
  npm_config_ignore_scripts: 'true',
@@ -167,6 +208,10 @@ const installDependenciesAndShadcn = async (appDir) => {
167
208
  stderr: 'inherit',
168
209
  };
169
210
  await (0, execa_1.default)('npm', ['install', '--include=dev', '--ignore-scripts', '--no-package-lock'], subprocessOptions);
211
+ // Chart types are single chart components (recharts/d3/SVG) — the shadcn
212
+ // UI kit would only be dead weight shipped in their published source.
213
+ if (!withShadcn)
214
+ return;
170
215
  await (0, execa_1.default)('npx', ['--yes', `shadcn@${exports.SHADCN_VERSION}`, 'init', '--defaults', '--force'], shadcnOptions);
171
216
  await (0, execa_1.default)('npx', [
172
217
  '--yes',
@@ -181,6 +226,8 @@ const createAppHandler = async (name, options) => {
181
226
  const startTime = Date.now();
182
227
  let success = false;
183
228
  globalState_1.default.setVerbose(options.verbose);
229
+ const isChartType = options.chartType ?? false;
230
+ const entityLabel = isChartType ? 'custom chart type' : 'data app';
184
231
  try {
185
232
  const slug = (0, exports.resolveLocalAppSlug)(name, options.slug);
186
233
  const appDir = path.join((0, contentAsCodePaths_1.getDownloadFolder)(options.path), 'apps', slug);
@@ -209,18 +256,20 @@ const createAppHandler = async (name, options) => {
209
256
  url: `/api/v1/ee/projects/${projectUuid}/apps/authoring-context?slug=${encodeURIComponent(slug)}`,
210
257
  body: undefined,
211
258
  });
212
- await confirmLocalPackageTooling(options.assumeYes ?? false);
259
+ await confirmLocalPackageTooling(options.assumeYes ?? false, entityLabel, !isChartType);
213
260
  const staticFiles = (0, scaffolding_1.buildStaticAuthoringFiles)({
214
261
  appName: name,
215
262
  sdkVersion: env_1.CLI_VERSION,
263
+ flavor: isChartType ? 'chart-type' : 'app',
216
264
  });
217
- await confirmDependencyInstall(parseDirectPackages(staticFiles), options.assumeYes ?? false);
265
+ await confirmDependencyInstall(parseDirectPackages(staticFiles), options.assumeYes ?? false, entityLabel, !isChartType);
218
266
  const manifest = (0, exports.buildLocalAppManifest)({
219
267
  name,
220
268
  description: options.description ?? '',
221
269
  projectUuid,
222
270
  slug,
223
271
  now: new Date(),
272
+ chartType: isChartType,
224
273
  });
225
274
  const appsDir = path.dirname(appDir);
226
275
  await fs_1.promises.mkdir(appsDir, { recursive: true });
@@ -230,30 +279,41 @@ const createAppHandler = async (name, options) => {
230
279
  manifest,
231
280
  files: (0, scaffolding_1.loadVendoredStarterSource)(),
232
281
  });
282
+ if (isChartType) {
283
+ // Swap the app placeholder for the viz starter matching the
284
+ // manifest's CHART_TYPE_STARTER_VIZ_SCHEMA declaration.
285
+ await (0, appCodeFiles_1.writeFilesToDir)(temporaryAppDir, [
286
+ (0, scaffolding_1.loadChartTypeStarterApp)(),
287
+ ]);
288
+ }
233
289
  await (0, appCodeFiles_1.writeFilesToDir)(temporaryAppDir, staticFiles);
234
290
  await (0, appCodeFiles_1.writeContextToDir)(temporaryAppDir, context);
235
- await installDependenciesAndShadcn(temporaryAppDir);
291
+ await installDependenciesAndShadcn(temporaryAppDir, !isChartType);
236
292
  // shadcn rewrites package versions and tailwind colors while it
237
293
  // generates source. Restore the reviewed template declarations so
238
294
  // the approval list stays exact and uploads do not mistake the
239
295
  // generated app for one with custom dependencies.
240
- await (0, appCodeFiles_1.writeFilesToDir)(temporaryAppDir, staticFiles.filter((file) => file.path === 'package.json' ||
241
- file.path === 'tailwind.config.js'));
296
+ if (!isChartType) {
297
+ await (0, appCodeFiles_1.writeFilesToDir)(temporaryAppDir, staticFiles.filter((file) => file.path === 'package.json' ||
298
+ file.path === 'tailwind.config.js'));
299
+ }
242
300
  await fs_1.promises.rename(temporaryAppDir, appDir);
243
301
  }
244
302
  catch (error) {
245
303
  await fs_1.promises.rm(temporaryAppDir, { recursive: true, force: true });
246
304
  throw error;
247
305
  }
248
- globalState_1.default.log(styles.success(`Created data app "${name}" at ${appDir}`));
249
- globalState_1.default.log(`\nNext:\n cd ${JSON.stringify(appDir)}\n npm run build\n lightdash upload --apps ${slug}`);
306
+ globalState_1.default.log(styles.success(`Created ${entityLabel} "${name}" at ${appDir}`));
307
+ globalState_1.default.log(isChartType
308
+ ? `\nNext:\n cd ${JSON.stringify(appDir)}\n lightdash apps validate --build\n lightdash upload --chart-types ${slug}\n\nThen open any explore in Lightdash and pick "${name}" in the chart type picker.`
309
+ : `\nNext:\n cd ${JSON.stringify(appDir)}\n npm run build\n lightdash upload --apps ${slug}`);
250
310
  success = true;
251
311
  }
252
312
  finally {
253
313
  await analytics_1.LightdashAnalytics.track({
254
314
  event: 'command.executed',
255
315
  properties: {
256
- command: 'create-app',
316
+ command: isChartType ? 'create-chart-type' : 'create-app',
257
317
  durationMs: Date.now() - startTime,
258
318
  success,
259
319
  },
@@ -56,6 +56,18 @@ describe('resolveLocalAppSlug', () => {
56
56
  });
57
57
  });
58
58
  describe('buildLocalAppManifest', () => {
59
+ it('marks chart types as data_app_viz with the starter vizSchema', () => {
60
+ const manifest = (0, createApp_1.buildLocalAppManifest)({
61
+ name: 'Radial Gauge',
62
+ description: '',
63
+ projectUuid: PROJECT_UUID,
64
+ slug: 'radial-gauge',
65
+ now: new Date('2026-01-01T00:00:00.000Z'),
66
+ chartType: true,
67
+ });
68
+ expect(manifest.template).toBe('data_app_viz');
69
+ expect(manifest.vizSchema).toEqual(createApp_1.CHART_TYPE_STARTER_VIZ_SCHEMA);
70
+ });
59
71
  it('builds a slug-identified manifest without an app uuid', () => {
60
72
  const manifest = (0, createApp_1.buildLocalAppManifest)({
61
73
  name: 'Revenue Explorer',
@@ -160,7 +172,7 @@ describe('createAppHandler', () => {
160
172
  expect.objectContaining({
161
173
  type: 'confirm',
162
174
  name: 'confirmed',
163
- message: 'Install these packages and create the app?',
175
+ message: 'Install these packages and create the data app?',
164
176
  default: false,
165
177
  }),
166
178
  ]);
@@ -183,6 +195,39 @@ describe('createAppHandler', () => {
183
195
  input: '\u001B[B\n',
184
196
  }));
185
197
  });
198
+ it('creates a custom chart type scaffold with the viz starter and no shadcn', async () => {
199
+ const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
200
+ await (0, createApp_1.createAppHandler)('Radial Gauge', {
201
+ description: 'A gauge with a needle',
202
+ path: root,
203
+ chartType: true,
204
+ verbose: false,
205
+ });
206
+ const appDir = path.join(root, 'apps', 'radial-gauge');
207
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
208
+ expect(bundle.manifest.template).toBe('data_app_viz');
209
+ expect(bundle.manifest.vizSchema).toEqual(createApp_1.CHART_TYPE_STARTER_VIZ_SCHEMA);
210
+ const appSource = await fs_1.promises.readFile(path.join(appDir, 'src/App.jsx'), 'utf8');
211
+ expect(appSource).toContain('useVizContext');
212
+ // No shadcn generation: chart types ship no UI-kit source.
213
+ expect(execaMock).not.toHaveBeenCalledWith('npx', expect.anything(), expect.anything());
214
+ // The viz contract + local workflow skills ship; the app-only skills
215
+ // do not.
216
+ await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/reusable-visualization/SKILL.md'))).resolves.toBeUndefined();
217
+ await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/developing-chart-types-locally/SKILL.md'))).resolves.toBeUndefined();
218
+ await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/lightdash-data-app/SKILL.md'))).rejects.toThrow();
219
+ await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/developing-data-apps-locally/SKILL.md'))).rejects.toThrow();
220
+ expect(await fs_1.promises.readFile(path.join(appDir, 'AGENTS.md'), 'utf8')).toContain('custom chart type');
221
+ expect(await fs_1.promises.readFile(path.join(appDir, 'README.md'), 'utf8')).toContain('Radial Gauge');
222
+ expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining('upload --chart-types radial-gauge'));
223
+ expect(analytics_1.LightdashAnalytics.track).toHaveBeenCalledWith({
224
+ event: 'command.executed',
225
+ properties: expect.objectContaining({
226
+ command: 'create-chart-type',
227
+ success: true,
228
+ }),
229
+ });
230
+ });
186
231
  it('requires npm before requesting app context', async () => {
187
232
  const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
188
233
  execaMock.mockRejectedValueOnce(new Error('npm not found'));
@@ -16,11 +16,21 @@ export declare const rewriteWorkspaceDeps: (packageJson: string, version: string
16
16
  export declare const loadTemplateDependencies: (sdkVersion: string) => Record<string, string>;
17
17
  export declare const loadVendoredBuildScaffold: (sdkVersion: string) => DataAppCodeFile[];
18
18
  export declare const loadVendoredStarterSource: () => DataAppCodeFile[];
19
+ /**
20
+ * The chart-type starter component: replaces the template's src/App.jsx when
21
+ * scaffolding a custom chart type (data_app_viz). The rest of the starter
22
+ * source (main.jsx already mounts VizContextProvider, lib/, css) is shared
23
+ * with data apps.
24
+ */
25
+ export declare const loadChartTypeStarterApp: () => DataAppCodeFile;
26
+ /** Which scaffold is being assembled: a data app or a custom chart type. */
27
+ export type AuthoringFlavor = 'app' | 'chart-type';
19
28
  /**
20
29
  * Assembles static authoring files (configs, skills, templates) to deploy alongside a data app.
21
30
  */
22
31
  export declare const buildStaticAuthoringFiles: (args: {
23
32
  appName: string;
24
33
  sdkVersion: string;
34
+ flavor?: AuthoringFlavor;
25
35
  }) => DataAppCodeFile[];
26
36
  //# sourceMappingURL=scaffolding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scaffolding.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/scaffolding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAKpE;AA6DD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,gBAChB,MAAM,WACV,MAAM,KAChB,MAAmE,CAAC;AAEvE;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eACrB,MAAM,KACnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAevB,CAAC;AAWF,eAAO,MAAM,yBAAyB,eACtB,MAAM,KACnB,eAAe,EAeb,CAAC;AAEN,eAAO,MAAM,yBAAyB,QAAO,eAAe,EAG3D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,yBAAyB,SAAU;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACtB,KAAG,eAAe,EAuDlB,CAAC"}
1
+ {"version":3,"file":"scaffolding.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/scaffolding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAKpE;AA6DD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,gBAChB,MAAM,WACV,MAAM,KAChB,MAAmE,CAAC;AAEvE;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eACrB,MAAM,KACnB,MAAM,CAAC,MAAM,EAAE,MAAM,CAevB,CAAC;AAWF,eAAO,MAAM,yBAAyB,eACtB,MAAM,KACnB,eAAe,EAeb,CAAC;AAEN,eAAO,MAAM,yBAAyB,QAAO,eAAe,EAG3D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,eAQ1C,CAAC;AAEF,4EAA4E;AAC5E,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,YAAY,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,yBAAyB,SAAU;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC5B,KAAG,eAAe,EAkFlB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildStaticAuthoringFiles = exports.loadVendoredStarterSource = exports.loadVendoredBuildScaffold = exports.loadTemplateDependencies = exports.rewriteWorkspaceDeps = void 0;
3
+ exports.buildStaticAuthoringFiles = exports.loadChartTypeStarterApp = exports.loadVendoredStarterSource = exports.loadVendoredBuildScaffold = exports.loadTemplateDependencies = exports.rewriteWorkspaceDeps = void 0;
4
4
  exports.firstExistingDir = firstExistingDir;
5
5
  const tslib_1 = require("tslib");
6
6
  const fs_1 = require("fs");
@@ -100,35 +100,65 @@ const loadVendoredStarterSource = () => {
100
100
  return walkDir(path.join(templateDir, 'src'), templateDir, new Set());
101
101
  };
102
102
  exports.loadVendoredStarterSource = loadVendoredStarterSource;
103
+ /**
104
+ * The chart-type starter component: replaces the template's src/App.jsx when
105
+ * scaffolding a custom chart type (data_app_viz). The rest of the starter
106
+ * source (main.jsx already mounts VizContextProvider, lib/, css) is shared
107
+ * with data apps.
108
+ */
109
+ const loadChartTypeStarterApp = () => {
110
+ const { authoringDir } = resolveVendorDirs();
111
+ return {
112
+ path: 'src/App.jsx',
113
+ contentBase64: (0, fs_1.readFileSync)(path.join(authoringDir, 'chart-type', 'App.jsx')).toString('base64'),
114
+ };
115
+ };
116
+ exports.loadChartTypeStarterApp = loadChartTypeStarterApp;
103
117
  /**
104
118
  * Assembles static authoring files (configs, skills, templates) to deploy alongside a data app.
105
119
  */
106
120
  const buildStaticAuthoringFiles = (args) => {
107
- const { appName, sdkVersion } = args;
121
+ const { appName, sdkVersion, flavor = 'app' } = args;
108
122
  const { templateDir, authoringDir } = resolveVendorDirs();
123
+ const isChartType = flavor === 'chart-type';
109
124
  const files = [];
110
125
  // 1. Template scaffold files (src/, scripts/, skill.md excluded)
111
126
  for (const file of (0, exports.loadVendoredBuildScaffold)(sdkVersion)) {
112
127
  files.push(file);
113
128
  }
114
- // 2. skill.md → .claude/skills/lightdash-data-app/SKILL.md
115
- files.push({
116
- path: '.claude/skills/lightdash-data-app/SKILL.md',
117
- contentBase64: (0, fs_1.readFileSync)(path.join(templateDir, 'skill.md')).toString('base64'),
118
- });
119
- // 3. authoring/developing-data-apps-locally/SKILL.md
120
- // → .claude/skills/developing-data-apps-locally/SKILL.md
121
- files.push({
122
- path: '.claude/skills/developing-data-apps-locally/SKILL.md',
123
- contentBase64: (0, fs_1.readFileSync)(path.join(authoringDir, 'developing-data-apps-locally', 'SKILL.md')).toString('base64'),
124
- });
129
+ // 2+3. Flavor-specific skills. A chart type must not query through the
130
+ // SDK, so the app-building skills would only mislead — it gets the local
131
+ // chart-type workflow skill instead, with the viz contract shipping via
132
+ // the template's .claude/skills/reusable-visualization (step 1).
133
+ if (isChartType) {
134
+ files.push({
135
+ path: '.claude/skills/developing-chart-types-locally/SKILL.md',
136
+ contentBase64: (0, fs_1.readFileSync)(path.join(authoringDir, 'developing-chart-types-locally', 'SKILL.md')).toString('base64'),
137
+ });
138
+ }
139
+ if (!isChartType) {
140
+ // skill.md → .claude/skills/lightdash-data-app/SKILL.md
141
+ files.push({
142
+ path: '.claude/skills/lightdash-data-app/SKILL.md',
143
+ contentBase64: (0, fs_1.readFileSync)(path.join(templateDir, 'skill.md')).toString('base64'),
144
+ });
145
+ // authoring/developing-data-apps-locally/SKILL.md
146
+ // → .claude/skills/developing-data-apps-locally/SKILL.md
147
+ files.push({
148
+ path: '.claude/skills/developing-data-apps-locally/SKILL.md',
149
+ contentBase64: (0, fs_1.readFileSync)(path.join(authoringDir, 'developing-data-apps-locally', 'SKILL.md')).toString('base64'),
150
+ });
151
+ }
152
+ const flavorDir = isChartType
153
+ ? path.join(authoringDir, 'chart-type')
154
+ : authoringDir;
125
155
  // 4. AGENTS.md.tmpl → AGENTS.md
126
156
  files.push({
127
157
  path: 'AGENTS.md',
128
- contentBase64: (0, fs_1.readFileSync)(path.join(authoringDir, 'AGENTS.md.tmpl')).toString('base64'),
158
+ contentBase64: (0, fs_1.readFileSync)(path.join(flavorDir, 'AGENTS.md.tmpl')).toString('base64'),
129
159
  });
130
160
  // 5. README.md.tmpl → README.md (substitute {{APP_NAME}})
131
- const readme = (0, fs_1.readFileSync)(path.join(authoringDir, 'README.md.tmpl'), 'utf-8').replace(/\{\{APP_NAME\}\}/g, appName);
161
+ const readme = (0, fs_1.readFileSync)(path.join(flavorDir, 'README.md.tmpl'), 'utf-8').replace(/\{\{APP_NAME\}\}/g, appName);
132
162
  files.push({
133
163
  path: 'README.md',
134
164
  contentBase64: Buffer.from(readme).toString('base64'),
package/dist/index.js CHANGED
@@ -445,14 +445,15 @@ const appsProgram = commander_1.program
445
445
  .description('Work with data apps (enterprise)');
446
446
  appsProgram
447
447
  .command('create <name>')
448
- .description('Creates a new data app locally')
448
+ .description('Creates a new data app or custom chart type locally')
449
+ .option('--chart-type', 'Create a custom chart type (a reusable visualization the explorer chart type picker offers) instead of a data app', false)
449
450
  .option('--description <text>', 'Set the app description', '')
450
451
  .option('--slug <slug>', 'Override the app slug')
451
452
  .option('-p, --path <path>', 'Specify the Lightdash content root (default: ./lightdash)')
452
453
  .option('--project <project uuid>', 'Specify the project the app will use', parseProjectArgument, undefined)
453
454
  .option('-y, --assume-yes', 'approve npm package installation', false)
454
455
  .option('--verbose', undefined, false)
455
- .addHelpText('after', `\n${styles.bold('Example:')}\n ${styles.title('⚡')}️lightdash ${styles.bold('apps create "Revenue explorer"')} ${styles.secondary('-- creates ./lightdash/apps/revenue-explorer')}\n`)
456
+ .addHelpText('after', `\n${styles.bold('Examples:')}\n ${styles.title('⚡')}️lightdash ${styles.bold('apps create "Revenue explorer"')} ${styles.secondary('-- creates ./lightdash/apps/revenue-explorer')}\n ${styles.title('⚡')}️lightdash ${styles.bold('apps create "Radial gauge" --chart-type')} ${styles.secondary('-- creates a custom chart type at ./lightdash/apps/radial-gauge')}\n`)
456
457
  .action(createApp_1.createAppHandler);
457
458
  appsProgram
458
459
  .command('preview [path]')
@@ -0,0 +1,24 @@
1
+ # Working on this Lightdash custom chart type
2
+
3
+ This folder is a **custom chart type** (a reusable visualization, the `data_app_viz` template) — ONE chart component that receives its data and settings from Lightdash, not a data app. It runs no query, owns no explore, and must not fetch anything itself.
4
+
5
+ Two skills in `.claude/skills/` describe how to edit it:
6
+ - `reusable-visualization` — the contract: the `useVizContext()` hook, the field/option declaration, resolved series colours, pivoted results.
7
+ - `developing-chart-types-locally` — the local workflow: the vizSchema lockstep rule, validate/upload/verify loop, and the path to the official registry.
8
+
9
+ The one difference from that skill here: the declaration is not emitted as structured output. It lives in this folder's `lightdash-app.yml` under `vizSchema`, and **must stay in lockstep with the component** — every key `src/App.jsx` reads from `fieldMapping` or `options` is declared there, and everything declared there is read. When you change what the component reads, update `vizSchema` in the same edit.
10
+
11
+ Edit only `src/`. Root config files are read-only reference — the server rebuilds against a trusted template.
12
+
13
+ ## The loop
14
+
15
+ 1. Edit `src/` (and `vizSchema` in `lightdash-app.yml` when the declaration changes).
16
+ 2. `lightdash apps validate --build` — Cloud-parity Vite build; a failure is a validation error with the Vite output.
17
+ 3. `lightdash upload --chart-types <slug>` — the server rebuilds; the chart type then appears in the explorer's chart type picker for that project.
18
+ 4. Verify in Lightdash: open any explore, run a query, pick this chart type, and map its fields.
19
+
20
+ There is no local preview for a chart type: `useVizContext()` waits for the Lightdash host, so the component renders nothing standalone (`lightdash apps preview` and `npm run dev` show a blank page). Upload and verify in the explorer instead.
21
+
22
+ Build with the template's preinstalled dependency set (see `package.json` — React, Recharts, d3, and more). Do not add npm packages or vendor library source into `src/`; chart types published to the official registry must stay template-deps-only.
23
+
24
+ `.npmrc` sets `ignore-scripts=true` — never remove it or run installs with scripts enabled.
@@ -0,0 +1,95 @@
1
+ import {
2
+ getFormatted,
3
+ getRaw,
4
+ resolveValueColor,
5
+ useVizContext,
6
+ } from '@lightdash/query-sdk';
7
+ import {
8
+ Bar,
9
+ BarChart,
10
+ Cell,
11
+ LabelList,
12
+ ResponsiveContainer,
13
+ Tooltip,
14
+ XAxis,
15
+ YAxis,
16
+ } from 'recharts';
17
+
18
+ // Starter custom chart type: a bar chart wired to the host the way every viz
19
+ // must be. It runs no query — Lightdash runs the query and pushes rows plus a
20
+ // mapping from the field names declared in lightdash-app.yml's vizSchema to
21
+ // the query's columns. Everything read here (`category`/`value` fields,
22
+ // `showLabels`/`maxBars` options) is declared there; keep the two in lockstep
23
+ // (see .claude/skills/reusable-visualization).
24
+ function App() {
25
+ const context = useVizContext();
26
+ const { fieldMapping, rows, options, ready } = context;
27
+ if (!ready) {
28
+ return <div style={{ height: '100vh' }} />;
29
+ }
30
+
31
+ const catField = fieldMapping.category;
32
+ const valField = fieldMapping.value;
33
+ if (!catField || !valField) {
34
+ return (
35
+ <div
36
+ style={{
37
+ height: '100vh',
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ }}
42
+ >
43
+ Map the Category and Value fields to see the chart.
44
+ </div>
45
+ );
46
+ }
47
+
48
+ const { showLabels, maxBars } = options;
49
+ const fallbackColors = ['#7162FF', '#1A1B1E'];
50
+ const data = rows.slice(0, maxBars).map((row) => ({
51
+ label: getFormatted(row, catField),
52
+ category: getRaw(row, catField),
53
+ value: Number(getRaw(row, valField) ?? 0),
54
+ }));
55
+
56
+ return (
57
+ // The root fills the viewport, so ResponsiveContainer has a height to
58
+ // measure.
59
+ <div style={{ height: '100vh' }}>
60
+ <ResponsiveContainer width="100%" height="100%">
61
+ <BarChart data={data}>
62
+ <XAxis
63
+ dataKey="label"
64
+ tickFormatter={(v) =>
65
+ v.length > 12 ? `${v.slice(0, 11)}…` : v
66
+ }
67
+ />
68
+ <YAxis tickFormatter={(v) => v.toLocaleString()} />
69
+ <Tooltip />
70
+ <Bar dataKey="value">
71
+ {data.map((d, i) => (
72
+ <Cell
73
+ key={`${d.label}-${i}`}
74
+ fill={
75
+ resolveValueColor(
76
+ context,
77
+ catField,
78
+ d.category,
79
+ i,
80
+ ) ??
81
+ fallbackColors[i % fallbackColors.length]
82
+ }
83
+ />
84
+ ))}
85
+ {showLabels && (
86
+ <LabelList dataKey="value" position="top" />
87
+ )}
88
+ </Bar>
89
+ </BarChart>
90
+ </ResponsiveContainer>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export default App;