@lightdash/cli 2.228.0 → 2.229.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.build +1 -0
- package/package.json +4 -4
- package/dist/.tsbuildinfo +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -72
- package/dist/dbt/context.test.d.ts +0 -2
- package/dist/dbt/context.test.d.ts.map +0 -1
- package/dist/dbt/context.test.js +0 -152
- package/dist/dbt/manifest.test.d.ts +0 -2
- package/dist/dbt/manifest.test.d.ts.map +0 -1
- package/dist/dbt/manifest.test.js +0 -170
- package/dist/dbt/models.test.d.ts +0 -2
- package/dist/dbt/models.test.d.ts.map +0 -1
- package/dist/dbt/models.test.js +0 -139
- package/dist/dbt/profiles.test.d.ts +0 -2
- package/dist/dbt/profiles.test.d.ts.map +0 -1
- package/dist/dbt/profiles.test.js +0 -50
- package/dist/dbt/targets/athena.test.d.ts +0 -2
- package/dist/dbt/targets/athena.test.d.ts.map +0 -1
- package/dist/dbt/targets/athena.test.js +0 -78
- package/dist/dbt/targets/duckdb.test.d.ts +0 -2
- package/dist/dbt/targets/duckdb.test.d.ts.map +0 -1
- package/dist/dbt/targets/duckdb.test.js +0 -149
- package/dist/dbt/targets/redshift.test.d.ts +0 -2
- package/dist/dbt/targets/redshift.test.d.ts.map +0 -1
- package/dist/dbt/targets/redshift.test.js +0 -198
- package/dist/dbt/templating.test.d.ts +0 -2
- package/dist/dbt/templating.test.d.ts.map +0 -1
- package/dist/dbt/templating.test.js +0 -51
- package/dist/dbt/validation.test.d.ts +0 -2
- package/dist/dbt/validation.test.d.ts.map +0 -1
- package/dist/dbt/validation.test.js +0 -67
- package/dist/handlers/apps/appCodeFiles.test.d.ts +0 -2
- package/dist/handlers/apps/appCodeFiles.test.d.ts.map +0 -1
- package/dist/handlers/apps/appCodeFiles.test.js +0 -391
- package/dist/handlers/apps/appsDownload.test.d.ts +0 -2
- package/dist/handlers/apps/appsDownload.test.d.ts.map +0 -1
- package/dist/handlers/apps/appsDownload.test.js +0 -639
- package/dist/handlers/apps/build.test.d.ts +0 -2
- package/dist/handlers/apps/build.test.d.ts.map +0 -1
- package/dist/handlers/apps/build.test.js +0 -252
- package/dist/handlers/apps/createApp.test.d.ts +0 -2
- package/dist/handlers/apps/createApp.test.d.ts.map +0 -1
- package/dist/handlers/apps/createApp.test.js +0 -340
- package/dist/handlers/apps/pathContainment.test.d.ts +0 -2
- package/dist/handlers/apps/pathContainment.test.d.ts.map +0 -1
- package/dist/handlers/apps/pathContainment.test.js +0 -66
- package/dist/handlers/apps/preview.test.d.ts +0 -2
- package/dist/handlers/apps/preview.test.d.ts.map +0 -1
- package/dist/handlers/apps/preview.test.js +0 -353
- package/dist/handlers/apps/previewProxy.test.d.ts +0 -2
- package/dist/handlers/apps/previewProxy.test.d.ts.map +0 -1
- package/dist/handlers/apps/previewProxy.test.js +0 -178
- package/dist/handlers/apps/scaffolding.test.d.ts +0 -2
- package/dist/handlers/apps/scaffolding.test.d.ts.map +0 -1
- package/dist/handlers/apps/scaffolding.test.js +0 -151
- package/dist/handlers/apps/uploadRetry.test.d.ts +0 -2
- package/dist/handlers/apps/uploadRetry.test.d.ts.map +0 -1
- package/dist/handlers/apps/uploadRetry.test.js +0 -103
- package/dist/handlers/apps/validate.test.d.ts +0 -2
- package/dist/handlers/apps/validate.test.d.ts.map +0 -1
- package/dist/handlers/apps/validate.test.js +0 -526
- package/dist/handlers/compile.test.d.ts +0 -2
- package/dist/handlers/compile.test.d.ts.map +0 -1
- package/dist/handlers/compile.test.js +0 -151
- package/dist/handlers/compileProject.test.d.ts +0 -2
- package/dist/handlers/compileProject.test.d.ts.map +0 -1
- package/dist/handlers/compileProject.test.js +0 -637
- package/dist/handlers/compileWarehouseColumnValidation.test.d.ts +0 -2
- package/dist/handlers/compileWarehouseColumnValidation.test.d.ts.map +0 -1
- package/dist/handlers/compileWarehouseColumnValidation.test.js +0 -493
- package/dist/handlers/connectSnowflake.test.d.ts +0 -2
- package/dist/handlers/connectSnowflake.test.d.ts.map +0 -1
- package/dist/handlers/connectSnowflake.test.js +0 -320
- package/dist/handlers/contentAsCode/fileNames.test.d.ts +0 -2
- package/dist/handlers/contentAsCode/fileNames.test.d.ts.map +0 -1
- package/dist/handlers/contentAsCode/fileNames.test.js +0 -79
- package/dist/handlers/contentAsCode/registry.test.d.ts +0 -2
- package/dist/handlers/contentAsCode/registry.test.d.ts.map +0 -1
- package/dist/handlers/contentAsCode/registry.test.js +0 -18
- package/dist/handlers/contentAsCode/resource.test.d.ts +0 -2
- package/dist/handlers/contentAsCode/resource.test.d.ts.map +0 -1
- package/dist/handlers/contentAsCode/resource.test.js +0 -121
- package/dist/handlers/dbt/getDbtVersion.test.d.ts +0 -2
- package/dist/handlers/dbt/getDbtVersion.test.d.ts.map +0 -1
- package/dist/handlers/dbt/getDbtVersion.test.js +0 -106
- package/dist/handlers/deploy.test.d.ts +0 -2
- package/dist/handlers/deploy.test.d.ts.map +0 -1
- package/dist/handlers/deploy.test.js +0 -244
- package/dist/handlers/download.test.d.ts +0 -2
- package/dist/handlers/download.test.d.ts.map +0 -1
- package/dist/handlers/download.test.js +0 -1739
- package/dist/handlers/exportChartImage.test.d.ts +0 -2
- package/dist/handlers/exportChartImage.test.d.ts.map +0 -1
- package/dist/handlers/exportChartImage.test.js +0 -55
- package/dist/handlers/homepagesAsCode.test.d.ts +0 -2
- package/dist/handlers/homepagesAsCode.test.d.ts.map +0 -1
- package/dist/handlers/homepagesAsCode.test.js +0 -72
- package/dist/handlers/lint.test.d.ts +0 -2
- package/dist/handlers/lint.test.d.ts.map +0 -1
- package/dist/handlers/lint.test.js +0 -221
- package/dist/handlers/login.test.d.ts +0 -2
- package/dist/handlers/login.test.d.ts.map +0 -1
- package/dist/handlers/login.test.js +0 -24
- package/dist/handlers/metadataFile.test.d.ts +0 -2
- package/dist/handlers/metadataFile.test.d.ts.map +0 -1
- package/dist/handlers/metadataFile.test.js +0 -46
- package/dist/handlers/organizationContent/customRoles.test.d.ts +0 -2
- package/dist/handlers/organizationContent/customRoles.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/customRoles.test.js +0 -179
- package/dist/handlers/organizationContent/groups.test.d.ts +0 -2
- package/dist/handlers/organizationContent/groups.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/groups.test.js +0 -135
- package/dist/handlers/organizationContent/index.test.d.ts +0 -2
- package/dist/handlers/organizationContent/index.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/index.test.js +0 -212
- package/dist/handlers/organizationContent/themes.test.d.ts +0 -2
- package/dist/handlers/organizationContent/themes.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/themes.test.js +0 -180
- package/dist/handlers/organizationContent/users.test.d.ts +0 -2
- package/dist/handlers/organizationContent/users.test.d.ts.map +0 -1
- package/dist/handlers/organizationContent/users.test.js +0 -155
- package/dist/handlers/preAggregateAudit.test.d.ts +0 -2
- package/dist/handlers/preAggregateAudit.test.d.ts.map +0 -1
- package/dist/handlers/preAggregateAudit.test.js +0 -102
- package/dist/handlers/renameHandler.test.d.ts +0 -2
- package/dist/handlers/renameHandler.test.d.ts.map +0 -1
- package/dist/handlers/renameHandler.test.js +0 -189
- package/dist/handlers/resolveProjectFlag.test.d.ts +0 -2
- package/dist/handlers/resolveProjectFlag.test.d.ts.map +0 -1
- package/dist/handlers/resolveProjectFlag.test.js +0 -48
- package/dist/handlers/selectProject.test.d.ts +0 -2
- package/dist/handlers/selectProject.test.d.ts.map +0 -1
- package/dist/handlers/selectProject.test.js +0 -224
- package/dist/handlers/setProject.test.d.ts +0 -2
- package/dist/handlers/setProject.test.d.ts.map +0 -1
- package/dist/handlers/setProject.test.js +0 -63
- package/dist/handlers/slugUpdate.test.d.ts +0 -2
- package/dist/handlers/slugUpdate.test.d.ts.map +0 -1
- package/dist/handlers/slugUpdate.test.js +0 -265
- package/dist/handlers/timestampConversion.test.d.ts +0 -2
- package/dist/handlers/timestampConversion.test.d.ts.map +0 -1
- package/dist/handlers/timestampConversion.test.js +0 -87
- package/dist/handlers/upgradeCheck.test.d.ts +0 -2
- package/dist/handlers/upgradeCheck.test.d.ts.map +0 -1
- package/dist/handlers/upgradeCheck.test.js +0 -227
- package/dist/handlers/validate.test.d.ts +0 -2
- package/dist/handlers/validate.test.d.ts.map +0 -1
- package/dist/handlers/validate.test.js +0 -318
- package/dist/handlers/validateSpaceFilter.test.d.ts +0 -2
- package/dist/handlers/validateSpaceFilter.test.d.ts.map +0 -1
- package/dist/handlers/validateSpaceFilter.test.js +0 -79
- package/dist/handlers/warehouseConnection.test.d.ts +0 -2
- package/dist/handlers/warehouseConnection.test.d.ts.map +0 -1
- package/dist/handlers/warehouseConnection.test.js +0 -75
- package/dist/lightdash-config/lightdash-config.test.d.ts +0 -2
- package/dist/lightdash-config/lightdash-config.test.d.ts.map +0 -1
- package/dist/lightdash-config/lightdash-config.test.js +0 -69
- package/dist/prompts/fuzzySearchPrompt.test.d.ts +0 -2
- package/dist/prompts/fuzzySearchPrompt.test.d.ts.map +0 -1
- package/dist/prompts/fuzzySearchPrompt.test.js +0 -53
- package/dist/refsArgument.test.d.ts +0 -2
- package/dist/refsArgument.test.d.ts.map +0 -1
- package/dist/refsArgument.test.js +0 -42
- package/dist/releaseSafety/index.test.d.ts +0 -2
- package/dist/releaseSafety/index.test.d.ts.map +0 -1
- package/dist/releaseSafety/index.test.js +0 -154
- package/dist/releaseSafety/version.test.d.ts +0 -2
- package/dist/releaseSafety/version.test.d.ts.map +0 -1
- package/dist/releaseSafety/version.test.js +0 -22
- package/dist/skills.test.d.ts +0 -2
- package/dist/skills.test.d.ts.map +0 -1
- package/dist/skills.test.js +0 -112
- package/dist/terminal/contentAsCodeOutput.test.d.ts +0 -2
- package/dist/terminal/contentAsCodeOutput.test.d.ts.map +0 -1
- package/dist/terminal/contentAsCodeOutput.test.js +0 -231
|
@@ -1,252 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"createApp.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/createApp.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
|
-
const execa_1 = tslib_1.__importDefault(require("execa"));
|
|
5
|
-
const fs_1 = require("fs");
|
|
6
|
-
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
7
|
-
const os = tslib_1.__importStar(require("os"));
|
|
8
|
-
const path = tslib_1.__importStar(require("path"));
|
|
9
|
-
const analytics_1 = require("../../analytics/analytics");
|
|
10
|
-
const config_1 = require("../../config");
|
|
11
|
-
const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
|
|
12
|
-
const apiClient_1 = require("../dbt/apiClient");
|
|
13
|
-
const selectProject_1 = require("../selectProject");
|
|
14
|
-
const appCodeFiles_1 = require("./appCodeFiles");
|
|
15
|
-
const createApp_1 = require("./createApp");
|
|
16
|
-
vi.mock('../../config', () => ({ getConfig: vi.fn() }));
|
|
17
|
-
vi.mock('../dbt/apiClient', () => ({
|
|
18
|
-
checkLightdashVersion: vi.fn(),
|
|
19
|
-
lightdashApi: vi.fn(),
|
|
20
|
-
}));
|
|
21
|
-
vi.mock('../selectProject', () => ({
|
|
22
|
-
selectProject: vi.fn(),
|
|
23
|
-
logSelectedProject: vi.fn(),
|
|
24
|
-
}));
|
|
25
|
-
vi.mock('../../analytics/analytics', () => ({
|
|
26
|
-
LightdashAnalytics: { track: vi.fn() },
|
|
27
|
-
}));
|
|
28
|
-
vi.mock('execa');
|
|
29
|
-
vi.mock('inquirer', () => ({
|
|
30
|
-
default: { prompt: vi.fn() },
|
|
31
|
-
}));
|
|
32
|
-
const execaMock = execa_1.default;
|
|
33
|
-
const promptMock = inquirer_1.default.prompt;
|
|
34
|
-
const PROJECT_UUID = '11111111-1111-4111-8111-111111111111';
|
|
35
|
-
const context = {
|
|
36
|
-
semanticLayer: {
|
|
37
|
-
path: '.lightdash/context/semantic-layer.yml',
|
|
38
|
-
contentBase64: Buffer.from('models: []\n').toString('base64'),
|
|
39
|
-
},
|
|
40
|
-
parameters: null,
|
|
41
|
-
promptHistory: {
|
|
42
|
-
path: '.lightdash/context/prompt-history.md',
|
|
43
|
-
contentBase64: Buffer.from('# Prompt history\n\n_No previous versions._\n').toString('base64'),
|
|
44
|
-
},
|
|
45
|
-
theme: { instructions: null, assets: [], skippedAssetCount: 0 },
|
|
46
|
-
};
|
|
47
|
-
describe('resolveLocalAppSlug', () => {
|
|
48
|
-
it('generates a slug from the app name', () => {
|
|
49
|
-
expect((0, createApp_1.resolveLocalAppSlug)('Revenue Explorer')).toBe('revenue-explorer');
|
|
50
|
-
});
|
|
51
|
-
it('accepts a valid explicit slug', () => {
|
|
52
|
-
expect((0, createApp_1.resolveLocalAppSlug)('Revenue Explorer', 'sales-overview')).toBe('sales-overview');
|
|
53
|
-
});
|
|
54
|
-
it('rejects an invalid explicit slug', () => {
|
|
55
|
-
expect(() => (0, createApp_1.resolveLocalAppSlug)('Revenue Explorer', '../invalid')).toThrow('Invalid data app slug');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe('buildLocalAppManifest', () => {
|
|
59
|
-
it('marks chart types as data_app_viz with the starter vizSchema', () => {
|
|
60
|
-
const manifest = (0, createApp_1.buildLocalAppManifest)({
|
|
61
|
-
name: 'Radial Gauge',
|
|
62
|
-
description: '',
|
|
63
|
-
projectUuid: PROJECT_UUID,
|
|
64
|
-
slug: 'radial-gauge',
|
|
65
|
-
now: new Date('2026-01-01T00:00:00.000Z'),
|
|
66
|
-
chartType: true,
|
|
67
|
-
});
|
|
68
|
-
expect(manifest.template).toBe('data_app_viz');
|
|
69
|
-
expect(manifest.vizSchema).toEqual(createApp_1.CHART_TYPE_STARTER_VIZ_SCHEMA);
|
|
70
|
-
});
|
|
71
|
-
it('builds a slug-identified manifest without an app uuid', () => {
|
|
72
|
-
const manifest = (0, createApp_1.buildLocalAppManifest)({
|
|
73
|
-
name: 'Revenue Explorer',
|
|
74
|
-
description: 'Revenue by customer',
|
|
75
|
-
projectUuid: PROJECT_UUID,
|
|
76
|
-
slug: 'revenue-explorer',
|
|
77
|
-
now: new Date('2026-07-30T10:00:00.000Z'),
|
|
78
|
-
});
|
|
79
|
-
expect(manifest).toMatchObject({
|
|
80
|
-
codeVersion: 1,
|
|
81
|
-
projectUuid: PROJECT_UUID,
|
|
82
|
-
slug: 'revenue-explorer',
|
|
83
|
-
version: 1,
|
|
84
|
-
name: 'Revenue Explorer',
|
|
85
|
-
description: 'Revenue by customer',
|
|
86
|
-
template: null,
|
|
87
|
-
downloadedAt: '2026-07-30T10:00:00.000Z',
|
|
88
|
-
});
|
|
89
|
-
expect(manifest.appUuid).toBeUndefined();
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
describe('createAppHandler', () => {
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
execaMock.mockImplementation(async (command, args = [], options) => {
|
|
95
|
-
if (command === 'npx' &&
|
|
96
|
-
args.includes('init') &&
|
|
97
|
-
options?.cwd) {
|
|
98
|
-
await fs_1.promises.writeFile(path.join(options.cwd, 'package.json'), '{}\n');
|
|
99
|
-
await fs_1.promises.writeFile(path.join(options.cwd, 'tailwind.config.js'), '// rewritten by shadcn\n');
|
|
100
|
-
}
|
|
101
|
-
if (command === 'npx' && args.includes('add') && options?.cwd) {
|
|
102
|
-
const uiDir = path.join(options.cwd, 'src/components/ui');
|
|
103
|
-
await fs_1.promises.mkdir(uiDir, { recursive: true });
|
|
104
|
-
await fs_1.promises.writeFile(path.join(uiDir, 'button.tsx'), 'export const Button = () => null;\n');
|
|
105
|
-
}
|
|
106
|
-
return { stdout: '' };
|
|
107
|
-
});
|
|
108
|
-
promptMock.mockResolvedValue({ confirmed: true });
|
|
109
|
-
vi.mocked(config_1.getConfig).mockResolvedValue({
|
|
110
|
-
context: {
|
|
111
|
-
apiKey: 'token',
|
|
112
|
-
serverUrl: 'https://lightdash.example.com',
|
|
113
|
-
project: PROJECT_UUID,
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
vi.mocked(selectProject_1.selectProject).mockResolvedValue({
|
|
117
|
-
projectUuid: PROJECT_UUID,
|
|
118
|
-
isPreview: false,
|
|
119
|
-
});
|
|
120
|
-
vi.mocked(apiClient_1.lightdashApi).mockResolvedValue(context);
|
|
121
|
-
vi.mocked(apiClient_1.checkLightdashVersion).mockResolvedValue(undefined);
|
|
122
|
-
vi.spyOn(globalState_1.default, 'log').mockImplementation(() => undefined);
|
|
123
|
-
vi.spyOn(globalState_1.default, 'isNonInteractive').mockReturnValue(false);
|
|
124
|
-
});
|
|
125
|
-
afterEach(() => {
|
|
126
|
-
vi.restoreAllMocks();
|
|
127
|
-
vi.clearAllMocks();
|
|
128
|
-
});
|
|
129
|
-
it('creates a complete local app scaffold and project context', async () => {
|
|
130
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
131
|
-
await (0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
132
|
-
description: 'Revenue by customer',
|
|
133
|
-
path: root,
|
|
134
|
-
verbose: false,
|
|
135
|
-
});
|
|
136
|
-
const appDir = path.join(root, 'apps', 'revenue-explorer');
|
|
137
|
-
const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
|
|
138
|
-
expect(bundle.manifest.slug).toBe('revenue-explorer');
|
|
139
|
-
expect(bundle.manifest.appUuid).toBeUndefined();
|
|
140
|
-
expect(bundle.files.map((file) => file.path)).toContain('src/App.jsx');
|
|
141
|
-
expect(bundle.files.map((file) => file.path)).toContain('src/components/ui/button.tsx');
|
|
142
|
-
expect(await fs_1.promises.readFile(path.join(appDir, 'package.json'), 'utf8')).toContain('@lightdash/query-sdk');
|
|
143
|
-
expect(await fs_1.promises.readFile(path.join(appDir, 'tailwind.config.js'), 'utf8')).not.toContain('rewritten by shadcn');
|
|
144
|
-
await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/lightdash-data-app/SKILL.md'))).resolves.toBeUndefined();
|
|
145
|
-
await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/developing-data-apps-locally/SKILL.md'))).resolves.toBeUndefined();
|
|
146
|
-
expect(await fs_1.promises.readFile(path.join(appDir, '.lightdash/context/semantic-layer.yml'), 'utf8')).toBe('models: []\n');
|
|
147
|
-
expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
|
|
148
|
-
method: 'GET',
|
|
149
|
-
url: `/api/v1/ee/projects/${PROJECT_UUID}/apps/authoring-context?slug=revenue-explorer`,
|
|
150
|
-
body: undefined,
|
|
151
|
-
});
|
|
152
|
-
expect(analytics_1.LightdashAnalytics.track).toHaveBeenCalledWith({
|
|
153
|
-
event: 'command.executed',
|
|
154
|
-
properties: expect.objectContaining({
|
|
155
|
-
command: 'create-app',
|
|
156
|
-
success: true,
|
|
157
|
-
}),
|
|
158
|
-
});
|
|
159
|
-
expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining('react@19.2.5'));
|
|
160
|
-
expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining('⚠ Local package installation'));
|
|
161
|
-
expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining('shadcn@2.3.0'));
|
|
162
|
-
expect(promptMock).toHaveBeenCalledTimes(2);
|
|
163
|
-
expect(promptMock).toHaveBeenNthCalledWith(1, [
|
|
164
|
-
expect.objectContaining({
|
|
165
|
-
type: 'confirm',
|
|
166
|
-
name: 'confirmed',
|
|
167
|
-
message: 'Continue and review the packages to be installed?',
|
|
168
|
-
default: false,
|
|
169
|
-
}),
|
|
170
|
-
]);
|
|
171
|
-
expect(promptMock).toHaveBeenNthCalledWith(2, [
|
|
172
|
-
expect.objectContaining({
|
|
173
|
-
type: 'confirm',
|
|
174
|
-
name: 'confirmed',
|
|
175
|
-
message: 'Install these packages and create the data app?',
|
|
176
|
-
default: false,
|
|
177
|
-
}),
|
|
178
|
-
]);
|
|
179
|
-
expect(execaMock).toHaveBeenCalledWith('npm', ['--version']);
|
|
180
|
-
expect(execaMock).toHaveBeenCalledWith('npm', [
|
|
181
|
-
'install',
|
|
182
|
-
'--include=dev',
|
|
183
|
-
'--ignore-scripts',
|
|
184
|
-
'--no-package-lock',
|
|
185
|
-
], expect.objectContaining({
|
|
186
|
-
cwd: expect.any(String),
|
|
187
|
-
stdio: 'inherit',
|
|
188
|
-
}));
|
|
189
|
-
expect(execaMock).toHaveBeenCalledWith('npx', expect.arrayContaining(['shadcn@2.3.0', 'init']), expect.objectContaining({
|
|
190
|
-
cwd: expect.any(String),
|
|
191
|
-
input: '\u001B[B\n',
|
|
192
|
-
}));
|
|
193
|
-
expect(execaMock).toHaveBeenCalledWith('npx', expect.arrayContaining(['shadcn@2.3.0', 'add', 'button']), expect.objectContaining({
|
|
194
|
-
cwd: expect.any(String),
|
|
195
|
-
input: '\u001B[B\n',
|
|
196
|
-
}));
|
|
197
|
-
});
|
|
198
|
-
it.each([false, true])('creates an uploadable chart type scaffold (custom path: %s)', async (customPath) => {
|
|
199
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
200
|
-
vi.spyOn(process, 'cwd').mockReturnValue(root);
|
|
201
|
-
const contentRoot = customPath
|
|
202
|
-
? root
|
|
203
|
-
: path.join(root, 'lightdash');
|
|
204
|
-
await (0, createApp_1.createAppHandler)('Radial Gauge', {
|
|
205
|
-
description: 'A gauge with a needle',
|
|
206
|
-
path: customPath ? root : undefined,
|
|
207
|
-
chartType: true,
|
|
208
|
-
verbose: false,
|
|
209
|
-
});
|
|
210
|
-
const appDir = path.join(contentRoot, 'chart-types', 'radial-gauge');
|
|
211
|
-
const bundle = await (0, appCodeFiles_1.readBundleFromDir)(appDir);
|
|
212
|
-
expect(bundle.manifest.template).toBe('data_app_viz');
|
|
213
|
-
expect(bundle.manifest.vizSchema).toEqual(createApp_1.CHART_TYPE_STARTER_VIZ_SCHEMA);
|
|
214
|
-
const appSource = await fs_1.promises.readFile(path.join(appDir, 'src/App.jsx'), 'utf8');
|
|
215
|
-
expect(appSource).toContain('useVizContext');
|
|
216
|
-
// No shadcn generation: chart types ship no UI-kit source.
|
|
217
|
-
expect(execaMock).not.toHaveBeenCalledWith('npx', expect.anything(), expect.anything());
|
|
218
|
-
// The viz contract + local workflow skills ship; the app-only skills
|
|
219
|
-
// do not.
|
|
220
|
-
await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/reusable-visualization/SKILL.md'))).resolves.toBeUndefined();
|
|
221
|
-
await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/developing-chart-types-locally/SKILL.md'))).resolves.toBeUndefined();
|
|
222
|
-
await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/lightdash-data-app/SKILL.md'))).rejects.toThrow();
|
|
223
|
-
await expect(fs_1.promises.access(path.join(appDir, '.claude/skills/developing-data-apps-locally/SKILL.md'))).rejects.toThrow();
|
|
224
|
-
expect(await fs_1.promises.readFile(path.join(appDir, 'AGENTS.md'), 'utf8')).toContain('custom chart type');
|
|
225
|
-
expect(await fs_1.promises.readFile(path.join(appDir, 'README.md'), 'utf8')).toContain('Radial Gauge');
|
|
226
|
-
const authoringDocs = await Promise.all([
|
|
227
|
-
'README.md',
|
|
228
|
-
'AGENTS.md',
|
|
229
|
-
'.claude/skills/developing-chart-types-locally/SKILL.md',
|
|
230
|
-
].map((file) => fs_1.promises.readFile(path.join(appDir, file), 'utf8')));
|
|
231
|
-
for (const doc of authoringDocs) {
|
|
232
|
-
expect(doc).toContain('upload --chart-types <slug> --path ../..');
|
|
233
|
-
}
|
|
234
|
-
expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining(`upload --chart-types radial-gauge --path ${JSON.stringify(contentRoot)}`));
|
|
235
|
-
expect(analytics_1.LightdashAnalytics.track).toHaveBeenCalledWith({
|
|
236
|
-
event: 'command.executed',
|
|
237
|
-
properties: expect.objectContaining({
|
|
238
|
-
command: 'create-chart-type',
|
|
239
|
-
success: true,
|
|
240
|
-
}),
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
it('requires npm before requesting app context', async () => {
|
|
244
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
245
|
-
execaMock.mockRejectedValueOnce(new Error('npm not found'));
|
|
246
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
247
|
-
path: root,
|
|
248
|
-
verbose: false,
|
|
249
|
-
})).rejects.toThrow('npm is required to create a data app');
|
|
250
|
-
expect(apiClient_1.lightdashApi).not.toHaveBeenCalled();
|
|
251
|
-
await expect(fs_1.promises.access(path.join(root, 'apps', 'revenue-explorer'))).rejects.toThrow();
|
|
252
|
-
});
|
|
253
|
-
it('does not create anything when package installation is declined', async () => {
|
|
254
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
255
|
-
promptMock
|
|
256
|
-
.mockResolvedValueOnce({ confirmed: true })
|
|
257
|
-
.mockResolvedValueOnce({ confirmed: false });
|
|
258
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
259
|
-
path: root,
|
|
260
|
-
verbose: false,
|
|
261
|
-
})).rejects.toThrow('Data app creation cancelled');
|
|
262
|
-
// Preflight (auth, project, slug availability) runs before the
|
|
263
|
-
// prompts, so the context call has already happened by then.
|
|
264
|
-
expect(apiClient_1.lightdashApi).toHaveBeenCalledTimes(1);
|
|
265
|
-
await expect(fs_1.promises.access(path.join(root, 'apps', 'revenue-explorer'))).rejects.toThrow();
|
|
266
|
-
});
|
|
267
|
-
it('does not list packages when local tooling approval is declined', async () => {
|
|
268
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
269
|
-
promptMock.mockResolvedValueOnce({ confirmed: false });
|
|
270
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
271
|
-
path: root,
|
|
272
|
-
verbose: false,
|
|
273
|
-
})).rejects.toThrow('Data app creation cancelled');
|
|
274
|
-
expect(promptMock).toHaveBeenCalledTimes(1);
|
|
275
|
-
expect(globalState_1.default.log).not.toHaveBeenCalledWith(expect.stringContaining('react@19.2.5'));
|
|
276
|
-
});
|
|
277
|
-
it('asks for no approvals when the server rejects the slug', async () => {
|
|
278
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
279
|
-
vi.mocked(apiClient_1.lightdashApi).mockRejectedValueOnce(new Error('Slug already in use'));
|
|
280
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
281
|
-
path: root,
|
|
282
|
-
verbose: false,
|
|
283
|
-
})).rejects.toThrow('Slug already in use');
|
|
284
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
285
|
-
});
|
|
286
|
-
it('asks for no approvals when not logged in', async () => {
|
|
287
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
288
|
-
vi.mocked(config_1.getConfig).mockResolvedValueOnce({});
|
|
289
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
290
|
-
path: root,
|
|
291
|
-
verbose: false,
|
|
292
|
-
})).rejects.toThrow('Not logged in');
|
|
293
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
294
|
-
});
|
|
295
|
-
it('requires explicit approval in non-interactive mode', async () => {
|
|
296
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
297
|
-
vi.mocked(globalState_1.default.isNonInteractive).mockReturnValueOnce(true);
|
|
298
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
299
|
-
path: root,
|
|
300
|
-
verbose: false,
|
|
301
|
-
})).rejects.toThrow('--assume-yes');
|
|
302
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
303
|
-
});
|
|
304
|
-
it('accepts --assume-yes in non-interactive mode', async () => {
|
|
305
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
306
|
-
vi.mocked(globalState_1.default.isNonInteractive).mockReturnValueOnce(true);
|
|
307
|
-
await (0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
308
|
-
assumeYes: true,
|
|
309
|
-
path: root,
|
|
310
|
-
verbose: false,
|
|
311
|
-
});
|
|
312
|
-
expect(promptMock).not.toHaveBeenCalled();
|
|
313
|
-
await expect(fs_1.promises.access(path.join(root, 'apps', 'revenue-explorer'))).resolves.toBeUndefined();
|
|
314
|
-
});
|
|
315
|
-
it('cleans up the temporary app when installation fails', async () => {
|
|
316
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
317
|
-
execaMock.mockImplementation(async (command, args = []) => {
|
|
318
|
-
if (command === 'npm' && args[0] === 'install') {
|
|
319
|
-
throw new Error('registry unavailable');
|
|
320
|
-
}
|
|
321
|
-
return { stdout: '' };
|
|
322
|
-
});
|
|
323
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
324
|
-
path: root,
|
|
325
|
-
verbose: false,
|
|
326
|
-
})).rejects.toThrow('registry unavailable');
|
|
327
|
-
expect(await fs_1.promises.readdir(path.join(root, 'apps'))).toEqual([]);
|
|
328
|
-
});
|
|
329
|
-
it('does not overwrite an existing app directory', async () => {
|
|
330
|
-
const root = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-create-app-'));
|
|
331
|
-
const appDir = path.join(root, 'apps', 'revenue-explorer');
|
|
332
|
-
await fs_1.promises.mkdir(appDir, { recursive: true });
|
|
333
|
-
await fs_1.promises.writeFile(path.join(appDir, 'keep.txt'), 'keep');
|
|
334
|
-
await expect((0, createApp_1.createAppHandler)('Revenue Explorer', {
|
|
335
|
-
path: root,
|
|
336
|
-
verbose: false,
|
|
337
|
-
})).rejects.toThrow('already exists');
|
|
338
|
-
expect(await fs_1.promises.readFile(path.join(appDir, 'keep.txt'), 'utf8')).toBe('keep');
|
|
339
|
-
});
|
|
340
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pathContainment.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/pathContainment.test.ts"],"names":[],"mappings":""}
|