@lightdash/cli 1.54.0 → 1.55.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/appCodeFiles.d.ts +5 -0
- package/dist/handlers/apps/appCodeFiles.d.ts.map +1 -1
- package/dist/handlers/apps/appCodeFiles.js +41 -1
- package/dist/handlers/apps/appCodeFiles.test.js +60 -0
- package/dist/handlers/apps/appsDownload.d.ts +49 -0
- package/dist/handlers/apps/appsDownload.d.ts.map +1 -1
- package/dist/handlers/apps/appsDownload.js +90 -1
- package/dist/handlers/apps/appsDownload.test.js +231 -0
- package/dist/handlers/download.d.ts +16 -1
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +334 -198
- package/dist/handlers/download.test.js +92 -1
- package/dist/vendor/authoring/AGENTS.md.tmpl +1 -1
- package/dist/vendor/authoring/README.md.tmpl +1 -1
- package/dist/vendor/authoring/developing-data-apps-locally/SKILL.md +3 -3
- package/dist/vendor/template/package.json +1 -1
- package/package.json +3 -3
|
@@ -38,7 +38,7 @@ vitest_1.vi.mock('./dbt/apiClient', async (importOriginal) => ({
|
|
|
38
38
|
getContentAsCodeUploadPermissions: vitest_1.vi.fn(),
|
|
39
39
|
lightdashApi: vitest_1.vi.fn(),
|
|
40
40
|
}));
|
|
41
|
-
const { assertUniqueSpacePaths, downloadSpaces, getDashboardChartSlugs, getFlatSpaceFileNames, hasContentFilters, isAiAgentsUnavailableError, isExternalConnectionsUnavailableError, downloadAiAgents, readAiAgentFiles, readSpaceFiles, readSpaceNames, sanitizeChartForDownload, shouldFallBackToEmbeddedSpaces, shouldDownloadAiAgents, summarizeUploadChanges, upsertAiAgents, upsertExternalConnections, upsertSpaces, upsertVirtualViews, validateSpaceIdentity, writeSpaceFiles, } = download_1.testHelpers;
|
|
41
|
+
const { assertUniqueSpacePaths, downloadSpaces, getDashboardAppSlugs, getDashboardChartSlugs, getFlatSpaceFileNames, hasContentFilters, isAiAgentsUnavailableError, isExternalConnectionsUnavailableError, downloadAiAgents, isFilteredWithNoDashboards, readAiAgentFiles, readSpaceFiles, readSpaceNames, sanitizeChartForDownload, shouldFallBackToEmbeddedSpaces, shouldDownloadAiAgents, summarizeUploadChanges, upsertAiAgents, upsertExternalConnections, upsertSpaces, upsertVirtualViews, validateSpaceIdentity, writeSpaceFiles, } = download_1.testHelpers;
|
|
42
42
|
const makeDownloadHandlerOptions = (overrides = {}) => ({
|
|
43
43
|
verbose: false,
|
|
44
44
|
charts: [],
|
|
@@ -155,6 +155,24 @@ const writeFolderDashboard = async (baseDir, slug, chartSlugs) => {
|
|
|
155
155
|
const yaml = `contentType: dashboard\nname: ${slug}\nslug: ${slug}\nspaceSlug: test-space\ntiles:\n${tilesYaml}\nversion: 1\n`;
|
|
156
156
|
await fs_1.promises.writeFile(path.join(baseDir, 'dashboards', `${slug}.yml`), yaml);
|
|
157
157
|
};
|
|
158
|
+
const makeLooseAppDashboard = (slug, appSlugs) => ({
|
|
159
|
+
slug,
|
|
160
|
+
name: slug,
|
|
161
|
+
spaceSlug: 'test-space',
|
|
162
|
+
version: 1,
|
|
163
|
+
tiles: appSlugs.map((appSlug) => ({
|
|
164
|
+
type: 'data_app',
|
|
165
|
+
properties: { appSlug },
|
|
166
|
+
})),
|
|
167
|
+
needsUpdating: false,
|
|
168
|
+
});
|
|
169
|
+
const writeFolderAppDashboard = async (baseDir, slug, appSlugs) => {
|
|
170
|
+
const tilesYaml = appSlugs
|
|
171
|
+
.map((s) => ` - properties:\n appSlug: ${s}\n type: data_app`)
|
|
172
|
+
.join('\n');
|
|
173
|
+
const yaml = `contentType: dashboard\nname: ${slug}\nslug: ${slug}\nspaceSlug: test-space\ntiles:\n${tilesYaml}\nversion: 1\n`;
|
|
174
|
+
await fs_1.promises.writeFile(path.join(baseDir, 'dashboards', `${slug}.yml`), yaml);
|
|
175
|
+
};
|
|
158
176
|
describe('summarizeUploadChanges', () => {
|
|
159
177
|
it('reports only phase deltas and warns when resources fail', () => {
|
|
160
178
|
expect(summarizeUploadChanges({ 'charts created': 1, 'spaces skipped': 3 }, {
|
|
@@ -282,6 +300,79 @@ describe('getDashboardChartSlugs', () => {
|
|
|
282
300
|
expect(slugs).toEqual(['real-chart']);
|
|
283
301
|
});
|
|
284
302
|
});
|
|
303
|
+
describe('extractAppSlugsFromDashboards', () => {
|
|
304
|
+
const dashboard = (tiles) => ({ slug: 'd', tiles });
|
|
305
|
+
it('collects app slugs and ignores other tile types', () => {
|
|
306
|
+
expect(download_1.testHelpers.extractAppSlugsFromDashboards([
|
|
307
|
+
dashboard([
|
|
308
|
+
{
|
|
309
|
+
type: 'data_app',
|
|
310
|
+
properties: { appSlug: 'revenue-explorer' },
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
type: 'saved_chart',
|
|
314
|
+
properties: { chartSlug: 'a-chart' },
|
|
315
|
+
},
|
|
316
|
+
{ type: 'markdown', properties: { content: 'hi' } },
|
|
317
|
+
]),
|
|
318
|
+
])).toEqual(['revenue-explorer']);
|
|
319
|
+
});
|
|
320
|
+
it('dedupes and drops null slugs', () => {
|
|
321
|
+
expect(download_1.testHelpers.extractAppSlugsFromDashboards([
|
|
322
|
+
dashboard([
|
|
323
|
+
{ type: 'data_app', properties: { appSlug: 'one' } },
|
|
324
|
+
{ type: 'data_app', properties: { appSlug: 'one' } },
|
|
325
|
+
{ type: 'data_app', properties: { appSlug: null } },
|
|
326
|
+
]),
|
|
327
|
+
])).toEqual(['one']);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
describe('getDashboardAppSlugs', () => {
|
|
331
|
+
let tmpDir;
|
|
332
|
+
beforeEach(async () => {
|
|
333
|
+
tmpDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'download-test-'));
|
|
334
|
+
await fs_1.promises.mkdir(path.join(tmpDir, 'dashboards'));
|
|
335
|
+
});
|
|
336
|
+
afterEach(async () => {
|
|
337
|
+
await fs_1.promises.rm(tmpDir, { recursive: true, force: true });
|
|
338
|
+
});
|
|
339
|
+
it('extracts app slugs from folder dashboards', async () => {
|
|
340
|
+
await writeFolderAppDashboard(tmpDir, 'folder-dash', [
|
|
341
|
+
'app-a',
|
|
342
|
+
'app-b',
|
|
343
|
+
]);
|
|
344
|
+
const slugs = await getDashboardAppSlugs([], tmpDir);
|
|
345
|
+
expect(slugs.sort()).toEqual(['app-a', 'app-b']);
|
|
346
|
+
});
|
|
347
|
+
it('filters to the selected dashboards only', async () => {
|
|
348
|
+
await writeFolderAppDashboard(tmpDir, 'wanted', ['wanted-app']);
|
|
349
|
+
const loose = makeLooseAppDashboard('other', ['other-app']);
|
|
350
|
+
const slugs = await getDashboardAppSlugs(['wanted'], tmpDir, [loose]);
|
|
351
|
+
expect(slugs).toEqual(['wanted-app']);
|
|
352
|
+
});
|
|
353
|
+
it('dedupes across dashboards and drops null slugs', async () => {
|
|
354
|
+
await writeFolderAppDashboard(tmpDir, 'folder-dash', ['shared-app']);
|
|
355
|
+
const loose = makeLooseAppDashboard('loose-dash', ['shared-app', null]);
|
|
356
|
+
const slugs = await getDashboardAppSlugs([], tmpDir, [loose]);
|
|
357
|
+
expect(slugs).toEqual(['shared-app']);
|
|
358
|
+
});
|
|
359
|
+
it('returns an empty array when no dashboard has an app tile', async () => {
|
|
360
|
+
await writeFolderDashboard(tmpDir, 'chart-only', ['chart-a']);
|
|
361
|
+
const slugs = await getDashboardAppSlugs([], tmpDir);
|
|
362
|
+
expect(slugs).toEqual([]);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
describe('isFilteredWithNoDashboards', () => {
|
|
366
|
+
it('is true for a filtered upload that selects no dashboards (e.g. --charts only)', () => {
|
|
367
|
+
expect(isFilteredWithNoDashboards(true, [])).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
it('is false for an unfiltered upload even with no dashboard slugs', () => {
|
|
370
|
+
expect(isFilteredWithNoDashboards(false, [])).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
it('is false once dashboard slugs are provided', () => {
|
|
373
|
+
expect(isFilteredWithNoDashboards(true, ['my-dashboard'])).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
285
376
|
describe('sanitizeChartForDownload', () => {
|
|
286
377
|
it('preserves per-value pivot series customizations by default', () => {
|
|
287
378
|
const chart = makeChart([
|
|
@@ -8,6 +8,6 @@ Edit only `src/`. Root config files are read-only reference — the server rebui
|
|
|
8
8
|
|
|
9
9
|
The local build is an optional pre-check. Apps made with `lightdash apps create` already have dependencies installed and can run `npm run build`; after `lightdash download`, use `npm install && npm run build` if you choose to build locally. If install fails, skip it and upload — never modify machine config, npmrc, or dependency files to force it; the server rebuild is authoritative.
|
|
10
10
|
|
|
11
|
-
Build with the template's preinstalled dependency set (see `package.json`). Adding new npm packages only works when the
|
|
11
|
+
Build with the template's preinstalled dependency set (see `package.json`). Adding new npm packages only works when the Lightdash organization has custom dependencies enabled — assume it does not; ask the user before considering a new library, and never vendor library source into `src/` as a workaround. Where enabled, adding a dependency is the one workflow that still requires pnpm: upload requires a matching `pnpm-lock.yaml`, so `pnpm add <pkg>` (or `pnpm install --lockfile-only`) must succeed locally. If it fails, stop and report the error — hand-editing `package.json` without the lockfile makes the upload fail.
|
|
12
12
|
|
|
13
13
|
`.npmrc` sets `ignore-scripts=true` — never remove it or run installs with scripts enabled; this app may be authored by someone else and their dependencies' lifecycle scripts must not run here.
|
|
@@ -13,7 +13,7 @@ Local preview uses your existing CLI login through a project- and route-restrict
|
|
|
13
13
|
## What's read-only
|
|
14
14
|
Most root config is read-only: `vite.config.js`, `tailwind.config.js`, `tsconfig.json`, etc. The server rebuilds against a trusted template, so local edits to those files don't change the deployed app.
|
|
15
15
|
|
|
16
|
-
The app builds against the template's fixed dependency set (see `package.json`) — design within it. Only when custom dependencies are enabled
|
|
16
|
+
The app builds against the template's fixed dependency set (see `package.json`) — design within it. Only when custom dependencies are enabled for your Lightdash organization, `package.json` becomes partially editable. This is the one workflow that still requires pnpm: run `pnpm add <pkg>` to declare extra npm dependencies (registry packages, plain semver versions only, up to 60 direct deps). `pnpm-lock.yaml` must be updated to match — `pnpm add` does this; if you edit `package.json` by hand, run `pnpm install --lockfile-only`. Upload rejects new dependencies without a matching lockfile. On upload the CLI warns which packages will be installed in the build sandbox; install scripts never run, and the server replaces `scripts` with the trusted template's (the sandbox build command is not uploader-controlled). The AI builder and in-app UI cannot change the dependency set — dependencies are declared by editing `package.json` and re-uploading.
|
|
17
17
|
|
|
18
18
|
`.npmrc` sets `ignore-scripts=true` so dependencies' lifecycle scripts never run on your machine either — downloaded apps may be authored by someone else. Keep it; explicit commands such as `npm run build` still work.
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ You are editing a Lightdash **data app** that was created or downloaded with the
|
|
|
22
22
|
## Library boundaries — build with what's preinstalled
|
|
23
23
|
|
|
24
24
|
- The app builds against a **fixed template dependency set** — see `package.json` (React, Recharts, d3 + d3-cloud/d3-sankey, Radix primitives, Tailwind, lucide-react, date-fns, html-to-image, jspdf, and more). **Design within this set**; it covers almost all data-app needs.
|
|
25
|
-
- Adding new npm packages only works when the Lightdash
|
|
25
|
+
- Adding new npm packages only works when the Lightdash organization has custom dependencies enabled — **assume it does not**. When disabled, upload rejects new dependency declarations. Do not attempt `pnpm add` to find out; ask the user whether their organization has custom dependencies enabled before considering a new library.
|
|
26
26
|
- Do **not** work around a missing library by vendoring its source into `src/`, inlining minified code, or fetching code at runtime. If the template set genuinely cannot express what's asked, say so and let the user decide.
|
|
27
27
|
|
|
28
28
|
## The edit → build → upload loop
|
|
@@ -36,7 +36,7 @@ You are editing a Lightdash **data app** that was created or downloaded with the
|
|
|
36
36
|
- If `npm install` fails (registry policy, an unavailable pinned SDK version, no network access), **skip the local build entirely and go straight to upload**. The server rebuild is authoritative and surfaces build errors on the app page.
|
|
37
37
|
- Do **not** modify machine configuration, `.npmrc` files, registry settings, or the project's dependency files to force an install to work.
|
|
38
38
|
- A missing `node_modules` is a normal state, not a problem to fix. Never run installs just because it is absent.
|
|
39
|
-
- **Exception — adding a dependency** (only
|
|
39
|
+
- **Exception — adding a dependency** (only for organizations with custom dependencies enabled — see "Library boundaries" above). This is the one workflow that still requires pnpm: upload rejects new dependencies unless `pnpm-lock.yaml` was regenerated to match `package.json`, so dependency resolution MUST succeed locally. Use `pnpm add <pkg>` — prefixed with Socket Firewall when available (`sfw pnpm add <pkg>`; check with `command -v sfw`) to block known-malicious packages — or after editing `package.json` run `pnpm install --lockfile-only` (updates the lockfile without installing). If resolution fails, **stop and report the exact pnpm error to the user** — never hand-edit `package.json` and proceed without the lockfile; the upload will fail.
|
|
40
40
|
- **Never run dependency lifecycle scripts.** The app's `.npmrc` sets `ignore-scripts=true` — leave it. A downloaded app can be authored by someone else, and their dependencies' install scripts must not execute on this machine. Explicit `npm run build`/`npm run dev` and `pnpm build`/`pnpm dev` commands still work.
|
|
41
41
|
|
|
42
42
|
## Preview locally against real data
|
|
@@ -65,4 +65,4 @@ You are editing a Lightdash **data app** that was created or downloaded with the
|
|
|
65
65
|
|
|
66
66
|
Most root config is reference only — editing it has **no effect** because the server rebuilds against its trusted template. This applies to `vite.config.js`, `tailwind.config.js`, `tsconfig.json`, and other build/tooling files.
|
|
67
67
|
|
|
68
|
-
`package.json` is **partially editable** only when custom dependencies are enabled
|
|
68
|
+
`package.json` is **partially editable** only when custom dependencies are enabled for the Lightdash organization — see "Library boundaries" above; treat it as read-only otherwise. When enabled, you may add npm dependencies with `pnpm add <pkg>` — registry packages with plain semver versions only (no git/file/url specs), up to 60 direct dependencies, and `pnpm-lock.yaml` must be updated alongside (see the exception above — this is the one step that still requires pnpm and must succeed locally). On upload the CLI warns which packages will be installed in the build sandbox; install scripts never run. Other root config (vite/tailwind/tsconfig) remains read-only.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.55.0",
|
|
4
4
|
"description": "Lightdash CLI tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"unique-names-generator": "4.7.1",
|
|
42
42
|
"uuid": "11.1.1",
|
|
43
43
|
"yaml": "2.8.3",
|
|
44
|
-
"@lightdash/common": "1.
|
|
45
|
-
"@lightdash/warehouses": "1.
|
|
44
|
+
"@lightdash/common": "1.55.0",
|
|
45
|
+
"@lightdash/warehouses": "1.55.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/inquirer": "8.2.4",
|