Skip to content

Commit 2c3fa4f

Browse files
committed
Rework CDS project-only compilation & CDL.qll
Reworks the CDS extractor such that it only does project-level compilation, which allows for consistently generating a single model.cds.json file in the base/root directory of its associated project, which allows for matching file paths relative to the project base directory. Updates the `CDL.qll` library file to use location/path values that are relative to the project base directory rather than relative to the source root directory.
1 parent 9bb6169 commit 2c3fa4f

File tree

23 files changed

+370
-1901
lines changed

23 files changed

+370
-1901
lines changed

extractors/cds/tools/cds-extractor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ try {
9999
for (const [projectDir, project] of dependencyGraph.projects.entries()) {
100100
cdsExtractorLog(
101101
'info',
102-
`Project: ${projectDir}, Status: ${project.status}, CDS files: ${project.cdsFiles.length}, Compilations to run: ${project.cdsFilesToCompile.length}`,
102+
`Project: ${projectDir}, Status: ${project.status}, CDS files: ${project.cdsFiles.length}, Compilation targets: ${project.compilationTargets.length}`,
103103
);
104104
}
105105
} else {

extractors/cds/tools/dist/cds-extractor.bundle.js

Lines changed: 68 additions & 309 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extractors/cds/tools/dist/cds-extractor.bundle.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 50 additions & 223 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { spawnSync, SpawnSyncOptions } from 'child_process';
2-
import { resolve, join, delimiter, relative } from 'path';
3-
4-
import { globSync } from 'glob';
2+
import { resolve, join, delimiter, relative, dirname, basename } from 'path';
53

64
import { CdsCompilationResult } from './types';
75
import { getCdsVersion } from './version';
@@ -22,12 +20,43 @@ function parseCommandForSpawn(commandString: string): { executable: string; base
2220
}
2321

2422
/**
25-
* Compiles a CDS file to JSON using robust, project-aware compilation only.
26-
* This function has been refactored to align with the autobuild.md vision by removing all
27-
* forms of individual file compilation and ensuring only project-aware compilation is used.
23+
* Determines compilation targets for a CDS project according to the new project-only compilation approach.
24+
* @param project The CDS project
25+
* @param sourceRoot The source root directory
26+
* @returns Array of compilation targets (directories or files relative to project base)
27+
*/
28+
function determineCompilationTargets(project: BasicCdsProject, sourceRoot: string): string[] {
29+
const projectAbsolutePath = join(sourceRoot, project.projectDir);
30+
31+
// Check for standard CAP directories
32+
const capDirectories = ['db', 'srv', 'app'];
33+
const existingCapDirs = capDirectories.filter(dir => dirExists(join(projectAbsolutePath, dir)));
34+
35+
if (existingCapDirs.length > 0) {
36+
// Use standard CAP directories
37+
return existingCapDirs;
38+
}
39+
40+
// Check for root-level CDS files
41+
const rootCdsFiles = project.cdsFiles
42+
.filter(file => dirname(join(sourceRoot, file)) === projectAbsolutePath)
43+
.map(file => basename(file));
44+
45+
if (rootCdsFiles.length > 0) {
46+
// Use root-level files
47+
return rootCdsFiles;
48+
}
49+
50+
// Use all CDS files with their relative paths
51+
return project.cdsFiles.map(file => relative(projectAbsolutePath, join(sourceRoot, file)));
52+
}
53+
54+
/**
55+
* Compiles a CDS project to JSON using project-level compilation only.
56+
* This function has been simplified to only use project-level compilation,
57+
* eliminating all individual file compilation logic and standardizing output
58+
* to a single model.cds.json file per project.
2859
*
29-
* For root files, this will compile them to their 1:1 .cds.json representation if and only
30-
* if the file is a true root file in a project.
3160
*
3261
* @param cdsFilePath The path to the CDS file to compile, relative to the `sourceRoot`.
3362
* @param sourceRoot The source root directory scanned by the CDS extractor.
@@ -73,131 +102,54 @@ export function compileCdsToJson(
73102
}
74103

75104
const project = projectMap.get(projectDir);
76-
const relativePath = relative(sourceRoot, resolvedCdsFilePath);
77-
78-
// Check if this is a project-level compilation marker.
79-
if (shouldUseProjectLevelCompilation(project)) {
80-
return compileProjectLevel(
81-
resolvedCdsFilePath,
82-
sourceRoot,
83-
projectDir,
84-
cdsCommand,
85-
spawnOptions,
86-
versionInfo,
87-
);
88-
}
89105

90-
// Check if this file is in the list of files to compile for this project.
91-
if (!shouldCompileIndividually(project, relativePath)) {
92-
cdsExtractorLog(
93-
'info',
94-
`${resolvedCdsFilePath} is imported by other files - will be compiled as part of a project ${versionInfo}...`,
95-
);
96-
const cdsJsonOutPath = `${resolvedCdsFilePath}.json`;
97-
return {
98-
success: true,
99-
outputPath: cdsJsonOutPath,
100-
compiledAsProject: true,
101-
message: 'File was compiled as part of a project-based compilation',
102-
};
103-
} else {
104-
// This is a root file - compile it using project-aware approach to its 1:1 representation
105-
cdsExtractorLog(
106-
'info',
107-
`${resolvedCdsFilePath} identified as a root CDS file - using project-aware compilation for root file ${versionInfo}...`,
108-
);
109-
return compileRootFileAsProject(
110-
resolvedCdsFilePath,
111-
sourceRoot,
112-
projectDir,
113-
cdsCommand,
114-
spawnOptions,
115-
);
116-
}
106+
// Always use project-level compilation
107+
return compileProject(sourceRoot, projectDir, cdsCommand, spawnOptions, versionInfo, project!);
117108
} catch (error) {
118109
return { success: false, message: String(error) };
119110
}
120111
}
121112

122113
/**
123-
* Handles project-level compilation for CAP projects with typical directory structure.
114+
* Handles project-level compilation for CAP projects.
124115
* CRITICAL: Uses the project base directory as cwd and calculates paths relative to project base directory.
125116
*
126-
* @param resolvedCdsFilePath The resolved CDS file path that triggered this compilation
127117
* @param sourceRoot The source root directory
128118
* @param projectDir The project directory (relative to sourceRoot)
129119
* @param cdsCommand The CDS command to use
130120
* @param spawnOptions Pre-configured spawn options with project base directory as cwd
131121
* @param versionInfo Version information for logging
122+
* @param project The CDS project instance
132123
* @returns Compilation result
133124
*/
134-
function compileProjectLevel(
135-
resolvedCdsFilePath: string,
125+
function compileProject(
136126
sourceRoot: string,
137127
projectDir: string,
138128
cdsCommand: string,
139129
spawnOptions: SpawnSyncOptions,
140130
versionInfo: string,
131+
project: BasicCdsProject,
141132
): CdsCompilationResult {
142133
cdsExtractorLog(
143134
'info',
144-
`${resolvedCdsFilePath} is part of a CAP project - using project-aware compilation ${versionInfo}...`,
135+
`Compiling CDS project using project-level compilation ${versionInfo}...`,
145136
);
146137

147-
// For project-level compilation, compile the entire project together
148-
// This follows the CAP best practice of compiling db and srv directories together
149-
const projectAbsolutePath = join(sourceRoot, projectDir);
150-
151-
// Common directories in CAP projects that should be compiled together
152-
const capDirectories = ['db', 'srv', 'app'];
153-
const existingDirectories: string[] = [];
154-
155-
for (const dir of capDirectories) {
156-
const dirPath = join(projectAbsolutePath, dir);
157-
if (dirExists(dirPath)) {
158-
existingDirectories.push(dir);
159-
}
160-
}
138+
// Determine compilation targets using the new centralized logic
139+
const compilationTargets = determineCompilationTargets(project, sourceRoot);
161140

162-
// Check if there are any CDS files in the project at all before proceeding
163-
const allCdsFiles = globSync(join(projectAbsolutePath, '**/*.cds'), {
164-
nodir: true,
165-
ignore: ['**/node_modules/**'],
166-
});
167-
168-
if (allCdsFiles.length === 0) {
141+
if (compilationTargets.length === 0) {
169142
throw new Error(
170143
`Project directory '${projectDir}' does not contain any CDS files and cannot be compiled`,
171144
);
172145
}
173146

174-
if (existingDirectories.length === 0) {
175-
// If no standard directories, check if there are CDS files in the root of the project.
176-
const rootCdsFiles = globSync(join(projectAbsolutePath, '*.cds'));
177-
if (rootCdsFiles.length > 0) {
178-
existingDirectories.push('.');
179-
} else {
180-
// Find directories that contain `.cds` files.
181-
const cdsFileParents = new Set(
182-
allCdsFiles.map((file: string) => {
183-
const relativePath = relative(projectAbsolutePath, file);
184-
const firstDir = relativePath.split('/')[0];
185-
return firstDir === relativePath ? '.' : firstDir;
186-
}),
187-
);
188-
existingDirectories.push(...Array.from(cdsFileParents));
189-
}
190-
}
191-
192-
// Generate output path for the compiled model - relative to project base directory.
147+
// Generate output path for the compiled model - always model.cds.json in project base directory
193148
const projectJsonOutPath = join(sourceRoot, projectDir, 'model.cds.json');
194149

195-
// Convert directories to be relative to project base directory.
196-
const projectRelativeDirectories = existingDirectories;
197-
198150
const compileArgs = [
199151
'compile',
200-
...projectRelativeDirectories, // Use paths relative to project base directory.
152+
...compilationTargets,
201153
'--to',
202154
'json',
203155
'--dest',
@@ -207,7 +159,7 @@ function compileProjectLevel(
207159
'warn',
208160
];
209161

210-
cdsExtractorLog('info', `Compiling CAP project directories: ${existingDirectories.join(', ')}`);
162+
cdsExtractorLog('info', `Compiling CDS project targets: ${compilationTargets.join(', ')}`);
211163
cdsExtractorLog(
212164
'info',
213165
`Running compilation task for CDS project '${projectDir}': command='${cdsCommand}' args='${JSON.stringify(compileArgs)}'`,
@@ -270,107 +222,6 @@ function compileProjectLevel(
270222
};
271223
}
272224

273-
/**
274-
* Compiles a root CDS file using project-aware approach for 1:1 .cds.json representation.
275-
* This follows the autobuild.md vision of project-aware compilation only.
276-
*
277-
* @param resolvedCdsFilePath The resolved CDS file path
278-
* @param sourceRoot The source root directory
279-
* @param projectDir The project directory
280-
* @param cdsCommand The CDS command to use
281-
* @param spawnOptions Pre-configured spawn options
282-
* @param versionInfo Version information for logging
283-
* @returns The {@link CdsCompilationResult}
284-
*/
285-
function compileRootFileAsProject(
286-
resolvedCdsFilePath: string,
287-
sourceRoot: string,
288-
projectDir: string,
289-
cdsCommand: string,
290-
spawnOptions: SpawnSyncOptions,
291-
): CdsCompilationResult {
292-
// Calculate project base directory and file path relative to project
293-
const projectBaseDir = join(sourceRoot, projectDir);
294-
const relativeCdsPath = relative(projectBaseDir, resolvedCdsFilePath);
295-
const cdsJsonOutPath = `${resolvedCdsFilePath}.json`;
296-
297-
// Use project-aware compilation with specific file target
298-
const compileArgs = [
299-
'compile',
300-
relativeCdsPath, // Compile the specific file relative to project base directory
301-
'--to',
302-
'json',
303-
'--dest',
304-
`${relativeCdsPath}.json`,
305-
'--locations',
306-
'--log-level',
307-
'warn',
308-
];
309-
310-
cdsExtractorLog(
311-
'info',
312-
`Compiling root CDS file using project-aware approach: ${relativeCdsPath}`,
313-
);
314-
cdsExtractorLog(
315-
'info',
316-
`Executing CDS command: command='${cdsCommand}' args='${JSON.stringify(compileArgs)}'`,
317-
);
318-
319-
// Execute the compilation
320-
// Parse command for proper spawnSync execution
321-
const { executable, baseArgs } = parseCommandForSpawn(cdsCommand);
322-
const allArgs = [...baseArgs, ...compileArgs];
323-
324-
const result = spawnSync(executable, allArgs, spawnOptions);
325-
326-
if (result.error) {
327-
cdsExtractorLog('error', `SpawnSync error: ${result.error.message}`);
328-
throw new Error(`Error executing CDS compiler: ${result.error.message}`);
329-
}
330-
331-
// Log stderr for debugging even on success
332-
if (result.stderr && result.stderr.length > 0) {
333-
cdsExtractorLog('warn', `CDS stderr output: ${result.stderr.toString()}`);
334-
}
335-
336-
if (result.status !== 0) {
337-
cdsExtractorLog('error', `CDS command failed with status ${result.status}`);
338-
cdsExtractorLog(
339-
'error',
340-
`Command: ${cdsCommand} ${compileArgs.map(arg => (arg.includes(' ') ? `"${arg}"` : arg)).join(' ')}`,
341-
);
342-
cdsExtractorLog('error', `Stdout: ${result.stdout?.toString() || 'No stdout'}`);
343-
cdsExtractorLog('error', `Stderr: ${result.stderr?.toString() || 'No stderr'}`);
344-
throw new Error(
345-
`Could not compile the root CDS file ${relativeCdsPath}.\nReported error(s):\n\`\`\`\n${
346-
result.stderr?.toString() || 'Unknown error'
347-
}\n\`\`\``,
348-
);
349-
}
350-
351-
if (!fileExists(cdsJsonOutPath) && !dirExists(cdsJsonOutPath)) {
352-
throw new Error(
353-
`Root CDS file '${relativeCdsPath}' was not compiled to JSON. Expected output: ${cdsJsonOutPath}`,
354-
);
355-
}
356-
357-
// Handle directory output if the CDS compiler generated a directory
358-
if (dirExists(cdsJsonOutPath)) {
359-
cdsExtractorLog('info', `CDS compiler generated JSON to output directory: ${cdsJsonOutPath}`);
360-
// Recursively rename all .json files to have a .cds.json extension
361-
recursivelyRenameJsonFiles(cdsJsonOutPath);
362-
} else {
363-
cdsExtractorLog('info', `CDS compiler generated JSON to file: ${cdsJsonOutPath}`);
364-
}
365-
366-
return {
367-
success: true,
368-
outputPath: cdsJsonOutPath,
369-
compiledAsProject: true,
370-
message: 'Root file compiled using project-aware compilation',
371-
};
372-
}
373-
374225
/**
375226
* Creates spawn options for CDS compilation processes.
376227
* CRITICAL: Always sets cwd to project base directory to ensure generated JSON paths are relative to project base directory.
@@ -425,27 +276,3 @@ function createSpawnOptions(
425276

426277
return spawnOptions;
427278
}
428-
429-
/**
430-
* Determines if a file should be compiled individually or skipped because it's part of a project.
431-
*
432-
* @param project The CDS project
433-
* @param relativePath The relative path of the file being checked
434-
* @returns true if the file should be compiled individually
435-
*/
436-
function shouldCompileIndividually(
437-
project: BasicCdsProject | undefined,
438-
relativePath: string,
439-
): boolean {
440-
return project?.cdsFilesToCompile?.includes(relativePath) ?? true;
441-
}
442-
443-
/**
444-
* Determines if the given project should use project-level compilation.
445-
*
446-
* @param project The CDS project to check
447-
* @returns true if project-level compilation should be used
448-
*/
449-
function shouldUseProjectLevelCompilation(project: BasicCdsProject | undefined): boolean {
450-
return project?.cdsFilesToCompile?.includes('__PROJECT_LEVEL_COMPILATION__') ?? false;
451-
}

0 commit comments

Comments
 (0)