@lightdash/cli 2.86.0 → 2.88.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.
@@ -0,0 +1,6 @@
1
+ export type AppsBuildOptions = {
2
+ outDir?: string;
3
+ verbose: boolean;
4
+ };
5
+ export declare const appsBuildHandler: (pathArg: string | undefined, options: AppsBuildOptions) => Promise<void>;
6
+ //# sourceMappingURL=build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/build.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,gBAAgB,GAAG;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,gBAAgB,YAChB,MAAM,GAAG,SAAS,WAClB,gBAAgB,KAC1B,OAAO,CAAC,IAAI,CA6Cd,CAAC"}
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.appsBuildHandler = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@lightdash/common");
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
8
+ const styles = tslib_1.__importStar(require("../../styles"));
9
+ const appCodeFiles_1 = require("./appCodeFiles");
10
+ const pathContainment_1 = require("./pathContainment");
11
+ const validate_1 = require("./validate");
12
+ const appsBuildHandler = async (pathArg, options) => {
13
+ globalState_1.default.setVerbose(options.verbose);
14
+ const appDir = path.resolve(process.cwd(), pathArg ?? '.');
15
+ const outDir = path.resolve(process.cwd(), options.outDir ?? path.join(appDir, 'dist'));
16
+ (0, pathContainment_1.assertOutDirDoesNotContainAppDir)(appDir, outDir);
17
+ // The lexical check above can be bypassed by symlinks (an appDir that is
18
+ // itself a symlink into outDir's real tree, or an outDir reached through
19
+ // a symlinked ancestor whose target contains appDir), so re-run the same
20
+ // check on the canonicalized paths before doing anything destructive.
21
+ const [canonicalAppDir, canonicalOutDir] = await Promise.all([
22
+ (0, pathContainment_1.canonicalizeForContainment)(appDir),
23
+ (0, pathContainment_1.canonicalizeForContainment)(outDir),
24
+ ]);
25
+ (0, pathContainment_1.assertOutDirDoesNotContainAppDir)(canonicalAppDir, canonicalOutDir);
26
+ let bundle;
27
+ try {
28
+ bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
29
+ }
30
+ catch (error) {
31
+ throw new common_1.ParameterError((0, common_1.getErrorMessage)(error));
32
+ }
33
+ if (bundle.files.length === 0) {
34
+ throw new common_1.ParameterError(`App bundle has no files under src/ to build: ${appDir}`);
35
+ }
36
+ const issues = await (0, validate_1.validateDataAppBuild)({ appDir, bundle, outDir });
37
+ if (issues.length > 0) {
38
+ for (const buildIssue of issues) {
39
+ globalState_1.default.log(`${styles.error('error')} ${(0, validate_1.formatIssue)(buildIssue)}`);
40
+ }
41
+ throw new common_1.ParameterError(`Build failed with ${issues.length} error(s).`);
42
+ }
43
+ globalState_1.default.log(styles.success(`✓ Built ${appDir} → ${outDir}`));
44
+ };
45
+ exports.appsBuildHandler = appsBuildHandler;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=build.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/build.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const common_1 = require("@lightdash/common");
5
+ const fs_1 = require("fs");
6
+ const os = tslib_1.__importStar(require("os"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const appCodeFiles_1 = require("./appCodeFiles");
9
+ const build_1 = require("./build");
10
+ const pathContainment_1 = require("./pathContainment");
11
+ const validate_1 = require("./validate");
12
+ const describePosix = process.platform === 'win32' ? describe.skip : describe;
13
+ const defaultManifest = {
14
+ codeVersion: 1,
15
+ projectUuid: 'project-uuid',
16
+ slug: 'orders-app',
17
+ version: 1,
18
+ name: 'Orders app',
19
+ description: '',
20
+ template: null,
21
+ downloadedAt: '2026-08-01T00:00:00.000Z',
22
+ externalConnections: [],
23
+ };
24
+ const validSource = `
25
+ import { query } from '@lightdash/query-sdk';
26
+ query('orders').dimensions(['status']);
27
+ `;
28
+ const makeApp = async () => {
29
+ const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-build-'));
30
+ const code = {
31
+ manifest: defaultManifest,
32
+ files: [
33
+ {
34
+ path: 'src/App.tsx',
35
+ contentBase64: Buffer.from(validSource).toString('base64'),
36
+ },
37
+ ],
38
+ };
39
+ await (0, appCodeFiles_1.writeBundleToDir)(dir, code);
40
+ await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
41
+ return dir;
42
+ };
43
+ const makeOutDir = async () => path.join(await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-build-outdir-')), 'dist');
44
+ // Writes an app bundle into an already-existing directory, for symlink
45
+ // bypass tests that need control over exactly where the real files live.
46
+ const writeAppBundleTo = async (dir) => {
47
+ const code = {
48
+ manifest: defaultManifest,
49
+ files: [
50
+ {
51
+ path: 'src/App.tsx',
52
+ contentBase64: Buffer.from(validSource).toString('base64'),
53
+ },
54
+ ],
55
+ };
56
+ await (0, appCodeFiles_1.writeBundleToDir)(dir, code);
57
+ await fs_1.promises.mkdir(path.join(dir, 'node_modules'));
58
+ };
59
+ // Fakes a successful `vite build` by writing dist/index.html into the build
60
+ // dir it receives, without actually running Vite.
61
+ const fakeSuccessfulViteBuild = async (command) => {
62
+ if (command.command !== 'vite')
63
+ return;
64
+ await fs_1.promises.mkdir(path.join(command.cwd, 'dist'));
65
+ await fs_1.promises.writeFile(path.join(command.cwd, 'dist', 'index.html'), '<html>built</html>');
66
+ };
67
+ describe('validateDataAppBuild outDir', () => {
68
+ it('copies the built dist output into outDir and still cleans the temp build dir', async () => {
69
+ const appDir = await makeApp();
70
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
71
+ const outDir = await makeOutDir();
72
+ const buildDirs = [];
73
+ const runCommand = async (command) => {
74
+ if (command.command === 'vite')
75
+ buildDirs.push(command.cwd);
76
+ await fakeSuccessfulViteBuild(command);
77
+ };
78
+ const issues = await (0, validate_1.validateDataAppBuild)({
79
+ appDir,
80
+ bundle,
81
+ runCommand,
82
+ outDir,
83
+ });
84
+ expect(issues).toEqual([]);
85
+ await expect(fs_1.promises.readFile(path.join(outDir, 'index.html'), 'utf-8')).resolves.toBe('<html>built</html>');
86
+ expect(buildDirs).toHaveLength(1);
87
+ await expect(fs_1.promises.stat(buildDirs[0])).rejects.toThrow();
88
+ });
89
+ it('clears stale files from a previous build before copying the new output', async () => {
90
+ const appDir = await makeApp();
91
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
92
+ const outDir = await makeOutDir();
93
+ await fs_1.promises.mkdir(outDir, { recursive: true });
94
+ await fs_1.promises.writeFile(path.join(outDir, 'stale-chunk-abc123.js'), '// stale');
95
+ const issues = await (0, validate_1.validateDataAppBuild)({
96
+ appDir,
97
+ bundle,
98
+ runCommand: fakeSuccessfulViteBuild,
99
+ outDir,
100
+ });
101
+ expect(issues).toEqual([]);
102
+ await expect(fs_1.promises.readFile(path.join(outDir, 'index.html'), 'utf-8')).resolves.toBe('<html>built</html>');
103
+ await expect(fs_1.promises.stat(path.join(outDir, 'stale-chunk-abc123.js'))).rejects.toThrow();
104
+ });
105
+ it('does not copy anything and leaves a pre-existing outDir untouched when the build fails', async () => {
106
+ const appDir = await makeApp();
107
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
108
+ const outDir = await makeOutDir();
109
+ await fs_1.promises.mkdir(outDir, { recursive: true });
110
+ await fs_1.promises.writeFile(path.join(outDir, 'previous-build.js'), '// kept');
111
+ const viteError = Object.assign(new Error('Command failed'), {
112
+ stderr: 'src/App.tsx:1: broken import',
113
+ });
114
+ const issues = await (0, validate_1.validateDataAppBuild)({
115
+ appDir,
116
+ bundle,
117
+ outDir,
118
+ runCommand: vi.fn().mockRejectedValue(viteError),
119
+ });
120
+ expect(issues).toEqual([
121
+ expect.objectContaining({
122
+ code: 'build',
123
+ message: expect.stringContaining('broken import'),
124
+ }),
125
+ ]);
126
+ await expect(fs_1.promises.readFile(path.join(outDir, 'previous-build.js'), 'utf-8')).resolves.toBe('// kept');
127
+ });
128
+ });
129
+ describe('appsBuildHandler', () => {
130
+ it('fails fast with a ParameterError when the directory has no lightdash-app.yml', async () => {
131
+ const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-build-missing-'));
132
+ await expect((0, build_1.appsBuildHandler)(dir, { verbose: false })).rejects.toThrow(common_1.ParameterError);
133
+ });
134
+ it('fails fast with a ParameterError when the bundle has no files under src/', async () => {
135
+ const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-build-empty-'));
136
+ await (0, appCodeFiles_1.writeBundleToDir)(dir, { manifest: defaultManifest, files: [] });
137
+ await expect((0, build_1.appsBuildHandler)(dir, { verbose: false })).rejects.toThrow(/no files under src\//);
138
+ });
139
+ it('refuses --out-dir equal to the app directory before doing any build work', async () => {
140
+ const appDir = await makeApp();
141
+ await expect((0, build_1.appsBuildHandler)(appDir, { outDir: appDir, verbose: false })).rejects.toThrow(common_1.ParameterError);
142
+ // The app bundle must still be intact — no wipe happened.
143
+ await expect(fs_1.promises.stat(path.join(appDir, 'src', 'App.tsx'))).resolves.toBeDefined();
144
+ });
145
+ it('refuses --out-dir that is a parent of the app directory before doing any build work', async () => {
146
+ const parentDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-build-parent-'));
147
+ const appDir = path.join(parentDir, 'app');
148
+ await fs_1.promises.mkdir(appDir);
149
+ const code = {
150
+ manifest: defaultManifest,
151
+ files: [
152
+ {
153
+ path: 'src/App.tsx',
154
+ contentBase64: Buffer.from(validSource).toString('base64'),
155
+ },
156
+ ],
157
+ };
158
+ await (0, appCodeFiles_1.writeBundleToDir)(appDir, code);
159
+ await fs_1.promises.mkdir(path.join(appDir, 'node_modules'));
160
+ await expect((0, build_1.appsBuildHandler)(appDir, {
161
+ outDir: parentDir,
162
+ verbose: false,
163
+ })).rejects.toThrow(common_1.ParameterError);
164
+ await expect(fs_1.promises.stat(path.join(appDir, 'src', 'App.tsx'))).resolves.toBeDefined();
165
+ });
166
+ });
167
+ describe('assertOutDirDoesNotContainAppDir', () => {
168
+ it('throws when outDir equals appDir', () => {
169
+ expect(() => (0, pathContainment_1.assertOutDirDoesNotContainAppDir)('/repo/apps/orders', '/repo/apps/orders')).toThrow(common_1.ParameterError);
170
+ });
171
+ it('throws when appDir is nested inside outDir', () => {
172
+ expect(() => (0, pathContainment_1.assertOutDirDoesNotContainAppDir)('/repo/apps/orders', '/repo/apps')).toThrow(common_1.ParameterError);
173
+ });
174
+ it('throws when outDir is the filesystem root and appDir is anywhere under it', () => {
175
+ expect(() => (0, pathContainment_1.assertOutDirDoesNotContainAppDir)('/repo/apps/orders', '/')).toThrow(common_1.ParameterError);
176
+ });
177
+ it('does not throw for a normal sibling out-dir', () => {
178
+ expect(() => (0, pathContainment_1.assertOutDirDoesNotContainAppDir)('/repo/apps/orders', '/repo/build-output')).not.toThrow();
179
+ });
180
+ it('does not throw for the default outDir nested inside appDir (appDir/dist)', () => {
181
+ expect(() => (0, pathContainment_1.assertOutDirDoesNotContainAppDir)('/repo/apps/orders', '/repo/apps/orders/dist')).not.toThrow();
182
+ });
183
+ });
184
+ describePosix('out-dir containment guard: symlink bypasses', () => {
185
+ it('refuses when appDir is a symlink whose real location is inside outDir', async () => {
186
+ // outDir already exists as a real directory, and the "app" the CLI
187
+ // is asked to build actually lives inside it — reached only via a
188
+ // symlink from elsewhere, so the lexical check alone would miss it.
189
+ const outDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-outdir-'));
190
+ const realAppDir = path.join(outDir, 'app-real');
191
+ await fs_1.promises.mkdir(realAppDir);
192
+ await writeAppBundleTo(realAppDir);
193
+ const linkParent = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-applink-'));
194
+ const appLink = path.join(linkParent, 'app-link');
195
+ await fs_1.promises.symlink(realAppDir, appLink);
196
+ await expect((0, build_1.appsBuildHandler)(appLink, { outDir, verbose: false })).rejects.toThrow(common_1.ParameterError);
197
+ await expect(fs_1.promises.stat(path.join(realAppDir, 'src', 'App.tsx'))).resolves.toBeDefined();
198
+ });
199
+ it('refuses when outDir is reached via a symlinked ancestor whose target contains appDir', async () => {
200
+ const parentDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-parent-'));
201
+ const appDir = path.join(parentDir, 'app');
202
+ await fs_1.promises.mkdir(appDir);
203
+ await writeAppBundleTo(appDir);
204
+ const linkParent = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-outlink-'));
205
+ const outDirViaSymlink = path.join(linkParent, 'link-to-parent');
206
+ await fs_1.promises.symlink(parentDir, outDirViaSymlink);
207
+ await expect((0, build_1.appsBuildHandler)(appDir, {
208
+ outDir: outDirViaSymlink,
209
+ verbose: false,
210
+ })).rejects.toThrow(common_1.ParameterError);
211
+ await expect(fs_1.promises.stat(path.join(appDir, 'src', 'App.tsx'))).resolves.toBeDefined();
212
+ });
213
+ it('allows a legitimate sibling outDir reached via a symlinked ancestor', async () => {
214
+ const rootDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-root-'));
215
+ const appDir = path.join(rootDir, 'app-real');
216
+ await fs_1.promises.mkdir(appDir);
217
+ await writeAppBundleTo(appDir);
218
+ const realOutDir = path.join(rootDir, 'out-real');
219
+ const linkParent = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-rootlink-'));
220
+ const linkToRoot = path.join(linkParent, 'link-to-root');
221
+ await fs_1.promises.symlink(rootDir, linkToRoot);
222
+ const outDirViaSymlink = path.join(linkToRoot, 'out-real');
223
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
224
+ const issues = await (0, validate_1.validateDataAppBuild)({
225
+ appDir,
226
+ bundle,
227
+ outDir: outDirViaSymlink,
228
+ runCommand: fakeSuccessfulViteBuild,
229
+ });
230
+ expect(issues).toEqual([]);
231
+ await expect(fs_1.promises.readFile(path.join(realOutDir, 'index.html'), 'utf-8')).resolves.toBe('<html>built</html>');
232
+ });
233
+ it('validateDataAppBuild rechecks containment itself when called directly with a dangerous pair', async () => {
234
+ // Bypasses appsBuildHandler's guard entirely, simulating any future
235
+ // caller of validateDataAppBuild that doesn't run the check itself.
236
+ const parentDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-direct-parent-'));
237
+ const appDir = path.join(parentDir, 'app');
238
+ await fs_1.promises.mkdir(appDir);
239
+ await writeAppBundleTo(appDir);
240
+ const linkParent = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-symlink-direct-link-'));
241
+ const outDirViaSymlink = path.join(linkParent, 'link-to-parent');
242
+ await fs_1.promises.symlink(parentDir, outDirViaSymlink);
243
+ const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
244
+ await expect((0, validate_1.validateDataAppBuild)({
245
+ appDir,
246
+ bundle,
247
+ outDir: outDirViaSymlink,
248
+ runCommand: fakeSuccessfulViteBuild,
249
+ })).rejects.toThrow(common_1.ParameterError);
250
+ await expect(fs_1.promises.stat(path.join(appDir, 'src', 'App.tsx'))).resolves.toBeDefined();
251
+ });
252
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Resolves `absolutePath` through any symlinks so it can be safely compared
3
+ * for containment against another canonicalized path. The path may not
4
+ * exist yet (e.g. an --out-dir that hasn't been built to), so this walks up
5
+ * to the nearest existing ancestor, realpaths that ancestor, and re-joins
6
+ * the non-existent remainder lexically.
7
+ *
8
+ * Any realpath failure other than "this segment doesn't exist" (permission
9
+ * denied, a symlink loop, ...) is rethrown rather than swallowed. Callers
10
+ * must treat that as "cannot prove this path is safe" and refuse the
11
+ * operation — never fall back to the un-canonicalized path.
12
+ */
13
+ export declare const canonicalizeForContainment: (absolutePath: string) => Promise<string>;
14
+ /**
15
+ * The build wipes outDir before copying the new output in. If outDir is (or
16
+ * contains) appDir, that wipe deletes the app source being built. Both
17
+ * arguments must already be resolved to absolute, canonical (symlink-free)
18
+ * paths — see `canonicalizeForContainment`.
19
+ */
20
+ export declare const assertOutDirDoesNotContainAppDir: (appDir: string, outDir: string) => void;
21
+ /**
22
+ * Defense-in-depth recheck for the deletion site itself: outDir may have
23
+ * changed since an earlier containment check ran, or a caller may not have
24
+ * run one at all. If outDir doesn't exist yet there is nothing to delete,
25
+ * so this is a no-op. If it exists, both paths are canonicalized and
26
+ * re-checked with the same predicate as the pre-build guard. Throws
27
+ * ParameterError on violation; never trusts the caller's un-canonicalized
28
+ * paths.
29
+ */
30
+ export declare const assertOutDirSafeToClear: (appDir: string, outDir: string) => Promise<void>;
31
+ //# sourceMappingURL=pathContainment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathContainment.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/pathContainment.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,iBACrB,MAAM,KACrB,OAAO,CAAC,MAAM,CAuBhB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,WACjC,MAAM,UACN,MAAM,KACf,IAWF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,WACxB,MAAM,UACN,MAAM,KACf,OAAO,CAAC,IAAI,CAcd,CAAC"}
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertOutDirSafeToClear = exports.assertOutDirDoesNotContainAppDir = exports.canonicalizeForContainment = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@lightdash/common");
6
+ const fs_1 = require("fs");
7
+ const path = tslib_1.__importStar(require("path"));
8
+ /**
9
+ * Resolves `absolutePath` through any symlinks so it can be safely compared
10
+ * for containment against another canonicalized path. The path may not
11
+ * exist yet (e.g. an --out-dir that hasn't been built to), so this walks up
12
+ * to the nearest existing ancestor, realpaths that ancestor, and re-joins
13
+ * the non-existent remainder lexically.
14
+ *
15
+ * Any realpath failure other than "this segment doesn't exist" (permission
16
+ * denied, a symlink loop, ...) is rethrown rather than swallowed. Callers
17
+ * must treat that as "cannot prove this path is safe" and refuse the
18
+ * operation — never fall back to the un-canonicalized path.
19
+ */
20
+ const canonicalizeForContainment = async (absolutePath) => {
21
+ let current = absolutePath;
22
+ const missingSegments = [];
23
+ while (true) {
24
+ try {
25
+ // eslint-disable-next-line no-await-in-loop
26
+ const real = await fs_1.promises.realpath(current);
27
+ return missingSegments.length > 0
28
+ ? path.join(real, ...missingSegments.reverse())
29
+ : real;
30
+ }
31
+ catch (error) {
32
+ if (error.code !== 'ENOENT') {
33
+ throw error;
34
+ }
35
+ const parent = path.dirname(current);
36
+ if (parent === current) {
37
+ // Reached the filesystem root and even it didn't resolve.
38
+ throw error;
39
+ }
40
+ missingSegments.push(path.basename(current));
41
+ current = parent;
42
+ }
43
+ }
44
+ };
45
+ exports.canonicalizeForContainment = canonicalizeForContainment;
46
+ /**
47
+ * The build wipes outDir before copying the new output in. If outDir is (or
48
+ * contains) appDir, that wipe deletes the app source being built. Both
49
+ * arguments must already be resolved to absolute, canonical (symlink-free)
50
+ * paths — see `canonicalizeForContainment`.
51
+ */
52
+ const assertOutDirDoesNotContainAppDir = (appDir, outDir) => {
53
+ const relativeAppDirFromOutDir = path.relative(outDir, appDir);
54
+ const appDirIsInsideOutDir = relativeAppDirFromOutDir !== '' &&
55
+ !relativeAppDirFromOutDir.startsWith('..') &&
56
+ !path.isAbsolute(relativeAppDirFromOutDir);
57
+ if (outDir === appDir || appDirIsInsideOutDir) {
58
+ throw new common_1.ParameterError('--out-dir must not contain the app directory (would delete the app source on build)');
59
+ }
60
+ };
61
+ exports.assertOutDirDoesNotContainAppDir = assertOutDirDoesNotContainAppDir;
62
+ /**
63
+ * Defense-in-depth recheck for the deletion site itself: outDir may have
64
+ * changed since an earlier containment check ran, or a caller may not have
65
+ * run one at all. If outDir doesn't exist yet there is nothing to delete,
66
+ * so this is a no-op. If it exists, both paths are canonicalized and
67
+ * re-checked with the same predicate as the pre-build guard. Throws
68
+ * ParameterError on violation; never trusts the caller's un-canonicalized
69
+ * paths.
70
+ */
71
+ const assertOutDirSafeToClear = async (appDir, outDir) => {
72
+ let canonicalOutDir;
73
+ try {
74
+ canonicalOutDir = await fs_1.promises.realpath(outDir);
75
+ }
76
+ catch (error) {
77
+ if (error.code === 'ENOENT') {
78
+ // Nothing exists at outDir yet, so there's nothing to delete.
79
+ return;
80
+ }
81
+ // Cannot prove outDir is safe to clear — refuse rather than guess.
82
+ throw error;
83
+ }
84
+ const canonicalAppDir = await (0, exports.canonicalizeForContainment)(appDir);
85
+ (0, exports.assertOutDirDoesNotContainAppDir)(canonicalAppDir, canonicalOutDir);
86
+ };
87
+ exports.assertOutDirSafeToClear = assertOutDirSafeToClear;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pathContainment.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathContainment.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/pathContainment.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const common_1 = require("@lightdash/common");
5
+ const fs_1 = require("fs");
6
+ const os = tslib_1.__importStar(require("os"));
7
+ const path = tslib_1.__importStar(require("path"));
8
+ const pathContainment_1 = require("./pathContainment");
9
+ const describePosix = process.platform === 'win32' ? describe.skip : describe;
10
+ const mkTmpDir = (prefix) => fs_1.promises.mkdtemp(path.join(os.tmpdir(), prefix));
11
+ describePosix('canonicalizeForContainment', () => {
12
+ it('returns the realpath of an existing directory unchanged when there are no symlinks', async () => {
13
+ const dir = await mkTmpDir('ld-canon-plain-');
14
+ await expect((0, pathContainment_1.canonicalizeForContainment)(dir)).resolves.toBe(await fs_1.promises.realpath(dir));
15
+ });
16
+ it('resolves a symlinked existing directory to its real target', async () => {
17
+ const target = await mkTmpDir('ld-canon-target-');
18
+ const linkParent = await mkTmpDir('ld-canon-linkparent-');
19
+ const link = path.join(linkParent, 'link');
20
+ await fs_1.promises.symlink(target, link);
21
+ await expect((0, pathContainment_1.canonicalizeForContainment)(link)).resolves.toBe(await fs_1.promises.realpath(target));
22
+ });
23
+ it('walks up to the nearest existing ancestor and rejoins non-existent segments', async () => {
24
+ const target = await mkTmpDir('ld-canon-target-');
25
+ const linkParent = await mkTmpDir('ld-canon-linkparent-');
26
+ const link = path.join(linkParent, 'link');
27
+ await fs_1.promises.symlink(target, link);
28
+ const nonExistent = path.join(link, 'not', 'yet', 'created');
29
+ await expect((0, pathContainment_1.canonicalizeForContainment)(nonExistent)).resolves.toBe(path.join(await fs_1.promises.realpath(target), 'not', 'yet', 'created'));
30
+ });
31
+ it('rethrows a non-ENOENT realpath failure instead of treating it as non-existent', async () => {
32
+ const dir = await mkTmpDir('ld-canon-notdir-');
33
+ const filePath = path.join(dir, 'a-file');
34
+ await fs_1.promises.writeFile(filePath, 'not a directory');
35
+ const impossiblePath = path.join(filePath, 'nested', 'more');
36
+ await expect((0, pathContainment_1.canonicalizeForContainment)(impossiblePath)).rejects.toMatchObject({ code: 'ENOTDIR' });
37
+ });
38
+ });
39
+ describePosix('assertOutDirSafeToClear', () => {
40
+ it('is a no-op when outDir does not exist yet', async () => {
41
+ const appDir = await mkTmpDir('ld-safeclear-app-');
42
+ const outDirParent = await mkTmpDir('ld-safeclear-outparent-');
43
+ const outDir = path.join(outDirParent, 'dist');
44
+ await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, outDir)).resolves.toBeUndefined();
45
+ });
46
+ it('does not throw for a disjoint existing outDir', async () => {
47
+ const appDir = await mkTmpDir('ld-safeclear-app-');
48
+ const outDir = await mkTmpDir('ld-safeclear-outdir-');
49
+ await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, outDir)).resolves.toBeUndefined();
50
+ });
51
+ it('throws ParameterError when an existing outDir canonically contains appDir', async () => {
52
+ const outDir = await mkTmpDir('ld-safeclear-container-');
53
+ const appDir = path.join(outDir, 'app');
54
+ await fs_1.promises.mkdir(appDir);
55
+ await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, outDir)).rejects.toThrow(common_1.ParameterError);
56
+ });
57
+ it('throws ParameterError when outDir is reached via a symlinked ancestor that contains appDir', async () => {
58
+ const parentDir = await mkTmpDir('ld-safeclear-parent-');
59
+ const appDir = path.join(parentDir, 'app');
60
+ await fs_1.promises.mkdir(appDir);
61
+ const linkParent = await mkTmpDir('ld-safeclear-linkparent-');
62
+ const link = path.join(linkParent, 'link-to-parent');
63
+ await fs_1.promises.symlink(parentDir, link);
64
+ await expect((0, pathContainment_1.assertOutDirSafeToClear)(appDir, link)).rejects.toThrow(common_1.ParameterError);
65
+ });
66
+ });
@@ -69,10 +69,12 @@ export declare const validateDataAppBuild: (args: {
69
69
  appDir: string;
70
70
  bundle: DataAppCode;
71
71
  runCommand?: RunDataAppBuildCommand;
72
+ outDir?: string;
72
73
  }) => Promise<AppsValidationIssue[]>;
73
74
  export declare const fetchLiveDataAppExploreIndex: (projectUuid: string) => Promise<DataAppExploreIndex>;
74
75
  export declare const validateLocalDataApp: (appPath: string, options: ValidateAppOptions) => Promise<AppValidationResult>;
75
76
  export declare const buildAppsValidationReport: (apps: AppValidationResult[], live: boolean, build?: boolean) => AppsValidationReport;
77
+ export declare const formatIssue: (entry: AppsValidationIssue) => string;
76
78
  /** "2 data app(s)", "1 custom chart type(s)", or both — driven by manifest templates. */
77
79
  export declare const describeValidatedBundles: (summary: AppsValidationReport['summary']) => string;
78
80
  export declare const renderAppsValidationHuman: (report: AppsValidationReport, verbose?: boolean) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/validate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/validate.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appsValidateHandler = exports.renderAppsValidationJson = exports.renderAppsValidationHuman = exports.describeValidatedBundles = exports.buildAppsValidationReport = exports.validateLocalDataApp = exports.fetchLiveDataAppExploreIndex = exports.validateDataAppBuild = void 0;
3
+ exports.appsValidateHandler = exports.renderAppsValidationJson = exports.renderAppsValidationHuman = exports.describeValidatedBundles = exports.formatIssue = 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
6
  const execa_1 = tslib_1.__importDefault(require("execa"));
@@ -14,6 +14,7 @@ const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
14
14
  const styles = tslib_1.__importStar(require("../../styles"));
15
15
  const apiClient_1 = require("../dbt/apiClient");
16
16
  const appCodeFiles_1 = require("./appCodeFiles");
17
+ const pathContainment_1 = require("./pathContainment");
17
18
  const scaffolding_1 = require("./scaffolding");
18
19
  const emptyCoverage = () => ({
19
20
  callSites: 0,
@@ -239,9 +240,26 @@ const validateDataAppBuild = async (args) => {
239
240
  issue('build', `Vite build failed:\n${commandFailureOutput(error)}`),
240
241
  ];
241
242
  }
243
+ if (args.outDir !== undefined) {
244
+ // Defense in depth: this deletion site must not trust that a
245
+ // caller already checked containment (or that outDir hasn't
246
+ // changed since it did). Re-resolve symlinks and refuse rather
247
+ // than delete anything we can't prove is safe.
248
+ await (0, pathContainment_1.assertOutDirSafeToClear)(args.appDir, args.outDir);
249
+ // Vite emits content-hashed chunk filenames, so a merge-only copy
250
+ // would leave stale chunks from a previous build behind. Clear
251
+ // outDir immediately before copying a successful build into it —
252
+ // never on the failure path, which must leave it untouched.
253
+ await fs_1.promises.rm(args.outDir, { recursive: true, force: true });
254
+ await fs_1.promises.cp(path.join(buildDir, 'dist'), args.outDir, {
255
+ recursive: true,
256
+ });
257
+ }
242
258
  return [];
243
259
  }
244
260
  catch (error) {
261
+ if (error instanceof common_1.ParameterError)
262
+ throw error;
245
263
  return [
246
264
  issue('build', `Could not prepare the Vite build: ${(0, common_1.getErrorMessage)(error)}`),
247
265
  ];
@@ -420,6 +438,7 @@ const formatIssue = (entry) => {
420
438
  : '';
421
439
  return `${prefix}${entry.message}`;
422
440
  };
441
+ exports.formatIssue = formatIssue;
423
442
  const referenceKindLabels = {
424
443
  query: 'query',
425
444
  savedChart: 'saved chart',
@@ -466,10 +485,10 @@ const renderAppsValidationHuman = (report, verbose = false) => {
466
485
  }
467
486
  }
468
487
  for (const warning of app.warnings) {
469
- lines.push(` ${styles.warning('warning')} ${formatIssue(warning)}`);
488
+ lines.push(` ${styles.warning('warning')} ${(0, exports.formatIssue)(warning)}`);
470
489
  }
471
490
  for (const error of app.errors) {
472
- lines.push(` ${styles.error('error')} ${formatIssue(error)}`);
491
+ lines.push(` ${styles.error('error')} ${(0, exports.formatIssue)(error)}`);
473
492
  }
474
493
  }
475
494
  lines.push('');
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ const uuid_1 = require("uuid");
8
8
  const env_1 = require("./env");
9
9
  const error_1 = require("./error");
10
10
  const globalState_1 = tslib_1.__importDefault(require("./globalState"));
11
+ const build_1 = require("./handlers/apps/build");
11
12
  const createApp_1 = require("./handlers/apps/createApp");
12
13
  const preview_1 = require("./handlers/apps/preview");
13
14
  const validate_1 = require("./handlers/apps/validate");
@@ -473,6 +474,12 @@ appsProgram
473
474
  .default('human'))
474
475
  .option('--verbose', undefined, false)
475
476
  .action(validate_1.appsValidateHandler);
477
+ appsProgram
478
+ .command('build [path]')
479
+ .description('Build a data app or custom chart type with the same Vite production build as Lightdash Cloud, and keep the output.')
480
+ .option('--out-dir <dir>', 'Directory to write the built dist output to. Relative paths resolve against the current working directory (default: <path>/dist)')
481
+ .option('--verbose', undefined, false)
482
+ .action(build_1.appsBuildHandler);
476
483
  commander_1.program
477
484
  .command('deploy')
478
485
  .description('Compiles and deploys a Lightdash project')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "2.86.0",
3
+ "version": "2.88.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "unique-names-generator": "4.7.1",
46
46
  "uuid": "11.1.1",
47
47
  "yaml": "2.8.3",
48
- "@lightdash/common": "2.86.0",
49
- "@lightdash/warehouses": "2.86.0"
48
+ "@lightdash/warehouses": "2.88.0",
49
+ "@lightdash/common": "2.88.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/inquirer": "8.2.4",