Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions docs/diagrams/sasjs-compile.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,40 @@ flowchart TD
H -->|no| J["compileFile(target, filePath, macroFolders, programFolders, ..., fileType, sourceFolder)\n→ loadDependencies() resolves %macro/[include] headers, writes resolved SAS in place"]
```

## `compileTestFile` — destination path resolution

```mermaid
flowchart TD
A["compileTestFile(target, filePath, testVar, saveToRoot, removeOriginalFile, compileTree, destinationPath?) — compileTestFile.ts:68"] --> B["loadDependencies(target, absolutePath(filePath), macroFolders, programFolders, SASJsFileType.test, compileTree)\n→ resolves %macro/[include] headers into final SAS content"]
B --> C["dependencies = (testVar ? testVar + newline : '') + newline + dependencies"]
C --> D{"destinationPath explicitly given?"}
D -->|yes| F["use it as-is"]
D -->|no| E["getTestFileDestinationFragment(filePath, buildDestinationFolderName, saveToRoot) — compileTestFile.ts:44\n(see subgraph below)"]
E --> F2["destinationPath = join(buildDestinationTestFolder, fragment)"]
F --> G["createFile(destinationPath, dependencies)"]
F2 --> G
G --> H{"removeOriginalFile?"}
H -->|yes| I["deleteFile(filePath) — source removed"]
H -->|no| J["source left in place"]
```

```mermaid
flowchart TD
A["getTestFileDestinationFragment(filePath, buildDestinationFolderName, saveToRoot)"] --> B["segments = filePath.split(/[\\\\/]/) — splits on both '/' and '\\'\nregardless of host OS or which separator the input string uses"]
B --> C{"saveToRoot?"}
C -->|true, e.g. testSetUp/testTearDown| D["return segments.pop() || ''\n→ just the file's basename, flattened to build-test-folder root"]
C -->|false, e.g. service/job test files| E["reduce over segments: drop everything up to and\nincluding buildDestinationFolderName, keep the remainder,\njoin with path.sep\n→ preserves the relative path fragment under services/jobs"]
```

## `compileTestFlow` — building `testFlow.json` + coverage

```mermaid
flowchart TD
A["compileTestFlow(target) — compileTestFile.ts:98"] --> B{"sasjsbuild/tests folder exists?"}
A["compileTestFlow(target) — compileTestFile.ts:130"] --> B{"sasjsbuild/tests folder exists?"}
B -->|no| Z["return undefined"]
B -->|yes| C["testFiles = listFilesAndSubFoldersInFolder(buildDestinationTestFolder)\nprefixed with 'tests/'"]
C --> D{"testSetUp / testTearDown configured\n(target.testConfig or root sasjsconfig.json)?"}
D --> E["match by basename against testFiles;\nremove matched entry from testFiles list,\nset testFlow.testSetUp / testFlow.testTearDown = 'tests/{basename}'"]
D --> E["match by basename against testFiles;\nremove matched entry from testFiles list,\nset testFlow.testSetUp / testFlow.testTearDown = 'tests/{basename}'\n(no match found → left unset, file stays a regular/standalone test entry)"]
E --> F["testFlow.tests = remaining testFiles (posix-joined)"]
F --> G["printTestCoverage(testFlow, buildDestinationFolder, target)"]
G --> H["collectCoverage() over sasjsbuild/services, sasjsbuild/jobs,\nand each macro folder (excluding *.test.sas)"]
Expand All @@ -66,14 +91,17 @@ flowchart TD
service/job folders are compiled with their relative path preserved under
`sasjsbuild/tests/{services|jobs}/...` (`saveToRoot = false`). Both paths go through
`getTestFileDestinationFragment(filePath, buildDestinationFolderName, saveToRoot)`
in `compileTestFile.ts`.
in `compileTestFile.ts`, which splits on both `/` and `\` so the result doesn't
depend on which separator style the input path happens to use.
- Macro test files (`*.test.sas` under any macro folder) are handled separately via
`copyTestMacroFiles()` (copy into `sasjsbuild/tests/macros`) + a second `compileFile()`
pass to resolve their dependencies - this happens after `compileJobsServicesTests`,
not inside it.
`copyTestMacroFiles()` (copy into `sasjsbuild/tests/macros`, skipping files that
already exist at the destination) + a second `compileFile()` pass to resolve their
dependencies - this happens after `compileJobsServicesTests`, not inside it.
- `compileTestFlow` only ever *reads* whatever already landed in
`sasjsbuild/tests` - it doesn't compile anything itself, it just assembles
`testFlow.json` and prints/writes coverage.
`testFlow.json` and prints/writes coverage. If a configured `testSetUp`/
`testTearDown` doesn't match any actual compiled file by basename, it's silently
left unset and the corresponding file (if any) is treated as a regular test.
- Dependency resolution (`%macro`/program includes) for every non-test file goes
through `loadDependencies()` → `loadDependenciesFile()` (from `@sasjs/utils`), which
is also memoized per-file via the `compileTree` (`{target}_compileTree.json`) to
Expand Down
106 changes: 106 additions & 0 deletions docs/diagrams/sasjs-doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# `sasjs doc` logic

Scope: `src/commands/docs/docsCommand.ts`, `src/commands/docs/generateDocs.ts`,
`src/commands/docs/generateDot.ts`, `src/commands/docs/initDocs.ts`,
`src/commands/docs/internal/*`. Entry point is `DocsCommand.execute()` at
`docsCommand.ts:55`.

## Top-level dispatch

```mermaid
flowchart TD
Start(["sasjs doc [subCommand] — docsCommand.ts:55"]) --> Sub{"subCommand?"}
Sub -->|"init"| Init["executeInitDocs() → initDocs()\n(see subgraph below)"]
Sub -->|"lineage"| Dot["executeGenerateDot() → generateDot(target, config, outDirectory)\n(see subgraph below)"]
Sub -->|"(none)"| Docs["executeGenerateDocs() → generateDocs(target, config, outDirectory)\n(see subgraph below)"]
Init --> Result["log success/error, return ReturnCode.Success | InternalError"]
Dot --> Result
Docs --> Result
```

## `initDocs()` — scaffold the doxy folder

```mermaid
flowchart TD
A["initDocs() — initDocs.ts:6"] --> B["setupDoxygen('.') — utils.ts\ncopy() the packaged src/doxy template\n(Doxyfile, DoxygenLayout.xml, favicon.ico, logo.png,\nnew_footer.html, new_header.html, new_stylesheet.css)\ninto {projectDir}/sasjs/doxy"]
```

## `generateDocs()` — the default subcommand

```mermaid
flowchart TD
A["generateDocs(target?, config?, outDirectory?) — generateDocs.ts:32"] --> B["getDocConfig(target, config, outDirectory)\nresolves: newOutDirectory (target > config > default),\nserverUrl (for lineage links), enableLineage (target > config > true),\ndoxyContent (config merged with target override)"]
B --> C["getFoldersForDocs(target, config)\ncollects macroCore/macro/program/service/job folders,\ntarget-level entries appended to root-level entries\n(macroCore uses target's displayMacroCore only if explicitly set there)"]
C --> D{"combinedFolders empty?"}
D -->|yes| E["throw: 'Unable to locate folders for generating docs.'"]
D -->|no| F["read {projectDir}/package.json for PROJECT_NAME / PROJECT_BRIEF\n(falls back to placeholder text if missing/unparseable)"]
F --> G["merge doxyContent defaults (favIcon/footer/header/layout/logo/readMe/\nstylesheet/path=sasjs/doxy) with docConfig.doxyContent override;\nresolve doxyContent.path to absolute if overridden"]
G --> H{"enableLineage?"}
H -->|yes| I["LAYOUT_FILE = doxyContent.path/DoxygenLayout.xml, used as-is"]
H -->|no| J["read DoxygenLayout.xml, strip the Lineage <tab> entry,\nwrite a timestamped tmp copy → LAYOUT_FILE"]
I --> K
J --> K["build readMePath = join(doxyContent.path, doxyContent.readMe);\nset DOXY_INPUT (readMePath + all folders), DOXY_MAINPAGE (readMePath),\nHTML_* / PROJECT_* / LAYOUT_FILE via setVariableCmd()\n(env-var-style shell prefix, OS-specific syntax)"]
K --> L["getDoxyConfigPath(doxyContent.path)\nlooks for Doxyfile, then DoxyFile, doxyfile, doxyFile as fallbacks;\nthrows if none found"]
L --> M["(re)create newOutDirectory"]
M --> N["shelljs.exec('doxyParams doxygen configPath')\nnon-silent only when LOG_LEVEL=Debug"]
N --> O{"!enableLineage?"}
O -->|yes| P["delete the tmp LAYOUT_FILE created earlier"]
O -->|no| Q
P --> Q{"exec code !== 0?"}
Q -->|yes, stderr starts with 'error: '| R["throw '\\n' + stderr"]
Q -->|yes, other stderr| S["throw 'Doxygen application is not installed or configured...' + stderr"]
Q -->|no| T{"enableLineage?"}
T -->|yes| U["createDotFiles(service+job folders, newOutDirectory, serverUrl)\n(see createDotFiles subgraph)"]
T -->|no| V
U --> V["return { outDirectory: newOutDirectory }"]
```

## `generateDot()` — `sasjs doc lineage` subcommand

```mermaid
flowchart TD
A["generateDot(target?, config?, outDirectory?) — generateDot.ts:14"] --> B["getDocConfig(target, config, outDirectory)\n→ serverUrl, newOutDirectory"]
B --> C["getFoldersForDocs(target, config)\n→ service + job folders only (no macro/program/macroCore)"]
C --> D["createDotFiles(folderList, newOutDirectory, serverUrl)\n(see subgraph below)"]
D --> E["return { outDirectory: newOutDirectory }"]
```

## `createDotFiles()` — lineage dot/svg generation

```mermaid
flowchart TD
A["createDotFiles(folderList, outDirectory, serverUrl) — createDotFiles.ts:14"] --> B["createFolder(outDirectory)"]
B --> C["getDotFileContent(folderList, serverUrl)\nparses each service/job file's Input/Output header annotations\ninto DOT graph nodes/edges (libs, tables, jobs), serverUrl\nprefixes node links to the Data Controller viewer if configured"]
C --> D["createFile(outDirectory/data_lineage.dot, dotFileContent)"]
D --> E["graphviz.dot(dotFileContent, 'svg') — node-graphviz"]
E --> F{"graphviz succeeds?"}
F -->|yes| G["createFile(outDirectory/data_lineage.svg, svg)"]
F -->|no| H["throw 'Unable to generate graph from generated Dot file.' + error"]
```

## Key facts

- `getDocConfig` establishes precedence rules used throughout: target-level
`docConfig` wins over root `sasjsconfig.json` `docConfig`, which wins over
built-in defaults. `enableLineage` defaults to `true` if neither specifies it.
- `getFoldersForDocs` behaves differently for the two generators:
`generateDocs` includes macro/macroCore/program folders in addition to
service/job folders (everything Doxygen needs to scan); `generateDot` only
needs service/job folders (the only place lineage-relevant Input/Output
annotations live).
- Doxygen itself is invoked as an external shell command (`doxygen`) - the whole
`generateDocs` flow assumes it's installed and on `PATH`; a non-zero exit code
is the only failure signal available, distinguished only by whether `stderr`
happens to start with `error: ` (a specific Doxygen-side config error) versus
anything else (treated as "not installed/configured").
- `enableLineage` controls two independent things: whether the Lineage tab
appears in the generated Doxygen layout (via a stripped tmp copy of
`DoxygenLayout.xml`), and whether `createDotFiles` runs afterward to produce
`data_lineage.dot`/`.svg`. Both are gated by the same flag but are otherwise
unrelated code paths.
- `getDoxyConfigPath` tries four case variants of the config filename
(`Doxyfile`, `DoxyFile`, `doxyfile`, `doxyFile`) in that order and uses
whichever is found first; it throws only if none exist.
- `initDocs` is purely a file-copy operation - it doesn't read or validate
any existing project state, so re-running it resets `sasjs/doxy/` back to
the packaged template.
60 changes: 46 additions & 14 deletions src/commands/compile/internal/compileTestFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,47 @@ import { loadDependencies } from './'

const getFileName = (filePath: string) => path.parse(filePath).base

/**
* Resolves the relative destination fragment for a compiled test file inside
* `sasjsbuild/tests` - either just the file's basename (saveToRoot, used for
* testSetUp/testTearDown), or the path fragment following the build
* destination folder name (used for service/job test files).
*
* `filePath` for testSetUp/testTearDown comes straight from a user's
* sasjsconfig.json, which conventionally uses forward slashes for
* portability - it is not guaranteed to match the current OS's separator.
* Splitting on both `/` and `\` (instead of only `path.sep`) means this
* works whether the config was authored with POSIX-style paths and the CLI
* is running on Windows (`path.sep === '\\'`) or vice versa. Splitting on
* `path.sep` alone silently no-ops when the string contains none of that
* separator, leaving the whole config path intact and producing a nested
* `sasjsbuild/tests/sasjs/tests/testsetup.sas` instead of a flat
* `sasjsbuild/tests/testsetup.sas`.
*/
export const getTestFileDestinationFragment = (
filePath: string,
buildDestinationFolderName: string | undefined,
saveToRoot: boolean
): string => {
const segments = filePath.split(/[\\/]/)

const destinationFragment = saveToRoot
? segments.pop() || ''
: segments
.reduce(
(acc: string[], item: string, i: number, arr: string[]) =>
acc.length
? [...acc, item]
: arr[i - 1] === buildDestinationFolderName
? [...acc, item]
: acc,
[] as string[]
)
.join(path.sep)

return destinationFragment
}

export async function compileTestFile(
target: Target,
filePath: string,
Expand Down Expand Up @@ -55,20 +96,11 @@ export async function compileTestFile(
destinationPath ||
path.join(
buildDestinationTestFolder,
saveToRoot
? filePath.split(path.sep).pop() || ''
: filePath
.split(path.sep)
.reduce(
(acc: any, item: any, i: any, arr: any) =>
acc.length
? [...acc, item]
: arr[i - 1] === buildDestinationFolderName
? [...acc, item]
: acc,
[]
)
.join(path.sep)
getTestFileDestinationFragment(
filePath,
buildDestinationFolderName,
saveToRoot
)
)

await createFile(destinationPath, dependencies)
Expand Down
Loading
Loading