From cca224f3acd9142cb4fa338d9865af2634a03992 Mon Sep 17 00:00:00 2001 From: Salome DO Date: Thu, 29 Aug 2024 11:13:57 +0200 Subject: [PATCH] feat: otter training - tools and services --- apps/showcase/package.json | 9 + apps/showcase/src/services/index.ts | 1 + .../src/services/webcontainer/index.ts | 3 + .../webcontainer/webcontainer-runner.ts | 254 ++++++++++++++++++ .../webcontainer/webcontainer.helpers.ts | 60 +++++ .../webcontainer/webcontainer.service.ts | 101 +++++++ package.json | 25 ++ packages/@o3r-training/tools/.compodocrc.json | 22 ++ packages/@o3r-training/tools/.eslintrc.js | 19 ++ packages/@o3r-training/tools/.gitignore | 3 + packages/@o3r-training/tools/.npmignore | 0 packages/@o3r-training/tools/README.md | 39 +++ .../extract-folder-structure.ts | 45 ++++ packages/@o3r-training/tools/jest.config.js | 10 + packages/@o3r-training/tools/package.json | 136 ++++++++++ packages/@o3r-training/tools/project.json | 67 +++++ .../@o3r-training/tools/src/public_api.ts | 1 + .../@o3r-training/tools/src/utility/index.ts | 1 + .../tools/src/utility/web-container.ts | 85 ++++++ .../tools/testing/jest.config.ut.builders.js | 15 ++ .../tools/testing/jest.config.ut.js | 16 ++ .../@o3r-training/tools/testing/setup-jest.ts | 0 .../@o3r-training/tools/tsconfig.build.json | 23 ++ .../@o3r-training/tools/tsconfig.cli.json | 22 ++ .../@o3r-training/tools/tsconfig.doc.json | 10 + .../@o3r-training/tools/tsconfig.eslint.json | 9 + packages/@o3r-training/tools/tsconfig.json | 15 ++ .../@o3r-training/tools/tsconfig.spec.json | 17 ++ tsconfig.build.json | 3 +- yarn.lock | 176 ++++++++++++ 30 files changed, 1186 insertions(+), 1 deletion(-) create mode 100644 apps/showcase/src/services/index.ts create mode 100644 apps/showcase/src/services/webcontainer/index.ts create mode 100644 apps/showcase/src/services/webcontainer/webcontainer-runner.ts create mode 100644 apps/showcase/src/services/webcontainer/webcontainer.helpers.ts create mode 100644 apps/showcase/src/services/webcontainer/webcontainer.service.ts create mode 100644 packages/@o3r-training/tools/.compodocrc.json create mode 100644 packages/@o3r-training/tools/.eslintrc.js create mode 100644 packages/@o3r-training/tools/.gitignore create mode 100644 packages/@o3r-training/tools/.npmignore create mode 100644 packages/@o3r-training/tools/README.md create mode 100644 packages/@o3r-training/tools/cli/extract-folder-structure/extract-folder-structure.ts create mode 100644 packages/@o3r-training/tools/jest.config.js create mode 100644 packages/@o3r-training/tools/package.json create mode 100644 packages/@o3r-training/tools/project.json create mode 100644 packages/@o3r-training/tools/src/public_api.ts create mode 100644 packages/@o3r-training/tools/src/utility/index.ts create mode 100644 packages/@o3r-training/tools/src/utility/web-container.ts create mode 100644 packages/@o3r-training/tools/testing/jest.config.ut.builders.js create mode 100644 packages/@o3r-training/tools/testing/jest.config.ut.js create mode 100644 packages/@o3r-training/tools/testing/setup-jest.ts create mode 100644 packages/@o3r-training/tools/tsconfig.build.json create mode 100644 packages/@o3r-training/tools/tsconfig.cli.json create mode 100644 packages/@o3r-training/tools/tsconfig.doc.json create mode 100644 packages/@o3r-training/tools/tsconfig.eslint.json create mode 100644 packages/@o3r-training/tools/tsconfig.json create mode 100644 packages/@o3r-training/tools/tsconfig.spec.json diff --git a/apps/showcase/package.json b/apps/showcase/package.json index 867e5e1e31..313e848aa5 100644 --- a/apps/showcase/package.json +++ b/apps/showcase/package.json @@ -49,6 +49,7 @@ "@ngrx/store-devtools": "~18.0.0", "@ngx-translate/core": "~15.0.0", "@nx/jest": "~19.5.0", + "@o3r-training/tools": "workspace:^", "@o3r/application": "workspace:^", "@o3r/components": "workspace:^", "@o3r/configuration": "workspace:^", @@ -61,12 +62,19 @@ "@o3r/styling": "workspace:^", "@o3r/testing": "workspace:^", "@popperjs/core": "^2.11.5", + "@vscode/codicons": "^0.0.35", + "@webcontainer/api": "1.3.0-internal.2", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.0.0", "ag-grid-angular": "~31.1.1", "ag-grid-community": "~32.0.1", "bootstrap": "5.3.3", "highlight.js": "^11.8.0", "intl-messageformat": "~10.5.1", + "monaco-editor": "~0.50.0", "ngx-highlightjs": "^12.0.0", + "ngx-monaco-editor-v2": "^18.0.0", + "ngx-monaco-tree": "^17.5.0", "pixelmatch": "^5.2.1", "pngjs": "^7.0.0", "rxjs": "^7.8.1", @@ -101,6 +109,7 @@ "@typescript-eslint/parser": "^7.14.1", "@typescript-eslint/types": "^7.14.1", "@typescript-eslint/utils": "^7.14.1", + "@webcontainer/api": "1.3.0-internal.2", "concurrently": "^8.0.0", "eslint": "^8.57.0", "eslint-import-resolver-node": "^0.3.9", diff --git a/apps/showcase/src/services/index.ts b/apps/showcase/src/services/index.ts new file mode 100644 index 0000000000..513b97620b --- /dev/null +++ b/apps/showcase/src/services/index.ts @@ -0,0 +1 @@ +export * from './webcontainer'; diff --git a/apps/showcase/src/services/webcontainer/index.ts b/apps/showcase/src/services/webcontainer/index.ts new file mode 100644 index 0000000000..eeb8195ee4 --- /dev/null +++ b/apps/showcase/src/services/webcontainer/index.ts @@ -0,0 +1,3 @@ +export * from './webcontainer.helpers'; +export * from './webcontainer.service'; +export * from './webcontainer-runner'; diff --git a/apps/showcase/src/services/webcontainer/webcontainer-runner.ts b/apps/showcase/src/services/webcontainer/webcontainer-runner.ts new file mode 100644 index 0000000000..4ecc16fd47 --- /dev/null +++ b/apps/showcase/src/services/webcontainer/webcontainer-runner.ts @@ -0,0 +1,254 @@ +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {FileSystemTree, IFSWatcher, WebContainer, WebContainerProcess} from '@webcontainer/api'; +import {Terminal} from '@xterm/xterm'; +import { + BehaviorSubject, + combineLatestWith, + distinctUntilChanged, + filter, + from, + map, + Observable, + switchMap +} from 'rxjs'; +import {withLatestFrom} from 'rxjs/operators'; +import {doesFolderExist, killTerminal} from './webcontainer.helpers'; + +const createTerminalStream = (terminal: Terminal, cb?: (data: string) => void | Promise) => new WritableStream({ + write: (data) => { + if (cb) { + void cb(data); + } + terminal.write(data); + } +}); + +const makeProcessWritable = (process: WebContainerProcess, terminal: Terminal) => { + const input = process.input.getWriter(); + terminal.onData((data) => input.write(data)); + return input; +}; + +export class WebContainerRunner { + /** + * WebContainer instance which is available after the boot of the WebContainer + */ + public readonly instancePromise: Promise; + private readonly commands = new BehaviorSubject<{queue: string[]; cwd: string}>({queue: [], cwd: ''}); + private readonly commandOnRun$: Observable<{command: string; cwd: string} | undefined> = this.commands.pipe( + map((commands) => ( + commands.queue.length > 0 ? {command: commands.queue[0], cwd: commands.cwd} : undefined + )) + ); + private readonly iframe = new BehaviorSubject(null); + private readonly shell = { + terminal: new BehaviorSubject(null), + process: new BehaviorSubject(null), + writer: new BehaviorSubject(null), + cwd: new BehaviorSubject(null) + }; + private readonly commandOutput = { + terminal: new BehaviorSubject(null), + process: new BehaviorSubject(null), + outputLocked: new BehaviorSubject(false) + }; + private watcher: IFSWatcher | null = null; + + constructor() { + this.instancePromise = WebContainer.boot().then((instance) => { + // eslint-disable-next-line no-console + instance.on('error', console.error); + return instance; + }); + this.commandOnRun$.pipe( + filter((currentCommand): currentCommand is {command: string; cwd: string} => !!currentCommand), + takeUntilDestroyed() + ).subscribe(({command, cwd}) => { + const commandElements = command.split(' '); + void this.runCommand(commandElements[0], commandElements.slice(1), cwd); + }); + + this.iframe.pipe( + filter((iframe): iframe is HTMLIFrameElement => !!iframe), + distinctUntilChanged(), + withLatestFrom(this.instancePromise), + takeUntilDestroyed() + ).subscribe(([iframe, instance]) => + instance.on('server-ready', (_port: number, url: string) => { + iframe.removeAttribute('srcdoc'); + iframe.src = url; + }) + ); + + this.commandOutput.process.pipe( + filter((process): process is WebContainerProcess => !!process && !process.output.locked), + combineLatestWith( + this.commandOutput.terminal.pipe( + filter((terminal): terminal is Terminal => !!terminal) + ) + ), + filter(([process]) => !process.output.locked), + takeUntilDestroyed() + ).subscribe(([process, terminal]) => + void process.output.pipeTo(createTerminalStream(terminal)) + ); + this.shell.writer.pipe( + filter((writer): writer is WritableStreamDefaultWriter => !!writer), + combineLatestWith( + this.shell.cwd.pipe(filter((cwd): cwd is string => !!cwd)) + ), + withLatestFrom(this.instancePromise), + takeUntilDestroyed() + ).subscribe(async ([[writer, processCwd], instance]) => { + try { + await writer.write(`cd ${instance.workdir}/${processCwd} && clear \n`); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e, processCwd); + } + }); + this.shell.process.pipe( + filter((process): process is null => !process), + combineLatestWith( + this.shell.terminal.pipe(filter((terminal): terminal is Terminal => !!terminal)) + ), + withLatestFrom(this.instancePromise), + switchMap(([[_, terminal], instance]) => { + const spawn = instance.spawn('jsh'); + return from(spawn).pipe( + map((process) => ({ + process, + terminal + })) + ); + }), + takeUntilDestroyed() + ).subscribe(({process, terminal}) => { + const cb = (data: string) => { + if (['CREATE', 'UPDATE', 'RENAME', 'DELETE'].some((action) => data.includes(action))) { + this.treeUpdateCallback(); + } + }; + void process.output.pipeTo(createTerminalStream(terminal, cb)); + this.shell.writer.next(makeProcessWritable(process, terminal)); + this.shell.process.next(process); + }); + } + + /** + * Callback on tree update + */ + private treeUpdateCallback = () => {}; + + /** + * Run a command in the requested cwd and unqueue the runner commandList + * @param command + * @param args + * @param cwd + */ + private async runCommand(command: string, args: string[], cwd: string) { + const instance = await this.instancePromise; + const process = await instance.spawn(command, args, {cwd: cwd}); + this.commandOutput.process.next(process); + const exitCode = await process.exit; + if (exitCode !== 0) { + throw new Error(`Command ${[command, ...args].join(' ')} failed with ${exitCode}!`); + } + this.commands.next({queue: this.commands.value.queue.slice(1), cwd}); + } + + /** + * Run a project. Mount requested files and run the associated commands in the cwd folder. + * @param files to mount + * @param commands to run in the project folder + * @param projectFolder + * @param override allow to mount files and override a project already mounted on the web container + */ + public async runProject(files: FileSystemTree, commands: string[], projectFolder: string, override = false) { + const instance = await this.instancePromise; + // Ensure boot is done and instance is ready for use + this.shell.cwd.next(projectFolder); + killTerminal(this.commandOutput.terminal, this.commandOutput.process); + const iframe = this.iframe.value; + if (iframe) { + iframe.src = ''; + iframe.srcdoc = 'Loading...'; + } + if (this.watcher) { + this.watcher.close(); + } + if (override || !(await doesFolderExist(projectFolder, instance))) { + await instance.mount({[projectFolder]: {directory: files}}); + } + this.treeUpdateCallback(); + this.commands.next({queue: commands, cwd: projectFolder}); + this.watcher = instance.fs.watch(`/${projectFolder}`, {encoding: 'utf-8'}, this.treeUpdateCallback); + } + + /** + * Register the method to call whenever the tree is updated + * @param callback + */ + public registerTreeUpdateCallback(callback: () => object) { + this.treeUpdateCallback = callback; + } + + /** + * Register a new terminal that will be used as shell for the webcontainer + * It is a dedicated sh process to input command. + * @param terminal + */ + public registerShell(terminal: Terminal) { + this.shell.terminal.next(terminal); + } + + /** + * Register a new terminal to display the current process output + * @param terminal + */ + public registerCommandOutputTerminal(terminal: Terminal) { + this.commandOutput.terminal.next(terminal); + } + + /** + * Register an iframe which will show the app resulting of the diverse commands run on the webcontainer + * @param iframe + */ + public registerIframe(iframe: HTMLIFrameElement | null) { + const previousIframe = this.iframe.value; + if (previousIframe) { + previousIframe.src = ''; + } + this.iframe.next(iframe); + } + + /** + * Kill the current shell process and unregister the shell terminal + */ + public disposeShell() { + this.shell.terminal.next(null); + void this.shell.writer.value?.close(); + this.shell.writer.next(null); + killTerminal(this.shell.terminal, this.shell.process); + } + + /** + * Kill the output terminal process and clear the console + */ + public disposeCommandOutputTerminal() { + killTerminal(this.shell.terminal, this.shell.process); + this.commandOutput.terminal.next(null); + } + + /** + * Kill all the webContainer processes and unregister the terminal and iframe. + */ + public killContainer() { + killTerminal(this.shell.terminal, this.shell.process); + killTerminal(this.commandOutput.terminal, this.commandOutput.process); + const iframe = this.iframe.value; + if (iframe) { + iframe.src = ''; + } + } +} diff --git a/apps/showcase/src/services/webcontainer/webcontainer.helpers.ts b/apps/showcase/src/services/webcontainer/webcontainer.helpers.ts new file mode 100644 index 0000000000..d83b4ef548 --- /dev/null +++ b/apps/showcase/src/services/webcontainer/webcontainer.helpers.ts @@ -0,0 +1,60 @@ +import {FileSystem, getFilesTree} from '@o3r-training/tools'; +import {DirectoryNode, FileNode, WebContainer, WebContainerProcess} from '@webcontainer/api'; +import {Terminal} from '@xterm/xterm'; +import {MonacoTreeElement} from 'ngx-monaco-tree'; +import {BehaviorSubject} from 'rxjs'; + +/** + * Convert the given path and node to a MonacoTreeElement + * @param path + * @param node + */ +export function convertTreeRec(path: string, node: DirectoryNode | FileNode): MonacoTreeElement { + return { + name: path, + content: (node as DirectoryNode).directory + ? Object.entries((node as DirectoryNode).directory) + .map(([p, n]) => convertTreeRec(p, n)) + : undefined + }; +} + +/** + * Checks if the folder exists at the root of the WebContainer instance + * @param folderName + * @param instance + * @private + */ +export async function doesFolderExist(folderName: string, instance: WebContainer) { + try { + await instance.fs.readdir(folderName); + return true; + } catch (_) { + return false; + } +} + +/** + * Get the file tree from the path of the File System of the given WebContainer instance + * @param instance + * @param path + * @private + */ +export async function getFilesTreeFromContainer(instance: WebContainer, excludedFilesOrDirectories: string[] = [], path = '/') { + return await getFilesTree([{ + path, + isDir: true + }], instance.fs as FileSystem, excludedFilesOrDirectories); +} + +/** + * Kill a terminal process and clear its content + * @param terminalSubject + * @param processSubject + * @private + */ +export function killTerminal(terminalSubject: BehaviorSubject, processSubject: BehaviorSubject) { + processSubject.value?.kill(); + processSubject.next(null); + terminalSubject.value?.clear(); +} diff --git a/apps/showcase/src/services/webcontainer/webcontainer.service.ts b/apps/showcase/src/services/webcontainer/webcontainer.service.ts new file mode 100644 index 0000000000..1cce6578b9 --- /dev/null +++ b/apps/showcase/src/services/webcontainer/webcontainer.service.ts @@ -0,0 +1,101 @@ +import {Injectable} from '@angular/core'; +import {FileSystemTree} from '@webcontainer/api'; +import {BehaviorSubject, distinctUntilChanged, map} from 'rxjs'; +import {MonacoTreeElement} from 'ngx-monaco-tree'; +import {convertTreeRec, getFilesTreeFromContainer} from './webcontainer.helpers'; +import {WebContainerRunner} from './webcontainer-runner'; + +/** List of files or directories to exclude from the file tree */ +const EXCLUDED_FILES_OR_DIRECTORY = ['node_modules', '.angular', '.vscode']; + +@Injectable({ + providedIn: 'root' +}) +export class WebContainerService { + public readonly runner = new WebContainerRunner(); + private readonly monacoTree = new BehaviorSubject([]); + + public monacoTree$ = this.monacoTree.pipe( + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)) + ); + public isReady$ = this.monacoTree$.pipe( + map((tree) => tree.length > 0) + ); + + /** + * Get the Monaco file tree from the given root path + * @param rootPath + * @private + */ + private async getMonacoTree(rootPath: string): Promise { + const instance = await this.runner.instancePromise; + const tree = await getFilesTreeFromContainer(instance, EXCLUDED_FILES_OR_DIRECTORY, rootPath); + return Object.entries(tree) + .reduce( + (acc: MonacoTreeElement[], [path, node]) => + acc.concat(convertTreeRec(path, node)), + [] + ); + } + + /** + * Load a new project: mount the files in the dedicated folder, update the monaco tree and watch the folder updates and + * run the initialization commands + * @param files + * @param commands + * @param exerciseName + */ + public async loadProject(files: FileSystemTree, commands: string[], exerciseName: string) { + this.monacoTree.next([]); + this.runner.registerTreeUpdateCallback(async () => { + const tree = await this.getMonacoTree(`/${exerciseName}`); + this.monacoTree.next(tree); + }); + return this.runner.runProject(files, commands, exerciseName); + } + + /** + * Writes a file with the provided content to the given path + * @param file + * @param content + */ + public async writeFile(file: string, content: string) { + const instance = await this.runner.instancePromise; + + return instance.fs.writeFile(file, content); + } + + /** + * Reads the file at the given path + * @param file + */ + public async readFile(file: string): Promise { + const instance = await this.runner.instancePromise; + + return instance.fs.readFile(file, 'utf-8'); + } + + /** + * Determine if the file path entry is a file + * @param filePath + */ + public async isFile(filePath: string) { + const instance = await this.runner.instancePromise; + const parent = `${!filePath.startsWith('/') ? '/' : ''}${filePath}` + .split('/') + .slice(0, -1) + .join('/'); + const fileEntries = await instance.fs.readdir(parent, {encoding: 'utf-8', withFileTypes: true}); + const fileEntry = fileEntries.find((file) => filePath.split('/').pop() === file.name); + return !!fileEntry?.isFile(); + } + + /** + * Log the file tree of the current instance (for debugging purposes) + */ + public async logTree() { + const instance = await this.runner.instancePromise; + // eslint-disable-next-line no-console + console.log(await getFilesTreeFromContainer(instance, EXCLUDED_FILES_OR_DIRECTORY)); + } +} diff --git a/package.json b/package.json index 1a62347437..c40e0ad78b 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,7 @@ "@nx/jest": "~19.5.0", "@nx/js": "~19.5.0", "@nx/workspace": "~19.5.0", + "@o3r-training/tools": "workspace:^", "@o3r/build-helpers": "workspace:^", "@o3r/eslint-config-otter": "workspace:^", "@o3r/eslint-plugin": "workspace:^", @@ -208,6 +209,9 @@ "@typescript-eslint/parser": "^7.14.1", "@typescript-eslint/types": "^7.14.1", "@typescript-eslint/utils": "^7.14.1", + "@webcontainer/api": "1.3.0-internal.2", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "*", "@yarnpkg/sdks": "^3.0.0", "ag-grid-angular": "~31.1.1", "ag-grid-community": "~32.0.1", @@ -238,8 +242,11 @@ "lighthouse": "9.6.8", "lint-staged": "^15.0.0", "minimist": "^1.2.6", + "monaco-editor": "~0.50.0", "ng-packagr": "~18.2.0", "ngx-highlightjs": "^12.0.0", + "ngx-monaco-editor-v2": "^18.0.0", + "ngx-monaco-tree": "^17.5.0", "npm-run-all2": "^6.0.0", "nx": "~19.5.0", "playwright-lighthouse": "2.2.2", @@ -267,8 +274,26 @@ "@swc/core": { "built": true }, + "@vscode/codicons": { + "unplugged": true + }, + "@xterm/addon-fit": { + "unplugged": true + }, + "@xterm/xterm": { + "unplugged": true + }, "esbuild": { "built": true + }, + "monaco-editor": { + "unplugged": true + }, + "ngx-monaco-editor-v2": { + "unplugged": true + }, + "ngx-monaco-tree": { + "unplugged": true } }, "engines": { diff --git a/packages/@o3r-training/tools/.compodocrc.json b/packages/@o3r-training/tools/.compodocrc.json new file mode 100644 index 0000000000..6b6ca256de --- /dev/null +++ b/packages/@o3r-training/tools/.compodocrc.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://raw.githubusercontent.com/compodoc/compodoc/develop/src/config/schema.json", + "name": "Schematics", + "output": "../../../generated-doc/schematics", + "tsconfig": "./tsconfig.doc.json", + "assetsFolder": "../../../.attachments", + "disableSourceCode": true, + "disableDomTree": true, + "disableTemplateTab": true, + "disableStyleTab": true, + "disableGraph": true, + "disableCoverage": true, + "disablePrivate": true, + "disableProtected": true, + "disableInternal": true, + "disableLifeCycleHooks": true, + "disableRoutesGraph": true, + "disableSearch": false, + "hideGenerator": true, + "customFavicon": "../../../assets/logo/flavors/otter-128x128.png", + "templates": "../../../compodoc-templates/package" +} diff --git a/packages/@o3r-training/tools/.eslintrc.js b/packages/@o3r-training/tools/.eslintrc.js new file mode 100644 index 0000000000..9b7f2d6ff2 --- /dev/null +++ b/packages/@o3r-training/tools/.eslintrc.js @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable quote-props */ + +module.exports = { + 'root': true, + 'parserOptions': { + 'tsconfigRootDir': __dirname, + 'project': [ + 'tsconfig.build.json', + 'tsconfig.spec.json', + 'tsconfig.cli.json', + 'tsconfig.eslint.json' + ], + 'sourceType': 'module' + }, + 'extends': [ + '../../../.eslintrc.js' + ] +}; diff --git a/packages/@o3r-training/tools/.gitignore b/packages/@o3r-training/tools/.gitignore new file mode 100644 index 0000000000..1eeceae556 --- /dev/null +++ b/packages/@o3r-training/tools/.gitignore @@ -0,0 +1,3 @@ +/build + +/dist* diff --git a/packages/@o3r-training/tools/.npmignore b/packages/@o3r-training/tools/.npmignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@o3r-training/tools/README.md b/packages/@o3r-training/tools/README.md new file mode 100644 index 0000000000..17d1a8fc29 --- /dev/null +++ b/packages/@o3r-training/tools/README.md @@ -0,0 +1,39 @@ +

Otter training tools

+

+ Super cute Otter! +

+ +This package is an [Otter Framework Module](https://github.com/AmadeusITGroup/otter/tree/main/docs/core/MODULE.md). +
+
+ +## Description + +[![Stable Version](https://img.shields.io/npm/v/@o3r/schematics?style=for-the-badge)](https://www.npmjs.com/package/@o3r/schematics) +[![Bundle Size](https://img.shields.io/bundlephobia/min/@o3r/schematics?color=green&style=for-the-badge)](https://www.npmjs.com/package/@o3r/schematics) + +This module provides basic utilities to use the Otter training such as: +- A code source extractor to generate JSON files compatible with [WebContainer](https://webcontainers.io/guides/working-with-the-file-system). + +## How to install + +```shell +ng add @o3r-training/tools +``` + +## How to use +You can use the extractor via the CLI to extract a folder from your file system: +```shell +o3r-extract-folder-structure --files \".\path-to-source-folder\" -o webcontainer-folder-structure.js +``` +You can also use the `getFilesTree` function with the `WebContainer` file system to serialize its tree: +```typescript +const serializedFiles = await getFilesTree([{ + path, + isDir: true + }], instance.fs as FileSystem, EXCLUDED_FILES_OR_DIRECTORY); +``` + +## Description + +This is a technical package to be used as dependency by [Otter modules](https://github.com/AmadeusITGroup/otter/tree/main/docs/core/MODULE.md) providing tools to used in the development of Otter training. diff --git a/packages/@o3r-training/tools/cli/extract-folder-structure/extract-folder-structure.ts b/packages/@o3r-training/tools/cli/extract-folder-structure/extract-folder-structure.ts new file mode 100644 index 0000000000..17102c414e --- /dev/null +++ b/packages/@o3r-training/tools/cli/extract-folder-structure/extract-folder-structure.ts @@ -0,0 +1,45 @@ +import {lstat, readdir, readFile, writeFile} from 'node:fs/promises'; +import {type FileSystem, getFilesTree} from '@o3r-training/tools'; +import {join, resolve} from 'node:path'; +import {program} from 'commander'; + +program + .description('Extract folder structure') + .requiredOption('--files ', 'List of files and folder to extract in addition to the path') + .option('-r, --root ', 'Root of the extraction') + .option('-o, --output ', 'Output folder path') + .parse(process.argv); + +const options: any = program.opts(); +const cwd = options.root ? resolve(process.cwd(), options.root) : process.cwd(); +const stringifyBigObject = (jsonObject: any): string => { + if (typeof jsonObject === 'string') { + return JSON.stringify(jsonObject); + } + if (Array.isArray(jsonObject)) { + return '[' + (jsonObject) + .map((value) => stringifyBigObject(value)) + .join(', ') + + ']'; + } + return '{' + + Object.entries(jsonObject) + .map(([key, value]) => `"${key}": ${stringifyBigObject(value)}`) + .join(', ') + + '}'; +}; +void (async () => { + const filesDescriptor = []; + for (const file of options.files.split(',')) { + const filePath = join(cwd, file); + filesDescriptor.push({isDir: (await lstat(file)).isDirectory(), path: filePath}); + } + const folderStructure = await getFilesTree(filesDescriptor, { + readdir: readdir, + readFile: readFile + } as FileSystem); + // TODO add a test not to exit the project; + const targetPath = join(cwd, options.output || 'folder-structure.json'); + const content = stringifyBigObject(folderStructure); + await writeFile(targetPath, content); +})(); diff --git a/packages/@o3r-training/tools/jest.config.js b/packages/@o3r-training/tools/jest.config.js new file mode 100644 index 0000000000..237fd70fc2 --- /dev/null +++ b/packages/@o3r-training/tools/jest.config.js @@ -0,0 +1,10 @@ +const getJestGlobalConfig = require('../../../jest.config.ut').getJestGlobalConfig; + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestGlobalConfig(), + projects: [ + '/testing/jest.config.ut.js', + '/testing/jest.config.ut.builders.js' + ] +}; diff --git a/packages/@o3r-training/tools/package.json b/packages/@o3r-training/tools/package.json new file mode 100644 index 0000000000..14cd083b63 --- /dev/null +++ b/packages/@o3r-training/tools/package.json @@ -0,0 +1,136 @@ +{ + "name": "@o3r-training/tools", + "version": "0.0.0-placeholder", + "private": true, + "description": "Otter training tools", + "module": "dist/src/public_api.js", + "esm2015": "dist/esm2015/public_api.js", + "esm2020": "dist/src/public_api.js", + "typings": "dist/src/public_api.d.ts", + "keywords": [ + "utilities", + "otter-training" + ], + "scripts": { + "nx": "nx", + "ng": "yarn nx", + "build:cjs": "swc src -d dist/cjs -C module.type=commonjs -q --strip-leading-paths", + "build:esm2015": "swc src -d dist/esm2015 -C module.type=es6 -q --strip-leading-paths", + "build:esm2020": "tsc -b tsconfig.build.json --pretty", + "build": "yarn nx build training-tools", + "postbuild": "yarn cpy 'package.json' dist && patch-package-json-main", + "prepare:publish": "prepare-publish ./dist", + "build:source": "yarn build:esm2020 && yarn build:cjs && yarn build:esm2015", + "build:cli": "tsc -b tsconfig.cli.json --pretty && yarn generate-cjs-manifest" + }, + "bin": { + "o3r-extract-folder-structure": "./dist/cli/extract-folder-structure/extract-folder-structure.js" + }, + "exports": { + "./package.json": { + "default": "./package.json" + }, + ".": { + "module": "./dist/src/public_api.js", + "esm2020": "./dist/src/public_api.js", + "esm2015": "./dist/esm2015/public_api.js", + "es2020": "./dist/cjs/public_api.js", + "default": "./dist/cjs/public_api.js", + "typings": "./dist/src/public_api.d.ts", + "import": "./dist/src/public_api.js", + "node": "./dist/cjs/public_api.js", + "require": "./dist/cjs/public_api.js" + } + }, + "peerDependencies": { + "@o3r/telemetry": "workspace:^", + "@schematics/angular": "~18.2.0", + "@webcontainer/api": "1.3.0-internal.2", + "eslint": "^8.57.0", + "rxjs": "^7.8.1", + "type-fest": "^4.10.2", + "typescript": "~5.5.4" + }, + "peerDependenciesMeta": { + "@angular-devkit/architect": { + "optional": true + }, + "@angular-devkit/core": { + "optional": true + }, + "@angular/cli": { + "optional": true + }, + "@o3r/telemetry": { + "optional": true + }, + "eslint": { + "optional": true + }, + "type-fest": { + "optional": true + } + }, + "dependencies": { + "commander": "^12.0.0", + "tslib": "^2.6.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~18.2.0", + "@angular-devkit/core": "~18.2.0", + "@angular-devkit/schematics": "~18.2.0", + "@angular-eslint/eslint-plugin": "~18.3.0", + "@angular/animations": "~18.2.0", + "@angular/cli": "~18.2.0", + "@angular/common": "~18.2.0", + "@angular/compiler": "~18.2.0", + "@angular/compiler-cli": "~18.2.0", + "@angular/core": "~18.2.0", + "@angular/platform-browser": "~18.2.0", + "@angular/platform-browser-dynamic": "~18.2.0", + "@babel/core": "~7.25.0", + "@babel/preset-typescript": "~7.24.0", + "@compodoc/compodoc": "^1.1.19", + "@nx/eslint": "~19.5.0", + "@nx/eslint-plugin": "~19.5.0", + "@nx/jest": "~19.5.0", + "@nx/js": "~19.5.0", + "@o3r/build-helpers": "workspace:^", + "@o3r/eslint-plugin": "workspace:^", + "@o3r/telemetry": "workspace:^", + "@schematics/angular": "~18.2.0", + "@stylistic/eslint-plugin-ts": "~2.4.0", + "@swc/cli": "~0.4.0", + "@swc/core": "~1.7.0", + "@swc/helpers": "~0.5.0", + "@types/inquirer": "~8.2.10", + "@types/jest": "~29.5.2", + "@types/node": "^20.0.0", + "@types/semver": "^7.3.13", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", + "@typescript-eslint/utils": "^7.14.1", + "@webcontainer/api": "1.3.0-internal.2", + "cpy-cli": "^5.0.0", + "eslint": "^8.57.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-plugin-jest": "~28.8.0", + "eslint-plugin-jsdoc": "~48.11.0", + "eslint-plugin-prefer-arrow": "~1.2.3", + "eslint-plugin-unicorn": "^54.0.0", + "jest": "~29.7.0", + "jest-junit": "~16.0.0", + "jsonc-eslint-parser": "~2.4.0", + "jsonschema": "~1.4.1", + "nx": "~19.5.0", + "rxjs": "^7.8.1", + "ts-jest": "~29.2.0", + "ts-node": "~10.9.2", + "type-fest": "^4.10.2", + "typescript": "~5.5.4", + "zone.js": "~0.14.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + } +} diff --git a/packages/@o3r-training/tools/project.json b/packages/@o3r-training/tools/project.json new file mode 100644 index 0000000000..1eb0750e83 --- /dev/null +++ b/packages/@o3r-training/tools/project.json @@ -0,0 +1,67 @@ +{ + "name": "training-tools", + "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/@o3r-training/tools/src", + "prefix": "o3r", + "targets": { + "build": { + "executor": "nx:run-script", + "outputs": ["{projectRoot}/dist/package.json"], + "options": { + "script": "postbuild" + }, + "dependsOn": [ + "^build", + "compile", + "build-cli" + ] + }, + "prepare-build-builders": { + "executor": "nx:run-script", + "options": { + "script": "prepare:build:builders" + } + }, + "build-cli": { + "executor": "nx:run-script", + "options": { + "script": "build:cli" + } + }, + "compile": { + "executor": "nx:run-script", + "options": { + "script": "build:source" + } + }, + "lint": { + "options": { + "eslintConfig": "packages/@o3r-training/tools/.eslintrc.js", + "lintFilePatterns": [ + "packages/@o3r-training/tools/src/**/*.ts", + "packages/@o3r-training/tools/package.json" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "options": { + "jestConfig": "packages/@o3r-training/tools/jest.config.js" + } + }, + "prepare-publish": { + "executor": "nx:run-script", + "options": { + "script": "prepare:publish" + } + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "npm publish packages/@o3r-training/tools/dist" + } + } + }, + "tags": [] +} diff --git a/packages/@o3r-training/tools/src/public_api.ts b/packages/@o3r-training/tools/src/public_api.ts new file mode 100644 index 0000000000..6738aa624a --- /dev/null +++ b/packages/@o3r-training/tools/src/public_api.ts @@ -0,0 +1 @@ +export * from './utility'; diff --git a/packages/@o3r-training/tools/src/utility/index.ts b/packages/@o3r-training/tools/src/utility/index.ts new file mode 100644 index 0000000000..b1342ccae3 --- /dev/null +++ b/packages/@o3r-training/tools/src/utility/index.ts @@ -0,0 +1 @@ +export * from './web-container'; diff --git a/packages/@o3r-training/tools/src/utility/web-container.ts b/packages/@o3r-training/tools/src/utility/web-container.ts new file mode 100644 index 0000000000..1834b5f1f3 --- /dev/null +++ b/packages/@o3r-training/tools/src/utility/web-container.ts @@ -0,0 +1,85 @@ +import type { BufferEncoding, DirEnt, FileSystemTree } from '@webcontainer/api'; + +/** + * Function to read a directory in the file system + */ +export type ReadDirFn = (root: string, options: { + encoding?: BufferEncoding | null | undefined; + recursive?: boolean | undefined; + withFileTypes?: boolean; +}) => Promise[]>; + +/** + * Function to read a file in the file system + */ +export type ReadFileFn = (root: string, encoding?: BufferEncoding | null | undefined) => Promise; + +/** + * File system operations + */ +export type FileSystem = { + /** Reads a given directory and returns its files and directories */ + readdir: ReadDirFn; + /** Reads a given file */ + readFile: ReadFileFn; +}; + +/** + * Reads a given directory and returns the files and their corresponding contents + * @param entry + * @param path + * @param fileSystem + * @param exclusionList + */ +const readDirRec = async (entry: DirEnt, path: string, fileSystem: FileSystem, exclusionList: string[] = []): + Promise => { + const entryPath = path + '/' + entry.name; + if (exclusionList.includes(entry.name)) { + return; + } + if (entry.isDirectory()) { + const dirEntries = await fileSystem.readdir(entryPath, { encoding: 'utf-8', withFileTypes: true }); + const tree = { [entry.name]: { directory: {} } }; + for (const subEntry of dirEntries) { + tree[entry.name].directory = { + ...tree[entry.name].directory, + ...(await readDirRec(subEntry, entryPath, fileSystem, exclusionList)) + }; + } + return tree; + } + return { + [entry.name]: { + file: { contents: await fileSystem.readFile(entryPath, 'utf-8') } + } + }; +}; + +/** + * Get the file system tree from the given file system taking into account the list of files/directories to exclude + * @param files + * @param fileSystem + * @param exclusionList + */ +export const getFilesTree = async (files: {isDir: boolean; path: string}[], fileSystem: FileSystem, exclusionList: string[] = []) => { + const tree: FileSystemTree = {}; + for (const {isDir, path} of files) { + if (isDir) { + const dirEntries = await fileSystem.readdir(path, { encoding: 'utf-8', withFileTypes: true }); + for (const entry of dirEntries) { + const subTree = await readDirRec(entry, path, fileSystem, exclusionList); + if (subTree) { + tree[entry.name] = subTree[entry.name]; + } + } + } else { + const name = path.match(/([^\\/]+)$/g)?.[0]; + if (name) { + tree[name] = { + file: { contents: await fileSystem.readFile(path, 'utf-8') } + }; + } + } + } + return tree; +}; diff --git a/packages/@o3r-training/tools/testing/jest.config.ut.builders.js b/packages/@o3r-training/tools/testing/jest.config.ut.builders.js new file mode 100644 index 0000000000..ed52ef11a8 --- /dev/null +++ b/packages/@o3r-training/tools/testing/jest.config.ut.builders.js @@ -0,0 +1,15 @@ +const path = require('node:path'); +const getJestProjectConfig = require('../../../../jest.config.ut').getJestProjectConfig; +const rootDir = path.join(__dirname, '..'); + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestProjectConfig(rootDir, false), + displayName: `${require('../package.json').name}/builders`, + rootDir, + testPathIgnorePatterns: [ + '/.*/templates/.*', + '/src/.*', + '\\.it\\.spec\\.ts$' + ] +}; diff --git a/packages/@o3r-training/tools/testing/jest.config.ut.js b/packages/@o3r-training/tools/testing/jest.config.ut.js new file mode 100644 index 0000000000..5eb96361ac --- /dev/null +++ b/packages/@o3r-training/tools/testing/jest.config.ut.js @@ -0,0 +1,16 @@ +const path = require('node:path'); +const getJestProjectConfig = require('../../../../jest.config.ut').getJestProjectConfig; +const rootDir = path.join(__dirname, '..'); + +/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ +module.exports = { + ...getJestProjectConfig(rootDir, false), + displayName: require('../package.json').name, + rootDir, + testPathIgnorePatterns: [ + '/.*/templates/.*', + '/builders/.*', + '/schematics/.*', + '\\.it\\.spec\\.ts$' + ] +}; diff --git a/packages/@o3r-training/tools/testing/setup-jest.ts b/packages/@o3r-training/tools/testing/setup-jest.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@o3r-training/tools/tsconfig.build.json b/packages/@o3r-training/tools/tsconfig.build.json new file mode 100644 index 0000000000..b0ed792e0f --- /dev/null +++ b/packages/@o3r-training/tools/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.build", + "compilerOptions": { + "sourceMap": true, + "incremental": true, + "composite": true, + "rootDir": "src", + "lib": [ + "dom", + "dom.iterable", + "scripthost", + "es2017.object", + "esnext" + ], + "declarationMap": true, + "target": "es2020", + "module": "es2020", + "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo", + "outDir": "./dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.spec.ts"], +} diff --git a/packages/@o3r-training/tools/tsconfig.cli.json b/packages/@o3r-training/tools/tsconfig.cli.json new file mode 100644 index 0000000000..4baefd790c --- /dev/null +++ b/packages/@o3r-training/tools/tsconfig.cli.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.build", + "compilerOptions": { + "incremental": true, + "composite": true, + "outDir": "./dist", + "module": "CommonJS", + "rootDir": ".", + "tsBuildInfoFile": "build/.tsbuildinfo.cli" + }, + "references": [ + { + "path": "./tsconfig.build.json" + } + ], + "include": [ + "cli/**/*.ts", + ], + "exclude": [ + "**/*.spec.ts" + ] +} diff --git a/packages/@o3r-training/tools/tsconfig.doc.json b/packages/@o3r-training/tools/tsconfig.doc.json new file mode 100644 index 0000000000..8d1e4cff88 --- /dev/null +++ b/packages/@o3r-training/tools/tsconfig.doc.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.doc", + "exclude": [ + "**/*reducer.ts", + "**/*.fixture.ts" + ], + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/@o3r-training/tools/tsconfig.eslint.json b/packages/@o3r-training/tools/tsconfig.eslint.json new file mode 100644 index 0000000000..1a2ca3327f --- /dev/null +++ b/packages/@o3r-training/tools/tsconfig.eslint.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.build", + "include": [ + ".eslintrc.js", + "jest.config.js", + "testing/*", + "tooling/**/*.js" + ] +} diff --git a/packages/@o3r-training/tools/tsconfig.json b/packages/@o3r-training/tools/tsconfig.json new file mode 100644 index 0000000000..f0e4777cad --- /dev/null +++ b/packages/@o3r-training/tools/tsconfig.json @@ -0,0 +1,15 @@ +/* For IDE usage only */ +{ + "extends": "../../../tsconfig.base", + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.cli.json" + } + ] +} diff --git a/packages/@o3r-training/tools/tsconfig.spec.json b/packages/@o3r-training/tools/tsconfig.spec.json new file mode 100644 index 0000000000..df863ed596 --- /dev/null +++ b/packages/@o3r-training/tools/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.jest", + "compilerOptions": { + "composite": true, + "outDir": "test", + "rootDir": ".", + }, + "include": [ + "./src/**/*.spec.ts" + ], + "exclude": [], + "references": [ + { + "path": "./tsconfig.build.json" + } + ] +} diff --git a/tsconfig.build.json b/tsconfig.build.json index fbe353506e..87d1cb236f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -52,7 +52,8 @@ "@o3r/testing": ["packages/@o3r/testing/dist", "packages/@o3r/testing/src/public_api"], "@o3r/testing/*": ["packages/@o3r/testing/dist/*", "packages/@o3r/testing/src/*"], "@o3r/third-party": ["packages/@o3r/third-party/dist", "packages/@o3r/third-party/src/public_api"], - "@o3r/workspace": ["packages/@o3r/workspace/dist"] + "@o3r/workspace": ["packages/@o3r/workspace/dist"], + "@o3r-training/tools": ["packages/@o3r-training/tools/dist", "packages/@o3r-training/tools/src/public_api"] } }, "angularCompilerOptions": { diff --git a/yarn.lock b/yarn.lock index 0d3f91640d..fe9f06eb59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6270,6 +6270,91 @@ __metadata: languageName: node linkType: hard +"@o3r-training/tools@workspace:^, @o3r-training/tools@workspace:packages/@o3r-training/tools": + version: 0.0.0-use.local + resolution: "@o3r-training/tools@workspace:packages/@o3r-training/tools" + dependencies: + "@angular-devkit/build-angular": "npm:~18.2.0" + "@angular-devkit/core": "npm:~18.2.0" + "@angular-devkit/schematics": "npm:~18.2.0" + "@angular-eslint/eslint-plugin": "npm:~18.3.0" + "@angular/animations": "npm:~18.2.0" + "@angular/cli": "npm:~18.2.0" + "@angular/common": "npm:~18.2.0" + "@angular/compiler": "npm:~18.2.0" + "@angular/compiler-cli": "npm:~18.2.0" + "@angular/core": "npm:~18.2.0" + "@angular/platform-browser": "npm:~18.2.0" + "@angular/platform-browser-dynamic": "npm:~18.2.0" + "@babel/core": "npm:~7.25.0" + "@babel/preset-typescript": "npm:~7.24.0" + "@compodoc/compodoc": "npm:^1.1.19" + "@nx/eslint": "npm:~19.5.0" + "@nx/eslint-plugin": "npm:~19.5.0" + "@nx/jest": "npm:~19.5.0" + "@nx/js": "npm:~19.5.0" + "@o3r/build-helpers": "workspace:^" + "@o3r/eslint-plugin": "workspace:^" + "@o3r/telemetry": "workspace:^" + "@schematics/angular": "npm:~18.2.0" + "@stylistic/eslint-plugin-ts": "npm:~2.4.0" + "@swc/cli": "npm:~0.4.0" + "@swc/core": "npm:~1.7.0" + "@swc/helpers": "npm:~0.5.0" + "@types/inquirer": "npm:~8.2.10" + "@types/jest": "npm:~29.5.2" + "@types/node": "npm:^20.0.0" + "@types/semver": "npm:^7.3.13" + "@typescript-eslint/eslint-plugin": "npm:^7.14.1" + "@typescript-eslint/parser": "npm:^7.14.1" + "@typescript-eslint/utils": "npm:^7.14.1" + "@webcontainer/api": "npm:1.3.0-internal.2" + commander: "npm:^12.0.0" + cpy-cli: "npm:^5.0.0" + eslint: "npm:^8.57.0" + eslint-import-resolver-node: "npm:^0.3.9" + eslint-plugin-jest: "npm:~28.8.0" + eslint-plugin-jsdoc: "npm:~48.11.0" + eslint-plugin-prefer-arrow: "npm:~1.2.3" + eslint-plugin-unicorn: "npm:^54.0.0" + jest: "npm:~29.7.0" + jest-junit: "npm:~16.0.0" + jsonc-eslint-parser: "npm:~2.4.0" + jsonschema: "npm:~1.4.1" + nx: "npm:~19.5.0" + rxjs: "npm:^7.8.1" + ts-jest: "npm:~29.2.0" + ts-node: "npm:~10.9.2" + tslib: "npm:^2.6.2" + type-fest: "npm:^4.10.2" + typescript: "npm:~5.5.4" + zone.js: "npm:~0.14.2" + peerDependencies: + "@o3r/telemetry": "workspace:^" + "@schematics/angular": ~18.2.0 + "@webcontainer/api": 1.3.0-internal.2 + eslint: ^8.57.0 + rxjs: ^7.8.1 + type-fest: ^4.10.2 + typescript: ~5.5.4 + peerDependenciesMeta: + "@angular-devkit/architect": + optional: true + "@angular-devkit/core": + optional: true + "@angular/cli": + optional: true + "@o3r/telemetry": + optional: true + eslint: + optional: true + type-fest: + optional: true + bin: + o3r-extract-folder-structure: ./dist/cli/extract-folder-structure/extract-folder-structure.js + languageName: unknown + linkType: soft + "@o3r/amaterasu-api-spec@workspace:^, @o3r/amaterasu-api-spec@workspace:packages/@o3r/amaterasu/amaterasu-api-spec": version: 0.0.0-use.local resolution: "@o3r/amaterasu-api-spec@workspace:packages/@o3r/amaterasu/amaterasu-api-spec" @@ -7935,6 +8020,7 @@ __metadata: "@nx/jest": "npm:~19.5.0" "@nx/js": "npm:~19.5.0" "@nx/workspace": "npm:~19.5.0" + "@o3r-training/tools": "workspace:^" "@o3r/build-helpers": "workspace:^" "@o3r/eslint-config-otter": "workspace:^" "@o3r/eslint-plugin": "workspace:^" @@ -7955,6 +8041,9 @@ __metadata: "@typescript-eslint/parser": "npm:^7.14.1" "@typescript-eslint/types": "npm:^7.14.1" "@typescript-eslint/utils": "npm:^7.14.1" + "@webcontainer/api": "npm:1.3.0-internal.2" + "@xterm/addon-fit": "npm:^0.10.0" + "@xterm/xterm": "npm:*" "@yarnpkg/sdks": "npm:^3.0.0" ag-grid-angular: "npm:~31.1.1" ag-grid-community: "npm:~32.0.1" @@ -7985,8 +8074,11 @@ __metadata: lighthouse: "npm:9.6.8" lint-staged: "npm:^15.0.0" minimist: "npm:^1.2.6" + monaco-editor: "npm:~0.50.0" ng-packagr: "npm:~18.2.0" ngx-highlightjs: "npm:^12.0.0" + ngx-monaco-editor-v2: "npm:^18.0.0" + ngx-monaco-tree: "npm:^17.5.0" npm-run-all2: "npm:^6.0.0" nx: "npm:~19.5.0" pixelmatch: "npm:^5.2.1" @@ -8017,8 +8109,20 @@ __metadata: dependenciesMeta: "@swc/core": built: true + "@vscode/codicons": + unplugged: true + "@xterm/addon-fit": + unplugged: true + "@xterm/xterm": + unplugged: true esbuild: built: true + monaco-editor: + unplugged: true + ngx-monaco-editor-v2: + unplugged: true + ngx-monaco-tree: + unplugged: true languageName: unknown linkType: soft @@ -8777,6 +8881,7 @@ __metadata: "@ngx-translate/core": "npm:~15.0.0" "@nx/eslint-plugin": "npm:~19.5.0" "@nx/jest": "npm:~19.5.0" + "@o3r-training/tools": "workspace:^" "@o3r/application": "workspace:^" "@o3r/components": "workspace:^" "@o3r/configuration": "workspace:^" @@ -8803,6 +8908,10 @@ __metadata: "@typescript-eslint/parser": "npm:^7.14.1" "@typescript-eslint/types": "npm:^7.14.1" "@typescript-eslint/utils": "npm:^7.14.1" + "@vscode/codicons": "npm:^0.0.35" + "@webcontainer/api": "npm:1.3.0-internal.2" + "@xterm/addon-fit": "npm:^0.10.0" + "@xterm/xterm": "npm:^5.0.0" ag-grid-angular: "npm:~31.1.1" ag-grid-community: "npm:~32.0.1" bootstrap: "npm:5.3.3" @@ -8822,7 +8931,10 @@ __metadata: jest-preset-angular: "npm:~14.2.0" jsonc-eslint-parser: "npm:~2.4.0" lighthouse: "npm:9.6.8" + monaco-editor: "npm:~0.50.0" ngx-highlightjs: "npm:^12.0.0" + ngx-monaco-editor-v2: "npm:^18.0.0" + ngx-monaco-tree: "npm:^17.5.0" pixelmatch: "npm:^5.2.1" playwright-lighthouse: "npm:2.2.2" pngjs: "npm:^7.0.0" @@ -12843,6 +12955,13 @@ __metadata: languageName: node linkType: hard +"@vscode/codicons@npm:^0.0.35": + version: 0.0.35 + resolution: "@vscode/codicons@npm:0.0.35" + checksum: 10/eed59049130e413467de7acac7b6627d07086978eab9480cce6ddb7d36eeeb2ace28176a855861c2e76b35739dc1540c49a5336194a15ea8c2cf765f6356a485 + languageName: node + linkType: hard + "@vscode/vsce-sign-alpine-arm64@npm:2.0.2": version: 2.0.2 resolution: "@vscode/vsce-sign-alpine-arm64@npm:2.0.2" @@ -13131,6 +13250,13 @@ __metadata: languageName: node linkType: hard +"@webcontainer/api@npm:1.3.0-internal.2": + version: 1.3.0-internal.2 + resolution: "@webcontainer/api@npm:1.3.0-internal.2" + checksum: 10/b24d11a9a20ae46f17f5a01a5c6f90986e6b81ffe9d3ef0c38a0c44b7e0a21ed2ce1b7223b10e8af3603022434a445dec53e9354051adf50079c8e20559c81d3 + languageName: node + linkType: hard + "@webpack-cli/configtest@npm:^2.1.1": version: 2.1.1 resolution: "@webpack-cli/configtest@npm:2.1.1" @@ -13164,6 +13290,22 @@ __metadata: languageName: node linkType: hard +"@xterm/addon-fit@npm:^0.10.0": + version: 0.10.0 + resolution: "@xterm/addon-fit@npm:0.10.0" + peerDependencies: + "@xterm/xterm": ^5.0.0 + checksum: 10/8edfad561c0d0316c5883cbe2ce56109f105a2b2bf53b71d5f8c788e656a3205c1093a659dddcf4025a459e4b7ff8e07b6c6a19815c8711deeded560de5f1893 + languageName: node + linkType: hard + +"@xterm/xterm@npm:*, @xterm/xterm@npm:^5.0.0": + version: 5.5.0 + resolution: "@xterm/xterm@npm:5.5.0" + checksum: 10/d4cdc402de81a83a3e0ef93f38072cb8f54abe4d65865f2e29b92cbc2593f86d052f6b993895c9e5dec97f47548f504e90bcea0aad6845917c09b03f2f3a4629 + languageName: node + linkType: hard + "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -24814,6 +24956,13 @@ __metadata: languageName: node linkType: hard +"monaco-editor@npm:~0.50.0": + version: 0.50.0 + resolution: "monaco-editor@npm:0.50.0" + checksum: 10/594f2a3812147c1ef41c4dba444c649c32af95c3eb09529049bb7a2cb38e1747facbae0a459a07140e77cf9d73df41f034399804f9123f9cfff45394321a3983 + languageName: node + linkType: hard + "morgan@npm:^1.10.0": version: 1.10.0 resolution: "morgan@npm:1.10.0" @@ -25079,6 +25228,33 @@ __metadata: languageName: node linkType: hard +"ngx-monaco-editor-v2@npm:^18.0.0": + version: 18.1.0 + resolution: "ngx-monaco-editor-v2@npm:18.1.0" + dependencies: + tslib: "npm:^2.1.0" + peerDependencies: + "@angular/common": ^18.1.0 + "@angular/core": ^18.1.0 + monaco-editor: ^0.50.0 + checksum: 10/b84ae926e3491934532ede9e94c7b98dd26575cd74ceb85a99c680815826bacad7543d73e47cd8c8a81368b826dcbc5d2f6b0ca36accd6b536b448edfcbf2131 + languageName: node + linkType: hard + +"ngx-monaco-tree@npm:^17.5.0": + version: 17.5.0 + resolution: "ngx-monaco-tree@npm:17.5.0" + dependencies: + tslib: "npm:^2.3.0" + peerDependencies: + "@angular/cdk": ^17.3.0 + "@angular/common": ^17.3.0 + "@angular/core": ^17.3.0 + "@vscode/codicons": ^0.0.32 + checksum: 10/9c2e66babc3efddd549ea423b41713b11706eeb15478ae7ca30b5ff41c12d7c3718fe08aadb3f42cf54301b1f28ade8aa7d13a4d0a29ea7630cbe51a42184376 + languageName: node + linkType: hard + "nice-napi@npm:^1.0.2": version: 1.0.2 resolution: "nice-napi@npm:1.0.2"