@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/handlers/apps/appsDownload.d.ts.map +1 -1
- package/dist/handlers/apps/appsDownload.js +5 -0
- package/dist/handlers/apps/appsDownload.test.js +22 -0
- package/dist/handlers/apps/createApp.d.ts +4 -1
- package/dist/handlers/apps/createApp.d.ts.map +1 -1
- package/dist/handlers/apps/createApp.js +81 -21
- package/dist/handlers/apps/createApp.test.js +46 -1
- package/dist/handlers/apps/scaffolding.d.ts +10 -0
- package/dist/handlers/apps/scaffolding.d.ts.map +1 -1
- package/dist/handlers/apps/scaffolding.js +45 -15
- package/dist/index.js +3 -2
- package/dist/vendor/authoring/chart-type/AGENTS.md.tmpl +24 -0
- package/dist/vendor/authoring/chart-type/App.jsx +95 -0
- package/dist/vendor/authoring/chart-type/README.md.tmpl +19 -0
- package/dist/vendor/authoring/developing-chart-types-locally/SKILL.md +46 -0
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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,
|
|
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
|
|
105
|
-
|
|
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(
|
|
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
|
-
|
|
164
|
+
`Creating this ${entityLabel} will install these direct npm packages:`,
|
|
128
165
|
...directPackages.map((packageSpec) => ` - ${packageSpec}`),
|
|
129
|
-
|
|
130
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
241
|
-
file.path === '
|
|
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
|
|
249
|
-
globalState_1.default.log(
|
|
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;
|
|
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.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
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(
|
|
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('
|
|
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;
|