@lightdash/cli 1.54.0 → 1.56.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.
@@ -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 instance 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.
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 on your Lightdash instance (most instances: not enabled), `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.
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 instance has custom dependencies enabled — **assume it does not**. When disabled, upload rejects new dependencies and apps that declare them cannot be downloaded. Do not attempt `pnpm add` to find out; ask the user whether their instance has custom dependencies enabled before considering a new library.
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 on instances 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.
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 on the Lightdash instance — 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.
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.
@@ -56,6 +56,6 @@
56
56
  "postcss": "8.5.18",
57
57
  "tailwindcss": "3.4.17",
58
58
  "typescript": "7.0.2",
59
- "vite": "8.0.5"
59
+ "vite": "8.0.16"
60
60
  }
61
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "1.54.0",
3
+ "version": "1.56.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.54.0",
45
- "@lightdash/warehouses": "1.54.0"
44
+ "@lightdash/common": "1.56.0",
45
+ "@lightdash/warehouses": "1.56.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",