@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.
- package/README.md +7 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/handlers/apps/scaffolding.d.ts +1 -0
- package/dist/handlers/apps/scaffolding.d.ts.map +1 -1
- package/dist/handlers/apps/scaffolding.js +10 -12
- package/dist/handlers/apps/scaffolding.test.js +19 -6
- package/dist/handlers/apps/validate.d.ts +24 -3
- package/dist/handlers/apps/validate.d.ts.map +1 -1
- package/dist/handlers/apps/validate.js +142 -5
- package/dist/handlers/apps/validate.test.js +151 -1
- package/dist/index.js +1 -0
- package/dist/vendor/authoring/AGENTS.md.tmpl +2 -2
- package/dist/vendor/authoring/README.md.tmpl +3 -4
- package/dist/vendor/authoring/developing-data-apps-locally/SKILL.md +7 -8
- package/package.json +3 -3
|
@@ -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,
|
|
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
|
|
105
|
-
|
|
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
|
|
78
|
-
expect(text('.claude/skills/developing-data-apps-locally/SKILL.md')).toContain('run `npm install`
|
|
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('
|
|
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('
|
|
89
|
-
expect(text(path)).toContain('
|
|
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,
|
|
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
|
-
|
|
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/`, `
|
|
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
|
-
|
|
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,
|
|
9
|
-
4.
|
|
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
|
|
32
|
-
3.
|
|
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
|
-
##
|
|
34
|
+
## Cloud-parity local builds
|
|
36
35
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
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.
|
|
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.
|
|
45
|
-
"@lightdash/warehouses": "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",
|