@lightdash/cli 2.86.0 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/handlers/apps/build.d.ts +6 -0
- package/dist/handlers/apps/build.d.ts.map +1 -0
- package/dist/handlers/apps/build.js +45 -0
- package/dist/handlers/apps/build.test.d.ts +2 -0
- package/dist/handlers/apps/build.test.d.ts.map +1 -0
- package/dist/handlers/apps/build.test.js +252 -0
- package/dist/handlers/apps/pathContainment.d.ts +31 -0
- package/dist/handlers/apps/pathContainment.d.ts.map +1 -0
- package/dist/handlers/apps/pathContainment.js +87 -0
- package/dist/handlers/apps/pathContainment.test.d.ts +2 -0
- package/dist/handlers/apps/pathContainment.test.d.ts.map +1 -0
- package/dist/handlers/apps/pathContainment.test.js +66 -0
- package/dist/handlers/apps/validate.d.ts +2 -0
- package/dist/handlers/apps/validate.d.ts.map +1 -1
- package/dist/handlers/apps/validate.js +22 -3
- package/dist/index.js +7 -0
- package/package.json +3 -3
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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":"AAAA,OAAO,EAgBH,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,qBAAqB,EAC7B,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAgBH,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,qBAAqB,EAC7B,MAAM,mBAAmB,CAAC;AAuB3B,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC7B,OAAO,GACP,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,iCAAiC,GAAG;IAC5C,IAAI,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;IAC1D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,qBAAqB,CAAC;CACnC,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,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IAC7C,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,QAAQ,EAAE,sBAAsB,CAAC;IACjC,oBAAoB,EAAE,iCAAiC,EAAE,CAAC;CAC7D,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,OAAO,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,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,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,WAAW,CAAC;KACvB,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;CACxC,CAAC;AAmLF,KAAK,mBAAmB,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,CACjC,OAAO,EAAE,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;AAmEnB,eAAO,MAAM,oBAAoB,SAAgB;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,KAAG,OAAO,CAAC,mBAAmB,EAAE,CAkJhC,CAAC;AAUF,eAAO,MAAM,4BAA4B,gBACxB,MAAM,KACpB,OAAO,CAAC,mBAAmB,CAqB7B,CAAC;AAEF,eAAO,MAAM,oBAAoB,YACpB,MAAM,WACN,kBAAkB,KAC5B,OAAO,CAAC,mBAAmB,CAkJ7B,CAAC;AAEF,eAAO,MAAM,yBAAyB,SAC5B,mBAAmB,EAAE,QACrB,OAAO,sBAEd,oBA8BF,CAAC;AAYF,eAAO,MAAM,WAAW,UAAW,mBAAmB,KAAG,MAKxD,CAAC;AAiBF,yFAAyF;AACzF,eAAO,MAAM,wBAAwB,YACxB,oBAAoB,CAAC,SAAS,CAAC,KACzC,MAQF,CAAC;AAQF,eAAO,MAAM,yBAAyB,WAC1B,oBAAoB,wBAE7B,MAkDF,CAAC;AAEF,eAAO,MAAM,wBAAwB,WACzB,oBAAoB,KAC7B,MAKM,CAAC;AAEV,eAAO,MAAM,mBAAmB,aAClB,MAAM,EAAE,GAAG,SAAS,WACrB,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CA4Cd,CAAC"}
|
|
@@ -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.
|
|
3
|
+
"version": "2.87.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.
|
|
49
|
-
"@lightdash/warehouses": "2.
|
|
48
|
+
"@lightdash/common": "2.87.0",
|
|
49
|
+
"@lightdash/warehouses": "2.87.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/inquirer": "8.2.4",
|