@lightdash/cli 1.89.1 → 1.91.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.
@@ -14,6 +14,7 @@ export declare const rewriteWorkspaceDeps: (packageJson: string, version: string
14
14
  * Returns {} on any read/parse error so callers degrade gracefully.
15
15
  */
16
16
  export declare const loadTemplateDependencies: (sdkVersion: string) => Record<string, string>;
17
+ export declare const loadVendoredBuildScaffold: (sdkVersion: string) => DataAppCodeFile[];
17
18
  export declare const loadVendoredStarterSource: () => DataAppCodeFile[];
18
19
  /**
19
20
  * Assembles static authoring files (configs, skills, templates) to deploy alongside a data app.
@@ -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,QAAO,eAAe,EAG3D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,yBAAyB,SAAU;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACtB,KAAG,eAAe,EAkElB,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;;GAEG;AACH,eAAO,MAAM,yBAAyB,SAAU;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACtB,KAAG,eAAe,EAuDlB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildStaticAuthoringFiles = exports.loadVendoredStarterSource = exports.loadTemplateDependencies = exports.rewriteWorkspaceDeps = void 0;
3
+ exports.buildStaticAuthoringFiles = 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");
@@ -88,6 +88,13 @@ const loadVendoredTemplate = () => {
88
88
  const SKIP = new Set(['skill.md', 'scripts', 'src']);
89
89
  return walkDir(templateDir, templateDir, SKIP);
90
90
  };
91
+ const loadVendoredBuildScaffold = (sdkVersion) => loadVendoredTemplate().map((file) => file.path === 'package.json'
92
+ ? {
93
+ ...file,
94
+ contentBase64: Buffer.from((0, exports.rewriteWorkspaceDeps)(Buffer.from(file.contentBase64, 'base64').toString('utf-8'), sdkVersion)).toString('base64'),
95
+ }
96
+ : file);
97
+ exports.loadVendoredBuildScaffold = loadVendoredBuildScaffold;
91
98
  const loadVendoredStarterSource = () => {
92
99
  const { templateDir } = resolveVendorDirs();
93
100
  return walkDir(path.join(templateDir, 'src'), templateDir, new Set());
@@ -101,17 +108,8 @@ const buildStaticAuthoringFiles = (args) => {
101
108
  const { templateDir, authoringDir } = resolveVendorDirs();
102
109
  const files = [];
103
110
  // 1. Template scaffold files (src/, scripts/, skill.md excluded)
104
- for (const file of loadVendoredTemplate()) {
105
- if (file.path === 'package.json') {
106
- const rewritten = (0, exports.rewriteWorkspaceDeps)(Buffer.from(file.contentBase64, 'base64').toString('utf-8'), sdkVersion);
107
- files.push({
108
- path: 'package.json',
109
- contentBase64: Buffer.from(rewritten).toString('base64'),
110
- });
111
- }
112
- else {
113
- files.push(file);
114
- }
111
+ for (const file of (0, exports.loadVendoredBuildScaffold)(sdkVersion)) {
112
+ files.push(file);
115
113
  }
116
114
  // 2. skill.md → .claude/skills/lightdash-data-app/SKILL.md
117
115
  files.push({
@@ -74,26 +74,39 @@ describe('buildStaticAuthoringFiles', () => {
74
74
  });
75
75
  it('uses npm for the standard local workflow', () => {
76
76
  expect(text('README.md')).toContain('run `npm install` first');
77
- expect(text('AGENTS.md')).toContain('npm install && npm run build');
78
- expect(text('.claude/skills/developing-data-apps-locally/SKILL.md')).toContain('run `npm install` first');
77
+ expect(text('AGENTS.md')).toContain('run `npm install` first');
78
+ expect(text('.claude/skills/developing-data-apps-locally/SKILL.md')).toContain('run `npm install` before');
79
79
  expect(text('.npmrc')).toContain('ignore-scripts=true');
80
80
  expect(text('.npmrc')).not.toContain('shamefully-hoist');
81
81
  });
82
- it('classifies local build failures as non-blocking warnings', () => {
82
+ it('documents Cloud-parity validation builds', () => {
83
83
  for (const path of [
84
84
  'README.md',
85
85
  'AGENTS.md',
86
86
  '.claude/skills/developing-data-apps-locally/SKILL.md',
87
87
  ]) {
88
- expect(text(path)).toContain('non-blocking warning');
89
- expect(text(path)).toContain('continue to upload');
90
- expect(text(path)).toContain('server build');
88
+ expect(text(path)).toContain('validate --build');
89
+ expect(text(path)).toContain('Cloud-parity');
91
90
  }
92
91
  });
93
92
  it('never writes app source (no src/ files)', () => {
94
93
  expect(files.every((f) => !f.path.startsWith('src/'))).toBe(true);
95
94
  });
96
95
  });
96
+ describe('loadVendoredBuildScaffold', () => {
97
+ const files = (0, scaffolding_1.loadVendoredBuildScaffold)('0.3275.0');
98
+ const byPath = (p) => files.find((file) => file.path === p);
99
+ it('loads the trusted build files without app source', () => {
100
+ expect(byPath('vite.config.js')).toBeDefined();
101
+ expect(byPath('index.html')).toBeDefined();
102
+ expect(files.every((file) => !file.path.startsWith('src/'))).toBe(true);
103
+ });
104
+ it('pins the SDK used by the local build', () => {
105
+ const packageJson = Buffer.from(byPath('package.json').contentBase64, 'base64').toString('utf-8');
106
+ expect(packageJson).toContain('"@lightdash/query-sdk": "0.3275.0"');
107
+ expect(packageJson).not.toContain('workspace:');
108
+ });
109
+ });
97
110
  describe('loadVendoredStarterSource', () => {
98
111
  const files = (0, scaffolding_1.loadVendoredStarterSource)();
99
112
  it('loads the starter app source with bundle-relative paths', () => {
@@ -1,11 +1,12 @@
1
- import { type DataAppDataReferences, type DataAppExploreIndex, type DataReferenceLocation } from '@lightdash/common';
1
+ import { type DataAppCode, type DataAppDataReferences, type DataAppExploreIndex, type DataReferenceLocation } from '@lightdash/common';
2
2
  export type AppsValidateFormat = 'human' | 'json';
3
3
  export type AppsValidateOptions = {
4
+ build: boolean;
4
5
  format: AppsValidateFormat;
5
6
  live: boolean;
6
7
  verbose: boolean;
7
8
  };
8
- export type AppsValidationIssueCode = 'bundle' | 'dependencies' | 'external_connection' | 'manifest' | 'semantic_layer' | 'semantic_reference' | 'source_parse';
9
+ export type AppsValidationIssueCode = 'build' | 'bundle' | 'dependencies' | 'external_connection' | 'manifest' | 'semantic_layer' | 'semantic_reference' | 'source_parse';
9
10
  export type AppsValidationIssue = {
10
11
  code: AppsValidationIssueCode;
11
12
  message: string;
@@ -24,6 +25,7 @@ export type AppValidationResult = {
24
25
  coverage: AppsValidationCoverage;
25
26
  };
26
27
  export type AppsValidationReport = {
28
+ build: boolean;
27
29
  valid: boolean;
28
30
  mode: 'live' | 'offline';
29
31
  summary: {
@@ -36,13 +38,32 @@ export type AppsValidationReport = {
36
38
  apps: AppValidationResult[];
37
39
  };
38
40
  type ValidateAppOptions = {
41
+ build?: boolean;
39
42
  live: boolean;
40
43
  liveProjectUuid?: string;
41
44
  loadLiveIndex: (projectUuid: string) => Promise<DataAppExploreIndex>;
45
+ runBuild?: (args: {
46
+ appDir: string;
47
+ bundle: DataAppCode;
48
+ }) => Promise<AppsValidationIssue[]>;
42
49
  };
50
+ type DataAppBuildCommand = {
51
+ command: string;
52
+ args: string[];
53
+ cwd: string;
54
+ env?: NodeJS.ProcessEnv;
55
+ preferLocal?: boolean;
56
+ timeoutMs: number;
57
+ };
58
+ export type RunDataAppBuildCommand = (command: DataAppBuildCommand) => Promise<void>;
59
+ export declare const validateDataAppBuild: (args: {
60
+ appDir: string;
61
+ bundle: DataAppCode;
62
+ runCommand?: RunDataAppBuildCommand;
63
+ }) => Promise<AppsValidationIssue[]>;
43
64
  export declare const fetchLiveDataAppExploreIndex: (projectUuid: string) => Promise<DataAppExploreIndex>;
44
65
  export declare const validateLocalDataApp: (appPath: string, options: ValidateAppOptions) => Promise<AppValidationResult>;
45
- export declare const buildAppsValidationReport: (apps: AppValidationResult[], live: boolean) => AppsValidationReport;
66
+ export declare const buildAppsValidationReport: (apps: AppValidationResult[], live: boolean, build?: boolean) => AppsValidationReport;
46
67
  export declare const renderAppsValidationHuman: (report: AppsValidationReport) => string;
47
68
  export declare const appsValidateHandler: (pathArgs: string[] | undefined, options: AppsValidateOptions) => Promise<void>;
48
69
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAcH,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAGxB,KAAK,qBAAqB,EAC7B,MAAM,mBAAmB,CAAC;AAgB3B,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC7B,QAAQ,GACR,cAAc,GACd,qBAAqB,GACrB,UAAU,GACV,gBAAgB,GAChB,oBAAoB,GACpB,cAAc,CAAC;AAErB,MAAM,MAAM,mBAAmB,GAAG;IAC9B,IAAI,EAAE,uBAAuB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,qBAAqB,CAAC,OAAO,CAAC,GAAG;IAClE,UAAU,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,QAAQ,EAAE,sBAAsB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,OAAO,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,mBAAmB,EAAE,MAAM,CAAC;KAC/B,CAAC;IACF,IAAI,EAAE,mBAAmB,EAAE,CAAC;CAC/B,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACxE,CAAC;AA2LF,eAAO,MAAM,4BAA4B,gBACxB,MAAM,KACpB,OAAO,CAAC,mBAAmB,CAqB7B,CAAC;AAEF,eAAO,MAAM,oBAAoB,YACpB,MAAM,WACN,kBAAkB,KAC5B,OAAO,CAAC,mBAAmB,CA4H7B,CAAC;AAEF,eAAO,MAAM,yBAAyB,SAC5B,mBAAmB,EAAE,QACrB,OAAO,KACd,oBAwBF,CAAC;AAmBF,eAAO,MAAM,yBAAyB,WAC1B,oBAAoB,KAC7B,MA4CF,CAAC;AAEF,eAAO,MAAM,mBAAmB,aAClB,MAAM,EAAE,GAAG,SAAS,WACrB,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CA2Cd,CAAC"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/validate.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appsValidateHandler = exports.renderAppsValidationHuman = exports.buildAppsValidationReport = exports.validateLocalDataApp = exports.fetchLiveDataAppExploreIndex = void 0;
3
+ exports.appsValidateHandler = exports.renderAppsValidationHuman = exports.buildAppsValidationReport = exports.validateLocalDataApp = exports.fetchLiveDataAppExploreIndex = exports.validateDataAppBuild = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
+ const execa_1 = tslib_1.__importDefault(require("execa"));
6
7
  const fs_1 = require("fs");
8
+ const os = tslib_1.__importStar(require("os"));
7
9
  const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
8
10
  const path = tslib_1.__importStar(require("path"));
9
11
  const config_1 = require("../../config");
@@ -127,6 +129,132 @@ const validateDependencies = async (appDir) => {
127
129
  return [issue('dependencies', (0, common_1.getErrorMessage)(error))];
128
130
  }
129
131
  };
132
+ const runDataAppBuildCommand = async (command) => {
133
+ await (0, execa_1.default)(command.command, command.args, {
134
+ cwd: command.cwd,
135
+ env: command.env,
136
+ localDir: command.preferLocal ? command.cwd : undefined,
137
+ preferLocal: command.preferLocal,
138
+ timeout: command.timeoutMs,
139
+ });
140
+ };
141
+ const commandFailureOutput = (error) => {
142
+ const commandError = typeof error === 'object' && error !== null
143
+ ? error
144
+ : {};
145
+ const output = [commandError.stderr, commandError.stdout]
146
+ .filter((value) => typeof value === 'string' || Buffer.isBuffer(value))
147
+ .map(String)
148
+ .filter(Boolean)
149
+ .join('\n')
150
+ .trim();
151
+ return (output || (0, common_1.getErrorMessage)(error)).slice(-2000);
152
+ };
153
+ const hasNodeModules = async (appDir) => fs_1.promises
154
+ .stat(path.join(appDir, 'node_modules'))
155
+ .then((entry) => entry.isDirectory())
156
+ .catch(() => false);
157
+ const buildPackageJsonForCustomDependencies = (scaffold, packageJson) => {
158
+ const packageFile = scaffold.find((file) => file.path === 'package.json');
159
+ if (packageFile === undefined) {
160
+ throw new Error('Vendored data app template is missing package.json.');
161
+ }
162
+ const templatePackage = JSON.parse(Buffer.from(packageFile.contentBase64, 'base64').toString('utf-8'));
163
+ if (templatePackage.scripts === undefined ||
164
+ templatePackage.devDependencies === undefined) {
165
+ throw new Error('Vendored data app template package.json is missing scripts or devDependencies.');
166
+ }
167
+ return (0, common_1.sanitizeAppPackageJsonScripts)(packageJson, templatePackage.scripts, templatePackage.devDependencies);
168
+ };
169
+ const validateDataAppBuild = async (args) => {
170
+ const runCommand = args.runCommand ?? runDataAppBuildCommand;
171
+ let dependencies;
172
+ let hasCustomDependencies;
173
+ try {
174
+ dependencies = await (0, appCodeFiles_1.readDependenciesFromDir)(args.appDir);
175
+ const templateDependencies = dependencies === null
176
+ ? (0, scaffolding_1.loadTemplateDependencies)(env_1.CLI_VERSION)
177
+ : (0, appCodeFiles_1.applySdkMirrorToTemplateDeps)((0, scaffolding_1.loadTemplateDependencies)(env_1.CLI_VERSION), dependencies.packageJson);
178
+ const customDependencies = dependencies === null
179
+ ? {}
180
+ : (0, common_1.computeCustomDependencies)(dependencies.packageJson, templateDependencies);
181
+ hasCustomDependencies = Object.keys(customDependencies).length > 0;
182
+ }
183
+ catch (error) {
184
+ return [issue('dependencies', (0, common_1.getErrorMessage)(error))];
185
+ }
186
+ if (!hasCustomDependencies && !(await hasNodeModules(args.appDir))) {
187
+ return [
188
+ issue('dependencies', `Dependencies are not installed. Run 'npm install' in ${args.appDir} before using lightdash apps validate --build.`),
189
+ ];
190
+ }
191
+ if (hasCustomDependencies &&
192
+ dependencies !== null &&
193
+ dependencies.lockfile === null) {
194
+ return [
195
+ issue('dependencies', 'Custom dependencies require pnpm-lock.yaml before the Cloud-parity build can run.'),
196
+ ];
197
+ }
198
+ let buildDir;
199
+ try {
200
+ buildDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'lightdash-app-build-'));
201
+ const scaffold = (0, scaffolding_1.loadVendoredBuildScaffold)(env_1.CLI_VERSION);
202
+ await (0, appCodeFiles_1.writeFilesToDir)(buildDir, [...scaffold, ...args.bundle.files]);
203
+ if (hasCustomDependencies &&
204
+ dependencies !== null &&
205
+ dependencies.lockfile !== null) {
206
+ await Promise.all([
207
+ fs_1.promises.writeFile(path.join(buildDir, 'package.json'), buildPackageJsonForCustomDependencies(scaffold, dependencies.packageJson)),
208
+ fs_1.promises.writeFile(path.join(buildDir, 'pnpm-lock.yaml'), dependencies.lockfile),
209
+ ]);
210
+ try {
211
+ await runCommand({
212
+ command: 'pnpm',
213
+ args: ['install', '--frozen-lockfile', '--ignore-scripts'],
214
+ cwd: buildDir,
215
+ env: { CI: 'true' },
216
+ timeoutMs: 120_000,
217
+ });
218
+ }
219
+ catch (error) {
220
+ return [
221
+ issue('dependencies', `Dependency install failed: ${commandFailureOutput(error)}`),
222
+ ];
223
+ }
224
+ }
225
+ else {
226
+ await fs_1.promises.symlink(path.resolve(args.appDir, 'node_modules'), path.join(buildDir, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir');
227
+ }
228
+ try {
229
+ await runCommand({
230
+ command: 'vite',
231
+ args: ['build'],
232
+ cwd: buildDir,
233
+ preferLocal: true,
234
+ timeoutMs: 60_000,
235
+ });
236
+ }
237
+ catch (error) {
238
+ return [
239
+ issue('build', `Vite build failed:\n${commandFailureOutput(error)}`),
240
+ ];
241
+ }
242
+ return [];
243
+ }
244
+ catch (error) {
245
+ return [
246
+ issue('build', `Could not prepare the Vite build: ${(0, common_1.getErrorMessage)(error)}`),
247
+ ];
248
+ }
249
+ finally {
250
+ if (buildDir !== undefined) {
251
+ await fs_1.promises
252
+ .rm(buildDir, { recursive: true, force: true })
253
+ .catch(() => undefined);
254
+ }
255
+ }
256
+ };
257
+ exports.validateDataAppBuild = validateDataAppBuild;
130
258
  const sourceFilesFromBundle = (files) => files.map((file) => ({
131
259
  path: file.path,
132
260
  content: Buffer.from(file.contentBase64, 'base64').toString('utf-8'),
@@ -170,7 +298,8 @@ const validateLocalDataApp = async (appPath, options) => {
170
298
  const { manifest } = bundle;
171
299
  const manifestResult = validateManifest(manifest);
172
300
  errors.push(...manifestResult.errors);
173
- errors.push(...(await validateDependencies(appDir)));
301
+ const dependencyErrors = await validateDependencies(appDir);
302
+ errors.push(...dependencyErrors);
174
303
  const sourceFiles = sourceFilesFromBundle(bundle.files);
175
304
  if (sourceFiles.length === 0) {
176
305
  errors.push(issue('bundle', 'App bundle has no files under src/ to validate.'));
@@ -227,6 +356,12 @@ const validateLocalDataApp = async (appPath, options) => {
227
356
  else if (typeof manifest.projectUuid === 'string') {
228
357
  projectUuid = manifest.projectUuid;
229
358
  }
359
+ if (options.build && dependencyErrors.length === 0) {
360
+ errors.push(...(await (options.runBuild ?? exports.validateDataAppBuild)({
361
+ appDir,
362
+ bundle,
363
+ })));
364
+ }
230
365
  return {
231
366
  path: appDir,
232
367
  name: typeof manifest.name === 'string' ? manifest.name : null,
@@ -238,10 +373,11 @@ const validateLocalDataApp = async (appPath, options) => {
238
373
  };
239
374
  };
240
375
  exports.validateLocalDataApp = validateLocalDataApp;
241
- const buildAppsValidationReport = (apps, live) => {
376
+ const buildAppsValidationReport = (apps, live, build = false) => {
242
377
  const errors = apps.reduce((count, app) => count + app.errors.length, 0);
243
378
  const warnings = apps.reduce((count, app) => count + app.warnings.length, 0);
244
379
  return {
380
+ build,
245
381
  valid: errors === 0,
246
382
  mode: live ? 'live' : 'offline',
247
383
  summary: {
@@ -274,7 +410,7 @@ const renderAppsValidationHuman = (report) => {
274
410
  const lines = [
275
411
  `Validating ${report.summary.apps} data app(s) using ${report.mode === 'live'
276
412
  ? 'the live project semantic layer'
277
- : 'local semantic layer snapshots'}.`,
413
+ : 'local semantic layer snapshots'}${report.build ? ' and running Vite production builds' : ''}.`,
278
414
  ];
279
415
  if (report.mode === 'offline') {
280
416
  lines.push(styles.secondary('Offline snapshots may be stale. Use --live for upload-time semantic validation parity.'));
@@ -320,8 +456,9 @@ const appsValidateHandler = async (pathArgs, options) => {
320
456
  live: options.live,
321
457
  liveProjectUuid,
322
458
  loadLiveIndex,
459
+ build: options.build,
323
460
  })));
324
- const report = (0, exports.buildAppsValidationReport)(apps, options.live);
461
+ const report = (0, exports.buildAppsValidationReport)(apps, options.live, options.build);
325
462
  process.stdout.write(options.format === 'json'
326
463
  ? `${JSON.stringify(report, null, 2)}\n`
327
464
  : (0, exports.renderAppsValidationHuman)(report));
@@ -241,6 +241,155 @@ describe('validateLocalDataApp', () => {
241
241
  expect(result.valid).toBe(true);
242
242
  expect(result.warnings).toContainEqual(expect.objectContaining({ code: 'source_parse' }));
243
243
  });
244
+ it('includes Cloud-parity build failures when --build is enabled', async () => {
245
+ const dir = await makeApp();
246
+ const runBuild = vi.fn().mockResolvedValue([
247
+ {
248
+ code: 'build',
249
+ message: 'Vite build failed: broken import',
250
+ location: null,
251
+ },
252
+ ]);
253
+ const result = await (0, validate_1.validateLocalDataApp)(dir, {
254
+ build: true,
255
+ live: false,
256
+ loadLiveIndex: unusedLiveLoader,
257
+ runBuild,
258
+ });
259
+ expect(runBuild).toHaveBeenCalledOnce();
260
+ expect(result.valid).toBe(false);
261
+ expect(result.errors).toContainEqual(expect.objectContaining({
262
+ code: 'build',
263
+ message: expect.stringContaining('broken import'),
264
+ }));
265
+ });
266
+ });
267
+ describe('validateDataAppBuild', () => {
268
+ it('builds source in an isolated vendored scaffold with bare Vite', async () => {
269
+ const dir = await makeApp();
270
+ await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
271
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(dir);
272
+ const runCommand = vi.fn(async (command) => {
273
+ expect(command).toMatchObject({
274
+ command: 'vite',
275
+ args: ['build'],
276
+ preferLocal: true,
277
+ timeoutMs: 60_000,
278
+ });
279
+ expect(command.args).not.toContain('tsc');
280
+ await expect(fs_1.promises.readFile(path.join(command.cwd, 'vite.config.js'), 'utf-8')).resolves.toContain('defineConfig');
281
+ await expect(fs_1.promises.readFile(path.join(command.cwd, 'src', 'App.tsx'), 'utf-8')).resolves.toContain("query('orders')");
282
+ expect((await fs_1.promises.lstat(path.join(command.cwd, 'node_modules'))).isSymbolicLink()).toBe(true);
283
+ });
284
+ const issues = await (0, validate_1.validateDataAppBuild)({
285
+ appDir: dir,
286
+ bundle,
287
+ runCommand,
288
+ });
289
+ expect(issues).toEqual([]);
290
+ expect(runCommand).toHaveBeenCalledOnce();
291
+ });
292
+ it('restores custom dependencies with a frozen lockfile before Vite', async () => {
293
+ const dir = await makeApp({
294
+ packageJson: JSON.stringify({
295
+ name: 'custom-app',
296
+ version: '1.0.0',
297
+ dependencies: { 'left-pad': '1.3.0' },
298
+ devDependencies: { malicious: '1.0.0' },
299
+ scripts: { build: 'tsc --noEmit' },
300
+ }),
301
+ lockfile: [
302
+ "lockfileVersion: '9.0'",
303
+ 'packages:',
304
+ ' left-pad@1.3.0: {}',
305
+ ].join('\n'),
306
+ });
307
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(dir);
308
+ const commands = [];
309
+ const runCommand = vi.fn(async (command) => {
310
+ commands.push(command);
311
+ if (command.command === 'pnpm') {
312
+ const packageJson = JSON.parse(await fs_1.promises.readFile(path.join(command.cwd, 'package.json'), 'utf-8'));
313
+ expect(packageJson.dependencies).toEqual({
314
+ 'left-pad': '1.3.0',
315
+ });
316
+ expect(packageJson.scripts).toEqual(expect.objectContaining({ build: 'vite build' }));
317
+ expect(packageJson.devDependencies).not.toHaveProperty('malicious');
318
+ }
319
+ });
320
+ const issues = await (0, validate_1.validateDataAppBuild)({
321
+ appDir: dir,
322
+ bundle,
323
+ runCommand,
324
+ });
325
+ expect(issues).toEqual([]);
326
+ expect(commands).toEqual([
327
+ expect.objectContaining({
328
+ command: 'pnpm',
329
+ args: ['install', '--frozen-lockfile', '--ignore-scripts'],
330
+ env: { CI: 'true' },
331
+ timeoutMs: 120_000,
332
+ }),
333
+ expect.objectContaining({
334
+ command: 'vite',
335
+ args: ['build'],
336
+ preferLocal: true,
337
+ }),
338
+ ]);
339
+ });
340
+ it('reports Vite output as a build validation error', async () => {
341
+ const dir = await makeApp();
342
+ await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
343
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(dir);
344
+ const viteError = Object.assign(new Error('Command failed'), {
345
+ stderr: 'src/App.tsx:7: Could not resolve "missing-package"',
346
+ stdout: 'vite v8 building for production',
347
+ });
348
+ const issues = await (0, validate_1.validateDataAppBuild)({
349
+ appDir: dir,
350
+ bundle,
351
+ runCommand: vi.fn().mockRejectedValue(viteError),
352
+ });
353
+ expect(issues).toEqual([
354
+ expect.objectContaining({
355
+ code: 'build',
356
+ message: expect.stringContaining('Could not resolve "missing-package"'),
357
+ }),
358
+ ]);
359
+ });
360
+ it('handles non-object command failures without masking them', async () => {
361
+ const dir = await makeApp();
362
+ await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
363
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(dir);
364
+ const issues = await (0, validate_1.validateDataAppBuild)({
365
+ appDir: dir,
366
+ bundle,
367
+ runCommand: vi.fn().mockRejectedValue(null),
368
+ });
369
+ expect(issues).toEqual([
370
+ expect.objectContaining({
371
+ code: 'build',
372
+ message: expect.stringContaining('Unknown object error'),
373
+ }),
374
+ ]);
375
+ });
376
+ it('requires local template dependencies without installing them', async () => {
377
+ const dir = await makeApp();
378
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(dir);
379
+ const runCommand = vi.fn();
380
+ const issues = await (0, validate_1.validateDataAppBuild)({
381
+ appDir: dir,
382
+ bundle,
383
+ runCommand,
384
+ });
385
+ expect(issues).toEqual([
386
+ expect.objectContaining({
387
+ code: 'dependencies',
388
+ message: expect.stringContaining("Run 'npm install'"),
389
+ }),
390
+ ]);
391
+ expect(runCommand).not.toHaveBeenCalled();
392
+ });
244
393
  });
245
394
  describe('renderAppsValidationHuman', () => {
246
395
  it('makes partial green coverage explicit', () => {
@@ -260,11 +409,12 @@ describe('renderAppsValidationHuman', () => {
260
409
  unanalyzed: 3,
261
410
  },
262
411
  },
263
- ], false);
412
+ ], false, true);
264
413
  const output = (0, validate_1.renderAppsValidationHuman)(report);
265
414
  expect(output).toContain("3 of 14 data-reference call site(s) couldn't be fully analyzed");
266
415
  expect(output).toContain('unresolved values were skipped');
267
416
  expect(output).toContain('Validation passed');
268
417
  expect(output).toContain('Offline snapshots may be stale');
418
+ expect(output).toContain('running Vite production builds');
269
419
  });
270
420
  });
package/dist/index.js CHANGED
@@ -451,6 +451,7 @@ appsProgram
451
451
  .command('validate [paths...]')
452
452
  .description('Validate data app source, manifests, dependencies, and semantic-layer references locally.')
453
453
  .option('--live', 'Validate against fresh explores from each app project instead of its downloaded semantic-layer snapshot.', false)
454
+ .option('--build', 'Run the same Vite production build as Lightdash Cloud.', false)
454
455
  .addOption(new commander_1.Option('--format <format>', 'Output format: human or json')
455
456
  .choices(['human', 'json'])
456
457
  .default('human'))
@@ -2,11 +2,11 @@
2
2
 
3
3
  Two skills in `.claude/skills/` describe how to edit this app:
4
4
  - `lightdash-data-app` — the Lightdash SDK surface (querying data, filters, downloads).
5
- - `developing-data-apps-locally` — the local workflow: edit `src/`, `npm run build`, `lightdash apps preview`, `lightdash upload --apps <slug>`; the SDK is the only data access; `.lightdash/context/` holds the project's semantic layer and theme.
5
+ - `developing-data-apps-locally` — the local workflow: edit `src/`, `lightdash apps validate --build`, `lightdash apps preview`, `lightdash upload --apps <slug>`; the SDK is the only data access; `.lightdash/context/` holds the project's semantic layer and theme.
6
6
 
7
7
  Edit only `src/`. Root config files are read-only reference — the server rebuilds against a trusted template.
8
8
 
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 the local install or build fails, report it as a non-blocking warning with a short failure summary, then continue to upload — never modify machine config, npmrc, or dependency files to force it. The local pre-check does not decide whether the app ships; the server build is authoritative and surfaces its result on the app page.
9
+ Use `lightdash apps validate --build` for the Cloud-parity local pre-check. It builds `src/` against the CLI's trusted template and reports Vite failures as validation errors. Apps made with `lightdash apps create` already have dependencies installed; after `lightdash download`, run `npm install` first if `node_modules` is absent. Never modify machine config, npmrc, or dependency files to force the check to pass. The server still rebuilds on upload and surfaces its final result on the app page.
10
10
 
11
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
 
@@ -4,10 +4,9 @@ Created or downloaded with the Lightdash CLI.
4
4
 
5
5
  ## Edit → build → upload
6
6
  1. Edit files under `src/`.
7
- 2. Run `lightdash apps validate` to check the source, manifest, dependencies, external-connection aliases, and semantic references against the downloaded context. Use `--live` for fresh project explores or `--format json` in CI.
8
- 3. Optionally, run `npm run build` to check it compiles locally. `lightdash apps create` installs the initial dependencies after asking permission; after `lightdash download`, run `npm install` first if `node_modules` is absent. If the local install or build fails in your environment, treat it as a non-blocking warning and continue to upload. Keep the failure details because they may predict a server build problem, but don't change machine or registry config to force the pre-check to pass — the server build on upload is authoritative and surfaces its result on the app page.
9
- 4. Optionally, preview against real data: `lightdash apps preview` (needs a successful `npm install`).
10
- 5. `lightdash upload --apps <slug>` (the `slug` from `lightdash-app.yml`) — Lightdash rebuilds and serves the app.
7
+ 2. Run `lightdash apps validate` to check the source, manifest, dependencies, external-connection aliases, and semantic references against the downloaded context. Use `lightdash apps validate --build` to add the Cloud-parity Vite production build; use `--live` for fresh project explores or `--format json` in CI. `lightdash apps create` installs the initial dependencies after asking permission; after `lightdash download`, run `npm install` first if `node_modules` is absent. Custom dependencies are installed from `pnpm-lock.yaml` with a frozen lockfile and lifecycle scripts disabled. A build failure is a validation error with the Vite output and a non-zero exit.
8
+ 3. Optionally, preview against real data: `lightdash apps preview` (needs a successful `npm install`).
9
+ 4. `lightdash upload --apps <slug>` (the `slug` from `lightdash-app.yml`) — Lightdash rebuilds and serves the app.
11
10
 
12
11
  Local preview uses your existing CLI login through a project- and route-restricted loopback proxy; the credential is not written into this folder or exposed to browser code. It runs local tooling as your OS user (with access to that user's files, including the existing CLI config) and shows data under your own permissions, so preview only source/dependencies you trust. Host-mediated features such as external connections and data-app-viz context still need testing after upload.
13
12
 
@@ -28,16 +28,15 @@ You are editing a Lightdash **data app** that was created or downloaded with the
28
28
  ## The edit → build → upload loop
29
29
 
30
30
  1. Edit files under `src/` only.
31
- 2. Run `lightdash apps validate` to check the source, manifest, dependencies, external-connection aliases, and semantic references against the downloaded context. Use `--live` to check against fresh project explores; use `--format json` in CI. A green run reports any call sites it could not fully analyze instead of silently claiming complete coverage.
32
- 3. Optionally, run `npm run build` to check it compiles. Apps made with `lightdash apps create` already have their initial dependencies installed. After `lightdash download`, if you choose to do the local pre-check and `node_modules` is absent, run `npm install` first. This is an **optional local pre-check** — see below.
33
- 4. `lightdash upload --apps <slug>` (the `slug` from this folder's `lightdash-app.yml`) — the **server** rebuilds and serves the app. The server rebuild, not your local build, is what ships.
31
+ 2. Run `lightdash apps validate` to check the source, manifest, dependencies, external-connection aliases, and semantic references against the downloaded context. Use `lightdash apps validate --build` to add the Cloud-parity Vite production build; use `--live` to check against fresh project explores or `--format json` in CI. A green run reports any call sites it could not fully analyze instead of silently claiming complete coverage.
32
+ 3. `lightdash upload --apps <slug>` (the `slug` from this folder's `lightdash-app.yml`) — the **server** rebuilds and serves the app. The server rebuild remains the final result that ships.
34
33
 
35
- ## The local build is optional — never fight a failing install
34
+ ## Cloud-parity local builds
36
35
 
37
- - 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.
38
- - Treat any standard local install or build failure as a **non-blocking warning**, not as an upload error or a failed task. Preserve a short summary of the local failure because it may predict a server build problem, then continue to upload. Make it explicit that the local pre-check did not complete, but that it does not decide whether the app ships; the server build does.
39
- - Do **not** modify machine configuration, `.npmrc` files, registry settings, or the project's dependency files to force an install to work.
40
- - A missing `node_modules` is a normal state, not a problem to fix. Never run installs just because it is absent.
36
+ - `lightdash apps validate --build` assembles `src/` in an isolated copy of the CLI's trusted template and invokes bare `vite build` — it deliberately does not typecheck because the Cloud build does not typecheck.
37
+ - Apps made with `lightdash apps create` already have the standard template dependencies installed. After `lightdash download`, run `npm install` before requesting `--build` if `node_modules` is absent. Validation does not install the standard dependency set implicitly.
38
+ - Apps with custom dependencies are restored into the isolated build directory with `pnpm install --frozen-lockfile --ignore-scripts`, matching Cloud. An install or Vite failure is a validation error, includes the command output, and exits non-zero.
39
+ - Do **not** modify machine configuration, `.npmrc` files, registry settings, or the project's dependency files to force the check to pass. The server rebuild on upload remains authoritative.
41
40
  - **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.
42
41
  - **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.
43
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "1.89.1",
3
+ "version": "1.91.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.89.1",
45
- "@lightdash/warehouses": "1.89.1"
44
+ "@lightdash/common": "1.91.0",
45
+ "@lightdash/warehouses": "1.91.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",