From 812ddfcf8113a15f5bf04cc8f1682388fe292481 Mon Sep 17 00:00:00 2001 From: Mahati Shankar <93712176+smahati@users.noreply.github.com> Date: Thu, 5 Sep 2024 12:42:21 +0200 Subject: [PATCH 1/2] Release August 2024 (#1230) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: René Jeglinsky Co-authored-by: Christian Georgi Co-authored-by: Jörg Mann <64193442+joergmann@users.noreply.github.com> Co-authored-by: Johannes Vogel <31311694+johannes-vogel@users.noreply.github.com> Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com> Co-authored-by: Heiko Witteborg Co-authored-by: ecklie <52252271+ecklie@users.noreply.github.com> Co-authored-by: Andre Meyering Co-authored-by: Dr. David A. Kunz Co-authored-by: Marc Becker Co-authored-by: hjboth <124381150+hjboth@users.noreply.github.com> Co-authored-by: Steffen Weinstock <79531202+stewsk@users.noreply.github.com> Co-authored-by: Steffen Waldmann Co-authored-by: Adrian Görler Co-authored-by: Arley Triana Morin Co-authored-by: Daniel Hutzel Co-authored-by: rashmiangadi11 Co-authored-by: Christian Georgi Co-authored-by: Lothar Bender Co-authored-by: Markus Ofterdinger Co-authored-by: Vladimir Co-authored-by: DJ Adams Co-authored-by: Daniel O'Grady <103028279+daogrady@users.noreply.github.com> Co-authored-by: mariayord Co-authored-by: Markus Haug Co-authored-by: Robin Co-authored-by: BraunMatthias <59841349+BraunMatthias@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Co-authored-by: Matthias Schur <107557548+MattSchur@users.noreply.github.com> Co-authored-by: Stefan Henke Co-authored-by: Thomas Bonk Co-authored-by: Gopal Anand <32189444+gopalanand333@users.noreply.github.com> Co-authored-by: Patrice Bender Co-authored-by: Olena Co-authored-by: D070615 Co-authored-by: Marten Schiwek Co-authored-by: simonoswald <126768147+simonoswald@users.noreply.github.com> Co-authored-by: RoshniNaveenaS <132035609+RoshniNaveenaS@users.noreply.github.com> Co-authored-by: Matthias Kuhr <52661546+MatKuhr@users.noreply.github.com> Co-authored-by: Oliver Klemenz <36187574+oklemenz2@users.noreply.github.com> Co-authored-by: Samuel Brucksch Co-authored-by: Preetam Kajal Rout <3712035+preetamkajalrout@users.noreply.github.com> Co-authored-by: Marcel Schwarz Co-authored-by: Daniel Schlachter Co-authored-by: Gregor Wolf Co-authored-by: Andrei Vishnevsky Co-authored-by: Evgeny Andreev --- .../cds-snippet-checker/check-cds-snippets.js | 2 +- .github/java-snippet-checker/.gitignore | 1 + .../check-java-snippets.js | 308 +++++ .github/java-snippet-checker/package.json | 17 + .github/workflows/PR.yml | 8 +- .vitepress/config.ts | 8 +- .vitepress/theme/Layout.vue | 2 + .vitepress/theme/components/ScrollToTop.vue | 74 ++ .../implvariants/ImpVariantsHint.vue | 8 +- .vitepress/theme/index.ts | 2 + advanced/analytics.md | 2 +- advanced/assets/database-statements.png | Bin 93323 -> 0 bytes advanced/assets/service.png | Bin 59513 -> 0 bytes advanced/hybrid-testing.md | 21 + advanced/monitoring.md | 34 - advanced/odata.md | 52 +- advanced/publishing-apis/openapi.md | 24 +- cds/cdl.md | 3 +- cds/compiler-v2.md | 2 + get-started/in-a-nutshell.md | 43 +- get-started/jumpstart.md | 7 +- get-started/troubleshooting.md | 62 +- guides/databases-hana.md | 44 + guides/databases-postgres.md | 45 +- guides/databases.md | 2 +- guides/domain-modeling.md | 2 +- guides/i18n.md | 2 +- guides/messaging/index.md | 5 +- guides/messaging/s4.md | 11 +- guides/providing-services.md | 9 +- guides/security/authorization.md | 5 +- guides/using-services.md | 2 +- java/cds-data.md | 58 +- java/change-tracking.md | 17 +- java/cqn-services/application-services.md | 2 +- java/cqn-services/persistence-services.md | 14 +- java/developing-applications/building.md | 2 +- java/developing-applications/properties.md | 2 + java/event-handlers/index.md | 5 +- java/getting-started.md | 2 +- java/migration.md | 3 + java/operating-applications/observability.md | 2 +- java/reflection-api.md | 4 +- java/working-with-cql/query-api.md | 2 +- java/working-with-cql/query-execution.md | 3 +- node.js/best-practices.md | 2 +- node.js/cds-log.md | 48 +- node.js/cds-reflect.md | 11 - node.js/fiori.md | 14 - node.js/typescript.md | 36 +- package-lock.json | 1117 ++++++++++------- package.json | 2 +- plugins/index.md | 2 +- tools/apis/cds-add.md | 8 +- tools/cds-cli.md | 16 +- 55 files changed, 1514 insertions(+), 665 deletions(-) create mode 100644 .github/java-snippet-checker/.gitignore create mode 100755 .github/java-snippet-checker/check-java-snippets.js create mode 100644 .github/java-snippet-checker/package.json create mode 100644 .vitepress/theme/components/ScrollToTop.vue delete mode 100644 advanced/assets/database-statements.png delete mode 100644 advanced/assets/service.png delete mode 100644 advanced/monitoring.md diff --git a/.github/cds-snippet-checker/check-cds-snippets.js b/.github/cds-snippet-checker/check-cds-snippets.js index 54a6b1f82..eec82432a 100755 --- a/.github/cds-snippet-checker/check-cds-snippets.js +++ b/.github/cds-snippet-checker/check-cds-snippets.js @@ -296,7 +296,7 @@ function* extractSnippets(section) { // Code snippets may have a configuration in form of an HTML comment. // When a cds-mode comment exists, we ignore the language. if (snippets[1]) { - const modeRegEx = /cds-mode: ([^,]+)$/; + const modeRegEx = /cds-mode: ([^,;]+)/; const result = modeRegEx.exec(snippets[1]); if (result && result[1]) mode = validateMode(result[1].trim()); diff --git a/.github/java-snippet-checker/.gitignore b/.github/java-snippet-checker/.gitignore new file mode 100644 index 000000000..d8b83df9c --- /dev/null +++ b/.github/java-snippet-checker/.gitignore @@ -0,0 +1 @@ +package-lock.json diff --git a/.github/java-snippet-checker/check-java-snippets.js b/.github/java-snippet-checker/check-java-snippets.js new file mode 100755 index 000000000..fe5e4172e --- /dev/null +++ b/.github/java-snippet-checker/check-java-snippets.js @@ -0,0 +1,308 @@ +#!/usr/bin/env node + +// Java Snippet Checker +// =================== +// +// Similar to the CDS snippet checker, check Java snippets for syntax errors. +// We use the "java-parser" NPM package for that. +// All code-blocks are extracted. If they were set to `java`, we extract the +// snippet and parse it. +// +// In case of errors, we try to wrap the snippet and parse it again. +// +// - First try to parser it again with a class surrounding the snippet. +// - If that fails, try the same with a method. +// - If that fails, mark snippet as invalid. +// +// Also, we run a few pre-processing steps and use heuristics: +// We remove `...` markers and `imports`, etc. +// +// You can disable checking of a snippet by prepending a `` +// comment right before the snippet. +// +// TODO: +// - [ ] combine code with the CDS snippet checker. + +'use strict'; + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse as parseJava } from 'java-parser'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const projectDir = path.resolve(__dirname, '../..'); +const verbose = process.argv[2] === '--verbose'; + +// Get base directories +const excludedDirs = [ + '.git', + '.github', + 'node_modules', + '.reuse', + '.vitepress', + '.idea', + '.vscode', + '.devcontainer', +]; +const baseDirs = fs.readdirSync(projectDir) + .filter(file => fs.statSync(path.join(projectDir, file)).isDirectory() && !excludedDirs.includes(file)) + .map(file => path.join(projectDir, file)); + +const JAVA_MODE_SYNTAX = 'syntax'; +const JAVA_MODE_IGNORE = 'ignore'; +const javaModes = [ + JAVA_MODE_SYNTAX, + JAVA_MODE_IGNORE, +]; + +let counter = 0; +let hasAnySnippetErrors = false; + +// Logging should always go to stderr. Have some convenience functions to minimize verbosity. +const log = (...args) => { console.error(...args); }; +const error = (...args) => { console.error(...args); }; +const debug = (...args) => { verbose && console.error(...args); }; + +for (const dir of baseDirs) { + const files = getFilesInDirectory(dir, /[.]md/); + log(`Checking ${files.length} markdown documents in ${path.relative(projectDir, dir)}`); + + for (const snippet of extractSnippetsFromFiles(files)) { + ++counter; + + if (snippet.mode === JAVA_MODE_IGNORE) + continue; + + snippet.original = snippet.content; + snippet.content = prepareSnippet(snippet.content); + + const variations = [ + { content: snippet.content, error: null }, + { content: snippetAsMethod(snippet.content), error: null }, + { content: snippetAsCode(snippet.content), error: null }, + ]; + + // We assume that the snippet has an error. + // If any of the variations _passes_, then the snippet is ok. + let snippetHasError = true; + for (const variation of variations) { + variation.error = compileSnippet(variation.content); + if (!variation.error) { + snippetHasError = false; + break; // success + } + } + + if (snippetHasError) { + hasAnySnippetErrors = true; + printErrorForSnippet(snippet, variations); + + } else if (verbose) { + log(`Snippet ${counter}`); + log(snippet.content); + } + } + + log(''); +} + +log(`Checked ${counter} snippets.`); + +if (hasAnySnippetErrors) { + error('\nError! Found syntax errors!'); + process.exit(1); + +} else { + log('Success! Found no syntax errors.'); + process.exit(0); +} + +// ---------------------------------------------------------------------------- + +function printErrorForSnippet(snippet, variations) { + log('--------------------------------------------------------------------') + log(`Errors in file ./${path.relative(projectDir, snippet.file)}`) + log('In following snippet\n') + log(' ```java') + log(indentLines(snippet.original, 2)) + log(' ```') + log('') + + for (const variation of variations) { + log(`which was modified and compiled again as: + \`\`\`java +${indentLines(variation.content, 2)} + \`\`\` + +which then ended up with errors: + +${indentLines(variation.error.message, 2)} + `); + } + log('') +} + +/** + * @param {string} content + */ +function compileSnippet(content) { + try { + parseJava(content); + return null; + + } catch (e) { + // the Java parser uses this string in its error messages + if (!e.message.includes('sad panda')) + throw e; + + if (e.message.length > 200) { + // cut off message text; the original length is too large + e.message = e.message.slice(0, 200); + } + return e; + } +} + +function prepareSnippet(content) { + // Delete "import" statements, as they are mixed in with other code. + content = content.replace(/^import .*$/mug, ''); + // `= ...;` is replaced by `= null` + content = content.replace(/= *[.][.][.];/mug, '= null;'); + // `= ...` is replaced by `= null;` (additional semicolon) + content = content.replace(/= [.][.][.]/mug, '= null;'); + // `, ...` is removed + content = content.replace(/, ?[.][.][.]/mug, ''); + content = content.replace(/[.][.][.] ?,/mug, ''); + // And other remaining `...` are removed + content = content.replace(/[.][.][.]|…/g, ''); + // Sometimes `---` is used as a delimiter + content = content.replace(/^---+.*$/gm, ''); + return content; +} + +/** + * @param {string} content + */ +function snippetAsMethod(content) { + return `// Snippet Checker +class MyClass { +${ indentLines(content.trim(), 2) } +} +`; +} + +/** + * @param {string} content + */ +function snippetAsCode(content) { + return `// Snippet Checker +class SnippetCheckerClass { + void snippetCheckerMethod() { +${ indentLines(content.trim(), 4) } + } +} +`; +} + +/** + * @param {string[]} files + */ +function* extractSnippetsFromFiles(files) { + for (const filename of files) { + for (const section of extractSections(filename)) { + for (const snippet of extractSnippets(section.content)) { + yield { + file: filename, + ...snippet, + }; + } + } + } +} + +/** + * @param {string} file + */ +function* extractSections(file) { + const content = fs.readFileSync(file, 'utf-8'); + const sections = content.split(/^#/gm); + + for (const content of sections) { + // Skip empty parts + if (content.trim() === "") + continue; + + const heading = content.slice(0, content.indexOf('\n')); + yield { + heading, + content, + }; + } +} + +/** + * @param {string} section + */ +function* extractSnippets(section) { + // Note: [^] matches any character, including newlines + const re = /^(?:\s*\n)?```([a-zA-Z]+)\s*\n([^]*?)\n```\s*$/gm; + + let snippets; + while ((snippets = re.exec(section)) !== null) { + const language = snippets[2].toLowerCase(); + const content = snippets[3]; + let mode; + + if ('java' !== language) + continue; + + // Code snippets may have a configuration in form of an HTML comment. + // When a cds-mode comment exists, we ignore the language. + if (snippets[1]) { + const modeRegEx = /mode: ([^,;]+)/; + const result = modeRegEx.exec(snippets[1]); + if (result && result[1]) + mode = result[1].trim(); + } + + yield { mode, content }; + } +} + +/** + * @param {string} dir + * @param {RegExp} fileRegEx + * @returns {string[]} + */ +function getFilesInDirectory(dir, fileRegEx) { + let results = []; + const files = fs.readdirSync(dir); + + for (let file of files) { + file = path.resolve(dir, file); + const stat = fs.statSync(file); + if (stat && stat.isDirectory()) { + results = results.concat(getFilesInDirectory(file)); + } else { + if (!fileRegEx || file.match(fileRegEx)) + results.push(file); + } + } + return results; +} + +/** + * Indent the given string by `indent` whitespace characters. + * + * @param {string} str + * @param {number} indent + * @returns {string} + */ +function indentLines(str, indent) { + const indentStr = ' '.repeat(indent); + const lines = str.split(/\r\n?|\n/); + return lines.map(s => indentStr + s).join('\n'); +} + diff --git a/.github/java-snippet-checker/package.json b/.github/java-snippet-checker/package.json new file mode 100644 index 000000000..2ae6c2ff1 --- /dev/null +++ b/.github/java-snippet-checker/package.json @@ -0,0 +1,17 @@ +{ + "name": "java-snippet-checker", + "version": "0.0.1", + "description": "Markdown checker for Java snippets", + "type": "module", + "main": "check-java-snippets.js", + "author": "SAP SE (https://www.sap.com)", + "license": "SEE LICENSE IN LICENSE", + "repository": "cap-js/docs", + "homepage": "https://cap.cloud.sap/", + "scripts": { + "check": "node check-java-snippets.js" + }, + "dependencies": { + "java-parser": "^2.3.0" + } +} diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml index 4bf51d7e4..fcbfc7d15 100644 --- a/.github/workflows/PR.yml +++ b/.github/workflows/PR.yml @@ -18,10 +18,16 @@ jobs: with: node-version: 18.x cache: 'npm' - - run: | + - name: Run CDS snippet checker + run: | cd .github/cds-snippet-checker npm install npm run check + - name: Run Java snippet checker + run: | + cd .github/java-snippet-checker + npm install + npm run check - run: npm ci - run: npm test - run: npm run docs:build diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 456e6d145..c87995f95 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -23,8 +23,8 @@ if (!siteURL.pathname.endsWith('/')) siteURL.pathname += '/' const redirectLinks: Record = {} const latestVersions = { - java_services: '3.1.0', - java_cds4j: '3.1.0' + java_services: '3.2.0', + java_cds4j: '3.2.0' } const localSearchOptions = { @@ -93,8 +93,8 @@ const config:UserConfig = { // IMPORTANT: Don't use getters here, as they are called again and again! sidebar: menu, nav: [ - Object.assign(nav.find(i => i.text === 'Getting Started')!, {text:'Get Started'}), - Object.assign(nav.find(i => i.text === 'Cookbook')!, {text:'Guides'}), + { ...nav.find(i => i.text === 'Getting Started') ?? {}, text: 'Get Started' }, + { ...nav.find(i => i.text === 'Cookbook') ?? {}, text: 'Guides' }, nav.find(i => i.text === 'CDS'), nav.find(i => i.text === 'Node'), nav.find(i => i.text === 'Java'), diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue index 16490bbe3..c0317d173 100644 --- a/.vitepress/theme/Layout.vue +++ b/.vitepress/theme/Layout.vue @@ -6,6 +6,7 @@ import ImplVariants from './components/implvariants/ImplVariants.vue' import NavScreenMenuItem from './components/implvariants/NavScreenMenuItem.vue' import NotFound from './components/NotFound.vue' import Ribbon from './components/Ribbon.vue' +import ScrollToTop from './components/ScrollToTop.vue' const isPreview = !!import.meta.env.VITE_CAPIRE_PREVIEW @@ -18,6 +19,7 @@ const { frontmatter } = useData()